zoukankan      html  css  js  c++  java
  • 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)\) ,就可以接受了

    我们构造一个数组 \(d_{1..n}\) ,其中

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

    也就是说 \(d\) 的每一项都是原来 \(a\) 的同位项与 \(a\) 的前一项的差

    反过来看的话,\(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)\) 查询(以后这三项简称为 \(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<bits/stdc++.h>
    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<bits/stdc++.h>
    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(n)-O(1)-O(n)\) 的优秀复杂度

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

    其实,大家有木有感觉到这个差分有点像离散版的微分(把 \(\Delta x\) 设为 \(1\) )?

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

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

  • 相关阅读:
    爬虫遇到取到网页为reload的问题
    【自用】爬虫配置XML时拼接URL中文转Unicode问题(例如北京转成%u5317%u4EAC)
    Could not find artifact com.sun:tools:jar:1.5.0解决方法
    在XP系统下搭建maven环境出的问题 Unable to locate the Javac Compiler in: C:Program FilesJavajre6..lib ools.jar
    如何修改latex标题,作者以及正文的间距
    LaTeX排版札记
    校验码
    Computer Architecture
    Latex 显示字体和背景设置
    Gurobi + CVX + Matlab
  • 原文地址:https://www.cnblogs.com/BrianPeng/p/12178151.html
Copyright © 2011-2022 走看看