链接: 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!!!】

 public static int maximalRectangleDP(char[][] matrix) {
if (matrix == null || matrix.length == 0) return 0;
int ROW = matrix.length;
int COL = matrix[0].length;
int[] left = new int[COL];
int[] right = new int[COL];
int[] height = new int[COL];
/**
* 初始化
*/
Arrays.fill(left, 0);
Arrays.fill(right, COL);
Arrays.fill(height, 0); int max = 0;//最大面积 /**
* 对每一行进行计算, 递推公式如下:
* 每一行开始时,左边界定为0, 右边界定为COL
* height[j]好算:
* 如果matrix[i][j] = 0, height[j]不变
* 如果matrix[i][j] = 1, height[j]++;
* left[j]从左往右算:
* 如果matrix[i][j] = 0, left[j]=0, 同时左边界变为当前j+1(因为潜在的左边界可能就在j+1)
* 如果matrix[i][j] = 1, left[j]= max(left[j], 左边界), 哪个大取哪个.
* (解释: 因为我们要的是过往所有行中0到该列位置最晚遇到1的位置)
* right[j]从右往左算:
* 如果matrix[i][j] = 0, right[j]=0, 同时右边界变为当前j(因为潜在的右边界就在当前j位置)
* 如果matrix[i][j] = 1, right[j]= min(right[j], 右边界), 哪个小取哪个.
* (解释: 因为我们要的是过往所有行中COL-1到该列位置最早遇到0的位置)
*/
for (int i = 0; i < ROW; i++) {
int leftBound = 0;
int rightBound = COL;//如果本行全为1, 那么从右往左第一个0应该在COL处, 这是个想象的位置, 只是为方便计算.
/**
* 算高度
*/
for (int j = 0; j < COL; j++) {
if (matrix[i][j] == '1') {
height[j]++;
} else {
height[j] = 0;
}
} /**
* 算左边界
*/
for (int j = 0; j < COL; j++) {
if (matrix[i][j] == '1') {
left[j] = Math.max(left[j], leftBound);
} else {
left[j] = 0;
leftBound = j + 1;
}
} /**
* 算右边界
*/
for (int j = COL - 1; j >=0; j--) {
if (matrix[i][j] == '1') {
right[j] = Math.min(right[j], rightBound);
} else {
rightBound = j;//当前行j到COL-1位置, 最早遇到0的位置可能就是当前
}
} /**
* 实时计算走到当前行的最大矩形面积
*
*/
for (int j = 0; j < COL; j++) {
max = Math.max((right[j] - left[j]) * height[j], max);
}
}
return max;
}

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!!!】

 public static int largestRectangleArea(int[] height) {
if (height == null || height.length == 0 )
return 0;
Stack<Integer> stack = new Stack<Integer>();
int res = 0;
int i = 0;
while(i < height.length) {
while(!stack.isEmpty() && height[stack.peek()] >= height[i]) {
int index = stack.pop();
int temp = stack.isEmpty() ? i * height[index]
: ( (i - 1) - (stack.peek() + 1) + 1 ) * height[index];
res = Math.max(res,temp);
}
stack.push(i);
i++;
}
int count = 1;
while (!stack.isEmpty()) {
int index = stack.pop();
if (!stack.isEmpty() && height[index] == height[stack.peek()]) {
count++;
continue;
}
int temp = count * height[index];
res = Math.max(res,temp);
temp = stack.isEmpty() ? i * height[index]
: ( i - stack.peek() - 1 ) * height[index];
res = Math.max(res,temp);
}
return res;
}

largestRectangleArea

LeetCode (85): Maximal Rectangle [含84题分析]的更多相关文章

  1. 求解最大矩形面积 — leetcode 85. Maximal Rectangle

    之前切了道求解最大正方形的题,题解猛戳 这里.这道题 Maximal Rectangle 题意与之类似,但是解法完全不一样. 先来看这道题 Largest Rectangle in Histogram ...

  2. leetcode[85] Maximal Rectangle

    给定一个只含0和1的数组,求含1的最大矩形面积. Given a 2D binary matrix filled with 0's and 1's, find the largest rectangl ...

  3. [LeetCode] 85. Maximal Rectangle 最大矩形

    Given a 2D binary matrix filled with 0's and 1's, find the largest rectangle containing only 1's and ...

  4. leetCode 85.Maximal Rectangle (最大矩阵) 解题思路和方法

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

  5. 刷题85. Maximal Rectangle

    一.题目说明 题目,85. Maximal Rectangle,计算只包含1的最大矩阵的面积.难度是Hard! 二.我的解答 看到这个题目,我首先想到的是dp,用dp[i][j]表示第i行第j列元素向 ...

  6. 85. Maximal Rectangle

    85. Maximal Rectangle Given a 2D binary matrix filled with 0's and 1's, find the largest rectangle c ...

  7. 【LeetCode】85. Maximal Rectangle 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 题目地址: https://leetcode.com/problems/maximal- ...

  8. 【leetcode】85. Maximal Rectangle(单调栈)

    Given a rows x cols binary matrix filled with 0's and 1's, find the largest rectangle containing onl ...

  9. 【LeetCode】85. Maximal Rectangle

    Maximal Rectangle Given a 2D binary matrix filled with 0's and 1's, find the largest rectangle conta ...

随机推荐

  1. tomcat+nginx+redis实现均衡负载、session共享(二)

    今天我们接着说上次还没完成session共享的部分,还没看过上一篇的朋友可以先看下上次内容,http://www.cnblogs.com/zhrxidian/p/5432886.html. 1.red ...

  2. Echarts使用随笔(2)-Echarts中mapType and data

    本文出处:http://blog.csdn.net/chenxiaodan_danny/article/details/39081071  series : [                {   ...

  3. [转帖]vivado & VS2013工具

    来源:http://bbs.csdn.net/topics/380057699 添加OpenCV库后,MFC在Debug模式下调试,提示应用程序无法正常启动(0xc000007b). 解决方法:在环境 ...

  4. install erlang environment on centos

    #(erlide in linux can't detect the runtime if build from source, but erlang shell works correctly)su ...

  5. oracle 数据库关闭的的几种方式总结

    shutdown的几种方式,shutdown abort的一些弊端有哪些   1.shutdown normal        正常方式关闭数据库.    2.shutdown immediate   ...

  6. 强大的Core Image框架,各种滤镜处理图像

    首先介绍一下Core Image,他是一个很强大的图像处理框架,他可以让你简单的应用各种滤镜来处理图像,比如说色相,饱和度,亮度等等...他是运用GPU(CPU)实时地处理图像数据和视频的帧.而且Co ...

  7. java SWT/Rap 计算器版本2(键盘鼠标兼容)

    package cn.lesaas.nof.rwtswt.ui.dialog; import java.math.BigDecimal; import org.eclipse.swt.SWT;impo ...

  8. 【转】 C语言自增自减运算符深入剖析

    转自:http://bbs.csdn.net/topics/330189207 C语言的自增++,自减--运算符对于初学者来说一直都是个难题,甚至很多老手也会产生困惑,最近我在网上看到一个问题:#in ...

  9. Keil的使用-1创建项目和工程

    下载keil,注意不要使用MDK版本(主要是arm开发使用),大小约54M 安装过程不再详述 安装Keil成功并运行后,新建项目,   创建新项目,然后弹出下图,选择对应的单片机芯片(双击)     ...

  10. SVN 使用的简单整理

    1. 在SVN服务器上创建存储Dir,并和个人主机建立联系.      现在SVN服务器上创建一个存储文件夹svn_storeDir.然后在个人电脑上建立一个本地文件夹local_Dir.    进入 ...