zoukankan      html  css  js  c++  java
  • 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

     

  • 相关阅读:
    数组快速排序法
    javascript 的基本优化
    wdlinux mysql innodb的安装
    中文逗号替换为英文逗号 ,中英文逗号互换
    wdlinux 编译pdo_mysql
    nodejs 简单对mongodb 操作
    node.js 安装
    开启 mysql 的远程连接功能
    文件下载到本地
    webpack基础+webpack配置文件常用配置项介绍+webpack-dev-server
  • 原文地址:https://www.cnblogs.com/lupx/p/leetcode-85.html
Copyright © 2011-2022 走看看