前缀和及其变形

本文探讨了前缀和的基本概念,如何通过前缀和快速计算区间和,并介绍了取模的前缀积和其在实际问题中的应用,如猜球游戏、糖果分配和多项式计算。还涵盖了广义前缀和的概念,以及如何通过差分和状态压缩处理复杂问题,如智乃的多项式和子集超集问题。

前缀和及其变形

前缀和

对于一般的前缀和来说,是方便求一个区间内任意区间和的一个操作,

对于
a 1 , a 2 , a 3 . . . . . . a n a_1,a_2,a_3......a_n a1,a2,a3......an
创建另一个前缀和数组
s u m 0 = 0 , s u m 1 = a 1 , s u m 2 = a 1 + a 2 . . . . . . , s u m n = a n + a n − 1 + a n − 2 . . . . . . a 1 sum_0=0,sum_1=a_1,sum_2=a_1+a_2......,sum_n=a_n+a_{n-1}+a_{n-2}......a_1 sum0=0,sum1=a1,sum2=a1+a2......,sumn=an+an1+an2......a1
通过观察我们可以发现,对于
s u m i = s u m i − 1 + a i sum_i=sum_{i-1}+a_i sumi=sumi1+ai
那么这个前缀和数组就可以表示成
s u m 0 = 0 , s u m 1 = a 1 + s u m 0 , . . . , s u m i = a i + s u m i − 1 , . . . sum_0=0,sum_1=a_1+sum_0, ... ,sum_i=a_i+sum_{i-1},... sum0=0,sum1=a1+sum0,...,sumi=ai+sumi1,...
要求给定区间和
a l + a l + 1 + a l + 2 + . . . . . . + a r a_l+a_{l+1}+a_{l+2}+......+a_r al+al+1+al+2+......+ar
同样我们可以发现
s u m l − 1 = a 1 + a 2 + . . . . . + a l − 1 , s u m r = a 1 + a 2 + a 3 + . . . . . . + a r sum_{l-1}=a_1+a_2+.....+a_{l-1},sum_r=a_1+a_2+a_3+......+a_r suml1=a1+a2+.....+al1,sumr=a1+a2+a3+......+ar
区间和就可以表示成
s u m r − s u m l − 1 sum_r-sum_{l-1} sumrsuml1

取模的前缀积

前置知识

费马小定理

1.对于给定的一个素数P

2.存在p个余数类: 0 , 1 , 2 , 3 , . . . . . . , p − 1 {0 , 1 , 2 , 3 , ...... , p-1} 0,1,2,3,......,p1

3.同余, 表示两个数mod同一个数后值相等 7 % 4 = 3 ,   11 % 4 = 3 7 \%4=3,\ 11\%4=3 7%4=3, 11%4=3,就称7和11是对4同余的

4.性质:

如果存在 a = b ( m o d   p ) a=b(mod\ p) a=b(mod p)以及 c = d ( m o d   p ) c=d(mod\ p) c=d(mod p)此时的等于表示同余关系

那么就有 ( a − b ) / p = z ( z 为 整 数 ) (a-b)/p=z(z为整数) (ab)/p=z(z)同理可以推出 ( c − d ) / p = z ( z 为 整 数 ) (c-d)/p=z(z为整数) (cd)/p=z(z)

就有 ( ( a + c ) − ( b + d ) ) / p = z ( z 为 整 数 ) ((a+c)-(b+d))/p=z(z为整数) ((a+c)(b+d))/p=z(z),就可以推出 a + c = b + d ( m o d   p ) a+c=b+d(mod\ p) a+c=b+d(mod p)

同理,我们还有 a − c = b − d ( m o d   p ) ,   a ∗ c = b ∗ d ( m o d   p ) ,   并 且   a n = b n ( m o d   p ) a-c=b-d(mod\ p),\ a*c=b*d(mod\ p),\ 并且\ a^n=b^n(mod\ p) ac=bd(mod p), ac=bd(mod p),  an=bn(mod p)

(加法,减法和乘法对于取模都是封闭的)

5.同余的逆:

对于一个非零的数a,如果这个数不能被p整除,即 a   m o d   p ≠ 0 a\ mod\ p \ne 0 a mod p=0

那么一定存在一组数b使得 a ∗ b   m o d   p = 1 a*b\ mod\ p=1 ab mod p=1

在同余的条件下,我们称ba的逆

  1. Fermat小定理:

对于所有互素的ap都存在 a p − 1 = 1 ( m o d   p ) a^{p-1}=1(mod\ p) ap1=1(mod p)

那么对于一个数a,与p互质,则存在一个b使得 a ∗ b = 1 ( m o d   p ) a*b=1(mod\ p) ab=1(mod p),由Fermat小定理可得, b = a p − 2 b=a^{p-2} b=ap2

所以,若a和p互质,就有a的逆为 a p − 2 a^{p-2} ap2.

前缀积

和前缀和类似,前缀积也可以用除法来实现O(1)查询某一段的积的作用

这个时候我们就要用Fermat小定理中的内容将除法转化为求逆元的操作,对于余数群来讲,是等价的,

由此我们可以用
a 1 , a 2 , a 3 , . . . . . . , a n a_1,a_2,a_3,......,a_n a1,a2,a3,......,an
来表示原数组,用
b 1 , b 2 , b 3 . . . . . . b n b_1,b_2,b_3......b_n b1,b2,b3......bn
来表示前缀积数组

对于某一个数的前缀积来讲,就有
b 0 = 1 , b 1 = a 1 ∗ b 0 , . . . . b i = a i ∗ b i − 1 , . . . . . . b_0=1,b_1=a_1*b_0,....b_i=a_i*b_{i-1},...... b0=1,b1=a1b0,....bi=aibi1,......
对于某一段的前缀积来讲就有
s u m = b r / b l − 1 , b l − 1 = b l − 1 p − 1 s u m = b r ∗ b l − 1 p − 2 sum=b_r/b_{l-1},\\b_{l-1}=b_{l-1}^{p-1}\\sum=b_r*b_{l-1}^{p-2} sum=br/bl1,bl1=bl1p1sum=brbl1p2

广义的前缀和

对于广义的前缀和来讲,是指前缀对后面产生了影响。同样的,我们也可以根据题意,对它进行一个反向的操作消除前面的影响,就像前缀和 a r − a l − 1 a_r-a_{l-1} aral1,前缀积 a r ∗ a l − 1 p − 2 a_r*a_{l-1}^{p-2} aral1p2,一样,如果一个操作对后面所有的都会产生影响,并且我们需要查询任意一段区间的影响时,我们就可以通过广义前缀和的思想,通过一个反向操作取消前面的操作对结果的影响。从而得到某一区间的影响

传送门

牛牛的猜球游戏

题意

有0~9十个小球分别装在十个瓶子中,有n次操作,每次操作选取其中两个瓶子并交换它们的位置,有q次询问,进行

l~r的操作,结果是多少

题解

既然这道题已经指明了是用前缀和来思考,那么我们怎么样将它转化为前缀和问题呢

首先我们可以发现,它的某一个阶段的结果都是由前面的操作影响而来,并且这些影响相互叠加,我们求某一段的影响,就可以用 s u m r sum_r sumr去掉前面 s u m l − 1 sum_{l-1} suml1的影响,由前面广义前缀和的定义,我们就可以将这个问题转化为前缀和问题了

那么现在的问题就转化为怎么样消除影响了,假设

0 0 1 2 3 4 5 6 7 8 9

在经过若干次变化后变成了

​ 1 7 9 8 6 4 5 1 3 2 0

又经过若干次变化变成了

​ 2 6 7 9 8 3 4 1 2 0 5

如果我们这个时候是求**12**的影响,那么我们只需要将0式子进行12的变换,也就是将0-9以此填入1式子中的瓶子中,然后再2中的这些瓶子中的小球顺序,就是所求的小球顺序

#include<bits/stdc++.h>

using namespace std;

#define debug cout<<"**"<<endl;
#define endl '\n'
#define _ 0
//#define int long long

//const double PI = cos(-1);
const int N = 1e5 + 10;
//const int mod = 10007;

vector<vector<int>>a(N, vector<int>(10));

void swp(int u,int v,int i) {
	a[i] = a[i - 1];
	swap(a[i][u], a[i][v]);
}

void print(int l, int r) {
	int c[10],b[10];
	for (int i = 0; i < 10; i++)b[a[l][i]] = i;
	for (int i = 0; i < 10; i++)c[i] = b[a[r][i]];
	for (int i = 0; i < 10; i++)cout << c[i] << " ";
	cout << endl;
}

void solve() {
	int n, m;
	cin >> n >> m; 
	for (int i = 0; i < 10; i++)a[0][i] = i;
	for (int i = 1; i <= n; i++) {
		int u, v;
		cin >> u >> v;
		swp(u, v, i);
	}
	int l, r;
	while (m--) {
		cin >> l >> r;
		print(l - 1, r);
	}
}

signed main() {
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	int T = 1; 
	//cin >> T;
	while (T--)solve();
	return ~~(0 ^ _ ^ 0);
}

补充前缀知识,矩阵

差分

计算贡献

1.一个区间加上同一个值(一维差分)

2.一个区间加上一段连续的值(二维差分)

3.一个区间内加上一段连续的值的平方(三维差分)

一般的差分(求多项式)

数学定理:任何一个n次多项式在做了n+1次差分之后都会变成一个常数

所以对于一个多项式,我们知道它的最高次项k后, 我们就可以做k+1次差分将这个序列变成一个常数, 然后再通过k+1次的前缀和求出它的值,但是这种只适用于指数较小的时候,指数大了之后这样就不是最优了。

当我们对一个比较低阶的多项式求和,类似 ∑ 1 n k 1 x 3 + k 2 x 2 + k 3 x + k 4 \sum_1^nk_1x^3+k_2x^2+k3x+k4 1nk1x3+k2x2+k3x+k4的时候,如果我们对于这个式子求四次差分,我们就可以讲它变成一个特殊的序列,这个序列是只有前面k位是非零,其他剩下的都是零

小w的糖果

题意

小w和他的两位队友 t e i t o 、 t o k i t s u k a z e teito、tokitsukaze teitotokitsukaze,他们让n个小朋友们排成一长排并且从左到右依次标号为1,2,3,4,5,6,7,8,9…n。三个人同时从 p o s pos pos开始将糖果发给 p o s pos pos及之后的人

t o k i t s u k a z e tokitsukaze tokitsukaze,她将会从一个位置 p o s pos pos开始,依次向右给每个人1个糖果。

t e i t o teito teito,他将会从一个位置 p o s pos pos开始,依次向右,他将会给他碰到的第一个人发1个糖果,给他碰到的第二个人发2个糖果,给他碰到的第三个人发3个糖果…碰到的第k个人发k个糖果,直到向右走到编号为n的人为止。

w i n t e r z z 1 winterzz1 winterzz1,他将会从一个位置 p o s pos pos开始,依次向右,它将会给他碰到的第一个人发1个糖果,给他碰到的第二个人发4个糖果,给他碰到的第三个人发9个糖果…碰到的第k个人发 k 2 k^2 k2个糖果直到向右走到编号为n的人为止。

发糖的福利一共进行了m轮,现在告诉你这m轮发糖的人和他们该轮发糖的起始位置 p o s pos pos,请你告诉我这m轮发糖结束后1到n每个人手中糖果的数量,为了避免这个数字过大,你只用输出每一个人手中糖的数量 m o d     1 0 9 + 7 mod  10^9+7 mod109+7后的结果即可。

题解

t o k i t s u k a z e tokitsukaze tokitsukaze是将后面所有人手上的糖果都加上一, t e i t o teito teito是将后面所有人,第i个人手上的糖果增加i个, w i n t e r z z 1 winterzz1 winterzz1则是将后面所有人,第i个人手上的糖果增加 i 2 i^2 i2

对于 t o k i t s u k a z e tokitsukaze tokitsukaze的方式,我们只需要将 p o s pos pos后面的都加上一就可以了,这个时候我们可以用一个差分来维护这个区间加的操作,然后是对于 t e i t o teito teito来讲,他发糖果的方式就像 f ( i ) = i f(i)=i f(i)=i一样,我们只需要进行两次差分,就可以得出一个 1 , 0 , 0 , 0... 1,0,0,0... 1,0,0,0...的序列,在后面求的时候我们也只需要对他进行两次前缀和,然后就可以加上了,最后是对于 w i n t e r z z 1 winterzz1 winterzz1来讲,他发糖果的方式是 f ( i ) = i 2 f(i)=i^2 f(i)=i2, 这个时候我们只需要进行对它进行三次差分,我们就可以得到 1 , 1 , 0 , 0 , 0 , 0 , 0... 1,1,0,0,0,0,0... 1,1,0,0,0,0,0...这样的序列,最后对他进行三次前缀和我们就可以得到加到每一个人身上的糖果数量了。

#include<bits/stdc++.h>

using namespace std;

#define debug cout<<"**"<<endl;
#define endl '\n'
#define _ 0
//#define int long long

//const double PI = cos(-1);
const int N = 1e5 + 10;
const int mod = 1e9 + 7;
int n, m, L, R;
int type, pos;
int cf1[N],cf2[N],cf3[N];

void add(int a[]) {for (int i = 1; i <= n; i++)a[i] = (a[i] + a[i - 1]) % mod;}

void solve() {
	cin >> n >> m ;
    for (int i = 1; i <= n; i++)cf1[i] = 0, cf2[i] = 0, cf3[i] = 0;
	while (m--) {
        cin >> type >> pos;
		if (type == 1) cf1[pos]++;
		else if (type == 2) cf2[pos]++;
		else {
			cf3[pos]++;
			cf3[pos + 1]++;
		}
	}
	add(cf1), add(cf2), add(cf2), add(cf3), add(cf3), add(cf3);
	for (int i = 1; i <= n; i++)cout << (cf1[i] + cf2[i] + cf3[i]) % mod << " ";
	cout << endl;
}

signed main() {
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int T = 1;
	cin >> T;
	while (T--)solve();
	return ~~(0 ^ _ ^ 0);
}

智乃的多项式

题意

接下来会进行M{M}M次修改操作,每次将会给你一个k次多项式 f ( x ) = c 0 x k + c 1 x k − 1 + . . . + c k − 1 x 1 + c k f(x)=c_0x^k+c_1x^{k−1}+...+c_{k−1}x^1+c_k f(x)=c0xk+c1xk1+...+ck1x1+ck,以及一段需要操作的目标数组区间 [ l , r ] [l,r] [l,r]

然后你需要对 a l + f ( 1 ) , a l + 1 + f ( 2 ) , . . . a_l+f(1),a_{l+1}+f(2),... al+f(1),al+1+f(2),...以此类推,也就是对目标区间的第一个数字加上一个f(1),第二个数字加上f(2)…

最后进行Q次查询,每次查询一个区间 [ l , r ] [l,r] [l,r]的和,由于数很大,所以我们需要对 1 0 9 + 7 10^9+7 109+7取模

题解

如果我们对一个k次的多项式进行 k + 1 k+1 k+1次差分之后,我们会得到一个前k项非零,其他项都为零的一个序列,那么我们再进行一次差分,我们会发现这个序列除了k之后第一个数变成不是零的数以外,其他的都是零,也就是说,我们对于一个k次的多项式,再进行了 k + 1 k+1 k+1以上的差分,我们会发现实际上并不会对我们的序列造成很大的影响。

那么回到这道题,给出的多项式的最高次是五次,那么我们可以直接对每一个多项式进行6次差分,然后将得到的序列加在数列中,需要注意的是,由于给定了范围,那么我们需要一种操作来消除对于范围之外的影响,那么这个时候我们就可以定义另外一个函数 g ( x ) g(x) g(x)来抵消范围之外的影响。最后对每一个序列进行7次前缀和,前六次是为了得到序列,最后一次就是简单的求和。

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

#define _ 0
#define int long long
const int N = 1e5 + 10;
const int inf = 2147483647;
const int mod = 1e9 + 7;
typedef pair<int, int> PII;
int n, m, q;
int f1[N], f2[N], a[N];
int ka[20];

void D(int size, int x[], int k = 1) {
    while (k--) {
        for (int i = size; i; i--) {
            x[i] -= x[i - 1];
            if (x[i] < 0)x[i] += mod;
        }
    }
}//差分

void P(int size, int x[], int k = 1) {
    while (k--) {
        for (int i = 1; i <= size; i++) {
            x[i] += x[i - 1];
            if (x[i] >= mod)x[i] -= mod;
        }
    }
}//前缀和

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

int g(int x, int c[], int k, int l, int r) {
    int y = (mod - f(x + r - l + 1, c, k)) % mod;
    return y;
}

void solve() {
    cin >> n >> m >> q;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    D(n, a, 6);
    while (m--) {
        int l, r, k;
        cin >> l >> r >> k;
        for (int i = 0; i <= k; i++) {
            cin >> ka[i];
        }
        for (int i = 1; i <= 10; i++) {
            f1[i] = f(i, ka, k);
            f2[i] = g(i, ka, k, l, r);
        }
        
        D(10, f1, 6);
        D(10, f2, 6);
        for (int i = 1; i <= 10; i++) {
            a[i + l - 1] += f1[i];
            a[i + l - 1] = a[i + l - 1] % mod;
            a[i + r] += f2[i];
            a[i + r] = a[i + r] % mod;
        }
    }
    P(n, a, 7);
    while (q--) {
        int l, r;
        cin >> l >> r;
        cout << ((a[r] - a[l - 1]) % mod + mod) % mod << endl;
    }
}


signed main() {
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	int T = 1;
	//cin >> T;
	while (T--)solve();
	return ~~(_^0   ^0);
}

智乃酱的子集与超集

前置知识

S O S d p SOSdp SOSdp

S O S d p SOSdp SOSdp和状态压缩比较像,都是将一个物体的集合用二进制来表示选或者不选,而如何用这个东西枚举出所有的子集呢

首先我们可以发现,我们可以通过枚举这个数二进制上所有的一,那么它一定是由它本身和另一个当前位位零,其它位全部相同的数转移而来,

比如10100,搜先我们枚举它的第一个一 ,它是由10100和00100转移而来,然后我们枚举第二个一,它是由10100和10000转移而来,而00100又是由00000和00100转移而来,因为我们每次枚举都会将此位为一的变成零然后转移,所以是不会重复的,并且对于每一个一我们都会枚举此位为零或者为一的状态,所以我们能做到不重不漏的全部枚举完。

超集与子集相反,是枚举它为零的状态

题意

给出N个数,表示物品的价值,然后给出M个询问,每个询问里面包括k个数,然后给出这些物品的编号,要我们求出包含这些物品的集合U,它的子集之和和它的超集之和

题解

我们可以先预处理出每一个位置的价格异或之和,然后处理出每一个位置的子集和超集之和,最后按照询问输出

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

#define _ 0
#define ll long long
//#define int long long
const int N = 1e5 + 10;
const int inf = 2147483647;
const int mod = 1e9 + 7;
long long pre_sum[1 << 21], sol_sum[1 << 21], a[25];
int n, m;

void solve() {
	cin >> n >> m;
	int maxbit = 1 << n;
	for (int i = 0; i < n; i++) {
		cin >> a[i];
	}
	for (int i = 0; i < maxbit; i++) {
		ll sum = 0;
		for (int j = 0; j < n; j++) {
			if (i & (1 << j))sum ^= a[j];
		}
		pre_sum[i] = sum;
		sol_sum[i] = sum;
	}
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < maxbit; j++) {
			if (j & (1 << i)) pre_sum[j] += pre_sum[j ^ (1 << i)];
			else sol_sum[j] += sol_sum[j ^ (1 << i)];
 		}
	}
	while (m--) {
		int nx;
		cin >> nx;
		int nin = 0;
		for (int i = 1; i <= nx; i++) {
			int x;
			cin >> x;
			nin |= (1 << (x - 1));
		}
		cout << pre_sum[nin] << ' ' << sol_sum[nin] << endl;
	}
}


signed main() {
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	int T = 1;
	//cin >> T;
	while (T--)solve();
	return ~~(_^0   ^0);
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值