[H前缀和] lc1074. 元素和为目标值的子矩阵数量(前缀和+哈希优化+知识理解)

1. 题目来源

链接:1074. 元素和为目标值的子矩阵数量

同类题:[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;
    }
};
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值