zoukankan      html  css  js  c++  java
  • 一维差分和二维差分

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

    一维差分

    对于\([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. 用邮票贴满网格图题解-枚举+二维前缀和+二维差分
    个性签名:时间会解决一切
  • 相关阅读:
    tcp粘包解决
    socket网络编程
    logging模块
    异常处理
    hashlib configparser模块
    列表推导式和生成器表达式和内置函数
    迭代器与生成器
    装饰器
    函数
    文件操作
  • 原文地址:https://www.cnblogs.com/lfri/p/15783592.html
Copyright © 2011-2022 走看看