LeetCode 刷题日记(12.14) (差分和前缀和的超详细介绍以及代码实现, 彻底搞懂)

题目 用邮票贴满网格图

难度 :困难

题目大意:给一个矩阵由0 1 组成的 grid0表示该位置被占据,给定邮票的高度h和宽度w

要求 :

  • 覆盖所有格子。
  • 不覆盖任何被占据的格子
  • 我们可以放入任意数目的邮票
  • 邮票可以相互有重叠部分
  • 邮票不允许旋转
  • 邮票必须完全在矩阵内

问是否能把整个没有被占据的区域占满, 如果可以就返回true否则false

注意 : 整个矩阵长度和宽度的乘积不超过10^5

思路

  • 首先因为可以放任意数目的邮票,所以我们如果找到一个地方可以放邮票,那么我们可以直接放
  • 如何判断一个区域是不是由被覆盖的点呢? 二维前缀和(后续介绍), 我们只需要对grid做一遍前缀和, 知道两个边界坐标可以判断是否含有被占据点
  • 思考怎么快速来张贴一张海报呢,这张海报来表示整个区域被覆盖了, 暴力的话开一个st数组, 用两个for循环,将这片区域每个点都标记成1, 但是这样的时间复杂度很高,不建议使用, 这里就要使用一个类似这种思路的常见方法 二维差分(后续介绍)
  • 张贴完所有的海报后,只要判断整个的数组中是不是还存在没有张贴的位置,如果还有就返回false, 反之就是true

前缀和介绍

一维前缀和

  • 一维前缀和的定义 : s[k] = a[1] + a[2] + a[3] + ... + a[k - 1] + a[k] (下标从1开始,方便后续处理), 根据定义我们可以写出s[k - 1] = a[1] + a[2] + a[3] + ... + a[k - 1] , 那么观察可知一个重要的性质s[k] = s[k - 1] + a[k](下标从1开始就是方便这里的k - 1, 这样数组就不会越界了)

  • 重要性质s[r] - s[l - 1]就表示lr这一段区间的和,这样我们只需要知道一段区间的左端点和右端点,我们就可以在 O ( 1 ) O(1) O(1)时间求一段区间的和, 证明也很简单,直接带入定义即可 在这里插入图片描述

  • 我们可以用s来表示grid的前缀和,那么很容易得到下面的s数组, 方法如下:
    for (int i = 1; i <= n; i ++) s[i] = s[i - 1] + grid[i];

二维前缀和

  • 二维前缀和的定义 : s[a][b] = ∑ ( s [ i < = a ] [ j < = b ] ) \sum(s[i <= a][j <= b]) (s[i<=a][j<=b])简单来说就是坐标(a, b)左上角所有元素的和为了处理方便,下标依旧从(1, 1)开始
    直观描述
  • 那么思考, 我们怎么类似一维前缀和来递推求前缀和呢 ?
    我们可以充分利用前缀和的定义和特点,可以得到这个性质s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + grid[i][j],通过这个公式,我们就可以遍历求出整个前缀和数组, 直观解释如下图
    直观解释
  • 对比一维前缀和数组,给出lr可以在 O ( 1 ) O(1) O(1)时间求出一段区间的和,那给定两个坐标(x1, y1), (x2, y2)二维怎么在 O ( 1 ) O(1) O(1)求一个区域的和呢, 依旧是根据定义, 直接给出式子s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1] 注意 (x2 >= x1, y2 >= y1) , 依旧给出直观解释, 如下图:
    直观解释

通过这几个方法就可以在 O ( 1 ) O(1) O(1)时间下求出任意区域的和

前缀和总结

一维前缀和

  • 递推前缀和数组方法 s[k] = s[k - 1] + a[k]
  • 求区间和的方法s[r] - s[l - 1]

二维前缀和

  • 递推前缀和数组方法 s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + grid[i][j]
  • 求区域和的方法 s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1]

差分介绍

  • 一维差分的定义 : f[i] = a[i] - a[i - 1], f数组就是a的差分数组
  • 主要应用 :我们可以用这个来在 O ( 1 ) O(1) O(1)时间给一段区间加上k, 这个要利用差分和前缀和的联系,差分数组的前缀和就是原数组

简单证明: 根据定义 f[i] = a[i] - a[i - 1], 写出前一项差分数组 f[i - 1] = a[i - 1] - a[i - 2], 那么可以知道f[i] + f[i - 1] = a[i] - a[i - 2], 继续递推下去可以得到 a[i] = ∑ 1 i ( f [ k ] ) \sum_1^{i}(f[k]) 1i(f[k])我们下标依旧是从1开始, a[0] = 0, 这不就是前缀和吗,这样就证明好了

  • 那我们怎么实现一段区间上整体加上k呢?
    我们想到差分数组的前缀和是原数组,我们可以在差分数组上操作,然后我们求一遍前缀和就可以得到之后处理过的数组了。
  • 具体怎么操作呢?
    我们利用前缀和的特点:前面一个点变动后面所有的点都会发生改变(可以自己在纸上模拟一下,这里不再展示),依靠这个特点,假设区间的左端点是l, 右端点是r, 则我们可以在l这个点加上一个k,求一遍前缀和的话,那么后面所有的点都会加上k, 并不是我们要的结果,这个时候思考怎么让右端点r之后的点全部不变呢? 我们可以在r + 1的位置加上一个-k,之后求前缀和的话,在r之后的所有点都会+k + (-k)就相当于没变了,这样就完成了只在l ~ r区间上的整体加k操作

简单代码示例

	int a[6] = {0, 1, 1, 1, 1, 1}; // a[0]默认是0
    int f[10]; // 差分数组
    memset(f, 0, sizeof f);
    //在一段区间内加上k
    function<void(int, int, int)> change = [&](int a, int b, int k) {
        f[a] += k, f[b + 1] -= k;
    };
    // 首先原地修改(相当于在一个点加上一个数)
    for (int i = 1; i <= 5; i ++) change(i, i, a[i]);
    int l = 2, r = 4;
    change(l, r, 1);
    for (int i = 1; i <= 5; i ++) f[i] += f[i - 1]; // 前缀和
    for (int i = 1; i <= 5; i ++) cout << f[i] << ' ';
输出结果 : 1 2 2 2 1

要注意一点就是首先要先初始化差分数组,也就是上面代码的原地修改


二维差分数组

  • 我们只需要类比一维差分,然后根据二维前缀和的思想来实现一个区域的整体加上一个数字k, 思路和一维差分基本上类似, 给出公式: 如果要在(x, y)(a, b)加上一个数k,那么可以有这个公式: s[x][y] += 1, s[x][b + 1] -= 1, s[a + 1][y] -= 1, s[a + 1][b + 1] += 1下面给出图解:
    图解
    简单解释一下: 如果在(a, b)上加上一个k那么做一遍前缀和之后第一张图的红色区域全部加上一个k, 第二、三个图类似,在对应的黄色区域会减去一个k, 此时在(a + 1, b + 1)也就是绿色的位置 这个位置之后的所有位置会被减去两次, 所以我们要在那个位置加上一个k,那么后面的位置就会保持不变了

回归正题

这个题目算法实现步骤:

  • 首先先对grid数组来一遍二维前缀和
  • 遍历整个数组,首先判断枚举的这个位置是不是可以张贴邮票, 如果可以就使用二维差分将这个区域标记一下
  • 对差分数组做一遍前缀和,得到张贴了邮票的数组,如果有邮票,那么这个点就是1,否则就是0
  • 最后判断是否存在既没有被占据又没有张贴邮票的点

具体实现代码如下:

class Solution {
public:
    using VVI = vector<vector<int>>;

    bool possibleToStamp(vector<vector<int>>& grid, int stampHeight, int stampWidth) {
        int n = grid.size(), m = grid[0].size();
        VVI s(n + 2, vector<int>(m + 2));
        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] + grid[i - 1][j - 1];
        
        VVI st(n + 2, vector<int>(m + 2));

        function<void(int, int, int, int)> insert = [&](int a, int b, int x, int y) -> void {
            st[a][b] += 1;
            st[a][y + 1] -= 1;
            st[x + 1][b] -= 1;
            st[x + 1][y + 1] += 1;
        };

        function<int(int, int, int, int, VVI&)> get = [&](int a, int b, int x, int y, VVI& g) {
            return g[x][y] - g[a - 1][y] - g[x][b - 1] + g[a - 1][b - 1];
        };

        for (int i = stampHeight; i <= n; i ++)
            for (int j = stampWidth; j <= m; j ++) {
                int x = i - stampHeight + 1, y = j - stampWidth + 1;
                int t = get(x, y, i, j, s);
                if (!t) insert(x, y, i, j);
            }
        
        for (int i = 1; i <= n; i ++)
            for (int j = 1; j <= m; j ++) {
                st[i][j] += st[i - 1][j] + st[i][j - 1] - st[i - 1][j - 1];
                if (!st[i][j] && !grid[i - 1][j - 1])
                    return false;
            }
        return true;
    }
};

时间复杂度: O ( n m ) O(nm) O(nm)

总结:

  • 差分和前缀和的思想应用很广也很灵活,多了解
  • 深入理解思想,不能死记硬背


    结束了!
    手也冻僵了
  • 27
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值