上一篇我从多处总结出了前缀和技巧和使用场景,这一章又有另外一个场景就是对某段区间进行操作(加/减),对于这样的场景我们又应该怎么实现呢,还是使用原来的前缀和?重新回顾一下前缀和的使用场景:前缀和主要适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和。显然前缀和的优势很难在这里体现出来。这样我们就推出了差分算法技巧。
场景
我们在日常刷算法的时候会遇到这么一种类型的题,要你对原数组对其随机区间进行n次操作(区间加/减),最后需要你返回操作后的结果。在生活中其实也有类似的场景,如:①乘坐公交车(交通工具),在i站上x人,在j站下y人,一顿操作下,问车是否超载呀?或者此时车上有多少人呀?
实战题
差分
我们从下面
- 什么是差分算法?
- 什么时候使用差分算法?
前缀和技巧回顾
需要看详细内容看回看上一篇:【前缀和】巧妙利用前缀和解决子数组问题(1)这里只简单回顾一下前缀和。
int n = nums.length;
// 前缀和数组
int[] preSum = new int[n + 1];
preSum[0] = 0;
for (int i = 0; i < n; i++)
preSum[i + 1] = preSum[i] + nums[i];
prefix[i]就代表着nums[0..i-1]所有元素的累加和,如果我们想求区间nums[i..j]的累加和,只要计算prefix[j+1] - prefix[i]即可,而不需要遍历整个区间求和。
差分算法技巧
本文讲的是差分,差分与前缀和都是对数组进行预计算,但是不同点在于,差分适用于会对原数组的区间进行操作修改的
看上面这道题,我们给这个数组一个默认初始值,比如数组
nums = [0,0,0,0,0],然后我们看操作,就是对区间[1,3]进行加2的操作,那么一般思路我们是不是采用for循环,定位到下标1到下标3处进行区间加操作。那如果我们对他进行n次操作,那岂不是每次操作的时候都要对数组进行遍历修改,那这个效率是不是很低。
这时候如果我们引入差分算法的话就不一样了。它也是要构造一个预计算的数组。我们对nums数组构造一个diff差分数组,diff[i]就是nums[i]和nums[i-1]之差:
int[] diff = new int[nums.length];
// 构造差分数组
for (int i = 1;i<nums.length;i++){
diff[i] = nums[i] - nums[i-1]
}
通过这个diff差分数组是可以反推出原始数组nums的,代码逻辑如下:
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];
}
这样构造差分数组diff,就可以快速进行区间增减的操作,如果你想对区间nums[i..j]的元素全部加 3,那么只需要让diff[i] += 3,然后再让diff[j+1] -= 3即可,
原理很简单,回想diff数组反推nums数组的过程,diff[i] += 3意味着给nums[i..]所有的元素都加了 3,然后diff[j+1] -= 3又意味着对于nums[j+1..]所有元素再减 3,那综合起来,是不是就是对nums[i..j]中的所有元素都加 3 了?
只要花费 O(1) 的时间修改diff数组,就相当于给nums的整个区间做了修改。多次修改diff,然后通过diff数组反推,即可得到nums修改后的结果。
根据上面的解释,我们用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];
}
// 对原数组进行区间操作,对`diff`进行更新 ,比如对区间[i,j]加x
diff[i] += x;
if(j < nums.length){
diff[j+1] -= x;
}
/* 返回结果数组 */
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];
}
当j+1 >= diff.length时,说明是对nums[i]及以后的整个数组都进行修改,那么就不需要再给diff数组减val了。
接下来还是刚刚那道题,我们根据上面提供的差分数组的构造一步步的推一下这个数组的形成。
最后我们可以得到这个差分数组是这样的
[-2,2,3,2,-2]。最后我们就可以通过这个差分数组去推出结果数组了。
总结
下面这个差分数组的工具类是- labuladong的小站公众号封装的一个工具类。
// 差分数组工具类
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;
}
}
牛刀小试
题目分析:
- 首先我们确定车上最初有
capacity个空座位,翻译过来就是我们可以创建一个我们最后的结果数组的值是不可以大于capacity。- 题目限制
0 <= fromi < toi <= 1000,翻译过来就是我们可以创建的数组容量最大不超过1000.- 数组
trips,trip[i] = [numPassengersi, fromi, toi]表示第i次旅行有numPassengersi乘客,接他们和放他们的位置分别是fromi和toi。翻译过来就是我们对数组做trips.length次,在区间[from,to]进行操作。
题目拆分:
- 对于这道题,其实我们并不需要知道原数组是怎么样的,因为从一开始我们就知道,车是空车,所以我们直接默认数组为空就可以了,竟然数组为空,那么我们初始化的差分数组就是一个空的,所以我们直接
new 一个空的,长度为capacity的数组就可以了。- 边界限制:对于这道题,和之前的例子有一个区别就是他有一个条件的限制,车位是capacity,那么我们就要保证我们在实现结果数组的时候这个下标对应的数组值
不能大于capacity。
根据上面的分析,我们就可以用代码进行解决了。
class Solution {
public boolean carPooling(int[][] trips, int capacity) {
int[] diff = new int[1001];
// 构造操作差分数组
for(int i = 0;i<trips.length;i++){
int op = trips[i][0];
int from = trips[i][1];
int to = trips[i][2];
diff[from] += op;
if(to < diff.length){
diff[to] -= op;
}
}
// 计算结果数组,判断是否满足条件
int[] res = new int[diff.length];
res[0] = diff[0];
// int res = diff[0];
for(int i = 1;i<diff.length;i++){
res[i] = diff[i] + res[i-1];
if(res[i] > capacity){
return false;
}
}
return true;
}
}
为什么是:
if(to < diff.length){ diff[to] -= op; }这里看一下题目要求
接他们和放他们的位置分别是fromi和to。那么就意味着人数的增加是在[fromi,to-1]区间之间的,到toi的位置的时候就结束了。
对于上面的代码其实是可以进行优化的,我们知道我们只需要去判断结果数组的当前位是否满足条件即可。我们可知结果数组是res[i] = res[i-1] + diff[i]得来的,也就是说我们只需要用一个变量记录一下前一次的结果就可以了。
class Solution {
public boolean carPooling(int[][] trips, int capacity) {
int[] diff = new int[1001];
// 构造操作差分数组
for(int i = 0;i<trips.length;i++){
int op = trips[i][0];
int from = trips[i][1];
int to = trips[i][2];
diff[from] += op;
if(to < diff.length){
diff[to] -= op;
}
}
// 计算结果数组优化成使用变量来记录,判断是否满足条件
int res = diff[0];
for(int i = 1;i<diff.length;i++){
res = diff[i] + res;
if(res > capacity){
return false;
}
}
return true;
}
}
这道题其实与上一道题大同小异,不同点在于它需要我们输出它的结果数组,那这不和我们总结的差分数组一致嘛? 题目分析:
- 预订记录
bookings[i] = [firsti, lasti, seatsi]意味着在从firsti到lasti(包含firsti和lasti)的 每个航班 上预订了seatsi个座位。翻译过来的意思就是对我们的差分数组在区间[firsti, lasti]进行操作seatsi。- 要求返回长度为n的数组,这不就告诉我们原始数组的长度为n,同时我们初始化差分数组的时候也知道长度是多少了。
接下来我们直接写代码:
class Solution {
public int[] corpFlightBookings(int[][] bookings, int n) {
int[] diff = new int[n];
for(int i = 0;i<bookings.length;i++){
int first = bookings[i][0]-1;
int last = bookings[i][1]-1;
int seat = bookings[i][2];
// 操作差分数组
diff[first] += seat;
if(last < n-1){
diff[last+1] -= seat;
}
}
// 计算返回结果数组
int[] answer = new int[n];
answer[0] = diff[0];
for(int i = 1;i<n;i++){
answer[i] = answer[i-1] + diff[i];
}
return answer;
}
}