差分进阶:累加等差数列类问题的求解

· · 个人记录

引言:

差分是信息学奥赛中的一个基础算法,但是其往往以难题的形式出现在竞赛的后两题中。本文将介绍差分的一种进阶应用:二次差分。这种方法主要应用在区间修改时累加等差数列的情况。

复习回顾:差分的基本概念

差分是前缀和的逆运算。顾名思义,差分是作差,前缀和是作和。具体而言,对于原数组a与差分数组d差分的基本公式如下:

d[i]=a[i]-a[i-1]

差分的主要用途是区间修改+单点查询。通过差分可以只用修改两个点的值就能对整个区间进行修改。对于区间[l,r],利用差分为其加上k的公式如下:

d[l]+=k;d[r+1]-=k

通过以上公式,我们可以在O(1)的时间内进行区间修改。若要还原数组,只需对差分数组做一次前缀和即可。

注意:差分仅适用于加减法的修改,无法进行乘除法修改。

进入正题:二次差分与等差数列问题

问题描述:现有一个数组a,需要在[l,r]的区间内将a的每一项增加一个值,所有增加量可以形成以x为首项,y为末项,d为公差的等差数列

这是典型的区间修改问题,大部分人都会想用差分实现,但此类问题与普通的差分问题有较大差异。我们可以就a数组的状态进行推导:(此处我们设等差数列的项数为4,过程如下表)

下标: l l+1 l+2 l+3(r) r+1 r+2
a数组: x x+d x+2d x+3d(y) 0 0
为了能降低以上操作的复杂度,我们对a数组进行差分得到b数组: 下标: l l+1 l+2 l+3(r) r+1 r+2
b数组 x d d d(y-2d) -x-3d(-y) 0
虽然我们进行了差分,但是未能达到降低复杂度的目的(差分后复杂度没有变化,仍为O(n),而我们的目标是O(1)实现修改)。观察b数组,我们发现,b[l+1],b[l+2]与b[l+3]的d可以相互抵消,于是我们对b数组进行差分,得到c数组: 下标: l l+1 l+2 l+3(r) r+1 r+2
c数组: x d-x 0 0 -y-d y

至此,我们已将a,b数组需要修改整个区间的方法转化为c数组只需要修改4个点的方法,复杂度降为O(1)

由此,我们得出:对于将区间[l,r]增加特定值,使所有增加量形成首项为x,末项为y,公差为d的等差数列,有以下公式:

c[l]+=x;c[l+1]+=(d-x);c[r+1]-=(y+d);c[r+2]+=y

其中,c为差分数组。这一操作因为做了两次差分,所以称为二次差分(又名二阶差分)

注意:由于我们进行了两次差分操作,在还原数组时需要进行两次前缀和

初步运用:P4231 三步必杀

根据题意,每一次攻击对于区间[l,r]范围内的柱子造成的伤害值为以s为首项,e为末项,d=(e-s)/(r-l)为公差的等差数列。因此,我们可以利用上文的结论,通过差分来解决本题。

代码如下:

#include<bits/stdc++.h>
#define int long long //记得开long long
using namespace std;
const int N=1e7+100;
int c[N],n,m,l,r,s,e;
int ans=0;
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);//关闭同步流,提速
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        cin>>l>>r>>s>>e;
        int d=(e-s)/(r-l);//计算公差
        c[l]+=s;c[l+1]+=(d-s);c[r+1]-=(e+d);c[r+2]+=e;//套用上文的结论
    }
    for(int i=1;i<=n;i++){
        c[i]+=c[i-1];
    }
    for(int i=1;i<=n;i++){
        c[i]+=c[i-1];
        ans=max(ans,c[i]);//求最大值
    }//记得还原数组时需要做两次前缀和
    for(int i=1;i<=n;i++){
        c[i]=c[i-1]^c[i];//求异或和
    }
    cout<<c[n]<<" "<<ans;
    return 0;
}

本题是对上文结论的基础运用,下面来看一道更难的题。

进阶运用:P5026 Lycanthropy

由题意可知,本题中也包含涉及修改为等差数列的操作,只不过操作的区间很多,具体情况如下:(时间关系,利用一下题解中的图)

我们可以尝试简化一下差分公式,以避免写程序时出现不必要的错误。

在区间[i-3v+1,i-2v]中进行操作:

i-3v+1 i-3v+2 i-2v+1 i-2v+2
1 0 -v-1 v

在区间[i-2v+1,i-v-1]中进行操作:

i-2v+1 i-2v+2 i-v i-v+1
v-1 -v 0 1
在区间[i-v+1,i]中进行操作: i-v+1 i-v+2 i+1 i+2
-1 0 v+1 -v

在区间[i+1,i+v-1]中进行操作:

i+1 i+2 i+v i+v+1
-v+1 v 0 -1

在区间[i+v+1,i+2v]中进行操作:

i+v+1 i+v+2 i+2v+1 i+2v+2
1 0 -v-1 v

在区间[i+2v+1,i+3v-1]中进行操作:

i+2v+1 i+2v+2 i+3v i+3v+1
v-1 -v 0 1

通过列举所有情况我们发现,许多的操作互相抵消,只剩下如下的操作:

c[i - 3 * v + 1] ++;
c[i - 2 * v + 1] -= 2;
c[i + 1] += 2;
c[i + 2 * v + 1] -= 2;
c[i + 3 * v + 1] ++;

然而,因为下标涉及减法,可能越界,所以我们需要将每一个下标加上一个大数,数组相应要多开4倍(注意是4倍,否则85分),最后做两次前缀和然后输出答案。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+100;
int c[4*N],v,x,n,m;
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>v>>x;
        c[x-3*v+1+N]++;//操作
        c[x-2*v+1+N]-=2;
        c[x+1+N]+=2;
        c[x+2*v+1+N]-=2;
        c[x+3*v+1+N]++;
    }
    for(int i=1;i<=N*2;i++){
        c[i]=c[i-1]+c[i];
    }
    for(int i=1;i<=N*2;i++){
        c[i]=c[i-1]+c[i];//两次前缀和
    }
    for(int i=N+1;i<=N+m;i++){
        cout<<c[i]<<" ";//输出,注意从N+1开始到N+m结束
    }
    return 0;
}

本题的难度较上题有较大提升,不仅在于操作更加复杂,还在于需要考虑数组越界问题。

结语:

二次差分问题是差分内容中较难的一个分支,然而它在求解修改等差数列类问题中效率较高,进而得到广泛应用。理解并掌握这一内容,需要进行细致的推理,以完全掌握此类规律。