ACM CSP竞赛笔记(十一)——前缀和与差分

参考课程是我高中信息竞赛邱老师的课程。

【12-1前缀和】 https://www.bilibili.com/video/BV1yc411r74s/?share_source=copy_web\&vd_source=2c56c6a2645587b49d62e5b12b253dca

【12-2差分】 https://www.bilibili.com/video/BV1fG41167Jz/?share_source=copy_web\&vd_source=2c56c6a2645587b49d62e5b12b253dca

完整的板子可以去资源获取,我设置0积分,应该是免费的,如果C收你费或者下不了B站私我。

前缀和(用于计算区间和 出现Σ都先考虑前缀和)

得到前缀和关系,计算区间和

方法两步:

①创建原数组 和 presum数组(从1开始)

②sumi=sumi-1+ai; 得到前缀和关系

②输出sumr-suml-1 输出区间和

一维前缀和

模板 P8218

https://www.luogu.com.cn/problem/P8218

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int a[1000005],sum[1000005];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        sum[i]=a[i]+sum[i-1];
    }
    int m;
    cin>>m;
    for(int i=0;i<m;i++){
        int r,l;
        cin>>l>>r;
        cout<<sum[r]-sum[l-1]<<endl;
    }
    return 0;
}

练习

P1614

https://www.luogu.com.cn/problem/P1614

cpp 复制代码
#include<bits/stdc++.h>
#include<limits.h>
using namespace std;
int a[3005],presum[3005];

int main(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        presum[i]=presum[i-1]+a[i];
    }
    int minn=INT_MAX;
    for(int i=0;i<=n-m;i++){
        minn=min(minn,(presum[i+m]-presum[i]));
    }
    cout<<minn;
    return 0;
}

二维前缀和

由于会涉及fij=fi-1j+fij-1-fi-1j-1+aij; 因此循环必须从1开始到n

模板 P1719

https://www.luogu.com.cn/problem/P1719

cpp 复制代码
#include<bits/stdc++.h>
#include<limits.h>
int a[125][125],f[125][125];
using namespace std;
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            cin>>a[i][j];
            f[i][j]=f[i-1][j]+f[i][j-1]-f[i-1][j-1]+a[i][j];
        }
    }
    int ans=INT_MIN;
    for(int a=1;a<=n;a++){
        for(int b=1;b<=n;b++){
            for(int c=a;c<=n;c++){
                for(int d=b;d<=n;d++){
                    int cur=f[c][d]+f[a-1][b-1]-f[c][b-1]-f[a-1][d];
                    ans=(ans>cur?ans:(cur));
                }
            }
        }
    }
    cout<<ans<<endl;
    return 0;
}

练习

P2004

https://www.luogu.com.cn/problem/P2004

cpp 复制代码
#include<bits/stdc++.h>
#include<limits.h>
using namespace std;
int a[3000][3000],presum[3000][3000];
int main(){
    int n,m,c;
    cin>>n>>m>>c;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            cin>>a[i][j];
            presum[i][j]=a[i][j]-presum[i-1][j-1]+presum[i-1][j]+presum[i][j-1];
        }
    }
    int val=INT_MIN;
    int max=INT_MIN;
    int cptx=1,cpty=1;
    //就是这里全减去一了?
    for(int i=1; i<=n-c+1; i++){
        for(int j=1; j<=m-c+1; j++){
            val = presum[i+c-1][j+c-1] 
                  + presum[i-1][j-1] 
                  - presum[i-1][j+c-1] 
                  - presum[i+c-1][j-1];
            if(val>max){
                cptx=i;
                cpty=j;
                max=val;
            }
        }
    }
    cout<<cptx<<" "<<cpty<<endl;
    return 0;
}

差分(用于区间修改)

也可以求 区间内满足某个性质的数有多少个

方法一步:

坑前加 坑后补

cpp 复制代码
    for(int i=0;i<p;i++){
        cin>>x>>y>>z;
        diff[x]+=z;
        //if(y+1<=n)
        diff[y+1]-=z;
    }

一维差分

差分和前缀和就是离散的积分和求导。

可以先差分后前缀和 也可以先前缀和再差分。

模板 P2367

https://www.luogu.com.cn/problem/P2367

差分是导数,所以要求变化后的数组需要计算变化量,变化量就是对导数积分,也就是对差分求前缀和。presum的值就是变化量,此时再把presum加回a数组即可得到变化后的数组。

cpp 复制代码
for(int i=1;i<=n;i++){
        presum[i]=presum[i-1]+diff[i];
        a[i]+=presum[i];
        min=(a[i]>min)?min:a[i];
    }
cpp 复制代码
#include<bits/stdc++.h>
#include<limits.h>
using namespace std;
int stu[5000005];
int diff[5000005]={0};
int presum[5000005]={0};
int n,p;
int main(){

    cin>>n>>p;
    
    for(int i=1;i<=n;i++){
        cin>>stu[i];
    }
    int x,y,z;
    for(int i=0;i<p;i++){
        cin>>x>>y>>z;
        diff[x]+=z;
        //if(y+1<=n)
        diff[y+1]-=z;
    }
    int min=INT_MAX;
    for(int i=1;i<=n;i++){
        presum[i]=presum[i-1]+diff[i];
        stu[i]+=presum[i];
        min=(stu[i]>min)?min:stu[i];
    }
    cout<<min<<endl;
    return 0;
}

练习

P3406

https://www.luogu.com.cn/problem/P3406

这题首先要看出是区间修改问题,然后要注意,得到的是城市的终点起点,而要累加的是路径,因此是比城市少一个的(两城市间一条路),所以在写差分时是startend而不是end+1

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

long long diff[100005];
//要看出是区间修改问题,每次从任意两个站点走,因此中间段都要+1,是区间修改
int main(){
    int N,M;
    cin>>N>>M;
    int p1,p2;
    cin>>p2;
    for(int i=1;i<M;i++){
        cin>>p1;
        int start =min(p1,p2),end=max(p1,p2);
        diff[start]++,diff[end]--;//为什么是end就减一 而不是end+1
        p2=p1;
    }
    long long a,b,c;
    long long num=0,money=0;
    for(int i=1;i<N;i++){
        cin>>a>>b>>c;
        num+=diff[i];//前缀和获得修改后的变化量,也就是各段乘坐的次数
        money+=min(num*a,num*b+c);//注意买卡只需要买一次,后续都可以用b的价格坐
    }
    cout << money << endl;

    return 0;
}

P2697

https://www.luogu.com.cn/problem/P2697

注意偏移下标是为了映射初次出现到负数域,如果不用偏移,那么下标恒为正无法映射负数。

偏移后必须同步拓展数组。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int emerg[2000005];
int presum[2000005];
int OFFSET=1e6;
int main(){
    memset(emerg,-1,sizeof(emerg));
    string s;
    cin>>s;
    int max_len=0;
    for(int i=1;i<=s.length();i++){
        if(s[i-1]=='G') {
            presum[i]=presum[i-1]+1;
        }else{
            presum[i]=presum[i-1]-1;
        }
        int idx=presum[i]+OFFSET;

        if(presum[i]==0){//因为这一定是当前最长的串
            max_len=i;
        }
        if(emerg[idx]!=-1){//说明是第二次出现
            max_len=max(max_len,i-emerg[idx]);
        }else{//说明是第一次出现
            emerg[idx]=i;//设置为第一次出现的下标
        }
    }
    cout<<max_len<<endl;
    return 0;
}

二维差分

模板 P3397

https://www.luogu.com.cn/problem/P3397

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int arr[1005][1005];
int diff[1005][1005];
int presum[1005][1005];
int n,m;
int main(){
    
    cin>>n>>m;
    
    for(int i=0;i<m;i++){
        int a,b,c,d;
        cin>>a>>b>>c>>d;
        diff[a][b]++;
        diff[c+1][d+1]++;
        diff[a][d+1]--;
        diff[c+1][b]--;
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            presum[i][j]=diff[i][j]+presum[i-1][j]+presum[i][j-1]-presum[i-1][j-1];
            arr[i][j]+=presum[i][j];
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            cout<<arr[i][j]<<" ";
        }
        cout<<endl;
    }
    return 0;
}

练习

P2280

https://www.luogu.com.cn/problem/P2280

注意,设置目标的时候必须统一差分图和前缀和图,也就是tar和presum,既然presum是要求从1开始,而题目给定的坐标是从0开始,所以必须tarx+1y+1+=v;

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

int n,m;
int presum[5005][5005];
int tar[5005][5005];
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        int x,y,v;
        cin>>x>>y>>v;
        tar[x+1][y+1]+=v;
    }
    for(int i=1;i<=5001;i++){
        for(int j=1;j<=5001;j++){
            presum[i][j]=presum[i][j-1]+presum[i-1][j]-presum[i-1][j-1]+tar[i][j];
        }
    }
    int max_val=0;
    m--;
    for(int i=1;i<=5001-m;i++){
        for(int j=1;j<=5001-m;j++){
            int a,b,c,d;
            a=i;b=j;c=i+m;d=j+m;
            int cur_val=presum[c][d]+presum[a-1][b-1]-presum[c][b-1]-presum[a-1][d];
            if(cur_val>max_val){
                max_val=cur_val;
            }
        }
    }
    cout<<max_val<<endl;
}

大综合 二分查找+前缀和+差分

https://www.luogu.com.cn/problem/P1083

P1083

核心是输出需要修改订单的申请人编号,也就是第几个m不满足,是一个单点搜索,要用二分。

m是单调的,可以使用

二分对象就是订单编号

搜索范围是0到M+1

推荐条件是 avai是否能满足。

接下来看check函数怎么写?

也就是查看在m个需求的时候是否能满足,

条件是ri>需求

需求可以用前缀和得到。

先差分,然后前缀和得到变化量,也就是每天的需求,然后再用条件判断。

一定要注意要初始化init,否则前缀和和差分会用此前的记录。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
//nm在1e6 rd在1e9
long long presum[1000005];
int dst[1000005][3];
long long diff[1000005];
int ava[1000005];
int n,m;
int M;
void init(){
    memset(diff,0,sizeof(diff));
    memset(presum,0,sizeof(presum));
}
bool check(int M){
    //M次请求 先差分
    init();
    for(int i=0;i<M;i++){
        //从dst中取1 2 位
        diff[dst[i][1]]+=dst[i][0];
        diff[dst[i][2]+1]-=dst[i][0];
    }
    //然后前缀和
    for(int i=1;i<=n;i++){
        presum[i]=presum[i-1]+diff[i];
        if(presum[i]>ava[i]) return false;
    }
    return true;
}
int find(){
    long long L=0,R=m+1;
    while(R>L){
        int M=(L+R)/2;
        if(check(M)){//如果M个订单足以满足 应该增加订单的数量
            L=M+1;
        }else{
            R=M;
        }
    }
    return L;
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>ava[i];//存教室空位
    }
    for(int i=0;i<m;i++){
        cin>>dst[i][0]>>dst[i][1]>>dst[i][2];
    }
    //先单独测一下m个订单是否都能满足
    if(check(m)){
        cout<<0<<endl;
    }else{
        int result=find();
        cout<<-1<<endl<<result;
    }
    
    return 0;
}

P1314

注意是整数二分找到最接近s的W。

也就是找到刚好和s差不多的W。

然后测试左侧和右侧整数哪个更接近。