一维差分和二维差分

差分都是结合前缀和使用的,应用于区间修改,且只最后查询一次的情形。

一维差分

对于\([a_1, a_2, a_3, ..., a_n]\),前缀和\(S_i = a_1 + a_2 + , ..., a_i\),差分\(diff_0 = a_0-0, \ diff_i = a_i - a_{i-1}\)
因此\(a_i = 0 + diff_0 + diff_1 + ... + diff_i\)
\(a[i...j]\)\(val\),可以直接转化为将 \(diff[i] += val, \ diff[j+1] -= val\),从前往后累加即可得到修改后的\(a_i\)

LC 1094. 拼车

题意:trips[i]个客人从trip[1]站上,trip[2]站下,判断车上是否会超过capacity
方法:相当于区间修改,将[trip[1], trip[2]-1] += val

class Solution {
public:
    void update(vector<int>& diff, int l, int r, int val) {
        diff[l] += val;
        diff[r+1] -= val;
    }
    bool carPooling(vector<vector<int>>& trips, int capacity) {
        const int maxn = 1000+5;
        vector<int>diff(maxn, 0);
        for(auto& trip : trips) {
            update(diff, trip[1], trip[2]-1, trip[0]);
        }
        int cur = 0;
        for(int i = 0;i < maxn;i++) {
            cur += diff[i];
            if(cur > capacity)  return false;
        }
        return true;
    }
};

LC1109. 航班预订统计

方法:差分模板题了,注意空间要开\(n+2\).

class Solution {
public:
    vector<int> corpFlightBookings(vector<vector<int>>& bookings, int n) {
        vector<int>diff(n+2, 0);  // 用[1...n],差分还需要多一位
        for(auto& book : bookings) {
            diff[book[0]] += book[2];
            diff[book[1]+1] -= book[2];
        }
        int cur = 0;
        vector<int>ans;
        for(int i = 1;i <= n;i++) {
            cur += diff[i];
            ans.push_back(cur);
        }
        return ans;
    }
};

LC 1893. 检查是否区域内所有整数都被覆盖

方法:模板题

class Solution {
public:
    bool isCovered(vector<vector<int>>& ranges, int left, int right) {
        vector<int>diff(55, 0);
        for(auto& ran : ranges) {
            diff[ran[0]] += 1;
            diff[ran[1]+1] -= 1;
        }
        int cur = 0;
        for(int i = 1;i <= right;i++) {
            cur += diff[i];
            if(i >= left && cur == 0)  return false;
        }
        return true;
    }
};

LC1854. 人口最多的年份

题意:和公交车那题类似,求人数最多的年份
方法:相当于每个人都有作用区间,转化为区间修改(记住这种思想)

class Solution {
public:
    int maximumPopulation(vector<vector<int>>& logs) {
        vector<int>diff(2050+2, 0);
        for(auto& log : logs) {
            diff[log[0]] += 1;
            diff[log[1]] -= 1;
        }
        int cur = 0, mymax = 0, year;
        for(int i = 1950;i <= 2050;i++) {
            cur += diff[i];
            if(cur > mymax) {
                mymax = cur;
                year = i;
            }
        }
        return year;
    }
};

LC732. 我的日程安排表 III

题意:还是和公交车类似,求某个时刻最大的重叠次数
方法:还是差分,但是时间的跨度很大,需要用map存储离散点,累加的时候还是要按顺序累计。

class MyCalendarThree {
public:
    map<int, int>mp;  // 不能用unordered_map
    MyCalendarThree() {

    }
    
    int book(int start, int end) {
        mp[start] += 1;
        mp[end] -= 1;
        int cur = 0, mymax = 0;
        for(auto it = mp.begin();it != mp.end();it++) {
            cur += it->second;
            if(cur > mymax)  mymax = cur;
        }
        return mymax;
    }
};

最后介绍一道隐蔽一点的,hard

LC 1674. 使数组互补的最少操作次数

题意:给定一个长度为偶数的数组,求使得 nums[i] + nums[n - 1 - i] 都等于同一个数 的最少操作次数,且要求修改后每个元素在1~limit之间。
方法:
方法一:暴力,枚举和,易知介于1~2*limit。对于指定的和,可能修改0次,1次,2次。

class Solution {
public:
    int myMinMoves(vector<int>& nums, int k, int limit) {
        int n = nums.size();
        int res = 0;
        for(int i = 0;i < n/2;i++) {
            int a = nums[i], b = nums[n-1-i];
            if(a+b == k)  continue;
            if(a >= k && b >= k)  res+=2;  // 两大
            else if(a >= k && b < k)  res+=1;   // 一大一小
            else if(b >=k && a < k)  res+=1;
            else if(max(a,b)+limit >= k) res +=1;  // 两小
            else res += 2;
        }
        return res;
    }

    int minMoves(vector<int>& nums, int limit) {
        int ans = 2*nums.size();
        for(int i = 1;i <= 2*limit;i++) {
            ans = min(ans, myMinMoves(nums, i, limit));
        }
        return ans;
    }
};

将判断条件简化一下,可以写成如下形式:

    int myMinMoves(vector<int>& nums, int k, int limit) {
        int n = nums.size();
        int res = 0;
        for(int i = 0;i < n/2;i++) {
            int a = min(nums[i], nums[n-1-i]);
            int b = max(nums[i], nums[n-1-i]);
            if(a+b == k)  continue;
            else if(a < k && k <= b+limit)  res += 1;
            else res += 2;
        }
        return res;
    }

方法二:利用区间修改的思想,我们可以在上面简化版的基础上优化,对于给定的[a, b],其贡献包括将区间[a+1, b+limit]加1,将[a+b, a+b]不变,其他加2。
用差分做的话,应该等同于将[2, 2*limit] += 2, [a+1, b+limit] -= 1, [a+b, a+b] -=1.

class Solution {
public:
    // https://leetcode-cn.com/problems/minimum-moves-to-make-array-complementary/solution/jie-zhe-ge-wen-ti-xue-xi-yi-xia-chai-fen-shu-zu-on/
    // 只要改端点,因为中间的差还是0,不变
    // 区间修改,懒得写线段树(只有区间修改,单点查询)
    // dp[i] 表示和为1时的最小操作次数
    // dp[i] = diff[0]+diff[1]+diff[2]...+diff[i]
    void update(vector<int>& diff, int l, int r, int val) {
        diff[l] += val;
        diff[r+1] -= val;
    }
    int minMoves(vector<int>& nums, int limit) {
        int n = nums.size();
        vector<int>diff(2*limit+2, 0);
        for(int i=0, j=n-1; i<j; i++,j--) {
            int a = min(nums[i], nums[j]);
            int b = max(nums[i], nums[j]);

            update(diff, 2, 2*limit, 2);
            update(diff, a+1, b+limit, -1);
            update(diff, a+b, a+b, -1);
        }
        int ans = n, sum=0;
        for(int i = 2;i <= 2*limit;i++) {
            sum += diff[i];
            if(sum < ans)  ans = sum;
        }
        return ans;
    }
};

注意:上面的这些例子,差分数组diff的初始值都为0,如果题目需要考虑原数组,累加的时候将原始值加上或修改diff的初始化就行。例如下面这题。

LC 995. K 连续位的最小翻转次数

方法:01翻转相当于加1再判断奇偶,遇到偶数就翻转连续K位。

class Solution {
public:
    // 从前往后,遇到偶数都要变
    int minKBitFlips(vector<int>& nums, int k) {
        int n = nums.size();
        vector<int>diff(n+2, 0);
        int cur = 0, ans = 0;
        for(int i = 0;i < n;i++) {
            // cur += diff[i];  // 这时候不能加,因为diff[i]可能要修改
            // cout << cur << endl;   
            if((cur+diff[i]+nums[i]) % 2 == 0) {
                ans++;
                diff[i] += 1;
                // diff[i+k]
                if(i+k > n)  return -1;  // 如果要翻转的i>n-k,说明不可能
                diff[i+k] -= 1;
            }
            cur += diff[i];
        }
        return ans;
    }
};

二维差分

模板:

    voif init(vector<vector<int>>& diff, int m, int n) {
        for(int i = 1;i <= m; i++)//预处理一波 
            for(int j = 1;j <= n; j++)
	        diff[i][j] = map[i][j] + diff[i - 1][j] + diff[i][j - 1] - diff[i - 1][j - 1];
    }

    void update(vector<vector<int>>& diff, int x1, int y1, int x2, int y2, int val) {
        diff[x1][y1] += val;
        diff[x1][y2+1] -= val;
        diff[x2+1][y1] -= val;
        diff[x2+1][y2+1] += val;
    }
    void restore(vector<vector<int>>& diff, int m, int n) {
        for(int i = 1;i <= m;i++) {
            for(int j = 1;j <= n;j++)
                diff[i][j] = diff[i][j] + diff[i-1][j] + diff[i][j-1] - diff[i-1][j-1];
        }
    }

通常原数组是全0,就不需要init步骤了,或者求完改变量,后面restore的时候再加上。
差分的前缀和就是原矩阵每个元素的改变量。

LC 598. 范围求和 II

方法:模板题,区域修改,最后统计一次(这里会超时,正解是贪心,此处只是记录模板)

class Solution {
public:
    void update(vector<vector<int>>& diff, int x1, int y1, int x2, int y2, int val) {
        diff[x1][y1] += val;
        diff[x1][y2+1] -= val;
        diff[x2+1][y1] -= val;
        diff[x2+1][y2+1] += val;
    }
    void restore(vector<vector<int>>& diff, int m, int n) {
        for(int i = 1;i <= m;i++) {
            for(int j = 1;j <= n;j++)
                diff[i][j] = diff[i][j] + diff[i-1][j] + diff[i][j-1] - diff[i-1][j-1];
        }
    }

    int maxCount(int m, int n, vector<vector<int>>& ops) {
        vector<vector<int>>diff(m+2, vector<int>(n+2, 0));
        for(auto& op : ops) {
            update(diff, 1, 1, op[0], op[1], 1);
        }
        restore(diff, m, n);
        int ans = 0;
        for(int i = 1;i <= m;i++) {
            for(int j = 1;j <= n;j++) {
                // cout << diff[i][j] << " ";
                if(diff[i][j] == diff[1][1]) {
                    ans++;
                } else {
                    break;
                }
            }
            // cout << endl;
        }
        return ans;
    }
};

LC 5931. 用邮票贴满网格图

题意:给定一个01矩阵,再给定h*w的邮票,不能旋转,可重叠,问是否能加所有0的位置铺满。
方法:枚举左上角,如果能贴就贴(前缀和为0来判断),贴的话就相当于将h*w区域加1.

class Solution {
public:
    int area_sum(vector<vector<int>>& sum, int x1, int y1, int x2, int y2) {
        return sum[x2][y2] - sum[x2][y1-1] - sum[x1-1][y2] + sum[x1-1][y1-1];
    }
    void update(vector<vector<int>>& diff, int x1, int y1, int x2, int y2, int val) {
        diff[x1][y1] += val;
        diff[x2+1][y1] -= val;
        diff[x1][y2+1] -= val;
        diff[x2+1][y2+1] += val;
    }
    void restore(vector<vector<int>>& diff, int m, int n) {
        for(int i = 1;i <= m;i++) {
            for(int j = 1;j <= n;j++) {
                diff[i][j] += diff[i-1][j] + diff[i][j-1] - diff[i-1][j-1];
            }
        }
    }
    bool possibleToStamp(vector<vector<int>>& grid, int stampHeight, int stampWidth) {
        int m = grid.size(), n = grid[0].size();
        vector<vector<int>>sum(m+1, vector<int>(n+1, 0));
        vector<vector<int>>diff(m+2, vector<int>(n+2, 0));
        for(int i = 1;i <= m;i++) {
            for(int j = 1;j <= n;j++) {
                sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + grid[i-1][j-1];
            }
        }
        for(int i = 1;i <= m;i++) {
            for(int j = 1;j <= n;j++) {
                if(grid[i-1][j-1] == 0) {
                    int x = i+stampHeight-1, y = j+stampWidth-1;
                    if(x > m || y > n)  continue;
                    int sub = area_sum(sum, i, j, x, y); // 前缀和只是用来判断区域是否全0
                    // cout << i << " " << j << " " << sub << endl;
                    if(!sub)  update(diff, i, j, x, y, 1);
                }
            }
        }
        restore(diff, m, n);
        for(int i = 1;i <= m;i++) {
            for(int j = 1;j <= n;j++) {
                if((!grid[i-1][j-1]) && (!diff[i][j]))  return false;
            }
        }
        return true;
    }
};

参考链接:

  1. LeetCode-【差分解决区间问题】解题技巧
  2. 【总结】前缀和与差分(一维差分、二维差分、树上差分(待学!))
  3. LC 5931. 用邮票贴满网格图题解-枚举+二维前缀和+二维差分
posted @ 2022-01-10 10:59  Rogn  阅读(114)  评论(0编辑  收藏  举报