1. 题目来源
同类题:[H前缀和] lc363. 矩形区域不超过 K 的最大数值和(二维前缀和+lower_bound()+好题)
相关题目:[线性dp] 最强对手矩阵(牛客+线性dp+二维前缀和+代码优化+思维+好题)
2. 题目解析
是一个经典题目了,二维前缀和枚举子矩阵左上方、右下方的点,时间复杂度 O ( n 2 m 2 ) O(n^2m^2) O(n2m2)。
class Solution {
public:
int numSubmatrixSumTarget(vector<vector<int>>& matrix, int target) {
int n = matrix.size(), m = matrix[0].size();
vector<vector<int>> s(n + 1, vector<int>(m + 1));
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + matrix[i - 1][j - 1];
int res = 0;
for (int x1 = 1; x1 <= n; x1 ++ )
for (int y1 = 1; y1 <= m; y1 ++ )
for (int x2 = x1; x2 <= n; x2 ++ )
for (int y2 = y1; y2 <= m; y2 ++ ) {
int cnt = s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1];
if (cnt == target) res ++ ;
}
return res;
}
};
在 lc
上超时,只能枚举三重循环,那么就枚举上边界,下边界,再枚举右边界,同时利用哈希表记录右边界左边的子矩阵值,相当于针对每一个右边界都记录了其左边的子矩阵大小。当存在 cur-target
时,即找到一个 左边界,则答案累加上出现的次数即可。
这样就将确定左边界的时间复杂度降到了 O ( 1 ) O(1) O(1)。
这种方法就是枚举边,是朴素二维前缀和枚举点的进阶版本。
注意:
-
我们每次是最后插入当前枚举的右边界子矩阵大小,为什么哈希表不能直接先插入当前子矩阵大小呢?
-
这就是一个经典误区了。
-
最后插入的话,右边界无法直接作为自己的左边界。
-
如果先插入,那么右边界可以作为自己的左边界。 当
target!=0
时没有影响。但当target==0
时,cur-target=cur-0=cur
必然存在,但 右边界此时可以作为自己的左边界了,故这个左边界必然存在,就是右边界它自己。不论枚举的右边界是多少,子矩阵是值到底是多少,答案一定会++
,相当于每次cur
进行了自我更新一次,导致了错误。故每次都需要将自我更新的一次答案减去。 -
当
target != 0
时,不会出现这个情况,因为不会让cur
进行自我更新,即右边界还是得正确的查找cur-target
的值,它一定是前面先出现过的,再进入哈希表的,而不是由当前cur
更新作为左边界的。 -
前缀和计算
(x1, y1)
、(x2, y2)
构成矩阵大小的时候,右边界枚举是从1~n
的,而左边界枚举的是从0~n-1
的,这样才能正确算出前缀和。也就是常见的一维前缀和中s[i]-s[j-1]
才是区间[j,i]
的区间和。在二维前缀和中同样如此,需要提前将下标 0 这个插入进哈希表中。它所代表的面积是 0,方案数是 1。
当 n>m
时,可以枚举列,再枚举行,转化为
O
(
n
m
2
)
O(nm^2)
O(nm2),这也是常见的转化方法。
也可以这样理解吧,关于前缀和数组是需要 s[0]
的,在此 s[0]
大小为 0,但也是一种方案,方案为 1。
时间复杂度: O ( n 2 m ) O(n^2m) O(n2m)
空间复杂度: O ( n 2 ) O(n^2) O(n2)
代码:
class Solution {
public:
int numSubmatrixSumTarget(vector<vector<int>>& matrix, int target) {
int n = matrix.size(), m = matrix[0].size();
vector<vector<int>> s(n + 1, vector<int>(m + 1));
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + matrix[i - 1][j - 1];
int res = 0;
for (int x1 = 1; x1 <= n; x1 ++ )
for (int x2 = x1; x2 <= n; x2 ++ ) {
unordered_map<int, int> h;
h[0] = 1; // 注意处理 cur=target 这个情况,因为 cur 是在最后才累加的
for (int y = 1; y <= m; y ++ ) {
int cur = s[x2][y] - s[x1 - 1][y];
if (h.count(cur - target)) res += h[cur - target];
h[cur] ++ ; // 累加需要放到后面
}
}
return res;
}
};
// 更为直观的写法
class Solution {
public:
int numSubmatrixSumTarget(vector<vector<int>>& matrix, int target) {
int n = matrix.size(), m = matrix[0].size();
vector<vector<int>> s(n + 1, vector<int>(m + 1));
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + matrix[i - 1][j - 1];
int res = 0;
for (int x1 = 1; x1 <= n; x1 ++ )
for (int x2 = x1; x2 <= n; x2 ++ ) {
unordered_map<int, int> h;
for (int y = 1; y <= m; y ++ ) {
int cur = s[x2][y] - s[x1 - 1][y];
if (cur == target) res ++ ; // 这个写法更加直观,右边界刚好即为 target,答案 +1
// 再找右边界之前的满足的左边界
if (h.count(cur - target)) res += h[cur - target];
h[cur] ++ ; // 累加需要放到后面
}
}
return res;
}
};