第一章 栈和队列
1.9 求最大子矩阵的大小
【题目】
给定一个整形矩阵 matrix,其中的值只有 0 和 1 两种,求其中全是 1 的矩形区域中,最大的矩形区域内 1 的数量。
【难度】
校 ★★★☆
【题解】
如果矩阵的大小为 O(N×M),本题可以做到时间复杂度为 O(N×M)。解法的具体过程如下;
- 矩阵的行数为 N,以每一行做切割,统计以当前行作为底,每个位置往上的 1 的数量。使用高度数组 height 表示。
- 对于每一次切割,都利用更新后的 height 数组来求出每一行为底时最大的矩形。直到最后最大的矩形即题中所求。
- 如果 height 数组的长度为 M,那么求解此过程可以做到时间复杂度为 O(M)。
- 对于 height 数组,可以看做一个直方图。那么实质上是在一个大的直方图中求最大矩形的面积。如果求出以每一根柱子拓展出去的最大矩形,找出其中最大的一个即可。
- 考虑每一根柱子最大能扩多大,其实就是找到柱子左边第一个比它小的柱子的位置,以及右边第一个比它小的柱子的位置。这一过程用栈很快实现:
- 生成一个栈,记为 stack,从左到右遍历 height 数组,每遍历到一个位置,都会把位置压入到 stack 中;
- 遍历到 height 的 0 位置时,此时 stack 为空,直接将位置 0 压入栈中;
- 遍历到 height 的 i 位置时,只有当前 i 位置的值 height[i] 大于当前栈顶位置所代表的值 height[stack.peek()],则 i 位置才可以压入栈中。所以 stack 中从栈顶到栈底的位置所代表的值是依次递减的,并且无重复值。
- 如果当前 i 位置的值 height[i] 小于或等于当前栈顶位置所代表的值 height[stack.peek()],则把栈中存的位置不断弹出,直到栈顶位置所代表的值小于 height[i],再把位置 i 压入栈中,并在此期间做如下处理:
- 假设当前弹出的栈顶位置记为 j,弹出栈顶后,新的栈顶记为 k。考虑位置 j 的柱子向右和向左最远能扩到哪里。
- 对于位置 j 的柱子,如果 height[j] > height[i],那么 i-1 位置就是向右能扩到的最远位置。因为 j 之所以被弹出,是因为遇到了第一个比位置 j 所代表的值小的位置。如果 height[j] = height[i],那么 i-1 位置不一定是向右能扩到的最远位置。但是可以肯定 i 位置的柱子向左必然可以扩到 j 位置,那么 j 位置的柱子扩出来的最大矩形和 i 位置的柱子扩出来的最大矩形是同一个。所以,此时可以先不计算 j 位置的柱子能扩出来的最大矩形,因为位置 i 是要压入到栈中,等到位置 i 被弹出的时候再进行计算。
- 对于位置 j 的柱子,向左最远能扩到 k+1 位置。首先,height[k+1…j-1] 之间不可能有小于或等于 height[k] 的值,否则 k 位置早就被弹出了。又因为再栈中 k 位置和 j 位置原本是相邻的,并且从栈顶到栈底的位置所代表的值是依次递减并且无重复值,所以在 height[k+1…j-1] 之间不可能有大于或等于 height[k],同时又小于或等于 height[j] 的值,否则 k 和 j 就不可能相邻。所以,height[k+1…j-1] 之间的值既大于 height[k],又大于 height [j]。于是,j 位置的柱子向左最远可以扩到 k+1 位置。
- 综上所述,j 位置的柱子能扩出来的最大矩形为 (i-k-1)*height[j]。
- 在遍历结束后,stack 中可能仍有位置没有经历扩的过程。此时因为 height 数组不能再向右扩了,所以认为 i = height.length 且越界之后的值非常小,然后开始弹出留在栈中的位置。等到栈为空,这个过程就结束了。
通过上述步骤,任何一个位置仅仅进出栈一次,所以时间复杂度为 O(M)。每做一次切割处理的时间复杂度为 O(M),一共做 N 次,则总的时间复杂度为 O(N×M)。
【实现】
- Matrix.java
import java.util.Stack;
public class Matrix {
private int[][] matrix;
private Integer maxSize;
public Matrix(int[][] arr) {
this.matrix = arr;
}
public void setMatrix(int[][] arr) {
this.matrix = arr;
compute();
}
public int getMaxSubMatrixSize() {
if (this.maxSize == null) {
compute();
}
return this.maxSize;
}
private void compute() {
if (this.matrix == null || this.matrix.length == 0 || this.matrix[0].length == 0) {
this.maxSize = 0;
return;
}
int[] height = new int[this.matrix[0].length];
for (int i = 0; i < this.matrix.length; ++i) {
for (int j = 0; j < this.matrix[0].length; ++j) {
height[j] = this.matrix[i][j] == 0 ? 0 : height[j] + 1;
}
maxSize(height);
}
}
private void maxSize(int[] height) {
if (height == null || height.length == 0) {
return;
}
this.maxSize = 0;
Stack<Integer> stack = new Stack<>();
/**
* 遍历 height 数组
*/
for (int i = 0; i < height.length; ++i) {
while (!stack.empty() && height[i] <= height[stack.peek()]) {
int j = stack.pop();
int k = stack.empty() ? -1 : stack.peek();
int size = (i - k - 1) * height[j];
if (size > this.maxSize) {
this.maxSize = size;
}
}
stack.push(i); // 索引
}
/**
* 处理 height 数组遍历完后 stack 未空的情况
*/
while (!stack.empty()) {
int j = stack.pop();
int k = stack.empty() ? -1 : stack.peek();
int size = (height.length - k - 1) * height[j];
if (size > this.maxSize) {
this.maxSize = size;
}
}
}
}
- MatrixTest.java
public class MatrixTest {
public static void main(String[] args) {
int[][] arr = {
{1, 0, 1, 1},
{0, 1, 0, 1},
{1, 1, 1, 0},
};
Matrix map = new Matrix(arr);
int size = map.getMaxSubMatrixSize();
System.out.println(size);
}
}