浅谈简单前缀和与差分问题

Posted hakurei-reimu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈简单前缀和与差分问题相关的知识,希望对你有一定的参考价值。

\(Part1:\) 前缀和与差分的简单定义

考虑一个数组\(A\),其项数为\(n\)项。有\(m\)次询问,每次询问给定两个参数\(l\)\(r\),要求求出\(A[l]+A[l+1]+...+A[r]\)

怎么做呢?

  • 暴力:显然是\(O(nm)\)
  • 数据结构维护:显然是\(O(mlogn)\)
    前缀和的用处就在于可以将这样的序列区间求和的问题用\(O(n+m)\)的复杂度,在线性的时间和空间内解决出。

那么前缀和究竟是什么呢?

在读入\(A\)数组的时候,我们预处理出一个前缀和数组\(S\),满足\(S[i]=A[1]+A[2]+...+A[i]\)。如何快速计算\(S\)?显然有\(S[i]=S[i-1]+A[i]\)\(S\)数组就可以这样线性递推出来了。

那么对于每次查询,就有\(A[l]+A[l+1]+...+A[r]=S[r]-S[l-1]\),实现了\(O(n)\)预处理,\(O(1)\)回答单次查询。

那么什么信息可以用这样的“前缀”方式维护呢?只要满足可加可减性也就是满足结合律的信息,都可以这样快速计算维护。例如:快速求区间和,快速求区间积,快速求区间异或和等等。

那么差分又是什么呢?差分是前缀和的逆运算。

在读入\(A\)数组的时候,我们预处理出一个差分数组\(T\),满足\(T[i]=A[i]-A[i-1]\)。差分数组就是原数组相邻两项的差。

考虑对\(n\)项的数组\(A\)给出\(m\)次询问,每次询问给定三个参数\(l\)\(r\)\(d\),要求对\(A[l],A[l+1],...,A[r]\)均执行\(+d\)操作。最后求出\(A\)数组的每一项的值。

利用差分运算,对\(A[l],A[l+1],...,A[r]\)均执行\(+d\)操作,就等价于对差分数据\(T\)进行\(T[l]+=d,T[r+1]-=d\)的操作。

那么直接维护差分数组\(T\),对差分数组做前缀和即可还原出原数组\(A\)

\(Part2:\) 二维前缀和与差分

和很多数据结构一样,前缀和与差分也可以推广到二维矩阵下。

二维前缀和:\(S[x][y]=S[x][y-1]+S[x-1][y]-S[x-1][y-1]+A[x][y]\)。就等价于一个容斥原理。

二维差分:对 \((x1,y1)\)~\((x2,y2)\)\(A\)数组\(+d\),等价于 \(T[x1][y1]+=d,T[x1][y2+1]?=d,T[x2+1][y1]?=d,T[x2+1][y2+1]+=d\)。知道\(T\)还原\(A\)做二维前缀和即可。

也可以将二维前缀和/二维差分,暴力拆成\(n\)个一维前缀和/一维差分进行运算。

\(Part3:\) 前缀和与差分的简单板子

P1115 最大子段和
最大子段和是一个经典问题,这里给出一种利用前缀和的线性做法。先预处理出一个前缀和数组\(S\)

一个子段和\(A[l]+A[l+1]+...+A[r]\)就等价于\(S[r]-S[l-1]\)。要使得这个子段和最大,在维持\(S[r]\)为定值的同时,还要使得\(S[l-1]\)尽可能小。那么从左往右枚举右端点\(r\),并维护左侧的所有\(S\)中的最小值\(mins\)数组,两者相减即为此时的最大子段和,尝试更新答案。

#include<bits/stdc++.h>//P1115 最大子段和
using namespace std;
#define re register
#define ll long long
#define il inline
#define dou double
#define un unsigned
il int read()

    char c=getchar();int x=0,f=1;
    while(c<'0'||c>'9')if(c=='-')f=-1;c=getchar();
    while(c>='0'&&c<='9')x=x*10+c-'0';c=getchar();
    return x*f;

#define INF 114514114
#define clr(x) memset(x,0,sizeof(x))
#define N 200000+10
int n,ans=-INF;
int a[N],s[N],mins[N];
int main()

    n=read();
    for(re int i=1;i<=n;i++)mins[i]=INF;
    for(re int i=1;i<=n;i++)
    
        a[i]=read();
        s[i]=s[i-1]+a[i];
        mins[i]=min(mins[i-1],s[i]);
    
    for(re int r=1;r<=n;r++)
        ans=max(ans,s[r]-mins[r-1]);
    cout<<ans<<endl;
    return 0;

P3397 地毯

二维前缀和与二维差分的板子题。可以暴力拆成\(n\)个一维前缀和/一维差分维护。

#include<bits/stdc++.h>//P3397 地毯
using namespace std;
#define re register
#define ll long long
#define il inline
#define dou double
#define un unsigned
il int read()

    char c=getchar();int x=0,f=1;
    while(c<'0'||c>'9')if(c=='-')f=-1;c=getchar();
    while(c>='0'&&c<='9')x=x*10+c-'0';c=getchar();
    return x*f;

#define INF 114514114
#define clr(x) memset(x,0,sizeof(x))
#define N 1000+10
int n,m,a,b,c,d;
int tag[N][N],sum[N][N];
int main()

    n=read();m=read();
    for(re int i=1;i<=m;i++)
    
        a=read();b=read();c=read();d=read();
        for(re int j=a;j<=c;j++)
        
            tag[j][b]++;
            tag[j][d+1]--;
        
    
    for(re int i=1;i<=n;i++)
    
        for(re int j=1;j<=n;j++)
        
            sum[i][j]=sum[i][j-1]+tag[i][j];
            printf("%d ",sum[i][j]);
        
        printf("\n");
    
    return 0;

P3406 海底高铁

用差分维护每一段道路经过的次数,对于每一段道路,记经过次数为\(s\),那么它的最小花费就是\(min(as,bs+c)\)。即购买纸质票和购买IC卡的费用取最小值。注意每一段道路起点和终点的编号先后顺序可能需要调整。

#include<bits/stdc++.h>//P3406 海底高铁
using namespace std;
#define re register
#define ll long long
#define il inline
#define dou double
#define un unsigned
il int read()

    char c=getchar();int x=0,f=1;
    while(c<'0'||c>'9')if(c=='-')f=-1;c=getchar();
    while(c>='0'&&c<='9')x=x*10+c-'0';c=getchar();
    return x*f;

#define INF 114514114
#define clr(x) memset(x,0,sizeof(x))
#define N 100000+10
#define M 100000+10
ll n,m,ans;
ll p[M],a[N],b[N],c[N],tag[N],s[N];
int main()

    n=read();m=read();
    for(re int i=1;i<=m;i++)p[i]=read();
    for(re int i=1;i<=n-1;i++)
    
        a[i]=read();b[i]=read();c[i]=read();
    
    for(re int i=1;i<=m-1;i++)
    
        if(p[i]<p[i+1])
        
            tag[p[i]]++;
            tag[p[i+1]]--;
        
        else
        
            tag[p[i+1]]++;
            tag[p[i]]--;
        
    
    for(re int i=1;i<=n;i++)s[i]=s[i-1]+tag[i];
    for(re int i=1;i<=n;i++)
        ans=ans+min(a[i]*s[i],b[i]*s[i]+c[i]);
    cout<<ans<<endl;
    return 0;

P1083 借教室

二分答案\(x\),只需判断\(x\)为答案是否合法。

如何\(check\)?用差分维护借教室情况,前缀和还原之后比较,判断是否合法。

#include<bits/stdc++.h>
using namespace std;
#define re register
#define ll long long
#define il inline
#define dou double
#define un unsigned
il int read()

    char c=getchar();int x=0,f=1;
    while(c<'0'||c>'9')if(c=='-')f=-1;c=getchar();
    while(c>='0'&&c<='9')x=x*10+c-'0';c=getchar();
    return x*f;

#define N 1000000+10
#define M 1000000+10
#define clr(x) memset(x,0,sizeof(x))
int n,m;
int a[N],d[M],s[M],t[M],tag[N],sum[N];
il bool check(int x)

    clr(tag);clr(sum);
    for(re int i=1;i<=x;i++)
    
        tag[s[i]]+=d[i];
        tag[t[i]+1]-=d[i];
    
    for(re int i=1;i<=n;i++)
    
        sum[i]=sum[i-1]+tag[i];
        if(sum[i]>a[i])
            return false;
    
    return true;

int main()

    n=read();m=read();
    for(re int i=1;i<=n;i++)
        a[i]=read();
    for(re int i=1;i<=m;i++)
    
        d[i]=read();s[i]=read();t[i]=read();
    
    int l=1,r=m;
    if(check(m))
    
        cout<<0<<endl;
        return 0;
    
    while(l<r)
    
        int mid=(l+r)>>1;
        if(check(mid))
            l=mid+1;
        else
            r=mid;
    
    cout<<-1<<endl<<l<<endl;
    return 0;

以上是关于浅谈简单前缀和与差分问题的主要内容,如果未能解决你的问题,请参考以下文章

基础算法 --- 前缀和与差分

前缀和与差分

前缀和与差分数组

[知识点] 2.7 前缀和与差分

[知识点] 2.7 前缀和与差分

前缀和与差分