文章目录
数据结构相关知识点
第一章 前缀和与差分
笔记
-
前缀和:
sum[i] = sum[i - 1] + a[i]
,故a[l] + ... + a[r] = sum[r] - sum[l - 1]
- 适用:
- 乘法:例题A:智乃酱的区间乘积(注意:除法要用逆元)
- 其他操作:连续若干操作产生的叠加影响可通过某种反向操作”撤销“
- 整体转移:例题F:牛牛的猜球游戏
- 满秩矩阵的链乘法:例题E:智乃酱的双塔问题
- 卷积
- 适用:
-
差分:
d[i] = a[i] - a[i - 1]
,故a[i] = a[i - 1] + d[i]
- 差分数组:操作
d
l
+
x
,
d
r
+
1
−
x
d_l+x,d_{r+1}-x
dl+x,dr+1−x,前缀和后原数组在
l
∼
r
l\sim r
l∼r 均
+
x
+x
+x
-
变形:加多项式
前提:最高次项为 n n n 的 n n n 阶多项式做 n + 1 n+1 n+1 阶差分后余项为常数。
-
- 差分数组:操作
d
l
+
x
,
d
r
+
1
−
x
d_l+x,d_{r+1}-x
dl+x,dr+1−x,前缀和后原数组在
l
∼
r
l\sim r
l∼r 均
+
x
+x
+x
-
高阶前缀和:组合数贡献
-
二维前缀和:
sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + a[i][j]
-
高维(k维)前缀和 SOSDP (Sum over Subsets):子集前缀和,例题B:智乃酱的子集与超集
-
容斥: O ( 2 k n ) O(2^kn) O(2kn)
sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + a[i][j]
-
SOSDP: O ( ( k + 1 ) n ) O((k+1)n) O((k+1)n)
rep(i, 1, n): sum[i][j] = a[i][j]; rep(i, 1, n): sum[i][j] += sum[i - 1][j]; rep(i, 1, n): sum[i][j] += sum[i][j - 1];
-
-
-
总结:
习题
地址:第一章习题
A. 智乃酱的区间乘积
-
题目链接:智乃酱的区间乘积
-
思路:求区间连续乘积,注意 − p r e m u l [ l − 1 ] -premul[l - 1] −premul[l−1] 时要用逆元
#include<bits/stdc++.h> using namespace std; #define rep(i, a, b) for(int i = (a); i <= (b); i++) #define per(i, a, b) for(int i = (a); i >= (b); i--) #define ll long long #define db double #define VI vector<int> #define PII pair<int, int> const db Pi = 3.141592653589793; const int INF = 0x7fffffff; const int N = 1e5 + 5; const db eps = 1e-10; const int mod = 1e9 + 7; int cas, n, m, l, r; ll a[N], premul[N]; ll inv(ll i){ if(i == 1) return 1; return (mod - mod / i) * inv(mod % i) % mod; } int main(){ cin >> n >> m; premul[0] = 1; rep(i, 1, n){ cin >> a[i]; premul[i] = (premul[i - 1] * a[i]) % mod; } while(m--){ cin >> l >> r; cout << (premul[r] * inv(premul[l - 1])) % mod << endl; } } /* 5 3 5 2 3 10 6 1 5 2 3 2 5 */
B. 智乃酱的子集与超集
-
题目链接:智乃酱的子集与超集
-
类型:高阶前缀和
-
思路:一共 20 20 20 个物品,显然需要状态压缩来表示。我们考虑每个组成集合的异或和,再处理出每个集合的子集,超集的异或和即可。
- 对于一共 2 20 2^{20} 220 个集合,逐一计算出其集合价值,即异或和
- 利用高阶前缀和求解(SOSDP做法):对于每个集合的子集,从低位到高位依次考虑每个集合的由来。通过对当前位已有物品的忽略,由缺少状态来计算当前状态。
- 超集同理
- 询问时,由物品添加为所求集合即可
#include<bits/stdc++.h> using namespace std; #define rep(i, a, b) for(int i = (a); i <= (b); i++) #define per(i, a, b) for(int i = (a); i >= (b); i--) #define ll long long #define db double #define VI vector<int> #define PII pair<int, int> const db Pi = 3.141592653589793; const int INF = 0x7fffffff; const int N = 21; const db eps = 1e-10; int n, m, k, maxbit, p; ll a[N], Xor[1 << N], pre_sum[1 << N], suf_sum[1 << N]; void init(){ //预处理 maxbit = (1 << n) - 1; //状压计算各集合异或和结果 rep(bit, 0, maxbit){ rep(i, 0, n - 1){ if(bit & (1 << i)) Xor[bit] ^= a[i]; } pre_sum[bit] = suf_sum[bit] = Xor[bit]; } rep(i, 0, n - 1){ rep(bit, 0, maxbit){ //二进制位为1,表示要选 if(bit & (1 << i)) pre_sum[bit] += pre_sum[bit ^ (1 << i)]; //二进制位为0,表示不选 else suf_sum[bit] += suf_sum[bit ^ (1 << i)]; } } } int main(){ ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); cin >> n >> m; rep(i, 0, n - 1) cin >> a[i]; init(); while(m--){ cin >> k; int nowbit = 0; rep(i, 1, k){ cin >> p; nowbit += (1 << (p - 1)); } cout << pre_sum[nowbit] << " " << suf_sum[nowbit] << endl; } } /* 3 5 1 5 9 1 1 1 2 2 1 2 3 1 2 3 0 */
C. 智乃酱的前缀和与差分
-
题目链接:智乃酱的前缀和与差分
-
思路:
D. 智乃酱的静态数组维护问题多项式
-
题目链接:智乃酱的静态数组维护问题多项式
-
思路:
- 结论: k k k 次多项式做 k + 1 k+1 k+1 次差分后为余项为常数
- 由于根据差分和前缀和均可以完全还原出原序列,则为了将所有多项式降为常数,我们可对所有序列进行 k + 1 k+1 k+1 次差分。(本题多项式最高 5 5 5 次,故 k + 1 = 5 + 1 = 6 k+1=5+1=6 k+1=5+1=6)
- 注意初始原序列进行 6 6 6 次差分
- 计算多项式函数,和 r r r 后对应相减函数,进行 6 6 6 次差分
- 统一加齐后,进行 6 6 6 次前缀和便可还原结果序列
- 求区间和可再次前缀和处理。
#include<bits/stdc++.h> using namespace std; #define rep(i, a, b) for(int i = (a); i <= (b); i++) #define per(i, a, b) for(int i = (a); i >= (b); i--) #define ll long long #define db double #define VI vector<int> #define PII pair<int, int> const db Pi = 3.141592653589793; const int INF = 0x7fffffff; const int N = 1e5 + 5; const db eps = 1e-10; const int mod = 1e9 + 7; int cas, n, m, q, l, r, k; ll c[10], f[10], a[N]; void sum(ll a[], int n){ rep(i, 1, n) a[i] = (a[i - 1] + a[i] + mod) % mod; } void diff(ll a[], int n){ //注意逆序!!! per(i, n, 1) a[i] = (a[i] - a[i - 1] + mod) % mod; } ll Pow(int x, int cnt){ ll tmp = 1; rep(i, 1, cnt) (tmp *= (ll)x) %= mod; return tmp; } ll funct(int x){ ll res = 0; rep(i, 0, 5) if(c[i]) (res += c[i] * Pow(x, i)) %= mod; return res; } int main(){ ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); cin >> n >> m >> q; rep(i, 1, n) cin >> a[i]; //差分预处理 rep(time, 1, 6) diff(a, n); rep(i, 1, m){ cin >> l >> r >> k; memset(c, 0, sizeof(c)); per(j, k, 0) cin >> c[j]; //加f rep(i, 1, 10) f[i] = funct(i); rep(time, 1, 6) diff(f, 10); rep(i, 1, 10) (a[l - 1 + i] += f[i]) %= mod; //减f rep(i, 1, 10) f[i] = funct(i + r - l + 1); rep(time, 1, 6) diff(f, 10); rep(i, 1, 10) ((a[r + i] -= f[i]) += mod) %= mod; } //还原 rep(time, 1, 6) sum(a, n); //准备输出答案 sum(a, n); while(q--){ cin >> l >> r; cout << (a[r] - a[l - 1] + mod) % mod << endl; } } /* 10 2 11 1000 1000 1000 100000 1000 1000 10000 10000 10000 100000 1 10 0 100 1 10 1 1 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 10 10 1 10 */
E. 智乃酱的双塔问题
-
题目链接:智乃酱的双塔问题
-
类型:矩阵前缀和
-
思路:从地面到达第 i i i 层楼可记录 2 × 2 2\times 2 2×2 矩阵 p r e s u m [ i ] presum[i] presum[i],这个矩阵
0 1 0 表示从左楼到左楼 表示从左楼到右楼 1 表示从右楼到左楼 表示从右楼到右楼 -
左上方向楼梯表示 [ 1 1 0 1 ] \begin{bmatrix}1&1\\0&1 \end{bmatrix} [1011],右上方向楼梯表示 [ 1 0 1 1 ] \begin{bmatrix}1&0\\1&1 \end{bmatrix} [1101],求出前缀矩阵。
-
对于需要楼层区间,只需要利用逆矩阵代替相减即可
-
注意矩阵乘法和逆矩阵的求解,见代码。
//矩阵前缀和 #include<bits/stdc++.h> using namespace std; #define rep(i, a, b) for(int i = (a); i <= (b); i++) #define per(i, a, b) for(int i = (a); i >= (b); i--) #define ll long long #define db double #define VI vector<int> #define PII pair<int, int> const db Pi = 3.141592653589793; const int INF = 0x7fffffff; const int N = 1e5 + 5; const db eps = 1e-10; const int mod = 1e9 + 7; int n, m, src, des, ps, pt; string s; ll A[5][5]; struct Mat{ ll a[2][2]; Mat(int a1 = 0, int a2 = 0, int b1 = 0, int b2 = 0){ a[0][0] = a1, a[0][1] = a2; a[1][0] = b1, a[1][1] = b2; } }presum[N]; Mat left_to_up = Mat(1, 1, 0, 1); Mat right_to_up = Mat(1, 0, 1, 1); Mat operator * (Mat x, Mat y){ Mat ans = Mat(0, 0, 0, 0); rep(i, 0, 1) rep(j, 0, 1) rep(k, 0, 1){ (ans.a[i][j] += x.a[i][k] * y.a[k][j] % mod) %= mod; } return ans; } ll inv(ll x){ if(x == 1) return 1; return (mod - mod / x) * inv(mod % x) % mod; } Mat getinv(Mat x, int n){ //求逆矩阵 //构造 rep(i, 0, n - 1) rep(j, 0, n - 1){ A[i][j] = x.a[i][j]; A[i][j + n] = (j == i ? 1 : 0); } //化为对角矩阵 rep(j, 0, n - 1){ //选择主元 if(!A[j][j]){ rep(i, j + 1, n - 1){ if(!A[i][j]) continue; //行交换 rep(k, 0, 2 * n - 1) swap(A[i][k], A[j][k]); break; } } //主元化为1 int inva = inv(A[j][j]); rep(k, 0, 2 * n - 1) (A[j][k] *= inva) %= mod; //消元 rep(i, j + 1, n - 1){ int mul = A[i][j]; rep(k, 0, 2 * n - 1) A[i][k] = (A[i][k] - mul * A[j][k] % mod + mod) % mod; } } //左半部分化为单位阵 per(j, n - 1, 0) per(i, j - 1, 0){ int mul = A[i][j]; rep(k, 0, 2 * n - 1) A[i][k] = (A[i][k] - mul * A[j][k] % mod + mod) % mod; } Mat res; rep(i, 0, n - 1) rep(k, 0, n - 1) res.a[i][k] = A[i][k + n]; return res; } int main(){ cin >> n >> m >> s; presum[1] = Mat(1, 0, 0, 1); rep(i, 2, n){ if(s[i - 2] == '/') presum[i] = presum[i - 1] * left_to_up; else presum[i] = presum[i - 1] * right_to_up; } while(m--){ cin >> src >> des >> ps >> pt; // -sum[l - 1] + sum[r] Mat ans = getinv(presum[src], 2) * presum[des]; cout << ans.a[ps][pt] << endl; } } /* 4 5 //\ 1 4 0 0 2 4 0 0 2 4 0 1 3 4 0 1 3 4 1 0 */
-
F. 牛牛的猜球游戏
-
题目链接:牛牛的猜球游戏
-
类型:序列排列变换前缀和
-
思路:每次排列都是一种全部改变,且有连续性。故可作为前缀和统计,在查找时需要定义减法操作,逆向还原即可。
//数组排列前缀和 #include<bits/stdc++.h> using namespace std; #define rep(i, a, b) for(int i = (a); i <= (b); i++) #define per(i, a, b) for(int i = (a); i >= (b); i--) #define ll long long #define db double #define VI vector<int> #define PII pair<int, int> const db Pi = 3.141592653589793; const int INF = 0x7fffffff; const int N = 1e5 + 5; const db eps = 1e-10; int cas, n, m, a, b, l, r; struct AC{ int presum[10]; }matrix[N]; AC operator - (AC a, AC b){ //a - b AC ans, tmp; rep(i, 0, 9) tmp.presum[b.presum[i]] = i; rep(i, 0, 9) ans.presum[i] = tmp.presum[a.presum[i]]; return ans; } void Print(AC x){ rep(i, 0, 9) printf("%d ", x.presum[i]); printf("\n"); } int main(){ cin >> n >> m; rep(i, 0, 9) matrix[0].presum[i] = i; rep(i, 1, n){ scanf("%d%d", &a, &b); matrix[i] = matrix[i - 1]; swap(matrix[i].presum[a], matrix[i].presum[b]); } while(m--){ scanf("%d%d", &l, &r); Print(matrix[r] - matrix[l - 1]); } } /* 5 3 0 1 1 2 2 3 0 1 9 0 1 5 5 5 3 5 */
G. 牛牛的Link Power I
-
题目链接:牛牛的Link Power I
-
思路:对于 i i i 处的节点对后面位置的影响为
a : i a : 0123456 d : 0111111 d d : 0100000 \begin{aligned} a:\;&i\\ a:\;&0123456\\ d:\;&0111111\\ dd:\;&0100000 \end{aligned} a:a:d:dd:i012345601111110100000
即两次差分后化为常数序列,则对所有数字 1 1 1 的位置处理,最后两次前缀和还原原序列后计算对应 1 1 1 的位置即可。#include<bits/stdc++.h> using namespace std; #define rep(i, a, b) for(int i = (a); i <= (b); i++) #define per(i, a, b) for(int i = (a); i >= (b); i--) #define ll long long #define db double #define VI vector<int> #define PII pair<int, int> const db Pi = 3.141592653589793; const int INF = 0x7fffffff; const int N = 1e5 + 5; const db eps = 1e-10; const int mod = 1e9 + 7; ll n, a[N], effect[N], tmp[N], ans = 0; char ch; vector<int> store; int main(){ cin >> n; store.clear(); rep(i, 1, n){ cin >> ch; a[i + 1] = (int)(ch - '0'); if(ch == '1') store.push_back(i); } rep(i, 1, n) tmp[i] = (tmp[i - 1] + a[i]) % mod; rep(i, 1, n) effect[i] = (effect[i - 1] + tmp[i]) % mod; for(auto i : store) (ans += effect[i]) %= mod; cout << ans << endl; } /* 5 00110 */
H. 小w的糖果
-
题目链接:小w的糖果
-
思路:对于加等差序列和平方序列,计算发现
-
等差序列中差分两次为常数
a : 1 , 2 , 3 , 4 , 5 , 6 d : 1 , 1 , 1 , 1 , 1 , 1 d d : 1 , 0 , 0 , 0 , 0 , 0 \begin{aligned} a:\;&1,2,3,4,5,6\\ d:\;&1,1,1,1,1,1\\ dd:\;&1,0,0,0,0,0 \end{aligned} a:d:dd:1,2,3,4,5,61,1,1,1,1,11,0,0,0,0,0 -
平方序列中差分两次为常数
a : 1 , 4 , 9 , 16 , 25 , 36 d : 1 , 3 , 5 , 7 , 9 , 11 d d : 1 , 2 , 2 , 2 , 2 , 2 d d d : 1 , 1 , 0 , 0 , 0 , 0 \begin{aligned} a:\;&1,4,9,16,25,36\\ d:\;&1,3,5,\;\;7,\;\;9,11\\ dd:\;&1,2,2,\;\;2,\;\;2,\;\;2\\ ddd:\;&1,1,0,\;\;0,\;\;0,\;\;0\\ \end{aligned} a:d:dd:ddd:1,4,9,16,25,361,3,5,7,9,111,2,2,2,2,21,1,0,0,0,0 -
于是可对三种操作分别预处理,最后不同次数前缀和还原即可。
#include<bits/stdc++.h> using namespace std; #define rep(i, a, b) for(int i = (a); i <= (b); i++) #define per(i, a, b) for(int i = (a); i >= (b); i--) #define ll long long #define db double #define VI vector<int> #define PII pair<int, int> const db Pi = 3.141592653589793; const int INF = 0x7fffffff; const int N = 1e5 + 5; const db eps = 1e-10; const int mod = 1e9 + 7; int cas, n, m; ll d1[N], d2[N], d3[N]; void init(){ rep(i, 1, n) d1[i] = d2[i] = d3[i] = 0; } void pre_sum(ll a[]){ rep(i, 1, n) a[i] = (a[i - 1] + a[i]) % mod; } int main(){ ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); cin >> cas; while(cas--){ cin >> n >> m; init(); rep(i, 1, m){ int type, pos; cin >> type >> pos; if(type == 1) d1[pos]++; if(type == 2) d2[pos]++; if(type == 3) d3[pos]++, d3[pos + 1]++; } pre_sum(d1); pre_sum(d2), pre_sum(d2); pre_sum(d3), pre_sum(d3), pre_sum(d3); rep(i, 1, n) cout << (d1[i] + d2[i] + d3[i]) % mod << " "; cout << endl; } } /* 4 10 1 1 1 10 1 2 2 10 1 3 3 10 3 1 1 2 2 3 3 */
-
I. 积木大赛
-
题目链接:积木大赛(来源:NOIP2013)
-
思路:差分处理计算正数即可
#include<bits/stdc++.h> using namespace std; #define rep(i, a, b) for(int i = (a); i <= (b); i++) #define per(i, a, b) for(int i = (a); i >= (b); i--) #define ll long long #define db double #define VI vector<int> #define PII pair<int, int> const db Pi = 3.141592653589793; const int INF = 0x7fffffff; const int N = 1e5 + 5; const db eps = 1e-10; int n; ll a[N], d[N], ans; //差分看正数 int main(){ cin >> n; rep(i, 1, n){ cin >> a[i]; d[i] = a[i] - a[i - 1]; if(d[i] > 0) ans += d[i]; } cout << ans << endl; } /* 5 2 3 4 1 2 */
J. 道路铺设
-
题目链接:道路铺设(来源:NOIP2018)
-
思路:同上题
#include<bits/stdc++.h> using namespace std; #define rep(i, a, b) for(int i = (a); i <= (b); i++) #define per(i, a, b) for(int i = (a); i >= (b); i--) #define ll long long #define db double #define VI vector<int> #define PII pair<int, int> const db Pi = 3.141592653589793; const int INF = 0x7fffffff; const int N = 1e5 + 5; const db eps = 1e-10; ll n, a[N], d[N], ans; int main(){ cin >> n; rep(i, 1, n){ cin >> a[i]; d[i] = a[i] - a[i - 1]; if(d[i] > 0) ans += d[i]; } cout << ans << endl; } /* 6 4 3 2 5 3 5 */