数据结构进阶:前缀和与差分

蓝桥杯已经结束,打下来很难受。自己对于算法的掌握还是不够,遂继续开启博客书写,激励自己学习。本系列文章以牛客数据结构课程为基础,记录自己对于课程的理解和部分代码。供自己学习复习使用!

基础前缀和

int a[N],s[N];//前缀和数组
s[i]=s[i-1]+a[i];//s[i]表示a1+a2+...+ai
所以查询[l,r]的值之和就可以s[r]-s[l-1]

基础差分

差分数组b[i]=a[i]-a[i-1]
所以对差分数组求前缀和就可以得到原数组。
差分数组一般用来维护给一段区间加上或减去一直相同的值
比如给[l,r]区间的每个值都加上c
void insert(int l, int r, int c)
{
    b[l] += c;
    b[r + 1] -= c;
}
构造差分数组 for(int i=1;i<=n;i++) insert(i,i,a[i]);
或者 for(int i=1;i<=n;i++) b[i]=a[i]-a[i-1];
最后对b数组求一次前缀和就可以得到结果
    
对原数组差分后可以发现正数之和加上负数之和等于0

区间乘积

智乃酱的区间乘积
由于乘法运算也是可差分的。所以我们配合逆元便可以求区间乘积。

int n,m;
int a[N],s[N];
int qmi(int a,int k,int p){
    int res=1%p;
    a%=p;
    while(k){
        if(k&1) res=(LL)res*a%p;
        a=(LL)a*a%p;
        k>>=1;
    }
    return res;
}
void Showball(){
    cin>>n>>m;
    s[0]=1;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        s[i]=(LL)s[i-1]*a[i]%mod;
    }
    int l,r;
    while(m--){
        cin>>l>>r;
        cout<<(LL)s[r]*qmi(s[l-1],mod-2,mod)%mod<<endl;
    }
}

前缀置换

对于一些置换,如果我们也可以通过一些方式将它还原回去,那么我们就可以进行类似前缀和的操作,比如矩阵和矩阵求逆等等。
牛牛的猜球游戏

对于这道题目,我们发现对于每次交换操作,我们可以通过下标转换消去操作带来的影响,那么我们就可以进行类似前缀和的操作进行维护。

在这里插入图片描述

int n,m;
int s[N][10];
void Showball(){
    cin>>n>>m;
    for(int i=0;i<10;i++) s[0][i]=i;
    for(int i=1;i<=n;i++){
        int x,y;
        cin>>x>>y;
        memcpy(s[i],s[i-1],sizeof s[i]);
        swap(s[i][x],s[i][y]);
    }
    int l,r;
    while(m--){
        cin>>l>>r;
        int p[10];
        for(int i=0;i<10;i++) p[s[l-1][i]]=i;
        for(int i=0;i<10;i++) cout<<p[s[r][i]]<<" \n"[i==9];
    }
}

智乃酱的双塔问题
这个题目就涉及到了矩阵的前缀和操作。
贴一个大佬的题解

这里贴一个线段树做法

#include<bits/stdc++.h>
using namespace std;

const int N=1e5+10,M=2;
const int mod=1e9+7;
int n,m;

char s[N];
struct node{
    int l,r;
    int mat[M][M];
}tr[N*4];

void mul(int c[M][M],int a[M][M],int b[M][M]){
    int ans[M][M];
    memset(ans,0,sizeof ans);
    for(int i=0;i<M;i++){
        for(int j=0;j<M;j++){
            for(int k=0;k<M;k++){
                ans[i][j]=(ans[i][j]+1LL*a[i][k]*b[k][j])%mod;
            }
        }
    }
    memcpy(c,ans,sizeof ans);
}
void pushup(node &u,node &l,node &r){
   mul(u.mat,l.mat,r.mat);
}

void pushup(int u){
    pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}

void build(int u,int l,int r){
    tr[u]={l,r};
    if(l==r){
       if(s[r]=='/'){
         auto &t=tr[u].mat;
         t[0][0]=t[0][1]=t[1][1]=1;
         t[1][0]=0;
       }else{
         auto &t=tr[u].mat;
         t[0][0]=t[1][0]=t[1][1]=1;
         t[0][1]=0;
       }
       return;
    }
    int mid=l+r>>1;
    build(u<<1,l,mid),build(u<<1|1,mid+1,r);
    pushup(u);
}

node query(int u,int l,int r){
    if(l<=tr[u].l&&tr[u].r<=r) return tr[u];
    int mid=tr[u].l+tr[u].r>>1;
    if(r<=mid) return query(u<<1,l,r);
    if(l>mid) return query(u<<1|1,l,r);
    node res;
    node left=query(u<<1,l,r);
    node right=query(u<<1|1,l,r);
    pushup(res,left,right);
    return res;
}
int main(){
    cin>>n>>m;
    for(int i=1;i<n;i++){
        cin>>s[i];
    }
    build(1,1,n-1);
    while(m--){
        int hs,ht,ps,pt;
        cin>>hs>>ht>>ps>>pt;
        cout<<query(1,hs,ht-1).mat[ps][pt]<<endl;
    }
    return 0;
}

经典差分性质题目

[NOIP2013]积木大赛
[NOIP2018]道路铺设
两道题目题意相似:都是有不同高度的积木,每次可以给一个区间加1,求最少操作构成积木。

差分性质
在这里插入图片描述
我们给原数组后面补一位0。这样我们发现差分数组的正数之和绝对值和负数之和绝对值相等。
而我们给一段区间加上1,相当于给差分数组一个值+1,一个值-1。那么最小的操作次数自然就为差分数组所有正数元素之和。

void Showball(){
   int n;
   cin>>n;
   vector<int> a(n+2),b(n+2);
   for(int i=1;i<=n;i++) cin>>a[i];
    int ans=0;
   for(int i=1;i<=n+1;i++){
    b[i]=a[i]-a[i-1];
    if(b[i]>0) ans+=b[i];
   }
   cout<<ans<<endl;
}

前缀和变种

前缀和+等差数列
对于一个等差数列 1 2 3 4 5 …
我们对其进行一次差分得到 1 1 1 1 1…
再进行一次差分即1 0 0 0 0 …
所以如果我们想要对于某一段区间加上 1,2,3,4,5,…
我们可以对它的二次差分数组该位加上1,然后求两次前缀和即可

前缀和+平方
同理 对于一个平方数列 1 4 9 16 25 …
我们对其进行一次差分得到 1 3 5 7 9 …
再进行一次差分得到 1 2 2 2 2 2…
再进行一次差分得到 1 1 0 0 0 0 …
所以如果我们想要对于某一段区间加上 1,4,9,16,25,…
我们可以对它的三次差分数组该位和下一位都加上1,然后求三次前缀和即可。

先来一道模板题
小w的糖果
三种操作分别对应普通区间加,区间加等差数列,区间加平方数列。
这里我们构建三个差分数组,然后进行对应操作即可。

#include<bits/stdc++.h>

using namespace std;

#define ff first
#define ss second
#define pb push_back
#define all(u) u.begin(), u.end()
#define endl '\n'
#define debug(x) cout<<#x<<":"<<x<<endl;

typedef pair<int, int> PII;
typedef long long LL;
const int inf = 0x3f3f3f3f;
const int N = 1e5 + 10, M = 105;
const int mod = 1e9 + 7;
const int cases = 1;

int n,m;
LL d1[N],d2[N],d3[N];
void presum(LL d[]){
    for(int i=1;i<=n;i++){
        d[i]+=d[i-1];
        if(d[i]>mod) d[i]-=mod;
    }
}
void Showball(){
    cin>>n>>m;
    memset(d1,0,sizeof d1);
    memset(d2,0,sizeof d2);
    memset(d3,0,sizeof d3);
    int t,p;
    while(m--){
      cin>>t>>p;
      if(t==1){
        d1[p]++;
      }else if(t==2){
        d2[p]++;
      }else{
        d3[p]++;
        d3[p+1]++;  
      }
    }

    presum(d1);
    presum(d2);
    presum(d2);
    presum(d3);
    presum(d3);
    presum(d3);

    for(int i=1;i<=n;i++){
     cout<<(d1[i]+d2[i]+d3[i]+mod)%mod<<" \n"[i==n];
 }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int T=1;
    if(cases) cin>>T;
    while(T--)
    Showball();
    return 0;
}

牛牛的Link Power I
题意:给你一个只含有0和1的字符串,两个1之间的距离为贡献。请你算出所有的贡献值之和。
举个例子:1010010
答案:2+5+3=10
我们算出每个1对于后面1的贡献,然后想加即可。
我们先看第一个1,然后我们发现第一个1对于后面的贡献分别为1 2 3 4 5 …
由此我们想到了上面讲到的前缀和+等差数列,我们可以对于每一个为1的位置加上一个等差数列。最后进行求前缀和。然后加上每一个1位置的值(也就是对应贡献)。即为答案。

int n;
LL sum[N];
string s;
void presum(){//求前缀和
    for(int i=0;s[i];i++){
        sum[i]+=sum[i-1];
        if(sum[i]>mod) sum[i]-=mod;
    }
}
void Showball(){
    cin>>n;
    cin>>s;
    for(int i=0;s[i];i++){
        if(s[i]=='1') sum[i+1]=1;//根据之前的性质,我们给差分数组的下一个位置+1。
    }

    presum();
    presum();//对差分数组求两次前缀和,得到原数组
    LL ans=0;
    for(int i=0;s[i];i++){
        if(s[i]=='1') ans+=sum[i];//对于等于1的位置,加上对应贡献
        if(ans>mod) ans-=mod;//取模
    }
    cout<<ans<<endl;
}

区间加多项式
之前我们处理的问题都是特殊情况,现在来考虑一般情况。给一段区间上加上一个多项式。我们还是按照之前的方式进行差分。可以发现对于任意一个最高次为k的多项式,我们对其进行k+1次差分,它一定会变成有限几项后面全为0的形式。那么我们就可以维护差分数组,然后求多次前缀和即可。

模板题:智乃酱的静态数组维护问题多项式

#include<bits/stdc++.h>

using namespace std;

#define ff first
#define ss second
#define pb push_back
#define all(u) u.begin(), u.end()
#define endl '\n'
#define debug(x) cout<<#x<<":"<<x<<endl;

typedef pair<int, int> PII;
typedef long long LL;
const int inf = 0x3f3f3f3f;
const int N = 1e5 + 10, M = 105;
const int mod = 1e9 + 7;
const int cases = 0;

LL a[N],ki[N],coef1[N],coef2[N];
int n,m,q;
/*
在数组a的l,r区间加上一个多项式
然后前缀和。
P函数表示求长度为len的数组a,cnt次前缀和
D函数表示求长度为len的数组a,cnt次差分
f函数:a数组表示多项式系数,k表示最高次项
p函数:其实为-f(x+len)
*/
void P(LL a[],int len,int cnt=1){
  while(cnt--){

    for(int i=1;i<=len;i++){
        a[i]+=a[i-1];
        if(a[i]>=mod) a[i]-=mod;
    }
  }
}

void D(LL a[],int len,int cnt=1){
   while(cnt--){

    for(int i=len;i;i--){
        a[i]-=a[i-1];
        if(a[i]<0) a[i]+=mod;
    }
   }
}

LL f(int x,LL a[],int k){
    LL res=0;
    LL base=1;
    for(int i=k;i>=0;i--){
       res+=base*a[i]%mod;
       if(res>=mod) res-=mod;
       base=base*x%mod;
    }
    return res;
}

LL g(int x,LL a[],int k,int l,int r){
    return (mod-f(x+r-l+1,a,k))%mod;
}
void Showball(){
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++) cin>>a[i];

    D(a,n,6);
    int l,r,k;
    while(m--){
      cin>>l>>r>>k;
      for(int i=0;i<=k;i++){
        cin>>ki[i];
      }

      for(int i=1;i<=10;i++){
        coef1[i]=f(i,ki,k);
        coef2[i]=g(i,ki,k,l,r);
      }
     
     D(coef1,10,6);
     D(coef2,10,6);
     
     for(int i=1;i<=10;i++){
        a[l+i-1]+=coef1[i];
        if(a[l+i-1]>=mod) a[l+i-1]-=mod;
        a[r+i]+=coef2[i];
        if(a[r+i]>=mod) a[r+i]-=mod;
     }
    }

    P(a,n,7);
    while(q--){
        int l,r;
        cin>>l>>r;
        cout<<(a[r]-a[l-1]+mod)%mod<<endl;
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int T=1;
    if(cases) cin>>T;
    while(T--)
    Showball();
    return 0;
}

高次前缀和

快速的求k次前缀和,当k很大时,就需要用一些高级的东西进行优化,这里用的是卷积进行优化。原理尚不是很明白,先贴一个板子。
智乃酱的前缀和与差分

#include<bits/stdc++.h>
using namespace std;
namespace NTT
{
const long long g = 3;
const long long p = 998244353;
long long wn[35];
long long pow2(long long a, long long b)
{
	long long res = 1;
	while (b)
	{
		if (b & 1) res = res * a % p;
		a = a * a % p;
		b >>= 1;
	}
	return res;
}
void getwn()
{
	for (int i = 0; i < 25; i++) wn[i] = pow2(g, (p - 1) / (1LL << i));
}
void ntt(long long *a, int len, int f)
{
	long long i, j = 0, t, k, w, id;
	for (i = 1; i < len - 1; i++)
	{
		for (t = len; j ^= t >>= 1, ~j & t;);
		if (i < j) swap(a[i], a[j]);
	}
	for (i = 1, id = 1; i < len; i <<= 1, id++)
	{
		t = i << 1;
		for (j = 0; j < len; j += t)
		{
			for (k = 0, w = 1; k < i; k++, w = w * wn[id] % p)
			{
				long long x = a[j + k], y = w * a[j + k + i] % p;
				a[j + k] = (x + y) % p;
				a[j + k + i] = (x - y + p) % p;
			}
		}
	}
	if (f)
	{
		for (i = 1, j = len - 1; i < j; i++, j--) swap(a[i], a[j]);
		long long inv = pow2(len, p - 2);
		for (i = 0; i < len; i++) a[i] = a[i] * inv % p;
	}
}
void mul(long long *a, long long *b, int l1, int l2)
{
	int len, i;
	for (len = 1; len <= l1 + l2; len <<= 1);
	for (i = l1 + 1; i <= len; i++) a[i] = 0;
	for (i = l2 + 1; i <= len; i++) b[i] = 0;
	ntt(a, len, 0); ntt(b, len, 0);
	for (i = 0; i < len; i++) a[i] = a[i] * b[i] % p;
	ntt(a, len, 1);
}
};
const int MAXN = 300005;
const long long mod = 998244353;
int n;
long long a[MAXN], inv[MAXN], ki[MAXN], k;
void init(long long n)
{
	inv[0] = inv[1] = 1;
	for (long long i = 2; i <= n; i++)
	{
		inv[i] = ((mod - mod / i) * inv[mod % i]) % mod;
	}
	return;
}
void get_ki(long long k, int len)
{
	k = (k % mod + mod) % mod;
	ki[0] = 1;
	for (int i = 1; i < len; ++i)
	{
		ki[i] = ki[i - 1] * inv[i] % mod * ((k + i - 1) % mod) % mod;
	}
}
int main()
{
	NTT::getwn();
	init(100000);
	scanf("%d %lld", &n, &k);
	get_ki(k, n);

	for (int i = 0; i < n; ++i)
	{
		scanf("%lld", &a[i]);
	}
	NTT::mul(a, ki, n, n);
	for (int i = 0; i < n; ++i)
	{
		printf("%lld%c", a[i], i == n - 1 ? '\n' : ' ');
	}
	return 0;
}

高维前缀和 (SOSDP)

对于一般的二维前缀和都是采用容斥原理进行计算的。

s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+a[i][j]

但是对于更高维度的就不太好写了。
这里推荐另外一种写法:

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

for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
s[i][j]+=s[i][j-1];

也就是对每一维分别做前缀和
然后配合状态压缩,我们就可以进行高维前缀和了。操作如下:

 for (int i = 0; i < n; ++i)
    {
        for (int j = 0; j < (1<<n); ++j)
        {
            if (j & (1 << i))pre_sum[j] += pre_sum[j ^ (1 << i)];
        }
    }

例题:
智乃酱的子集与超集

#include<bits/stdc++.h>

using namespace std;

#define ff first
#define ss second
#define pb push_back
#define all(u) u.begin(), u.end()
#define endl '\n'
#define debug(x) cout<<#x<<":"<<x<<endl;

typedef pair<int, int> PII;
typedef long long LL;
const int inf = 0x3f3f3f3f;
const int N = 1e5 + 10, M = 105;
const int mod = 1e9 + 7;
const int cases = 0;

int n,m;
LL pre_sum[1<<20],suf_sum[1<<20];
LL a[M];
void Showball(){
    cin>>n>>m;
    LL sum=0;
    for(int i=0;i<n;i++){
       cin>>a[i];
    }
    for(int i=0;i<(1<<n);i++){
        LL sum=0;
        for(int j=0;j<n;j++){
            if((i>>j)&1) sum^=a[j];
        }
        pre_sum[i]=sum;
        suf_sum[i]=sum;
    }

    for(int i=0;i<n;i++){
        for(int j=0;j<(1<<n);j++){
            if(j&(1<<i)) pre_sum[j]+=pre_sum[j^(1<<i)];
            else suf_sum[j]+=suf_sum[j^(1<<i)];
        }
    }
    int k;
    while(m--){
      cin>>k;
      LL p=0;
      for(int i=0;i<k;i++){
         int x;
         cin>>x;
         p|=(1<<(x-1));
      }
      cout<<pre_sum[p]<<" "<<suf_sum[p]<<endl;
    }

}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int T=1;
    if(cases) cin>>T;
    while(T--)
    Showball();
    return 0;
}

完结撒花~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Showball.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值