前言
差分和前缀和一样,也是很重要的基础算法。
一、一维差分
1.内容
当给出一个数组,每次操作让数组某个区间上的值全增加,最后要求返回整个数组的结果。若是一次一次去遍历,时间复杂度肯定很难看。差分可以做到在时间复杂度良好的情况下解决这一类问题。
注意,差分只能做到所有操作结束后返回结果,不能做到边操作边查询。
2.模板——航班预订统计
class Solution {
public:
vector<int> corpFlightBookings(vector<vector<int>>& bookings, int n) {
vector<int>help(n+2,0);
for(int i=0;i<bookings.size();i++)
{
help[bookings[i][0]]+=bookings[i][2];
help[bookings[i][1]+1]-=bookings[i][2];
}
vector<int>ans(n,0);
ans[0]=help[1];
for(int i=1;i<n;i++)
{
ans[i]=ans[i-1]+help[i+1];
}
return ans;
}
};
分析题意会发现这个题就是差分的模板题。
一维差分的方法就是,当操作区间为(l,r)时,在数组l位置加上要增加的数值,数组r+1位置减去要增加的数值,最后构建前缀和数组即为答案。
二、等差数列差分
1.内容
和一维差分差不多,不一样的是每次操作在区间上每个位置增加的值构成一个等差数列。
2.模板——三步必杀
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
void solve()
{
ll n;
scanf("%lld",&n);
ll m;
scanf("%lld",&m);
vector<vector<ll> >kill(m,vector<ll>(4,0));
for(ll i=0;i<m;i++)
{
for(ll j=0;j<4;j++)
{
scanf("%lld",&kill[i][j]);
}
}
vector<ll>ans(n+3,0);
for(ll i=0,d;i<m;i++)
{
d=(kill[i][3]-kill[i][2])/(kill[i][1]-kill[i][0]);
ans[kill[i][0]-1]+=kill[i][2];
ans[kill[i][0]]+=d-kill[i][2];
ans[kill[i][1]]-=d+kill[i][3];
ans[kill[i][1]+1]+=kill[i][3];
}
for(int i=0;i<2;i++)
{
for(ll j=1;j<=n;j++)
{
ans[j]+=ans[j-1];
}
}
ll max_num=0;
ll eor=0;
for(ll i=0;i<n;i++)
{
max_num=max(max_num,ans[i]);
eor^=ans[i];
}
printf("%lld %lld",eor,max_num);
}
int main()
{
solve();
return 0;
}
这个基本上就是等差数列差分的模板题。
等差数列差分的方法是,每次操作在数组l位置加上等差数列的首项,在l+1位置加上公差-首项,在r+1位置减去公差+末项,在r+2位置加上末项,最后构建两次前缀和数组即可。
3.练习——Lycanthropy
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll offset=30001;
void cal(ll l,ll r,ll s,ll e,ll d,vector<ll>&help)
{
help[l+offset]+=s;
help[l+1+offset]+=d-s;
help[r+1+offset]-=d+e;
help[r+2+offset]+=e;
}
void solve()
{
ll n;
scanf("%lld",&n);
ll m;
scanf("%lld",&m);
vector<vector<int> >fall(n,vector<int>(2,0));
for(ll i=0;i<n;i++)
{
for(ll j=0;j<2;j++)//0:v 1:loc
{
scanf("%lld",&fall[i][j]);
}
}
vector<ll>help(m+2*offset,0);
for(ll i=0;i<n;i++)
{
cal(fall[i][1]-fall[i][0]+1,fall[i][1],-1,-fall[i][0],-1,help);
cal(fall[i][1]+1,fall[i][1]+fall[i][0]-1,-fall[i][0]+1,-1,1,help);
cal(fall[i][1]-2*fall[i][0],fall[i][1]-fall[i][0]-1,fall[i][0],1,-1,help);
cal(fall[i][1]-3*fall[i][0]+1,fall[i][1]-2*fall[i][0]-1,1,fall[i][0]-1,1,help);
cal(fall[i][1]+fall[i][0]+1,fall[i][1]+2*fall[i][0],1,fall[i][0],1,help);
cal(fall[i][1]+2*fall[i][0]+1,fall[i][1]+3*fall[i][0]-1,fall[i][0]-1,1,-1,help);
}
for(int i=0;i<2;i++)
{
for(ll j=1;j<=m+offset;j++)
{
help[j]+=help[j-1];
}
}
for(ll i=0;i<m;i++)
{
printf("%lld ",help[i+offset+1]);
}
}
int main()
{
solve();
return 0;
}
这个题看上去很复杂,但画个图分析一下会发现,每次落水后,左右两侧水位的变化呈等差数列,所以可以对其进行多次等差数列差分。
所有差分问题都有一个坑,就是数组下标越界的问题,所以每次准备时要准备大一点的辅助数组。而这个题又不一样,其下标可能会越界非常多,所以为了处理这个问题,考虑扩大数组大小,并最后用offset索引正确区间。
具体方法是,让答案数组的0位置对应辅助数组的offset+1位置。这里的offset为“溅起水花的最大长度”,用来处理在水池端点处落水的情况,所以在答案数组左右两侧分别加上一个offset长度。
还要注意的是,因为要求前缀和,所以不鞥只从offset+1位置开始,要从辅助数组开头开始,一直到m+offset位置。
总结
从最后一个题能看出,coding能力还要练啊!