鏈接: 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 }
【解法二: 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 }