1 柱状图中的最大矩形
1.1 问题描述
给定n个非负整数,用来表示每个柱子的高度,每个柱子彼此相邻,宽度为1,求解在该柱状图中能够画出的矩形的最大面积
1.2 解题思路
1.2.1 暴力查找
- 计算每个矩形的高确定后,能够画出的最大矩形的面积
- 枚举每一根柱子,往两边找,比它高时说明可以维持这个高度,比它矮的时候说明不能再用这个矩形的高画矩形了
时间复杂度:O(n^2),枚举n次,每次枚举的最差情况都是到达边界
1.2.2 单调栈
我们对于每一个柱子,计算以这个柱子的高为矩形高的最大矩形,寻找其左边第一个比它矮的,以及这个柱子右边第一个比它矮的,这样就知道了横向的矩形的最大的宽的长度,优化的地方在于,已经遍历过的高矮关系之后不需要再重复。
首先为每根柱子寻找其左边第一个比它矮的柱子,通过单调栈来实现,寻找一个柱子的左边第一个矮的柱子过程如下:
- 将栈内的所有比当前柱子高(或等高)的其他柱子的序号弹出,直到栈顶柱子比当前柱子矮或者栈为空
- 如果栈为空,表示左边的柱子都比当前柱子高,那么当前柱子的左边第一矮序号计为-1
- 如果栈不为空,当前栈顶元素就是当前柱子左边第一个比当前柱子矮的,记下其序号
- 当前柱子序号入栈
枚举柱子的每一次,我们都将栈内比当前柱子高的柱子弹出,之后将当前柱子压入
- Q: 已经弹出的柱子会不会对后续柱子寻找左边第一矮的柱子造成影响?
- A: 不会,假设现在有ABCD从左到右4个柱子,其中高度 C < B
- 寻找C的左边第一矮的柱子时,我们把B弹出了
- 寻找D的左边第一矮的柱子时
- 如果C < D,那么直接找到
- 如果C > D,那么B > D,此时栈里没有B,只是省去了我们的判断
我们找完每个柱子的左边第一矮的柱子,可以如法炮制,倒序寻找每个柱子右边第一矮的柱子,最后逐个柱子计算以当前柱子为高的最大矩形的面积,一一比较,得到答案
时间复杂度: O(n), 计算左边最小的过程中,每个柱子进栈都是1次,最多弹栈一次;右边最小也一样;而最后计算每个最大矩形的时候,只要遍历一遍柱子,所以时间复杂度为O(n)
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
int[]left = new int[n];
int[]right = new int[n];
Stack<Integer> mono_stack = new Stack<Integer>();
for(int i= 0; i< n; i++){
while(! mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]){
mono_stack.pop();
}
left[i] = mono_stack.isEmpty()?-1:mono_stack.peek();
mono_stack.push(i);
}
mono_stack.clear();
for(int i = n - 1; i>=0; i--){
while(!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]){
mono_stack.pop();
}
right[i] = mono_stack.isEmpty()?n:mono_stack.peek();
mono_stack.push(i);
}
int ans = 0;
for(int i = 0; i < n; i++){
ans = Math.max((right[i] - left[i] - 1) * heights[i],ans);
}
return ans;
}
}
1.2.3 进一步优化
考虑是否有必要左边算一遍,右边算一遍 假设我们计算左边第一个矮柱的时候,栈顶为A,进站元素为B,B < A
- 此时A需要出栈,站在A的角度来说,B比A矮所以A要出栈保持单调性
- A和B之间不可能存在比B矮的其他元素
- 如果存在,假设为C,C在B前面,C比B矮,那么C也比A矮,在B之前C会入栈,C比A矮所以A要出栈保持单调性,所以B之前A就已经出栈,这种情况不成立
- 所以A出栈时,右边第一矮的元素就是将要入栈的柱子序号
我们可以不必左边右边分开算,一次入栈会确定入栈柱子的左边第一个比它矮的柱子序号,一次出栈会确定出栈柱子右边第一个比它矮的柱子序号; 要注意的是并不是所有柱子都会出栈,遍历完所有柱子后,还留在栈内的柱子,其右边所有的柱子都比它高,标记右边第一个比它矮的柱子的序号为n。
时间复杂度 :O(N),大致缩短了原来1/3的时间
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
int[]left = new int[n];
int[]right = new int[n];
Arrays.fill(right,n);
Stack<Integer> mono_stack = new Stack<Integer>();
for(int i= 0; i< n; i++){
while(! mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]){
right[mono_stack.pop()] = i;
}
left[i] = mono_stack.isEmpty()?-1:mono_stack.peek();
mono_stack.push(i);
}
int ans = 0;
for(int i = 0; i < n; i++){
ans = Math.max((right[i] - left[i] - 1) * heights[i],ans);
}
return ans;
}
}
2 问题变种 01矩阵中的最大矩形
2.1问题描述
输入一个m * n 01矩阵,求其中只包含1的最大矩形面积,每个格子长宽为1
| 1 | 0 | 1 | 0 | 0 |
| 1 | 0 | 1 | 1 | 1 |
| 1 | 1 | 1 | 1 | 1 |
| 1 | 0 | 0 | 1 | 0 |
2.2 求解思路
2.2.1 暴力求解
实现思路
- 对于每一行,计算横向最大矩形,从左到右枚举每个格子,求以该格子结尾的最大矩形面积
- 如果该格子为0,没有计算的必要
- 如果该格子为1
- 位于第一列直接计为1
- 不位于第一列,以该格子结尾的最大矩形是前面一格的最大矩形加1
- 遍历每个格子,计算以该格子为右下角的最大矩形面积
- 当前格子为0,没有计算的必要
- 当前格子为1
- 当前格子的横向矩形最大面积视为宽,高为1,最大矩形面积暂定为横向最大矩形面积,与全局最大矩形面积比较,全局最大矩形面积变为两者取大
- 向上遍历,直到上方的格子为0(上方格子不存在横向最大矩形)
- 每向上一行,高度增加1,宽度为当前宽和上一行宽取小
- 每向上一行,重新计算最大矩形面积,全局最大矩形面积变为 最大矩形面积与全局最大矩形面积取大
时间复杂度:O(m^2 * n),假设矩阵为m * n, 求解横向最大矩形需要遍历 m * n,求解全局最大矩形除了需要遍历每个位置,在每个位置还需要至多往上矩阵的行数m,故时间复杂度为O(m^2 * n)
class Solution {
public int maximalRectangle(char[][] matrix) {
if(matrix.length == 0){
return 0;
}
int[][] horizantal = new int[matrix.length][matrix[0].length];
int[][] max = new int[matrix.length][matrix[0].length];
//横向矩形的长度
for(int i = 0; i < matrix.length; i++){
for(int j = 0; j < matrix[0].length; j++){
if(matrix[i][j] == '1'){
if(j == 0){
horizantal[i][j] = 1;
}else{
horizantal[i][j] = horizantal[i][j - 1] + 1;
}
}
}
}
//矩形大小
int maxArea = 0;
for(int i = 0; i < matrix.length; i++){
for(int j = 0; j < matrix[0].length; j++){
if(matrix[i][j] == '1'){
//以该点为右下角 向上寻找最大面积
int row = i;
int height = 1;
int width = horizantal[i][j];
while(row >= 0 && horizantal[row][j] > 0){
//宽度取小
width = Math.min(width, horizantal[row][j]);
maxArea = Math.max(maxArea,width * height);
row--;
height ++;
}
}
}
}
return maxArea;
}
}
2.2.2 单调栈
我们可以把每一行及其以上的部分看作是一个求解柱状图当中最大矩形问题
| 1 | 0 | 1 | 0 | 0 | . | 1 | 0 | 1 | 0 | 0 |
| 1 | 0 | 1 | 1 | 1 | . | 2 | 0 | 2 | 1 | 1 |
| 1 | 1 | 1 | 1 | 1 | . | 3 | 1 | 3 | 2 | 2 |
| 1 | 0 | 0 | 1 | 0 | . | 4 | 0 | 0 | 3 | 0 |
求解思路
- 先求出每一行及其以上部分的柱状图的高度
- 一共m行,就相当于存在m个柱状图
- 柱状图当中每个柱的高度是从当前位置往上连续1的个数
- 当前位置为0,柱的高度直接为0
- 当前位置为1,柱子的高度为上一行同一位置的柱子的高度加1
- 对于每个柱状图求解最大矩形,然后从最大矩形当中取最大值
时间复杂度:O(m * n)
- 求柱状图的高度需要O(m * n)
- 求解单个柱状图需要O(n)
- 栈的出入O(n)
- 对每个圆柱的高求最大矩形面积 O(n)
- 一共m个柱状图,求解所有柱状图是O(m * n),故时间复杂度为O(m * n)
class Solution {
public int maximalRectangle(char[][] matrix) {
if(matrix.length == 0){
return 0;
}
int m = matrix.length;
int n = matrix[0].length;
int [][]column = new int[m][n];
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(i == 0){
if(matrix[i][j] == '1'){
column[i][j] = 1;
}else{
column[i][j] = 0;
}
}else{
if(matrix[i][j] == '1'){
column[i][j] = column[i - 1][j] + 1;
}
}
}
}
Stack<Integer> stack = new Stack<Integer>();
int left[] = new int[n];
int right[] = new int[n];
int maxArea = 0;
int currentArea = 0;
for(int i = 0; i < m; i++){
Arrays.fill(right,n);
stack.clear();
for(int j = 0; j < n; j++){
while(!stack.isEmpty()&&column[i][j] <= column[i][ stack.peek()]){
right[stack.pop()] = j;
}
left[j] = (stack.isEmpty())?(-1):stack.peek();
stack.push(j);
}
for(int j = 0; j < n; j++){
currentArea = column[i][j] * (right[j] - left[j] -1);
maxArea = Math.max(maxArea,currentArea);
}
}
return maxArea;
}
}