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

 

 

posted @ 2015-10-20 10:20  RockIt  阅读(4131)  评论(1编辑  收藏  举报