1 前缀和与差分
导图
智乃酱的子集与超集(SOSdp)
题意描述
()个物品做为一个全集,第个物品价值,一个集合的价值为集合物品价值的异或和。次询问,询问选择其中一些物品 { },它的所有子集价值之和 与 所有全集价值之和。
分析
前置芝士:高维前缀和
以二维为例理解计算方法:
第一步:
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) sum[i][j] = a[i][j];
就是单个点,相当于是0维前缀和
第二步:
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) sum[i][j] += sum[i - 1][j]
此时存了当前列的前缀和,即计算了1维前缀和
第三步:
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) sum[i][j] += sum[i][j - 1];
此时已经完成了2维前缀和的计算
因此可以推广一下这个做法,**求维前缀和**,则从0维开始,每次对一个维度做一次前缀和,相当于起到了一个降维的作用,做了 次就可以求解出 维前缀和了。时间复杂度
代码:
for (i : 空间元素) sum[i] = a[i]; for (i = 1; i <= k; i++) for (j : 空间元素) sum[j] += sum[k] [k的第i维 + 1 = j]
下面就是这题的具体思路了
高维前缀和的一个应用就是 (),常与 结合使用。
在这题中,可以构建一个维空间,每一维长度都是,取值和。每个物品都是一个维度,不选取,选了取。
因此维空间中的某一个点都表示一种物品集合,它的维前缀和就是它所有子集的价值之和,它的维后缀和就是它所有超集的价值之和。
代码:
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int N = 30, S = 1 << 20; int n, m, k; ll a[N], presum[S], sufsum[S]; int main() { scanf("%d%d", &n, &m); for (int i = 0; i < n; i++) scanf("%lld", &a[i]); for (int i = 0; i < 1 << n; i++) { ll sum = 0; for (int j = 0; j < n; j++) if (i & (1 << j)) sum ^= a[j]; presum[i] = sum; sufsum[i] = sum; } for (int i = 0; i < n; i++) for (int j = 0; j < 1 << n; j++) if (j & (1 << i)) presum[j] += presum[j ^ (1 << i)]; else sufsum[j] += sufsum[j ^ (1 << i)]; while (m--) { scanf("%d", &k); int q = 0; for (int i = 1; i <= k; i++) { int x; scanf("%d", &x); q |= 1 << x - 1; } printf("%lld %lld\n", presum[q], sufsum[q]); } return 0; }
智乃酱的前缀和与差分(高阶前缀和)
题意描述
给长度是 的序列 ,求其 次前缀和,前缀和对 取模。
分析
如果序列做前缀和取的模数大与序列的长度,那么前缀和序列会以长度为模数的循环节循环出现。
这里由于模数大于序列长度,可以对负数的 (差分) 取模后转化为前缀和。
NTT卷积求解:
首先对于序列 [] 列出做前缀和后的表格:
序号 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
原序列 | |||||
一次前缀和 | |||||
二次前缀和 | |||||
三次前缀和 | |||||
… |
只保留其中的系数得到:
序号 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
原序列 | 1 | 0 | 0 | 0 | 0 |
一次前缀和 | 1 | 1 | 1 | 1 | 1 |
二次前缀和 | 1 | 2 | 3 | 4 | 5 |
三次前缀和 | 1 | 3 | 6 | 10 | 15 |
… |
首先容易得到一个递推式,第次前缀和序号的系数
这个递推式是有实际含义的:在网格中从(1,0)出发,每次只能向下或向右移动一格,求移动到(k,i)的方案数。
答案就是
因此可以通过组合数递推的方式 得到做 次前缀和后 的系数序列
如果把 都考虑进来,做 次前缀和的结果像下面这样计算(以序列长度为3,做3次前缀和为例):
这时已经可以意识到 序列 次 前缀和的结果就是 序列 与做次前缀和的 系数序列 的卷积。
代码
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int INF = 0x3f3f3f3f; const int mod = 998244353; const int N = 100010; const int G = 3; int a[N << 2], b[N << 2], rev[N << 2]; int inv[N]; void getb(ll k, int n) { k = (k % mod + mod) % mod; b[0] = 1; for (int i = 1; i < n; i++) b[i] = (ll)b[i - 1] * (i + k - 1) % mod * inv[i] % mod; } int qmi(int a, int b, int p) { int ans = 1; for (; b; b >>= 1) { if (b & 1) ans = (ll)ans * a % p; a = (ll)a * a % p; } return ans; } void ntt(int a[], int tot, int sgn, int p) { for (int i = 0; i < tot; i++) if (i < rev[i]) swap(a[i], a[rev[i]]); int g = sgn == 1 ? G : qmi(G, p - 2, p); for (int mid = 1, t = 1; mid < tot; mid <<= 1, t++) { int wn = qmi(g, (p - 1) >> t, p); for (int i = 0; i < tot; i += mid << 1) { int w = 1; for (int j = 0; j < mid; j++, w = (ll)w * wn % p) { int x = a[i + j], y = (ll)w * a[i + mid + j] % p; a[i + j] = (x + y) % p; a[i + mid + j] = ((x - y) % p + p) % p; } } } if (sgn == -1) { int invtot = qmi(tot, p - 2, p); for (int i = 0; i < tot; i++) a[i] = (ll)a[i] * invtot % p; } } int main() { inv[1] = 1; for (int i = 2; i < N; i++) inv[i] = (ll)(mod - mod / i) * inv[mod % i] % mod; int n; ll k; scanf("%d%lld", &n, &k); for (int i = 0; i < n; i++) scanf("%d", &a[i]); getb(k, n); n--; int tot = 1, bit = 0; while (tot < n + n + 1) tot <<= 1, ++bit; for (int i = 0; i < tot; i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1)); ntt(a, tot, 1, mod), ntt(b, tot, 1, mod); for (int i = 0; i < tot; i++) a[i] = (ll)a[i] * b[i] % mod; ntt(a, tot, -1, mod); n++; for (int i = 0; i < n; i++) printf("%d%c", a[i], i == n - 1 ? '\n' : ' '); return 0; }
智乃酱的静态数组维护问题多项式(多项式前缀和)
题意描述:
对于一个序列,每次选择 [l, r] 加上一个多项式
(即 )
最后询问 [l, r] 的元素之和。
分析:
数学定理:最高次项为 n 次的 n 阶多项式做 n + 1 阶差分后余项为常数(常数0)。
可以将原数组做 次差分,然后对差分数组做有限次修改,最后前缀和求回来即可。
代码:
#include <bits/stdc++.h> using namespace std; #define fi first #define se second #define pii pair<int,int> #define pll pair<ll,ll> #define pli pair<ll,int> #define Min(a,b,c) min(a,min(b,c)) #define Max(a,b,c) max(a,max(b,c)) typedef long long ll; typedef unsigned long long ull; const double pi = 3.141592653589793; const double eps = 1e-8; const int INF = 0x3f3f3f3f; const int N = 100010; const ll mod = 1000000007; ll a[N], p[10], f1[10], f2[10]; void S(ll a[], int n, int t) { while (t--) { for (int i = 1; i <= n; i++) a[i] = (a[i - 1] + a[i]) % mod; } } void D(ll a[], int n, int t) { while (t--) { for (int i = n; i >= 1; i--) a[i] = (a[i] - a[i - 1]) % mod; } } ll f(ll x, ll p[], int k) { ll base = 1, ans = 0; for (int i = 0; i <= k; i++) { ans = (ans + p[i] * base % mod) % mod; base = base * x % mod; } return ans; } int main() { int n, m, q; scanf("%d%d%d", &n, &m, &q); for (int i = 1; i <= n; i++) scanf("%lld", &a[i]); D(a, n, 6); while (m--) { int l, r, k; scanf("%d%d%d", &l, &r, &k); for (int i = k; i >= 0; i--) scanf("%lld", &p[i]); for (int i = 1; i <= 6; i++) { f1[i] = f(i, p, k); f2[i] = (mod - f(i + r - l + 1, p, k)) % mod; } D(f1, 6, 6); D(f2, 6, 6); for (int i = 1; i <= 6; i++) { a[l + i - 1] = (a[l + i - 1] + f1[i]) % mod; a[r + i] = (a[r + i] + f2[i]) % mod; } } S(a, n, 7); while (q--) { int l, r; scanf("%d%d", &l, &r); printf("%d\n", ((a[r] - a[l - 1]) % mod + mod) % mod); } return 0; }
智乃酱的双塔问题(DP、前缀和、矩阵)
题意描述
左右有两个高度为n个塔,对于每个塔可以从下一层直接到上一层。对于每层(不包括顶层)额外有一个楼梯要么是从左塔上到右塔,要么是从右塔往上走一层到左塔。
每次询问从左边或右边塔的第 i 层走到左边或右边的第 j 层的方案数(i < j)。
分析
首先想到,表示左边的塔从第一层走到第层的方案数,表示右边的塔从第一层走到第层的方案数。那么就有转移方程:
$$
因此从某一层到某一层的方案数,就是这一段矩阵的连乘积。
因此可以预处理出矩阵的前缀和,因为两种可能出现的矩阵都是可逆矩阵,最后为了消除左半部分影响,乘一下左半部分矩阵的逆即可。(要注意是左乘)
代码
#include <bits/stdc++.h> using namespace std; #define fi first #define se second #define pii pair<int,int> #define pll pair<ll,ll> #define pli pair<ll,int> #define Min(a,b,c) min(a,min(b,c)) #define Max(a,b,c) max(a,max(b,c)) typedef long long ll; typedef unsigned long long ull; const double pi = 3.141592653589793; const double eps = 1e-8; const int INF = 0x3f3f3f3f; const int N = 100010; const ll mod = 1000000007; struct mat { ll a[2][2]; mat() {memset(a, 0, sizeof(a));} mat(int a1, int a2, int a3, int a4) { a[0][0] = a1; a[0][1] = a2; a[1][0] = a3; a[1][1] = a4; } }; int n, m; char s[N]; mat sum[N], A(1, 1, 0, 1), B(1, 0, 1, 1); ll qmi(ll a, ll b) { ll ans = 1; for ( ; b; b >>= 1) { if (b & 1) ans = ans * a % mod; a = a * a % mod; } return ans; } ll inv(ll x) { return qmi(x, mod - 2); } void mul(ll a[][2], ll b[][2]) { ll c[2][2]; memset(c, 0, sizeof(c)); for (int i = 0; i < 2; i++) for (int j = 0; j < 2; j++) for (int k = 0; k < 2; k++) c[i][j] = (c[i][j] + a[i][k] * b[k][j] % mod) % mod; memcpy(a, c, sizeof(c)); } void inv(ll a[][2]) { int n = 2, is[2], js[2]; memset(is, 0, sizeof(is)); memset(js, 0, sizeof(js)); for (int k = 0; k < n; k++) { for (int i = k, j; i < n; i++) { for (j = k; j < n && !a[i][j]; j++); is[k] = i, js[k] = j; } for (int i = 0; i < n; i++) swap(a[k][i], a[is[k]][i]); for (int i = 0; i < n; i++) swap(a[i][k], a[i][js[k]]); if (!a[k][k]) { puts("No Solution"); exit(0); } a[k][k] = inv(a[k][k]); for (int j = 0; j < n; j++) if (j != k) (a[k][j] *= a[k][k]) %= mod; for (int i = 0; i < n; i++) if (i != k) { ll temp = a[i][k]; a[i][k] = 0; for(int j = 0; j < n; ++j) (a[i][j] += mod - a[k][j] * temp % mod) %= mod; } } for (int k = n - 1; k >= 0; k--) { for (int i = 0; i < n; i++) swap(a[js[k]][i], a[k][i]); for (int i = 0; i < n; i++) swap(a[i][is[k]],a[i][k]); } } int main() { scanf("%d%d", &n, &m); scanf("%s", s + 1); sum[0] = mat(1, 0, 0, 1); for (int i = 1; i < n; i++) { sum[i] = sum[i - 1]; if (s[i] == '/') mul(sum[i].a, A.a); else mul(sum[i].a, B.a); } while (m--) { int l, r, pl, pr; scanf("%d%d%d%d", &l, &r, &pl, &pr); mat ans = sum[l - 1]; inv(ans.a); mul(ans.a, sum[r - 1].a); printf("%d\n", ans.a[pl][pr]); } return 0; }
牛牛的猜球游戏(置换前缀和)
题意描述
给定1~10的排列,有一个长度是的操作序列,序列中每个操作是交换排列中的两个元素。接下来有m次询问,每次询问,对于 ,运用 l ~ r 的操作序列,排列是什么样的。
分析
一系列操作,每次操作后会得到一个新的排列,维护前缀排列即可。
代码
#include <bits/stdc++.h> using namespace std; typedef long long ll; typedef unsigned long long ull; const double pi = 3.141592653589793; const double eps = 1e-8; const int INF = 0x3f3f3f3f; const int N = 100010; int a[N][10]; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 0; i < 10; i++) a[0][i] = i; for (int i = 1; i <= n; i++) { int x, y; scanf("%d%d", &x, &y); for (int j = 0; j < 10; j++) a[i][j] = a[i - 1][j]; swap(a[i][x], a[i][y]); } while (m--) { int l, r; scanf("%d%d", &l, &r); int t[10]; for (int i = 0; i < 10; i++) t[a[l - 1][i]] = i; for (int i = 0; i < 10; i++) printf("%d ", t[a[r][i]]); puts(""); } return 0; }
[NOIP2013]积木大赛(差分,经典题)
题意描述
给定长度是n的序列a,序列元素。还有一个同样长度全0的b序列,每次可以选择b序列一段区间 [l, r],让区间中每个元素加1。问最少做多少次操作让b=a。
分析
考虑用差分数组实现区间修改,如果要让 [l, r] 加1,就是在差分数组中 d[l]+=1, d[r] -= 1。
那么对序列a做一次差分,序列中正数之和是等于负数之和的绝对值的。
例如:
a 3 5 2 1 4
d 3 2 -3 -1 3 -4
那么最后的答案就等于差分序列中正数之和或者负数之和的绝对值。
代码
#include <bits/stdc++.h> using namespace std; #define fi first #define se second #define pii pair<int,int> #define pll pair<ll,ll> #define pli pair<ll,int> #define Min(a,b,c) min(a,min(b,c)) #define Max(a,b,c) max(a,max(b,c)) typedef long long ll; typedef unsigned long long ull; const double pi = 3.141592653589793; const double eps = 1e-8; const int INF = 0x3f3f3f3f; const int N = 100010; int a[N]; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) scanf("%d", &a[i]); int ans = 0; for (int i = n; i >= 1; i--) { a[i] = a[i] - a[i - 1]; ans += a[i] > 0 ? a[i] : 0; } printf("%d\n", ans); return 0; }