LeetCode (85): Maximal Rectangle [含84題分析]


鏈接: https://leetcode.com/problems/maximal-rectangle/

【描述】

Given a 2D binary matrix filled with '0's and '1's, find the largest rectangle containing all ones and return its area.

【中文描述】

給一個二維數組, 算出里面最大的全1矩形面積,比如:

[

['1','1','1','0'],

['1','1','1','1']

]

顯然,最大矩形面積是6, 就是圖中粗體1組成的矩形。

————————————————————————————————————————————————————————————

【初始思路】

剛拿到題的時候毫無頭緒, 想了半天想了個O(n3)的算法,遍歷每一行,從每一行開始和后面的行進行'&'操作,每一步把與的結果和層數相乘得到面積,更新到max中, 最后返回max。

代碼我根本沒寫,這個題肯定沒這么蠢,寫出來肯定也是TLE。 

 

【Discuss】

最后無奈,去discuss看了下大家的做法,竟然看到了DP做法!當時驚呆了。根據最近學習DP的成果來看,DP用來計算那些求方案總數,求能不能等只需要最終結果的題非常適用。最大最小也可以用DP,但是我怎么都沒想到要用DP去做這個題!畢竟這個題的DP遞推函數太難構造了,所以就放棄了。后來看了大神的解法,頓時把膝蓋跪爛了!!!也不得不承認,人和人之間智力的差距太大了!

這里先介紹下DP算法。

 

【解法一: DP】

這個題的DP函數確實很復雜,很抽象,也太難構造出來了。如果面試時候在毫無准備的時候想到這個函數的構造方法的,基本可以當場hire你了!

我來介紹一下大神算法的核心思想:

(1)以行為單位來看

(2)每一行計算當前情況以及之前行的積累情況, 根據兩者的比較計算, 將計算結果存儲在中間結果數組當前行里, 供下一行用,這確實是DP思想。

(3)每一行實時計算走到當前行,最大矩形的面積。

 

上面的描述太籠統,(1)(3)好理解, 對於DP來講,這是肯定的。(2)的理解是本解法的精髓和核心。(2)理解了,這個算法其實非常簡單!

那么,如何來做所謂的當前行的計算呢?

首先,可以確定的是,對於每一行來說, 肯定還是得根據不同的列來分別計算對待, 我們用 i 標記行,用 j 標記列。

那么DP函數到底怎么構造?二維數組?我們來分析下二維數組到底行不行。

我們定義dp[i][j]表示從[0][0]到[i][j]位置這個范圍內最大的矩陣面積。看似合理,我們看下面這個例子:

                           

左邊是matrix, 右邊是dp數組,那么"?"位置(dp[1][4])應該怎么填?左邊是4,同時matrix[i][j-1]為1, 所以這里可以填5。 沒錯。 但是很顯然,它可以組成一個面積為6的矩形,可是我們不知道從哪里算出這個6,因為上面、左面的數字並不能代表唯一的含義。 比如上面右圖里的4, 到底代表的是同一行4個1組成的矩形?還是上下兩行共同組成的矩形?

所以這樣的dp函數是肯定不行的!可是除此之外我們也想不到其他的dp函數了,那就試試一維吧!

一維的dp[],每一位只能代表一個唯一的含義。仔細觀察matrix矩陣,其實所謂的矩形還不是靠不同的列組成的,恰好這些列都是1,那么好,我們假設一個height[]函數, 每一位記錄各個位有多少個1。然后我們實時根據height累積高度不就知道各個列的高度了么?面積就可以根據這個height算出來了,比如上面的matrix, 第二列計算完后, height = {2,1,2,2,2}, 那么最大面積根據LeetCode84算法,很容易求得6。

看似正確,我們用下面例子來看看:

                       

按照上面算法, height此時就是1,2,3,2,1,根據leetCode84題做法, 最大面積是6。 顯然是錯的!

原因在於, 對於leetcode84題, 算直方圖面積的時候,其實是在一維線性空間內計算的。而對於矩陣內求矩形面積, 變成了二維空間內計算。 不同行內1的長度相同的情況下,他們的起始終止位置可能是不同的, 那么面積就不能簡單的用直方圖的算法去計算。

 

所以, 大神算法在引入上面height函數的時候, 還加入了兩個函數, left[j]、right[j], 用來標記在 j 列的位置如果是1的情況下, 它的左邊界位置和右邊界位置。

左邊界的定義: 從0到該j位置, 第一個1的位置。

右邊界的定義: 從j 到該行末尾,第一個0的位置 

同時,對每一行給兩個變量: leftBound(總體左邊界) = 0、rightBound(總體右邊界) = 總列數+1 (因為初始狀態,rightBound應該在數組邊界外)

比如, 上面矩陣第一行:  1  1  1  0  0, 對於第一列(其實是0列), 由於它是1, 所以它的左邊界left[j] = max(總體左邊界, left[j]) (*解釋在下面),  對於第二列,也是總體左邊界, 第三列還是總體左邊界. 第四列的時候, matrix[0][3] == 0, 我們知道當前位是0,但是后一位是不是0不知道,我們姑且更新總體leftBound為j+1,因為很有可能下一個就是1, 那么總體左邊界就是j + 1。然后j++之后, 到第5列, matrix[0][4] == 0, 總體左邊界繼續更新為j+1.

而對於第一行的右邊界數組, 應該從右往左計算。最右邊第5列為0, 根據定義,總體右邊界rightBound可能為j(因為左邊很有可能就是1了, 那么當前就是左邊界, 所以右邊界更新為j), 所以總體右邊界更新為j。第4列,還是0, 那么總體右邊界更新為j。第3列為1, 那么right[j] = min(rightBound, right[j])(*解釋在下面)。第2列為1, right[j] = rightBound, 第一列同樣, right[j] = rightBound。

同樣再對第二行做如此計算。

對於left要取當前left[j]和leftBound中大值的解釋:

    我們可以把left[j]這個函數加深理解為, 從過往所有行到當前行, 在 j 這個位置,遇到的最晚的一個1在哪里, 那么當前left就應該更新到哪里。 因為left函數記錄了過往所有行的左邊界情況,所以,考慮左邊界的時候不僅僅要考慮當前行的情況,還要根據過往所有行的情況(保存在left[j]里), 選擇里面最大的一個(也就是最靠后的一個作為左邊界)。 因為如果之前的左邊界比現在的小,由於當前行左邊界更靠后,那么在計算面積的時候也不能按照之前的左邊界計算,只能按照當前左邊界計算。如下圖:

                             

在對第二行進行處理的時候, j = 3的時候, left[j] = 1, 而當前的leftBound = 3。這個時候, left[j]應該更新為3,參與最后的計算。同理,看下圖:

                             

還是對第二行處理的時候, j =3,  left[j] = 5, 而當前的leftBound = 3。 這個時候, left[j]應該更新為兩者中大的,也就是更新成5, 因為當前行做計算的時候, 矩形左邊界也只能從5開始算, 不能從3開始算。

同理, 右邊界取兩者中最小值也可以這么分析得出!

 

可能大家要問,根據這個怎么算面積?

我們看第一行左右邊界全部更新完后的左右邊界數組:

left:   [0,0,0,0,0]

right:  [3,3,3,5,5]

再結合height數組, height: [1,1,1,0,0].

面積 = height[j] * (right[j] - left[j]).

最大矩形面積就是:[3, 3, 3, 0, 0], 取其中最大值出來,更新到max里, 所以第一行執行完后, 最大面積為3。我們看和圖上是相符的。

第二行更新完后的左右數組為:

left:  [0, 1,  1,  1,  0]

right:  [0,  3,  3,  4,  5]

height: [1, 2, 2, 1, 0]

最大矩形面積:[0, 4, 4, 3, 0], 最大矩形面積是4, 符合圖上情況。

 

我們再看下面這個例子:

                                           

按照上面算法,第二行處理完后:

left:    [0,   0,  0, 3, 3,  0, 6,  6,  6,  6]

right:  [10,10,10, 5, 5,10,10,10,10,10] 

height:[0,   1,  1, 1, 1, 0,  2,  2,  2,  2]

那么, 根據上面公式算出來, 矩形面積:[0, 10, 10, 2, 2, 0, 8, 8, 8, 8]。 最大矩形面積是10, 這顯然不對啊!

問題出在哪里?

left,right我們已經分析過了, 計算方法肯定沒問題。那么問題只能出在height上。

問題出在了,當前行 j 為0的時候, 我們把上面的height[j]繼承到了這一行上。 為什么不能繼承?

因為之前行已經全部計算過了,之前 j 位不為0, 而當前行 j 位為0的時候, 說明之前的矩形計算已經徹底結束了, 不應該再繼承到這一行來。 

所以height的更新機制應該修改為: 當matrix[i][j]==1時, height[j]++. 當matrix[i][j] == 0時, height[j] = 0. 這樣就可以避免上面的結果影響到這一行的計算上來。

 

最后,我們來看看代碼。

【Show me the Code!!!】

 1 public static int maximalRectangleDP(char[][] matrix) {
 2         if (matrix == null || matrix.length == 0) return 0;
 3         int ROW = matrix.length;
 4         int COL = matrix[0].length;
 5         int[] left = new int[COL];
 6         int[] right = new int[COL];
 7         int[] height = new int[COL];
 8         /**
 9          * 初始化
10          */
11         Arrays.fill(left, 0);
12         Arrays.fill(right, COL);
13         Arrays.fill(height, 0);
14 
15         int max = 0;//最大面積
16 
17         /**
18          * 對每一行進行計算, 遞推公式如下:
19          * 每一行開始時,左邊界定為0, 右邊界定為COL
20          * height[j]好算:
21          *    如果matrix[i][j] = 0, height[j]不變
22          *    如果matrix[i][j] = 1, height[j]++;
23          * left[j]從左往右算:
24          *    如果matrix[i][j] = 0, left[j]=0, 同時左邊界變為當前j+1(因為潛在的左邊界可能就在j+1)
25          *    如果matrix[i][j] = 1, left[j]= max(left[j], 左邊界), 哪個大取哪個.
26          *    (解釋: 因為我們要的是過往所有行中0到該列位置最晚遇到1的位置)
27          * right[j]從右往左算:
28          *    如果matrix[i][j] = 0, right[j]=0, 同時右邊界變為當前j(因為潛在的右邊界就在當前j位置)
29          *    如果matrix[i][j] = 1, right[j]= min(right[j], 右邊界), 哪個小取哪個.
30          *    (解釋: 因為我們要的是過往所有行中COL-1到該列位置最早遇到0的位置)
31          */
32         for (int i = 0; i < ROW; i++) {
33             int leftBound = 0;
34             int rightBound = COL;//如果本行全為1, 那么從右往左第一個0應該在COL處, 這是個想象的位置, 只是為方便計算.
35             /**
36              * 算高度
37              */
38             for (int j = 0; j < COL; j++) {
39                 if (matrix[i][j] == '1') {
40                     height[j]++;
41                 } else {
42                     height[j] = 0;
43                 }
44             }
45 
46             /**
47              * 算左邊界
48              */
49             for (int j = 0; j < COL; j++) {
50                 if (matrix[i][j] == '1') {
51                     left[j] = Math.max(left[j], leftBound);
52                 } else {
53                     left[j] = 0;
54                     leftBound = j + 1;
55                 }
56             }
57 
58             /**
59              * 算右邊界
60              */
61             for (int j = COL - 1; j >=0; j--) {
62                 if (matrix[i][j] == '1') {
63                     right[j] = Math.min(right[j], rightBound);
64                 } else {
65                     rightBound = j;//當前行j到COL-1位置, 最早遇到0的位置可能就是當前
66                 }
67             }
68 
69             /**
70              * 實時計算走到當前行的最大矩形面積
71              *
72              */
73             for (int j = 0; j < COL; j++) {
74                 max = Math.max((right[j] - left[j]) * height[j], max);
75             }
76         }
77         return max;
78     }
maximalRectangleDP

 

【解法二: Stack】

該題實際上還有一個解法,那就是也用84題用到的stack求解,事實上,這正是它緊隨84題出現的原因。

要理解它為什么可以用84題解法來做,我們先從只有一行的矩陣來看。

          1 0 1 1 1 0 0 1 1 1 1

如果我們把上面數字都理解成bar的高度,每個bar寬度為1, 那么上面這一行矩陣不就是84題么?!只有一行的矩陣,其實和84題的情況是等價的。換句話說, 84題是退化成一行矩陣情況的85題!

那么, 既然84題是退化成一行的,我們應該有個直覺, 那就是85題可以對每一行執行一次84題的算法, 最終應該能算出最大面積來!

對每一行計算,自然計算出的就是這一行里的最大面積。 要計算全部呢?簡單, 如果當前行 j 位仍然是1,那么height[j]++。否則height[j]更新為0,這其實和上面DP算法的考慮是一樣的,只要矩形無法連續, 那之前的結果也不應參與當前行的計算。

比如上面一行, 經過84題算法, 得出最面積為4. 再來第二行:

         1 0 1 1 1 0 0 1 1 1 1

         0 1 0 0 1 1 1 1 1 1 1

到第二行的時候, 我們的矩陣其實經過加和變為了: 0 1 0 0 2 1 1 2 2 2 2,  那么通過84題算法可以輕松算出最大面積為8。

好了, 說了這么多, 84題到底是怎么回事?

 

【84題:Largest Rectangle in Histogram】

鏈接:https://leetcode.com/problems/largest-rectangle-in-histogram/

給一個數組, 代表了一堆bar, 里面的數字代表了每個bar的高度。要求算出最大直方圖面積, 比如下圖:

                      

最大面積是10, 5和6兩個bar的公共部分,組成的面積就是10。

好吧, 這個題拿棧來做, 其實我很不喜歡這種題,除了一個很tricky的技巧外,學不到任何通用性的知識。但是這里還是記錄下這個做法吧。

觀察這個題,你會發現, 最大的面積肯定出現在這樣的情況里:高的那些bar里。 或者, 存在於矮的但是覆蓋比較寬的bar里。

OK, 那我們只需要從所有這些高的bar計算出面積來, 再從最后覆蓋寬的bar計算出面積來, 最終比較其中最大的就可以了。

那么,怎么找出這些高的bar來,這是這個題的精髓所在。高的bar有個共同特征, 左右都低於它(廢話!)。那我們遍歷這些bar,如果遇到當前bar比之前的bar矮,我們就肯定可以認為當前bar是之前bar的右邊,那我們就可以把左邊的bar拿出來和當前bar做個比較,算出一個面積來。可是如果前一個bar做完計算后,它的前一個還是比現在這個高呢?那前一個也需要拿出來做計算。這個時候,我們需要有一個機制從前往后記錄bar值,同時還能從后往前取bar值。顯然,用stack!

好,算法描述如下:

     遍歷這些bar:

     如果遇到比前一個矮的,就把前一個出棧,然后計算一下面積,然后再次和棧頂做比較,如果還是比棧頂矮,棧頂繼續出棧,計算面積。如果棧頂bar已經比當前還矮了,當前bar入棧,繼續。

     如果比前一個高,就直接入棧,繼續。

 

有一種情況,也需要考慮進去。上面算法結束后,明顯stack里還可能會剩余bar,剩余的bar應該是一個不增序列。 所以,我們最終還需要針對stack里剩余的bar再算一次面積,最終更新max面積。

 

【Show me the Code!!!】

 1 public static int largestRectangleArea(int[] height) {
 2         if (height == null || height.length == 0 )
 3             return 0;
 4         Stack<Integer> stack = new Stack<Integer>();
 5         int res = 0;
 6         int i = 0;
 7         while(i < height.length) {
 8             while(!stack.isEmpty() && height[stack.peek()] >= height[i]) {
 9                 int index = stack.pop();
10                 int temp = stack.isEmpty() ? i * height[index]
11                         : ( (i - 1) - (stack.peek() + 1) + 1 ) * height[index];
12                 res = Math.max(res,temp);
13             }
14             stack.push(i);
15             i++;
16         }
17         int count = 1;
18         while (!stack.isEmpty()) {
19             int index = stack.pop();
20             if (!stack.isEmpty() && height[index] == height[stack.peek()]) {
21                 count++;
22                 continue;
23             }
24             int temp = count * height[index];
25             res = Math.max(res,temp);
26             temp = stack.isEmpty() ? i * height[index]
27                     : ( i - stack.peek() - 1 ) * height[index];
28             res = Math.max(res,temp);
29         }
30         return res;
31     }
largestRectangleArea

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM