Basic Thought / Data Structure: 差分 Difference

Intro:

作为查询界的\(O(1)\)王者——前缀和的亲兄弟,差分,他可是修改界的\(O(1)\)王者


Prerequisite knowledge:

前缀和


Function: 仅单次询问的区间修改

模板题:洛谷P2367 语文成绩

先想一想朴素算法怎么做吧

对于输入的每一组\((x,y)\),遍历序列\(a_{x..y}\),每一项加上\(z\),代码如下

while(p--)for(int i(x);i<=y;++i)a[i]+=z;

Time complexity: \(O(np)\)

Memory complexity: \(O(n)\)

这个时间复杂度明显不行,只要修改长度大了,修改时间就会变长

发现查询只有一次

也就是说如果可以把数组变一下,使得修改\(O(1)\),而查询\(O(n)\),就可以接受了

怎么办呢?这要从一个古(xia)老(bian)的故事开始

在很久很久以前,有一个数组\(a_{1..n}\),他日复一日地被执行着区间修改的痛苦折磨,因为每次他都要把自己从头到尾遍历一遍

有一天,一道闪电袭来,正好击中小数组\(a\)的尾部,结果这个尾部(\(a_n\))把自己的值减去了前面一节(\(a_{n-1}\))的值,然后发生了连锁反应,每一节都把自己的值前去了前面一节的值,最后,闪电赋予了小数组\(a\)新的名字: \(d_{1..n}\)

简单来说: \(d_i=a_i-a_{i-1}, 1\leqslant i\leqslant n\)

现在小数组\(d\)回顾了一下自己的前世,发现他以前遭受的每一次痛苦的区间修改,对于现在的自己来说,只不过是简单的两发单点修改而已

于是,小数组\(d\)再也不会受到区间修改的痛苦折磨了,Happy ending!

闪电的魔力究竟是什么呢?就在这里

\(d_i=a_i-a_{i-1}\)

反过来看的话,\(a_i=a_{i-1}+d_i\)(是不是有点眼熟呢)

没错!\(a_i=\sum_{j=1}^id_j\),是前缀和!

考虑一下对\(d_i\)加上\(v\)\(a\)会有什么效果

对于\(a_{1..i-1}\),没效果

对于\(a_{i..n}\),每一个值都加上了\(v\)(认真思考下为什么)

所以对于每一次区间修改\((l,r,v)\)(表示对\(a_{l..r}\)每一个元素加上\(v\)),对\(d\)来说,只不过是把\(d_l\)加上\(v\)\(d_{r+1}\)减去\(v\)而已

这就是\(d_{1..n}\),差分数组

最后如果想要把\(d\)数组变回\(a\)数组,只要对\(d\)构造前缀和数组即可(这就是为什么我说差分是前缀和的逆运算了)


Code:

构造\(d_{1..n}\)

for(int i(n);i>=1;--i)d[i]=a[i]-a[i-1];

P.s: 其实顺序没有关系,但是如果原址构造必须倒序

修改

d[l]+=v,d[r+1]-=v;

更新原数组

for(int i(1);i<=n;++i)a[i]=a[i-1]+d[i]

Time complexity: \(O(n)\)预处理\(O(1)\)修改\(O(n)\)查询

Memory complexity: \(O(n)\)

P.s 同样的,可以直接将原数组变成差分数组,则不需要额外空间,代码如下(必须倒序)

for(int i(n);i>=2;--i)a[i]-=a[i-1];

于是本题的答案就出来啦,具体见代码(记得开long long)

//This program is written by Brian Peng.
#pragma GCC optimize("Ofast","inline","-ffast-math")
#pragma GCC target("avx,sse2,sse3,sse4,mmx")
#include
using namespace std;
#define int long long
#define Rd(a) (a=read())
#define Gc(a) (a=getchar())
#define Pc(a) putchar(a)
inline int read(){
    register int x;register char c(getchar());register bool k;
    while(!isdigit(c)&&c^'-')if(Gc(c)==EOF)exit(0);
    if(c^'-')k=1,x=c&15;else k=x=0;
    while(isdigit(Gc(c)))x=(x<<1)+(x<<3)+(c&15);
    return k?x:-x;
}
void wr(register int a){
    if(a<0)Pc('-'),a=-a;
    if(a<=9)Pc(a|'0');
    else wr(a/10),Pc((a%10)|'0');
}
signed const INF(0x3f3f3f3f),NINF(0xc3c3c3c3);
long long const LINF(0x3f3f3f3f3f3f3f3fLL),LNINF(0xc3c3c3c3c3c3c3c3LL);
#define Ps Pc(' ')
#define Pe Pc('\n')
#define Frn0(i,a,b) for(register int i(a);i<(b);++i)
#define Frn1(i,a,b) for(register int i(a);i<=(b);++i)
#define Frn_(i,a,b) for(register int i(a);i>=(b);--i)
#define Mst(a,b) memset(a,b,sizeof(a))
#define File(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout)
#define N (5000010)
int n,p,a[N],x,y,z,ans(LINF);
signed main(){
    Rd(n),Rd(p);
    Frn1(i,1,n)Rd(a[i]);
    Frn_(i,n,1)a[i]-=a[i-1];
    while(p--)Rd(x),Rd(y),a[x]+=Rd(z),a[y+1]-=z;
    Frn1(i,1,n)ans=min(ans,a[i]+=a[i-1]);
    wr(ans),exit(0);
}

Example:

洛谷P3717 [AHOI2017初中组]cover

(这不是暴力就可以了吗)

当然正解就是暴力,所以假设\(n,m,r\leqslant 1000\),再看看这道题

这道题就是典型的区间修改(将可以覆盖的地方\(+1\))与单次查询(最后问\(>0\)的有几个)

于是以行为单位差分,每一次修改相当于对每一行的元素做区间修改

但是每一行被修改的位置和长度互不相同,具体取决于半径\(r\)和行号\(i\)到圆心所在行号\(y\)的距离(\(|i-y|\)

假设存在\(e_{0..r}\)数组,\(e_i\)表示斜边为\(r\),对边为\(i\)的直角三角形的邻边长度的整数部分,即\(e_i=\lfloor\sqrt{r^2-i^2}\rfloor\)

那么对于第\(i\)行,如果圆心是\((x,y)\),半径为\(r\),覆盖的区间就是\([max(1,x-e_{|i-y|}),min(n,x+e_{|i-y|})]\)(可别忘了处理边界)

最后是如何计算\(e\)数组

(当然可以直接使用sqrt())

但是注意到\(e\)数组具有不上升单调性,可以使用一种类似递推的方法做,保证计算\(O(n)\)(虽然说其实完全没有必要啦)

具体做法就请大家自己想啦(其实是本蒟蒻太懒了),上代码

//This program is written by Brian Peng.
#pragma GCC optimize("Ofast","inline","-ffast-math")
#pragma GCC target("avx,sse2,sse3,sse4,mmx")
#include
using namespace std;
#define Rd(a) (a=read())
#define Gc(a) (a=getchar())
#define Pc(a) putchar(a)
inline int read(){
    register int x;register char c(getchar());register bool k;
    while(!isdigit(c)&&c^'-')if(Gc(c)==EOF)exit(0);
    if(c^'-')k=1,x=c&15;else k=x=0;
    while(isdigit(Gc(c)))x=(x<<1)+(x<<3)+(c&15);
    return k?x:-x;
}
void wr(register int a){
    if(a<0)Pc('-'),a=-a;
    if(a<=9)Pc(a|'0');
    else wr(a/10),Pc((a%10)|'0');
}
signed const INF(0x3f3f3f3f),NINF(0xc3c3c3c3);
long long const LINF(0x3f3f3f3f3f3f3f3fLL),LNINF(0xc3c3c3c3c3c3c3c3LL);
#define Ps Pc(' ')
#define Pe Pc('\n')
#define Frn0(i,a,b) for(register int i(a);i<(b);++i)
#define Frn1(i,a,b) for(register int i(a);i<=(b);++i)
#define Frn_(i,a,b) for(register int i(a);i>=(b);--i)
#define Mst(a,b) memset(a,b,sizeof(a))
#define File(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout)
#define N (110)
int n,m,r,a[N][N],e[N],x,y,ans;
signed main(){
    Rd(n),Rd(m),*e=Rd(r);
    Frn1(i,1,r){e[i]=e[i-1];while(e[i]*e[i]+i*i>r*r)--e[i];}
    while(m--){
        Rd(x),Rd(y);
        Frn1(i,max(1,y-r),min(n,y+r)){
            ++a[max(1,x-e[abs(i-y)])][i];
            --a[min(n,x+e[abs(i-y)])+1][i];
        }
    }
    Frn1(i,1,n)Frn1(j,1,n)if(a[i][j]+=a[i-1][j])++ans;
    wr(ans),exit(0);
}

(个人认为如果\(n,m,r\leqslant 1000\),这道题至少上绿)

到此为止差分的所有基本操作都讲完啦!


Conclusion & Extension:

差分在区间修改和单词查询问题上,有着\(O(1)-O(n)\)的优秀复杂度

它的基本思想就是用一个数字的变化表示一段区间的整体变化,达到优化的目的

当差分遇上数据结构,一段美妙的旅程又将开始:树状数组(嘻嘻嘻又是我)

到此为止本篇文章就圆满结束啦,请各位奆佬们多多指教和支持,THX!

你可能感兴趣的:(Basic Thought / Data Structure: 差分 Difference)