前文我们写过前缀的技巧是非常常用的算法技巧,前缀和主要适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和。
而本文讲一个和前缀和思想非常类似的算法技巧,没错就是差分数组,差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减。
类似前缀和技巧构造的preNum数组,我们先对nums数组构造一个diff差分数组,diff[i]就是nums[i]和nums[i-1]之差。
核心代码如下
```Java
int[] diff = new int[nums.length];
// 构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
diff[i] = nums[i] - nums[i - 1];
}
```
同样,我们也可以通过差分数组反推原始数组,代码逻辑如下
```Java
int[] res = new int[diff.length];
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
res[i] = res[i - 1] + diff[i];
}
```
构造了差分数组,就可以快速进行区间增减的操作,如果你想对区间nus[i...j]的元素全部加3,那么只需要让diff[i]+=3,然后再让diff[j+1]-=3即可。
原理非常简单,可以很直观的从反推原始数组的逻辑中看出,原始数组的值是等与前一个下标的值加上当前索引对应差分数组的值,当差分数组加3,就代表原始数组当前索引之后的所有元素都加3,为了保证是某个区间上加3,后面当然要减3。
理解了差分数组的原理,那就让我们一起通过刷题彻底手撕差分数组相关的题。
1.区间加法
leetcode370题
要求:假设你有一个长度为n的数组,初始情况下所有的数字均为0,你将会被给出k个更新的操作,在某个区间加上一个数。返回k次操作后的数组。
解题思路:很明显这道题是多次进行区间加法,我们也可以通过for循环来做,这样时间复杂度O(N),而且修改频繁,效率会很低。而使用差分数组来解时间复杂度O(1)。
我们把差分数组抽象成一个类,包含区间加法方increment法和result方法。
```Java
class Difference {
// 差分数组
private int[] diff;
/* 输⼊⼀个初始数组,区间操作将在这个数组上进⾏ */
public Difference(int[] nums) {
assert nums.length > 0;
diff = new int[nums.length];
// 根据初始数组构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
diff[i] = nums[i] - nums[i - 1];
}
}
/* 给闭区间 [i,j] 增加 val(可以是负数)*/
public void increment(int i, int j, int val) {
diff[i] += val;
if (j + 1 < diff.length) {
diff[j + 1] -= val;
}
}
/* 返回结果数组 */
public int[] result() {
int[] res = new int[diff.length];
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
res[i] = res[i - 1] + diff[i];
}
return res;
}
}
```
那么我们直接复⽤刚才实现的 Difference 类就能把这道题解决掉
```Java
int[] getModifiedArray(int length, int[][] updates) {
// nums 初始化为全 0
int[] nums = new int[length];
// 构造差分解法
Difference df = new Difference(nums);
for (int[] update : updates) {
int i = update[0];
int j = update[1];
int val = update[2];
df.increment(i, j, val);
}
```
但实际的问题不会很容易让你看出来是使用差分数组技巧。
2.航班预定统计
leetcode1109题
要求:这里有 n 个航班,它们分别从 1 到 n 进行编号。有一份航班预订表 bookings ,表中第 i 条预订记录 bookings[i] = [firsti, lasti, seatsi] 意味着在从 firsti 到 lasti (包含 firsti 和 lasti )的 每个航班 上预订了 seatsi 个座位。
请你返回一个长度为 n 的数组 answer,里面的元素是每个航班预定的座位总数。
解题思路:这题目弯弯绕绕,其实就是个差分数组问题。
```Java
public int[] corpFlightBookings(int[][] bookings, int n) {
int[] nums=new int[n];
Difference df=new Difference(nums);
for(int[] booking:bookings){
int i=booking[0]-1;
int j=booking[1]-1;
int val=booking[2];
df.increment(i,j,val);
}
return df.result();
}
```
3.拼车
leetcode1094题
要求:一个公交车司机,公交车的最大载客量为capacity,沿途要经过若干车站,给你一份乘客行程表int[][] trips,其中trips=[nums,start,end]代表着有num个旅客要从站点start上车,到站点end下车,请你计算是否能够一次把所有旅客运送完毕。
解题思路:start和end代表一组区间,上车和下车是一次区间操作,只要每一站公交车的载客量都小于最大载客量,就说明可以一次把旅客运送完毕。
```Java
class Solution {
public boolean carPooling(int[][] trips, int capacity) {
int [] nums=new int[1001];
Difference df=new Difference(nums);
for(int[] trip:trips){
int val=trip[0];
int i=trip[1];
int j=trip[2]-1;//j站乘客已经下车
df.increment(i,j,val);
}
int[] res=df.result();
for(int i=0;i<res.length;i++){
if(res[i]>capacity){
return false;
}
}
return true;
}
}
```