一维差分和二维差分
差分都是结合前缀和使用的,应用于区间修改,且只最后查询一次的情形。
一维差分
对于\([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;
}
};
参考链接: