文章目录
- 基础知识
- 相关模板
- 相关题目
- 1109.航班预计统计
- 1450.在既定时间做作业的学生人数
- 6158.字母移位II
- 798.得分最高的最小轮调
- 2406.将区间分为最少组数
「差分」算法是另一种「区间求和」的算法,之前笔者在算法学习-前缀和,学会了起码秒50题也介绍了一种区间求和的算法,在宫水三叶的题解中对这几种区间求和的算法进行了总结,其中差分可以用在数组区间修改,单点查询的场景下,笔者将自己的刷题路径总结如下。
基础知识
对于差分的理解,可以将其理解为前缀和之前的一步预操作,特征为「区间修改,单点查询」。
题目特征为,对于一个将区间[l,r]整体增加一个值v的操作,我们可以对差分数组 c的影响看成两部分:
- 对c[l]+=v:由于差分是前缀和的预先操作,我们最后会对差分数组c求和,因此这个操作对于将来的查询而言,带来的影响是对于所有的下标大于等于l的位置都增加了值v; 对c[r+1]-=v:由于我们期望只对区间[l,r]产生影响,因此需要对下标大于r的位置进行减值操作,从而抵消对前缀和的“影响”。
对于最后的构造答案,可看做是对「每个下标做单点查询」操作,只需要对差分数组求前缀和即可。
感性的理解起来是非常巧妙的,将一个对区间[l,r]的改变,变成了对c[l]+=v以及c[r+1]-=v两个边界点的改变,通过「前缀和」将边界点的增减改变推广到了两个边界值中间区间的增减改变,因此可以将遍历某个区间对每个元素增减的复杂度O(N)降为两个边界点修改的O(1),算上外层循环的话,整体复杂度从O(N^2)降为O(2*N)。
相关模板
见 1109.航班预计统计
class Solution {
public int[] corpFlightBookings(int[][] bookings, int n) {
//差分数组需要比bookings中的时间最大值(航班总数)大1方便存储r+1
//c的开辟需要满足能将整体数据范围包含在内
int[]c=new int[n+1];
for(int[]b:bookings){
//下标从0开始存储
int f=b[0]-1,l=b[1]-1,s=b[2];
c[f]+=s;
c[l+1]-=s;
}
int[]ans=new int[n];
//同样是从0开始计算
//初始化ans[0]为差分数组第一个
ans[0]=c[0];
//每个ans就是之前的ans加上当前的c
for(int i=1;i<n;i++){
ans[i]=ans[i-1]+c[i];
}
return ans;
//上面是返回一个数组的所有答案,因此需要逐个存储,有些题目只需要返回一个值,直接一次累加就行
//最后返回c[queryTime]一个值
for(int i=1;i<=queryTime;i++){
c[i]+=c[i-1];
}
return c[queryTime];
}
}
相关题目
1109.航班预计统计
差分数组记录每个时间点相对于之前时间段前缀和的变化人数,相当于对每个时间段区间的航班数进行区间修改,修改值为该时间段的订票人数,差分转化为时间段上下界时间点的修改,最后单点查询每个时间点的预计人数。
class Solution {
public int[] corpFlightBookings(int[][] bookings, int n) {
//差分数组需要比数组边界大以便存储r+1
int[]c=new int[n+1];
for(int[]b:bookings){
int f=b[0]-1,l=b[1]-1,s=b[2];
c[f]+=s;
c[l+1]-=s;
}
int[]ans=new int[n];
ans[0]=c[0];
//每个ans就是之前的ans加上当前的c
//最后返回ans数组
for(int i=1;i<n;i++){
ans[i]=ans[i-1]+c[i];
}
return ans;
}
}
1450.在既定时间做作业的学生人数
差分数组记录每个时间点相对于之前时间段前缀和的变化人数,相当于对某一时间段的学生人数进行区间修改,修改值为该时间段做作业的学生人数(固定为1),差分转化为对该时间段上下时间点的修改,最后单点查询在queryTime时刻正在做作业的学生人数。
class Solution {
public int busyStudent(int[] startTime, int[] endTime, int queryTime) {
int[]c=new int[1010];
for(int i=0;i<startTime.length;i++){
c[startTime[i]]++;
c[endTime[i]+1]--;
}
//最后返回c[queryTime]一个值
for(int i=1;i<=queryTime;i++){
c[i]+=c[i-1];
}
return c[queryTime];
}
}
6158.字母移位II
class Solution {
public String shiftingLetters(String s, int[][] shifts) {
int len=s.length();
int[]c=new int[len+1];
int[]change=new int[len];
for(int[]sh:shifts){
int start=sh[0],end=sh[1],v=sh[2];
c[start]+=v==0?-1:1;
c[end+1]+=v==0?1:-1;
}
StringBuilder sb=new StringBuilder();
for(int i=0,shiftNum=0;i<len;i++){
shiftNum+=c[i];
int gap=s.charAt(i)-'a';
//+26可以让负数变为相对于'a'的正数,对于正数却无影响
int newGap=((gap+shiftNum)%26+26)%26;
char res=(char)(newGap+'a');
sb.append(res);
}
return sb.toString();
}
}
798.得分最高的最小轮调
这题是比较难想到差分的,差分数组记录的是在某个轮调次数的区间上,nums[i] <= i个数的增加或者减少。在遍历每个数字nums[i]及其下标i的过程中,对该数字调换次数(可以理解为数字不变,下标移动)的差分区间进行修改,修改值为该数组nums[i]能否记入得分(1为记入,-1为不计入),差分方法将其转换为边界上下界的修改。要查找最大的k,只要查询边界数组中,所有可取的边界范围前缀和的最大值的下标。
最难的在于根据每个数字nums[i],算出其可以算入得分的上下界k,最后前缀和就是每个k可以算入得分的数字总数。
相对于之前两题直接有上下界,这题根据数组元素nums[i]构造上下界多了一步,从而可以来做差分。
参考benhao的题解中的题解思路以及彤哥的配图。
class Solution {
public int bestRotation(int[] nums) {
int len=nums.length;
//差分数组定义为本次轮调次数得分相对于上一次轮调次数得分的变化,比len大1
int[]c=new int[len+1];
for(int i=0;i<len;i++){
if(i>=nums[i]){
//论调次数
c[0]++;
//当前数字的i轮调到nums[i]-1下标就不计入得分
c[i-nums[i]+1]--;
//轮调到n-1的位置,默认到最大轮调次数为len-1
c[i+1]++;
}else{
c[i+1]++;
//轮调到nums[i]-1就不计入得分
c[i+1+len-nums[i]]--;
}
}
int maxV=0;
int index=0;
int cur=0;
for(int i=0;i<len;i++){
cur+=c[i];
if(cur>maxV){
maxV=cur;
index=i;
}
}
return index;
}
}
2406.将区间分为最少组数
这题看似是求最小区间贪心,其实可以就用差分,将每个区间看成是给区间内的数+1,这样相当于就是尽可能在给定的数据范围内,将每个区间平铺开来。最终有重叠部分的最大值,相当于就是前缀和最大值,就是需要的组数。
class Solution {
public int minGroups(int[][] intervals) {
int len=intervals.length;
//开辟的数组大小需要包含值域范围
int[]c=new int[(int)1e6+10];
for(int[]i:intervals){
c[i[0]]+=1;
c[i[1]+1]-=1;
}
int sum=0;
int res=0;
for(int cc:c){
sum+=cc;
res=Math.max(res,sum);
}
return res;
}
}