题目地址:
https://leetcode.com/problems/count-submatrices-with-all-ones/
给定一个 m × n m\times n m×n的 0 − 1 0-1 0−1矩阵 A A A,问其 1 1 1子矩阵的数量。
法1:位运算。本质上是枚举矩阵宽度。对于 A [ i ] [ j ] A[i][j] A[i][j],我们看一下其与其右边最多能延伸出多少个 1 1 1,如果能延伸出 x x x个 1 1 1,那么显然长宽都为 1 1 1的 1 1 1子矩阵数量就是 1 + 2 + . . . + x = ( 1 + x ) x 2 1+2+...+x=\frac{(1+x)x}{2} 1+2+...+x=2(1+x)x个。如果我们令 A [ i ] [ j ] A[i][j] A[i][j]都按位与一下 A [ i + 1 ] [ j ] A[i+1][j] A[i+1][j],那么此时的 A [ i ] [ j ] A[i][j] A[i][j]就代表了原先矩阵 A [ i : i + 1 ] [ j ] A[i:i+1][j] A[i:i+1][j]是否都是 1 1 1,这样如果改变后的矩阵从 A [ i ] [ j ] A[i][j] A[i][j]能向右延伸出 x x x个 1 1 1,就说明以 A [ i ] [ j ] A[i][j] A[i][j]为左上角的宽度为 2 2 2的矩形的最大长度是 x x x,那么宽为 2 2 2的 1 1 1子矩阵数量就是 1 + 2 + . . . + x = ( 1 + x ) x 2 1+2+...+x=\frac{(1+x)x}{2} 1+2+...+x=2(1+x)x个。这样就能枚举出以 A [ i ] [ j ] A[i][j] A[i][j]为左上角的所有 1 1 1矩阵的数量了。代码如下:
public class Solution {
public int numSubmat(int[][] mat) {
// write your code here
int res = 0;
int m = mat.length, n = mat[0].length;
// 计算宽度为m - bound的以A[i][j]为左上角的1矩阵数量
for (int bound = m - 1; bound >= 0; bound--) {
for (int i = 0; i <= bound; i++) {
int width = 0;
for (int j = 0; j < n; j++) {
if (mat[i][j] == 1) {
width++;
} else {
width = 0;
}
// 累加1矩阵数量
res += width;
// 算完了,就向下做按位与
if (i < bound) {
mat[i][j] &= mat[i + 1][j];
}
}
}
}
return res;
}
}
时间复杂度 O ( m 2 n ) O(m^2n) O(m2n),空间 O ( 1 ) O(1) O(1)。
法2:动态规划。设 f [ i ] [ j ] f[i][j] f[i][j]是从 A [ i ] [ j ] A[i][j] A[i][j]开始向右最多能延伸出多少个 1 1 1。则 f [ i ] [ j ] = { 0 , A [ i ] [ j ] = 0 1 + f [ i ] [ j + 1 ] , A [ i ] [ j ] = 1 f[i][j]=\begin{cases} 0,A[i][j]=0\\ 1+f[i][j + 1], A[i][j]=1 \end{cases} f[i][j]={0,A[i][j]=01+f[i][j+1],A[i][j]=1接下来我们开始求以 A [ i ] [ j ] A[i][j] A[i][j]为左上角的 1 1 1矩阵的数量。比如遍历到 ( i , j ) (i,j) (i,j)的时候,以 A [ i ] [ j ] A[i][j] A[i][j]为左上角的上下宽度为 1 1 1的 1 1 1矩阵的数量就是 f [ i ] [ j ] f[i][j] f[i][j],而以 A [ i ] [ j ] A[i][j] A[i][j]为左上角的上下宽度为 2 2 2的 1 1 1矩阵的数量就是 min { f [ i ] [ j ] , f [ i + 1 ] [ j ] } \min\{f[i][j],f[i+1][j]\} min{f[i][j],f[i+1][j]}(类似于法1里的求按位与),接着,以 A [ i ] [ j ] A[i][j] A[i][j]为左上角的上下宽度为 3 3 3的 1 1 1矩阵的数量就是 min { f [ i ] [ j ] , f [ i + 1 ] [ j ] , f [ i + 2 ] [ j ] } \min\{f[i][j],f[i+1][j],f[i+2][j]\} min{f[i][j],f[i+1][j],f[i+2][j]},等等。这样就能枚举出所有以 A [ i ] [ j ] A[i][j] A[i][j]为左上角的 1 1 1矩阵的数量了,累加起来即可。代码如下:
public class Solution {
public int numSubmat(int[][] mat) {
int m = mat.length, n = mat[0].length;
// 为了节省空间,可以直接把mat当成dp
for (int j = n - 2; j >= 0; j--) {
for (int i = 0; i < m; i++) {
if (mat[i][j] == 1) {
mat[i][j] += mat[i][j + 1];
}
}
}
int res = 0;
for (int j = 0; j < n; j++) {
for (int i = 0; i < m; i++) {
// 存mat[i : k, j]的最小值
int min = Integer.MAX_VALUE;
for (int k = i; k < m; k++) {
min = Math.min(min, mat[k][j]);
// 累加矩阵数量
res += min;
}
}
}
return res;
}
}
时空复杂度与法1同。
法3:单调栈。首先考虑一维的情形,如果给定每个柱子的高度,以数组
h
h
h表示,考虑以下底边为底的矩形个数。如下图:
思路是单调栈。维护一个单调上升的栈,当遇到比栈顶低或相等(这里可以低,也可以低或相等,不影响最后答案)的高度时,设栈顶对应的高度是
h
t
h_t
ht,下标为
x
t
x_t
xt,那么栈顶压在下面的高度是其左边第一个比其矮的高度,设为
h
i
h_i
hi,下标为
x
i
x_i
xi,新来的数则是栈顶右边第一个比栈顶低或相等的高度,设为
h
j
h_j
hj,下标为
x
j
x_j
xj,此时,我们计算一下以高度在
[
max
{
h
i
,
h
j
}
+
1
,
h
t
]
[\max\{h_i, h_j\}+1,h_t]
[max{hi,hj}+1,ht]区间内,左右延伸是从
x
i
+
1
x_i+1
xi+1到
x
j
−
1
x_j-1
xj−1之间的矩形个数,那么满足上面条件的矩形的个数实际上就等于高度为
1
,
2
,
.
.
.
h
t
−
max
{
h
i
,
h
j
}
1,2,...h_t-\max\{h_i, h_j\}
1,2,...ht−max{hi,hj},宽为
x
j
−
x
i
−
1
x_j-x_i-1
xj−xi−1的矩形里的矩形个数(原因是,确定了这样的矩形之后,一路把下面的
1
1
1全含进去,就得到了满足上面条件的矩形),而这样的矩形个数是
(
h
t
−
max
{
h
i
,
h
j
}
)
∗
(
x
j
−
x
i
−
1
)
∗
(
x
j
−
x
i
)
/
2
(h_t-\max\{h_i, h_j\})*(x_j-x_i-1)*(x_j-x_i)/2
(ht−max{hi,hj})∗(xj−xi−1)∗(xj−xi)/2这个公式的由来是这样的,首先,底边宽度是
w
=
x
j
−
x
i
−
1
w=x_j-x_i-1
w=xj−xi−1,底边肯定要选连续的数,那么选法就是
w
+
(
w
2
)
=
w
(
w
+
1
)
/
2
w+{w\choose2}=w(w+1)/2
w+(2w)=w(w+1)/2,其次上沿边的选取方案有
h
t
−
max
{
h
i
,
h
j
}
h_t-\max\{h_i, h_j\}
ht−max{hi,hj}种,所以一共就是
(
h
t
−
max
{
h
i
,
h
j
}
)
∗
(
x
j
−
x
i
−
1
)
∗
(
x
j
−
x
i
)
/
2
(h_t-\max\{h_i, h_j\})*(x_j-x_i-1)*(x_j-x_i)/2
(ht−max{hi,hj})∗(xj−xi−1)∗(xj−xi)/2这么多个。如果
x
i
x_i
xi和
x
j
x_j
xj不存在,那么就想象一下下标为
−
1
-1
−1和
l
h
l_h
lh的地方有一个高度为
0
0
0的柱子即可。搞定一维的情况之后,考虑二维的情况。首先,我们对每一行计算以该行为底的柱状图每个柱子的高度,然后再按照上面的方式计算以该行为底的矩形个数,最后将所有行的数目累加起来即可。设
f
[
i
]
[
j
]
f[i][j]
f[i][j]为以
A
[
i
]
[
j
]
A[i][j]
A[i][j]为底的
1
1
1柱子的高度,则
f
[
i
]
[
j
]
=
{
0
,
A
[
i
]
[
j
]
=
0
1
+
f
[
i
−
1
]
[
j
]
,
A
[
i
]
[
j
]
=
1
f[i][j]=\begin{cases} 0,A[i][j]=0\\ 1+f[i-1][j], A[i][j]=1 \end{cases}
f[i][j]={0,A[i][j]=01+f[i−1][j],A[i][j]=1如此就能得出每行为底的柱状图柱子高度了。代码如下:
import java.util.ArrayDeque;
import java.util.Deque;
public class Solution {
public int numSubmat(int[][] mat) {
int m = mat.length, n = mat[0].length;
// 为了节省空间,直接在mat上面操作计算f数组
for (int i = 1; i < m; i++) {
for (int j = 0; j < n; j++) {
if (mat[i][j] == 1) {
mat[i][j] += mat[i - 1][j];
}
}
}
int res = 0;
// 枚举行编号,计算以mat[i]为底的矩形数目,累加到res上去
for (int i = 0; i < m; i++) {
int count = calculate(mat[i]);
res += count;
}
return res;
}
private int calculate(int[] h) {
int res = 0;
Deque<Integer> stack = new ArrayDeque<>();
// 加入下标-1,想象-1的地方有一个高度为0的柱子
stack.push(-1);
for (int i = 0; i < h.length; i++) {
// 如果发现了更矮或相等的高度,则违反了单调上升性质,要进行出栈并执行计算
while (stack.peek() != -1 && h[stack.peek()] >= h[i]) {
int top = stack.pop();
// 求一下top位置比两边高出来的高度
int height = 0;
if (stack.peek() != -1) {
height = h[top] - Math.max(h[i], h[stack.peek()]);
} else {
height = h[top] - h[i];
}
// 求一下宽度
int width = i - stack.peek() - 1;
// 累加矩形数量
res += height * (width + 1) * width / 2;
}
stack.push(i);
}
// 如果栈不空,则说明栈里的柱子右边已经没有比其更矮或者一样的高度了,出栈并执行计算
while (stack.peek() != -1) {
int top = stack.pop();
int height = 0;
if (stack.peek() != -1) {
height = h[top] - h[stack.peek()];
} else {
height = h[top];
}
// 此时计算宽度,要想象h.length的地方有一个高度为0的柱子,然后计算
int width = h.length - stack.peek() - 1;
// 累加矩形数量
res += height * (width + 1) * width / 2;
}
return res;
}
}
时间复杂度 O ( m n ) O(mn) O(mn),空间 O ( n ) O(n) O(n)。
法4:单调栈。还是先递推一下 f [ i ] [ j ] f[i][j] f[i][j],使得 f [ i ] [ j ] f[i][j] f[i][j]是从 ( i , j ) (i,j) (i,j)向上最多有多少个连续的 1 1 1。接着枚举以 A [ i ] [ j ] A[i][j] A[i][j]为右下角的矩形有多少个。我们枚举矩形的底边长度。设 h [ j ] = f [ i ] [ j ] h[j]=f[i][j] h[j]=f[i][j],长度为 1 1 1的时候是 h [ j ] h[j] h[j]个,那么从 h [ j ] h[j] h[j]向左走,走到第一个小于 h [ j ] h[j] h[j]的位置 h [ l ] h[l] h[l]之前,底边从 1 , 2 , . . . 1,2,... 1,2,...开始增长,而最高高度一直都是 h [ j ] h[j] h[j],从而 1 1 1矩阵的数量是 h [ j ] h[j] h[j]乘以下标增长的范围;当走到第一个小于 h [ j ] h[j] h[j]的位置 h [ l ] h[l] h[l]之后,之后统计的 1 1 1矩阵高度不会超过 h [ l ] h[l] h[l],并且个数事实上和以 A [ i ] [ l ] A[i][l] A[i][l]为右下角的 1 1 1矩阵个数是一样多的。所以我们可以用单调上升栈来做,在push的时候找左边第一个小于当前数的位置。代码如下:
import java.util.Deque;
import java.util.LinkedList;
public class Solution {
public int numSubmat(int[][] mat) {
for (int i = 1; i < mat.length; i++) {
for (int j = 0; j < mat[0].length; j++) {
if (mat[i][j] == 1) {
mat[i][j] += mat[i - 1][j];
}
}
}
int res = 0;
for (int[] row : mat) {
res += compute(row);
}
return res;
}
private int compute(int[] h) {
int res = 0;
Deque<int[]> stk = new LinkedList<>();
for (int i = 0; i < h.length; i++) {
int s = 0;
// 找到左边第一个小于h[i]的位置
while (!stk.isEmpty() && h[stk.peek()[0]] >= h[i]) {
stk.pop();
}
// 如果栈不空,则矩阵分为最高高度是h[i]的部分,和最高高度是h[l]的部分;
// 否则就只有最高高度是h[i]的部分
if (!stk.isEmpty()) {
s += (i - stk.peek()[0]) * h[i];
s += stk.peek()[1];
} else {
s += (i + 1) * h[i];
}
stk.push(new int[]{i, s});
res += s;
}
return res;
}
}
时间复杂度 O ( m n ) O(mn) O(mn),空间 O ( n ) O(n) O(n)。