├── .gitattributes
├── .gitignore
├── README - 副本.md
├── README.md
├── fonts
├── HYRunYuan-55S.ttf
└── HYRunYuan-75S.ttf
├── latex
├── cirno.docx
├── cirno.pdf
├── cover.docx
├── cover.pdf
├── cover2.docx
├── cover_new.docx
├── cover_new.pdf
├── cover_old.docx
├── cover_old.pdf
├── image
│ ├── 69150734_p0.jpg
│ ├── 72211605_p0.png
│ ├── 73564338_p0.jpg
│ ├── 76711648_p0.png
│ ├── 77432520_p0.png
│ ├── F0463CBF1E7C530E8E5FD40ADE8C92B4.jpg
│ ├── alice.jpg
│ ├── alice_halloween.jpg
│ ├── konata.jpg
│ ├── lily.jpg
│ ├── lily_black.png
│ ├── lily_face.png
│ ├── lily_white.png
│ └── sweeping.jpg
├── kunming.pdf
├── kunming.tex
├── macau.pdf
├── macau.tex
├── main.pdf
└── main.tex
├── old
├── 整合.md
├── 整合.pdf
└── 黑白.pdf
├── src
├── DP
│ ├── 103388A.cpp
│ ├── 例题.tex
│ └── 决策单调性.cpp
├── attention
│ ├── 做题策略与心态调节.tex
│ ├── 场外相关.tex
│ └── 常见下毒手法.tex
├── cover
│ ├── cover.tex
│ ├── last-commit.py
│ └── last-compiled.py
├── datastructure
│ ├── CDQ分治.cpp
│ ├── LCT(不换根).cpp
│ ├── LCT(换根).cpp
│ ├── LCT维护子树信息.cpp
│ ├── Treap.cpp
│ ├── ec20g.cpp
│ ├── tarjanlca.cpp
│ ├── 二叉堆.cpp
│ ├── 分治并查集.cpp
│ ├── 动态KD树.cpp
│ ├── 动态QTREE4.cpp
│ ├── 动态树分治.cpp
│ ├── 动态树形DP.cpp
│ ├── 常见根号思路.tex
│ ├── 整体二分.cpp
│ ├── 文艺平衡树.cpp
│ ├── 无旋Treap.cpp
│ ├── 梯子剖分.cpp
│ ├── 紫荆花之恋.cpp
│ ├── 线段树维护矩形并.cpp
│ ├── 莫队.tex
│ ├── 莫队二次离线.cpp
│ ├── 莫队二次离线在线化.cpp
│ ├── 莫队例题.tex
│ ├── 虚树.cpp
│ ├── 长链剖分.cpp
│ ├── 非递归线段树.tex
│ ├── 非递归线段树区间加区间求和.cpp
│ ├── 非递归线段树区间加区间求最大值.cpp
│ └── 非递归线段树单点修改.cpp
├── geometry
│ ├── delaunay.cpp
│ ├── 最近点对.png
│ └── 最近点对.tex
├── graph
│ ├── 2sat-tarjan.cpp
│ ├── 2sat.cpp
│ ├── 2sat.tex
│ ├── Boruvka.tex
│ ├── Dinic.cpp
│ ├── HLPP.cpp
│ ├── ISAP.cpp
│ ├── KM二分图最大权匹配.cpp
│ ├── SPFA费用流.cpp
│ ├── dijkstra费用流.cpp
│ ├── dijkstra费用流.tex
│ ├── hopcroft-karp.cpp
│ ├── hungary.cpp
│ ├── johnson.tex
│ ├── k短路.cpp
│ ├── mdst.cpp
│ ├── prufer.tex
│ ├── steiner.cpp
│ ├── steiner.tex
│ ├── stoer-wagner.cpp
│ ├── 一般图匹配原理.tex
│ ├── 上下界网络流.png
│ ├── 二分图原理.tex
│ ├── 仙人掌DP.cpp
│ ├── 割点点双.cpp
│ ├── 动态最小生成树.cpp
│ ├── 动态最小生成树.tex
│ ├── 基于线性代数的一般图匹配.cpp
│ ├── 带权带花树.cpp
│ ├── 带花树.cpp
│ ├── 弦图相关.tex
│ ├── 强连通分量.cpp
│ ├── 支配树.cpp
│ ├── 整理.md
│ ├── 最小树形图.cpp
│ ├── 最小树形图.tex
│ ├── 最小直径生成树.tex
│ ├── 有源汇上下界最大流.cpp
│ ├── 欧拉回路.cpp
│ ├── 网络流原理.tex
│ ├── 边双.cpp
│ └── 预流推进费用流.cpp
├── math
│ ├── Berlekamp-Massey.cpp
│ ├── Berlekamp-Massey.tex
│ ├── Berlekamp-Massey应用.tex
│ ├── Bostan-Mori.cpp
│ ├── Bostan-Mori.tex
│ ├── FFT.cpp
│ ├── FWT.cpp
│ ├── MTT.cpp
│ ├── NTT.cpp
│ ├── SG定理.tex
│ ├── fwt3.cpp
│ ├── gauss_jordan.cpp
│ ├── minmax.tex
│ ├── simpson.cpp
│ ├── 三模数NTT.cpp
│ ├── 任意模数卷积.tex
│ ├── 伯努利数.tex
│ ├── 分拆数.cpp
│ ├── 分治FFT.cpp
│ ├── 半在线卷积.cpp
│ ├── 单位根反演.tex
│ ├── 单纯形.cpp
│ ├── 博弈论例题.tex
│ ├── 卡特兰数.tex
│ ├── 多项式复合.cpp
│ ├── 多项式复合.tex
│ ├── 多项式复合逆.cpp
│ ├── 多项式复合逆.tex
│ ├── 多项式快速插值.cpp
│ ├── 多项式快速插值.tex
│ ├── 多项式操作.cpp
│ ├── 常用NTT素数及原根.md
│ ├── 常见数列打表.md
│ ├── 常见生成函数.tex
│ ├── 康托展开.tex
│ ├── 快速线性递推-BostanMori.cpp
│ ├── 快速线性递推-多项式除法.cpp
│ ├── 快速线性递推.tex
│ ├── 快速阶乘算法.tex
│ ├── 拆系数FFT.cpp
│ ├── 拉格朗日插值.tex
│ ├── 斐波那契数.tex
│ ├── 斯特林数.tex
│ ├── 方差.tex
│ ├── 更优秀的多项式多点求值.cpp
│ ├── 欧拉数.tex
│ ├── 牛顿插值.cpp
│ ├── 牛顿插值.tex
│ ├── 矩阵乘法.cpp
│ ├── 矩阵树定理.tex
│ ├── 纳什均衡.tex
│ ├── 线性代数.tex
│ ├── 线性基.cpp
│ ├── 线性基.md
│ ├── 线性规划对偶原理.tex
│ ├── 线性齐次线性常系数递推.png
│ ├── 经典博弈.tex
│ ├── 行列式取模.cpp
│ ├── 解稀疏方程组.cpp
│ ├── 贝尔数.tex
│ ├── 连通图计数.tex
│ ├── 高斯消元.tex
│ ├── 高斯消元与自由元搜索.cpp
│ └── 齐次线性递推.cpp
├── misc
│ ├── O(1)快速乘.cpp
│ ├── STL.tex
│ ├── cheat.pdf
│ ├── cpp.tex
│ ├── decimal.py
│ ├── garsiawachs.tex
│ ├── hash.cpp
│ ├── kahan.cpp
│ ├── keyboard-shortcuts-linux.pdf
│ ├── keyboard-shortcuts-windows.pdf
│ ├── oeis.tex
│ ├── pbds.tex
│ ├── pbds_heap.png
│ ├── rope.cpp
│ ├── vscode.tex
│ ├── xorshift.cpp
│ ├── 常用NTT素数及原根.tex
│ ├── 德扑.tex
│ ├── 枚举子集.tex
│ ├── 炉石.tex
│ ├── 笛卡尔树.cpp
│ ├── 编译选项.tex
│ ├── 骂人的艺术.tex
│ ├── 高低奥马哈.cpp
│ └── 高精度.cpp
├── numbertheory
│ ├── Miller-Rabin.cpp
│ ├── O(n)求逆元.cpp
│ ├── PN.tex
│ ├── Pollard-Rho.cpp
│ ├── crt.tex
│ ├── excrt.tex
│ ├── exgcd.cpp
│ ├── exgcd.tex
│ ├── min25.cpp
│ ├── min25.tex
│ ├── 二次剩余.cpp
│ ├── 公式.tex
│ ├── 原根.tex
│ ├── 扩展线性筛.cpp
│ ├── 杜教筛.cpp
│ ├── 杜教筛.tex
│ ├── 洲阁筛.cpp
│ ├── 洲阁筛.tex
│ └── 类欧.cpp
└── string
│ ├── AC自动机.cpp
│ ├── KMP.cpp
│ ├── SAMSA.cpp
│ ├── exKMP.cpp
│ ├── manacher.cpp
│ ├── sa.cpp
│ ├── sais.cpp
│ ├── samlct.cpp
│ ├── samlct2.cpp
│ ├── 区间本质不同子串统计.tex
│ ├── 后缀平衡树.tex
│ ├── 后缀自动机.cpp
│ ├── 回文树.cpp
│ ├── 字符串原理.tex
│ ├── 广义后缀自动机.cpp
│ └── 广义回文树.cpp
└── 备忘.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /latex/*.toc
2 | /latex/*.aux
3 | /latex/*.log
4 | /latex/*.out
5 | /latex/*.synctex.gz
6 | /latex/_minted-main/
7 | /latex/_minted-macau/
8 | /latex/_minted-kunming/
9 | /_minted-main/
10 | /.vscode/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Standard Code Library
2 |
3 | 源代码参见`/src`,编译出的PDF参见`/latex/main.pdf`。
4 |
5 | 目前还没有完全完成,所以想fork的话建议经常更新。也不要急着打印PDF,因为更新很杂,维护起来很麻烦。
6 |
7 | PDF页边距看起来不一样的原因是改了改模板,换成了双面打印的(装订的一边稍微宽一些)。
8 |
9 |
10 |
11 | > 不必恐惧黑夜,它只是黎明的前奏;
12 | >
13 | > 待尘埃落定时,你的光芒必将盖过满天繁星。
14 |
15 | ### TODO
16 |
17 | - 网络流例题、费用流建图方法
18 | - DP例题
19 | - 博弈论例题
--------------------------------------------------------------------------------
/fonts/HYRunYuan-55S.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/fonts/HYRunYuan-55S.ttf
--------------------------------------------------------------------------------
/fonts/HYRunYuan-75S.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/fonts/HYRunYuan-75S.ttf
--------------------------------------------------------------------------------
/latex/cirno.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/cirno.docx
--------------------------------------------------------------------------------
/latex/cirno.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/cirno.pdf
--------------------------------------------------------------------------------
/latex/cover.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/cover.docx
--------------------------------------------------------------------------------
/latex/cover.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/cover.pdf
--------------------------------------------------------------------------------
/latex/cover2.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/cover2.docx
--------------------------------------------------------------------------------
/latex/cover_new.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/cover_new.docx
--------------------------------------------------------------------------------
/latex/cover_new.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/cover_new.pdf
--------------------------------------------------------------------------------
/latex/cover_old.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/cover_old.docx
--------------------------------------------------------------------------------
/latex/cover_old.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/cover_old.pdf
--------------------------------------------------------------------------------
/latex/image/69150734_p0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/image/69150734_p0.jpg
--------------------------------------------------------------------------------
/latex/image/72211605_p0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/image/72211605_p0.png
--------------------------------------------------------------------------------
/latex/image/73564338_p0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/image/73564338_p0.jpg
--------------------------------------------------------------------------------
/latex/image/76711648_p0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/image/76711648_p0.png
--------------------------------------------------------------------------------
/latex/image/77432520_p0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/image/77432520_p0.png
--------------------------------------------------------------------------------
/latex/image/F0463CBF1E7C530E8E5FD40ADE8C92B4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/image/F0463CBF1E7C530E8E5FD40ADE8C92B4.jpg
--------------------------------------------------------------------------------
/latex/image/alice.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/image/alice.jpg
--------------------------------------------------------------------------------
/latex/image/alice_halloween.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/image/alice_halloween.jpg
--------------------------------------------------------------------------------
/latex/image/konata.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/image/konata.jpg
--------------------------------------------------------------------------------
/latex/image/lily.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/image/lily.jpg
--------------------------------------------------------------------------------
/latex/image/lily_black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/image/lily_black.png
--------------------------------------------------------------------------------
/latex/image/lily_face.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/image/lily_face.png
--------------------------------------------------------------------------------
/latex/image/lily_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/image/lily_white.png
--------------------------------------------------------------------------------
/latex/image/sweeping.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/image/sweeping.jpg
--------------------------------------------------------------------------------
/latex/kunming.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/kunming.pdf
--------------------------------------------------------------------------------
/latex/macau.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/macau.pdf
--------------------------------------------------------------------------------
/latex/main.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/latex/main.pdf
--------------------------------------------------------------------------------
/old/整合.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/old/整合.pdf
--------------------------------------------------------------------------------
/old/黑白.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/old/黑白.pdf
--------------------------------------------------------------------------------
/src/DP/103388A.cpp:
--------------------------------------------------------------------------------
1 | constexpr int maxn = 5005, p = (int)1e9 + 7;
2 |
3 | int inv[maxn];
4 | int a[maxn], f[maxn][maxn], dp[maxn];
5 |
6 | int main() {
7 |
8 | int n, m;
9 | scanf("%d%d", &n, &m);
10 |
11 | inv[1] = 1;
12 | for (int i = 2; i <= n; i++)
13 | inv[i] = (long long)(p - p / i) * inv[p % i] % p;
14 |
15 | for (int i = 1; i <= n; i++) {
16 | scanf("%d", &a[i]);
17 | a[i] = m - a[i] + 1;
18 | }
19 |
20 | if (any_of(a + 1, a + n + 1, [] (int x) {return x <= 0;})) {
21 | printf("0\n");
22 | return 0;
23 | }
24 |
25 | for (int i = n - 1; i; i--)
26 | a[i] = min(a[i], a[i + 1]);
27 |
28 | // b_i >= b_{i - 1} && b_i <= a_i
29 | // 我们可以假设 b_i <= a_i 是必定被满足的,然后对 bi 非严格递增的条件进行容斥,枚举某一段是严格递减的
30 | // 如果 [j, i] 严格递减,显然它们都 <= a_j,所以这个区间的方案数是 {a_j \choose i - j + 1}
31 | // 如果 i 是合法的,直接一个个转移即可,因为这一部分的转移和区间长度没有关系
32 |
33 | for (int i = 1; i <= n; i++) {
34 | f[i][0] = 1;
35 |
36 | for (int j = 1; j <= n - i + 1 && j <= a[i]; j++)
37 | f[i][j] = (long long)f[i][j - 1] * (a[i] - j + 1) % p * inv[j] % p;
38 | }
39 |
40 | dp[0] = 1;
41 |
42 | for (int i = 1; i <= n; i++) {
43 | dp[i] = (long long)dp[i - 1] * a[i] % p;
44 |
45 | for (int j = 1; j < i; j++) {
46 | int tmp = (long long)dp[j - 1] * f[j][i - j + 1] % p;
47 |
48 | if ((i - j) % 2)
49 | tmp = p - tmp;
50 |
51 | dp[i] = (dp[i] + tmp) % p;
52 | }
53 | }
54 |
55 | printf("%d\n", dp[n]);
56 |
57 | return 0;
58 | }
--------------------------------------------------------------------------------
/src/DP/例题.tex:
--------------------------------------------------------------------------------
1 | \subsubsection{103388A Assigning Prizes 容斥}
2 |
3 | \paragraph{题意} 给定一个长为 $n$ 的序列 $a_i$,要求构造非严格递减序列 $b_i$,满足 $a_i \le b_i \le R$,求方案数。$n \le 5 \times 10 ^ 3, R, a_i \le 10 ^ 9$。
4 |
5 | \paragraph{做法} $a_i$ 的范围太大了,不能简单地记录上一位的值。
6 |
7 | 考虑使用容斥。方便起见把 $a_i$ 直接变成 $R - a_i + 1$,条件就变成了 $b_i \le a_i$ 且 $b_i \ge b_{i - 1}$。
8 |
9 | 这里有两个限制条件,可以固定 $b_i \le a_i$ 是必须满足的条件,只对 $b_i \ge b_{i - 1}$ 使用容斥,枚举哪些位置是比上一位小的(违反限制),其他位置随意。
10 |
11 | 枚举后的形态一定是有若干个区间是严格递减的,其他位置随意。考虑如果一个区间 $[l, r]$ 是严格递减的,显然所有的数都 $< a_l$,所以这段区间的方案数就是 ${a_l \choose r - l + 1}$。另外实际上 $b_l$ 是没有违反限制的,所以这里对系数的贡献是 $(-1) ^ {r - l}$。
12 |
13 | 考虑令 $dp_i$ 表示只考虑前 $i$ 个位置的答案,转移时自然就是枚举一个 $j$,然后计算 $dp_{j - 1}$ 乘上区间 $[j, i]$ 严格递减的方案数。另外还有一种情况是 $b_i$ 没有违反限制,这时显然直接在 $dp_{i - 1}$ 的基础上乘上一个 $a_i$ 就好了。(转移时还要注意,由于枚举的是严格递减区间,自然就不能枚举只有一个数的区间。
14 |
15 | \inputminted{cpp}{../src/dp/103388A.cpp}
16 |
--------------------------------------------------------------------------------
/src/DP/决策单调性.cpp:
--------------------------------------------------------------------------------
1 | int a[MAXN], q[MAXN], p[MAXN], g[MAXN]; // 存左端点,右端点就是下一个左端点 - 1
2 |
3 | long long f[MAXN], s[MAXN];
4 |
5 | int n, m;
6 |
7 | long long calc(int l, int r) {
8 | if (r < l)
9 | return 0;
10 |
11 | int mid = (l + r) / 2;
12 | if ((r - l + 1) % 2 == 0)
13 | return (s[r] - s[mid]) - (s[mid] - s[l - 1]);
14 | else
15 | return (s[r] - s[mid]) - (s[mid - 1] - s[l - 1]);
16 | }
17 |
18 | int solve(long long tmp) {
19 | memset(f, 63, sizeof(f));
20 | f[0] = 0;
21 |
22 | int head = 1, tail = 0;
23 |
24 | for (int i = 1; i <= n; i++) {
25 | f[i] = calc(1, i);
26 | g[i] = 1;
27 |
28 | while (head < tail && p[head + 1] <= i)
29 | head++;
30 | if (head <= tail) {
31 | if (f[q[head]] + calc(q[head] + 1, i) < f[i]) {
32 | f[i] = f[q[head]] + calc(q[head] + 1, i);
33 | g[i] = g[q[head]] + 1;
34 | }
35 | while (head < tail && p[head + 1] <= i + 1)
36 | head++;
37 | if (head <= tail)
38 | p[head] = i + 1;
39 | }
40 | f[i] += tmp;
41 |
42 | int r = n;
43 |
44 | while(head <= tail) {
45 | if (f[q[tail]] + calc(q[tail] + 1, p[tail]) > f[i] + calc(i + 1, p[tail])) {
46 | r = p[tail] - 1;
47 | tail--;
48 | }
49 | else if (f[q[tail]] + calc(q[tail] + 1, r) <= f[i] + calc(i + 1, r)) {
50 | if (r < n) {
51 | q[++tail] = i;
52 | p[tail] = r + 1;
53 | }
54 | break;
55 | }
56 | else {
57 | int L = p[tail], R = r;
58 | while (L < R) {
59 | int M = (L + R) / 2;
60 |
61 | if (f[q[tail]] + calc(q[tail] + 1, M) <= f[i] + calc(i + 1, M))
62 | L = M + 1;
63 | else
64 | R = M;
65 | }
66 |
67 | q[++tail] = i;
68 | p[tail] = L;
69 |
70 | break;
71 | }
72 | }
73 | if (head > tail) {
74 | q[++tail] = i;
75 | p[tail] = i + 1;
76 | }
77 | }
78 |
79 | return g[n];
80 | }
81 |
--------------------------------------------------------------------------------
/src/attention/做题策略与心态调节.tex:
--------------------------------------------------------------------------------
1 | \noindent
2 | \begin{itemize}
3 | \item 拿到题后立刻按照商量好的顺序读题, 前半小时最好跳过题意太复杂的题(除非被过穿了)
4 |
5 | \item 签到题写完不要激动, 稍微检查一下最可能的下毒点再交, 避免无谓的罚时
6 | \subitem 一两行的那种傻逼题就算了
7 |
8 | \item 读完题及时输出题意, 一方面避免重复读题, 一方面也可以让队友有一个初步印象, 方便之后决定开题顺序
9 |
10 | \item 如果不能确定题意就不要贸然输出甚至上机, 尤其是签到题, 因为样例一般都很弱
11 |
12 | \item 一个题如果卡了很久又有其他题可以写, 那不妨先放掉写更容易的题, 不要在一棵树上吊死
13 | \subitem 不要被一两道题搞得心态爆炸, 一方面急也没有意义, 一方面你很可能真的离AC就差一步
14 |
15 | \item 榜是不会骗人的, 一个题如果被不少人过了就说明这个题很可能并没有那么难;如果不是有十足的把握就不要轻易开没什么人交的题;另外不要忘记最后一小时会封榜
16 |
17 | \item 想不出题/找不出毒自然容易犯困, 一定不要放任自己昏昏欲睡, 最好去洗手间冷静一下, 没有条件就站起来踱步
18 |
19 | \item 思考的时候不要挂机, 一定要在草稿纸上画一画, 最好说出声来最不容易断掉思路
20 |
21 | \item 出完算法一定要check一下样例和一些trivial的情况, 不然容易写了半天发现写了个假算法
22 |
23 | \item 上机前有时间就提前给需要思考怎么写的地方打草稿, 不要浪费机时
24 |
25 | \item 查毒时如果最难的地方反复check也没有问题, 就从头到脚仔仔细细查一遍, 不要放过任何细节, 即使是并查集和sort这种东西也不能想当然
26 |
27 | \item 后半场如果时间不充裕就不要冒险开难题, 除非真的无事可做
28 | \subitem 如果是没写过的东西也不要轻举妄动, 在有其他好写的题的时候就等一会再说
29 |
30 | \item 大多数时候都要听队长安排, 虽然不一定最正确但可以保持组织性
31 |
32 | % \item 最好注意一下影响, 就算忍不住嘴臭也不要太大声
33 |
34 | \item 任何时候都不要着急, 着急不能解决问题, 不要当喆国王
35 |
36 | \item 输了游戏, 还有人生;赢了游戏, 还有人生.
37 | \end{itemize}
--------------------------------------------------------------------------------
/src/attention/场外相关.tex:
--------------------------------------------------------------------------------
1 | \noindent
2 | \begin{itemize}
3 | \item 安顿好之后查一下附近的咖啡店,打印店,便利店之类的位置,以备不时之需
4 | \item 热身赛记得检查一下编译注意事项中的代码能否过编译,还有熟悉比赛场地,清楚洗手间在哪儿,测试打印机(如果可以)
5 | \item 比赛前至少要翻一遍板子,尤其要看原理与例题
6 | \item 比赛前一两天不要摸鱼,要早睡,有条件最好洗个澡;比赛当天不要起太晚,维持好的状态
7 | \item 赛前记得买咖啡,最好直接安排三人份,记得要咖啡因比较足的;如果主办方允许,就带些巧克力之类的高热量零食
8 | \item 入场之后记得检查机器,尤其要逐个检查键盘按键有没有坏的;如果可以的话,调一下gedit设置
9 | \item 开赛之前调整好心态,比赛而已,不必心急.
10 | \end{itemize}
--------------------------------------------------------------------------------
/src/attention/常见下毒手法.tex:
--------------------------------------------------------------------------------
1 | \noindent
2 | \begin{itemize}
3 | \item 0/1base是不是搞混了
4 | \item 高精度高低位搞反了吗
5 | \item 线性筛抄对了吗
6 | \item 快速乘抄对了吗
7 | \item \mintinline{cpp}{i <= n, j <= m}
8 | \item sort比较函数是不是比了个寂寞
9 | \item 该取模的地方都取模了吗
10 | \item 边界情况(+1-1之类的)有没有想清楚
11 | \item \bfseries{特判是否有必要, 确定写对了吗}
12 | \end{itemize}
--------------------------------------------------------------------------------
/src/cover/cover.tex:
--------------------------------------------------------------------------------
1 | % \ThisCenterWallPaper{1.1}{image/alice.jpg}
2 |
3 | \fontspec{TeX Gyre Pagella}\selectfont{\color{white}{
4 | \maketitle
5 |
6 | \centerline{Last Compiled: \input{|"python ../src/cover/last-compiled.py"}}
7 | }}
8 |
9 | \thispagestyle{empty}
10 |
--------------------------------------------------------------------------------
/src/cover/last-commit.py:
--------------------------------------------------------------------------------
1 | import datetime, subprocess
2 |
3 | result = subprocess.run(["git", "log", "-n", "1"], stdout = subprocess.PIPE)
4 |
5 | for bytes in result.stdout.splitlines():
6 | line = bytes.decode()
7 |
8 | if line.startswith("commit"):
9 | commit = line.split()[1]
10 | elif line.startswith("Merge:"):
11 | merge = (line.split()[1], line.split()[2])
12 | elif line.startswith("Author:"):
13 | author = line.split()[1] # No need for email
14 | elif line.startswith("Date:"):
15 | date_str = " ".join(line.split()[1:-1]) # No need for timezone
16 | date_format = "%a %b %d %H:%M:%S %Y"
17 | date = datetime.datetime.strptime(date_str, date_format)
18 | elif line.startswith(" "):
19 | message = line.strip()
20 |
21 | print(f"{date.strftime('%H:%M %h %d, %Y')}" # ({message})"
22 | .replace("&", "\\&")
23 | .replace("%", "\\%")
24 | .replace("#", "\\#")) # LaTeX escape
25 |
--------------------------------------------------------------------------------
/src/cover/last-compiled.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | # print the current date and time
4 |
5 | print(datetime.datetime.now().strftime("%H:%M %h %d, %Y"))
6 |
--------------------------------------------------------------------------------
/src/datastructure/CDQ分治.cpp:
--------------------------------------------------------------------------------
1 | // 四维偏序
2 |
3 | void CDQ1(int l, int r) {
4 | if (l >= r)
5 | return;
6 |
7 | int mid = (l + r) / 2;
8 |
9 | CDQ1(l, mid);
10 | CDQ1(mid + 1, r);
11 |
12 | int i = l, j = mid + 1, k = l;
13 |
14 | while (i <= mid && j <= r) {
15 | if (a[i].x < a[j].x) {
16 | a[i].ins = true;
17 | b[k++] = a[i++];
18 | }
19 | else {
20 | a[j].ins = false;
21 | b[k++] = a[j++];
22 | }
23 | }
24 |
25 | while (i <= mid) {
26 | a[i].ins = true;
27 | b[k++] = a[i++];
28 | }
29 |
30 | while (j <= r) {
31 | a[j].ins = false;
32 | b[k++] = a[j++];
33 | }
34 |
35 | copy(b + l, b + r + 1, a + l); // 后面的分治会破坏排序, 所以要复制一份
36 |
37 | CDQ2(l, r);
38 | }
39 |
40 | void CDQ2(int l, int r) {
41 | if (l >= r)
42 | return;
43 |
44 | int mid = (l + r) / 2;
45 |
46 | CDQ2(l, mid);
47 | CDQ2(mid + 1, r);
48 |
49 | int i = l, j = mid + 1, k = l;
50 |
51 | while (i <= mid && j <= r) {
52 | if (b[i].y < b[j].y) {
53 | if (b[i].ins)
54 | add(b[i].z, 1); // 树状数组
55 |
56 | t[k++] = b[i++];
57 | }
58 | else{
59 | if (!b[j].ins)
60 | ans += query(b[j].z - 1);
61 |
62 | t[k++] = b[j++];
63 | }
64 | }
65 |
66 | while (i <= mid) {
67 | if (b[i].ins)
68 | add(b[i].z, 1);
69 |
70 | t[k++] = b[i++];
71 | }
72 |
73 | while (j <= r) {
74 | if (!b[j].ins)
75 | ans += query(b[j].z - 1);
76 |
77 | t[k++] = b[j++];
78 | }
79 |
80 | for (i = l; i <= mid; i++)
81 | if (b[i].ins)
82 | add(b[i].z, -1);
83 |
84 | copy(t + l, t + r + 1, b + l);
85 | }
--------------------------------------------------------------------------------
/src/datastructure/LCT(不换根).cpp:
--------------------------------------------------------------------------------
1 | #define isroot(x) ((x) != (x) -> p -> ch[0] && (x) != (x) -> p -> ch[1]) // 判断是不是Splay的根
2 | #define dir(x) ((x) == (x) -> p -> ch[1]) // 判断它是它父亲的左 / 右儿子
3 |
4 | struct node { // 结点类定义
5 | int size; // Splay的子树大小
6 | node *ch[2], *p;
7 |
8 | node() : size(1) {}
9 | void refresh() {
10 | size = ch[0] -> size + ch[1] -> size + 1;
11 | } // 附加信息维护
12 | } null[maxn];
13 |
14 | // 在主函数开头加上这句初始化
15 | null -> size = 0;
16 |
17 | // 初始化结点
18 | void initalize(node *x) {
19 | x -> ch[0] = x -> ch[1] = x -> p = null;
20 | }
21 |
22 | // Access 均摊O(\log n)
23 | // LCT核心操作, 把结点到根的路径打通, 顺便把与重儿子的连边变成轻边
24 | // 需要调用splay
25 | node *access(node *x) {
26 | node *y = null;
27 |
28 | while (x != null) {
29 | splay(x);
30 |
31 | x -> ch[1] = y;
32 | (y = x) -> refresh();
33 |
34 | x = x -> p;
35 | }
36 |
37 | return y;
38 | }
39 |
40 | // Link 均摊O(\log n)
41 | // 把x的父亲设为y
42 | // 要求x必须为所在树的根节点,否则会导致后续各种莫名其妙的问题
43 | // 需要调用splay
44 | void link(node *x, node *y) {
45 | splay(x);
46 | x -> p = y;
47 | }
48 |
49 | // Cut 均摊O(\log n)
50 | // 把x与其父亲的连边断掉
51 | // x可以是所在树的根节点, 这时此操作没有任何实质效果
52 | // 需要调用access和splay
53 | void cut(node *x) {
54 | access(x);
55 | splay(x);
56 |
57 | x -> ch[0] -> p = null;
58 | x -> ch[0] = null;
59 |
60 | x -> refresh();
61 | }
62 |
63 | // Splay 均摊O(\log n)
64 | // 需要调用旋转
65 | void splay(node *x) {
66 | while (!isroot(x)) {
67 | if (isroot(x -> p)) {
68 | rot(x -> p, dir(x) ^ 1);
69 | break;
70 | }
71 |
72 | if (dir(x) == dir(x -> p))
73 | rot(x -> p -> p, dir(x -> p) ^ 1);
74 | else
75 | rot(x -> p, dir(x) ^ 1);
76 | rot(x -> p, dir(x) ^ 1);
77 | }
78 | }
79 |
80 | // 旋转(LCT版本) O(1)
81 | // 平衡树基本操作
82 | // 要求对应儿子必须存在, 否则会导致后续各种莫名其妙的问题
83 | void rot(node *x, int d) {
84 | node *y = x -> ch[d ^ 1];
85 |
86 | y -> p = x -> p;
87 | if (!isroot(x))
88 | x -> p -> ch[dir(x)] = y;
89 |
90 | if ((x -> ch[d ^ 1] = y -> ch[d]) != null)
91 | y -> ch[d] -> p = x;
92 | (y -> ch[d] = x) -> p = y;
93 |
94 | x -> refresh();
95 | y -> refresh();
96 | }
97 |
--------------------------------------------------------------------------------
/src/datastructure/Treap.cpp:
--------------------------------------------------------------------------------
1 | // 注意: 相同键值可以共存
2 |
3 | struct node { // 结点类定义
4 | int key, size, p; // 分别为键值, 子树大小, 优先度
5 | node *ch[2]; // 0表示左儿子, 1表示右儿子
6 |
7 | node(int key = 0) : key(key), size(1), p(rand()) {}
8 |
9 | void refresh() {
10 | size = ch[0] -> size + ch[1] -> size + 1;
11 | } // 更新子树大小(和附加信息, 如果有的话)
12 | } null[maxn], *root = null, *ptr = null; // 数组名叫做null是为了方便开哨兵节点
13 | // 如果需要删除而空间不能直接开下所有结点, 则需要再写一个垃圾回收
14 | // 注意: 数组里的元素一定不能delete, 否则会导致RE
15 |
16 | // 重要!在主函数最开始一定要加上以下预处理:
17 | null -> ch[0] = null -> ch[1] = null;
18 | null -> size = 0;
19 |
20 | // 伪构造函数 O(1)
21 | // 为了方便, 在结点类外面再定义一个伪构造函数
22 | node *newnode(int x) { // 键值为x
23 | *++ptr = node(x);
24 | ptr -> ch[0] = ptr -> ch[1] = null;
25 | return ptr;
26 | }
27 |
28 | // 插入键值 期望O(\log n)
29 | // 需要调用旋转
30 | void insert(int x, node *&rt) { // rt为当前结点, 建议调用时传入root, 下同
31 | if (rt == null) {
32 | rt = newnode(x);
33 | return;
34 | }
35 |
36 | int d = x > rt -> key;
37 | insert(x, rt -> ch[d]);
38 | rt -> refresh();
39 |
40 | if (rt -> ch[d] -> p < rt -> p)
41 | rot(rt, d ^ 1);
42 | }
43 |
44 | // 删除一个键值 期望O(\log n)
45 | // 要求键值必须存在至少一个, 否则会导致RE
46 | // 需要调用旋转
47 | void erase(int x, node *&rt) {
48 | if (x == rt -> key) {
49 | if (rt -> ch[0] != null && rt -> ch[1] != null) {
50 | int d = rt -> ch[0] -> p < rt -> ch[1] -> p;
51 | rot(rt, d);
52 | erase(x, rt -> ch[d]);
53 | }
54 | else
55 | rt = rt -> ch[rt -> ch[0] == null];
56 | }
57 | else
58 | erase(x, rt -> ch[x > rt -> key]);
59 |
60 | if (rt != null)
61 | rt -> refresh();
62 | }
63 |
64 | // 求元素的排名(严格小于键值的个数 + 1) 期望O(\log n)
65 | // 非递归
66 | int rank(int x, node *rt) {
67 | int ans = 1, d;
68 | while (rt != null) {
69 | if ((d = x > rt -> key))
70 | ans += rt -> ch[0] -> size + 1;
71 |
72 | rt = rt -> ch[d];
73 | }
74 |
75 | return ans;
76 | }
77 |
78 | // 返回排名第k(从1开始)的键值对应的指针 期望O(\log n)
79 | // 非递归
80 | node *kth(int x, node *rt) {
81 | int d;
82 | while (rt != null) {
83 | if (x == rt -> ch[0] -> size + 1)
84 | return rt;
85 |
86 | if ((d = x > rt -> ch[0] -> size))
87 | x -= rt -> ch[0] -> size + 1;
88 |
89 | rt = rt -> ch[d];
90 | }
91 |
92 | return rt;
93 | }
94 |
95 | // 返回前驱(最大的比给定键值小的键值)对应的指针 期望O(\log n)
96 | // 非递归
97 | node *pred(int x, node *rt) {
98 | node *y = null;
99 | int d;
100 |
101 | while (rt != null) {
102 | if ((d = x > rt -> key))
103 | y = rt;
104 |
105 | rt = rt -> ch[d];
106 | }
107 |
108 | return y;
109 | }
110 |
111 | // 返回后继(最小的比给定键值大的键值)对应的指针 期望O(\log n)
112 | // 非递归
113 | node *succ(int x, node *rt) {
114 | node *y = null;
115 | int d;
116 |
117 | while (rt != null) {
118 | if ((d = x < rt -> key))
119 | y = rt;
120 |
121 | rt = rt -> ch[d ^ 1];
122 | }
123 |
124 | return y;
125 | }
126 |
127 | // 旋转(Treap版本) O(1)
128 | // 平衡树基础操作
129 | // 要求对应儿子必须存在, 否则会导致后续各种莫名其妙的问题
130 | void rot(node *&x, int d) { // x为被转下去的结点, 会被修改以维护树结构
131 | node *y = x -> ch[d ^ 1];
132 |
133 | x -> ch[d ^ 1] = y -> ch[d];
134 | y -> ch[d] = x;
135 |
136 | x -> refresh();
137 | (x = y) -> refresh();
138 | }
--------------------------------------------------------------------------------
/src/datastructure/ec20g.cpp:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | using namespace std;
4 |
5 | constexpr int maxn = (1 << 20) + 5;
6 |
7 | int cnt[maxn][2], mul[maxn][2];
8 | bool rev[maxn];
9 | long long sum[maxn];
10 |
11 | int now;
12 |
13 | void build(int l, int r, int o) {
14 | cnt[o][0] = r - l + 1;
15 |
16 | if (l == r)
17 | return;
18 |
19 | int mid = (l + r) / 2;
20 |
21 | build(l, mid, o * 2);
22 | build(mid + 1, r, o * 2 + 1);
23 | }
24 |
25 | void apply(int o, bool flip, long long w0, long long w1) {
26 | sum[o] += w0 * cnt[o][0] + w1 * cnt[o][1];
27 |
28 | if (flip)
29 | swap(cnt[o][0], cnt[o][1]);
30 |
31 | if (rev[o])
32 | swap(w0, w1);
33 |
34 | mul[o][0] += w0;
35 | mul[o][1] += w1;
36 | rev[o] ^= flip;
37 | }
38 |
39 | void pushdown(int o) {
40 | if (!mul[o][0] && !mul[o][1] && !rev[o])
41 | return;
42 |
43 | apply(o * 2, rev[o], mul[o][0], mul[o][1]);
44 | apply(o * 2 + 1, rev[o], mul[o][0], mul[o][1]);
45 |
46 | mul[o][0] = mul[o][1] = 0;
47 | rev[o] = false;
48 | }
49 |
50 | void update(int o) {
51 | cnt[o][0] = cnt[o * 2][0] + cnt[o * 2 + 1][0];
52 | cnt[o][1] = cnt[o * 2][1] + cnt[o * 2 + 1][1];
53 |
54 | sum[o] = sum[o * 2] + sum[o * 2 + 1];
55 | }
56 |
57 | int s, t;
58 |
59 | void modify(int l, int r, int o) {
60 | if (s <= l && t >= r) {
61 | apply(o, true, 0, 0);
62 | return;
63 | }
64 |
65 | int mid = (l + r) / 2;
66 | pushdown(o);
67 |
68 | if (s <= mid)
69 | modify(l, mid, o * 2);
70 | if (t > mid)
71 | modify(mid + 1, r, o * 2 + 1);
72 |
73 | update(o);
74 | }
75 |
76 | long long query(int l, int r, int o) {
77 | if (s <= l && t >= r)
78 | return sum[o];
79 |
80 | int mid = (l + r) / 2;
81 | pushdown(o);
82 |
83 | long long ans = 0;
84 | if (s <= mid)
85 | ans += query(l, mid, o * 2);
86 | if (t > mid)
87 | ans += query(mid + 1, r, o * 2 + 1);
88 |
89 | return ans;
90 | }
91 |
92 | vector > vec[maxn]; // pos, id
93 |
94 | long long ans[maxn];
95 | int a[maxn], last[maxn];
96 |
97 | int main() {
98 |
99 | int n;
100 | scanf("%d", &n);
101 |
102 | build(1, n, 1);
103 |
104 | for (int i = 1; i <= n; i++)
105 | scanf("%d", &a[i]);
106 |
107 | int m;
108 | scanf("%d", &m);
109 |
110 | for (int i = 1; i <= m; i++) {
111 | int l, r;
112 | scanf("%d%d", &l, &r);
113 |
114 | vec[r].emplace_back(l, i);
115 | }
116 |
117 | for (int i = 1; i <= n; i++) {
118 | s = last[a[i]] + 1;
119 | t = now = i;
120 |
121 | modify(1, n, 1);
122 | apply(1, false, 0, 1);
123 |
124 | for (auto [l, k] : vec[i]) {
125 | s = l;
126 | ans[k] = query(1, n, 1);
127 | }
128 |
129 | last[a[i]] = i;
130 | }
131 |
132 | for (int i = 1; i <= m; i++)
133 | printf("%lld\n", ans[i]);
134 |
135 | return 0;
136 | }
--------------------------------------------------------------------------------
/src/datastructure/tarjanlca.cpp:
--------------------------------------------------------------------------------
1 | vector > q[maxn];
2 | int lca[maxn];
3 |
4 | void dfs(int x) {
5 | dfn[x] = ++tim; // 其实求LCA是用不到DFS序的, 但是一般其他步骤要用
6 | ufs[x] = x;
7 |
8 | for (auto pi : q[x]) {
9 | int y = pi.first, i = pi.second;
10 | if (dfn[y])
11 | lca[i] = findufs(y);
12 | }
13 |
14 | for (int y : G[x])
15 | if (y != p[x]) {
16 | p[y] = x;
17 | dfs(y);
18 | }
19 |
20 | ufs[x] = p[x];
21 | }
--------------------------------------------------------------------------------
/src/datastructure/二叉堆.cpp:
--------------------------------------------------------------------------------
1 | struct my_binary_heap {
2 | static constexpr int maxn = 100005;
3 |
4 | int a[maxn], size;
5 |
6 | my_binary_heap() : size(0) {}
7 |
8 | void push(int val) {
9 | a[++size] = val;
10 |
11 | for (int x = size; x > 1; x /= 2) {
12 | if (a[x] < a[x / 2])
13 | swap(a[x], a[x / 2]);
14 | else
15 | break;
16 | }
17 | }
18 |
19 | int &top() {
20 | return a[1];
21 | }
22 |
23 | int pop() {
24 | int res = a[1];
25 | a[1] = a[size--];
26 |
27 | for (int x = 1, son; ; x = son) {
28 | if (x * 2 == size)
29 | son = x * 2;
30 | else if (x * 2 > size)
31 | break;
32 | else if (a[x * 2] < a[x * 2 + 1])
33 | son = x * 2;
34 | else
35 | son = x * 2 + 1;
36 |
37 | if (a[son] < a[x])
38 | swap(a[x], a[son]);
39 | else
40 | break;
41 | }
42 |
43 | return res;
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/src/datastructure/分治并查集.cpp:
--------------------------------------------------------------------------------
1 | vector > seg[(1 << 22) + 5];
2 |
3 | int s, t;
4 | pair d;
5 |
6 | void add(int l, int r, int o) {
7 | if (s > t)
8 | return;
9 |
10 | if (s <= l && t >= r) {
11 | seg[o].push_back(d);
12 | return;
13 | }
14 |
15 | int mid = (l + r) / 2;
16 |
17 | if (s <= mid)
18 | add(l, mid, o * 2);
19 | if (t > mid)
20 | add(mid + 1, r, o * 2 + 1);
21 | }
22 |
23 | int ufs[maxn], sz[maxn], stk[maxn], top;
24 |
25 | int findufs(int x) {
26 | while (ufs[x] != x)
27 | x = ufs[x];
28 |
29 | return ufs[x];
30 | }
31 |
32 | void link(int x, int y) {
33 | x = findufs(x);
34 | y = findufs(y);
35 |
36 | if (x == y)
37 | return;
38 |
39 | if (sz[x] < sz[y])
40 | swap(x, y);
41 |
42 | ufs[y] = x;
43 | sz[x] += sz[y];
44 | stk[++top] = y;
45 | }
46 |
47 | int ans[maxm];
48 |
49 | void solve(int l, int r, int o) {
50 | int tmp = top;
51 |
52 | for (auto pi : seg[o])
53 | link(pi.first, pi.second);
54 |
55 | if (l == r)
56 | ans[l] = top;
57 | else {
58 | int mid = (l + r) / 2;
59 |
60 | solve(l, mid, o * 2);
61 | solve(mid + 1, r, o * 2 + 1);
62 | }
63 |
64 | for (int i = top; i > tmp; i--) {
65 | int x = stk[i];
66 |
67 | sz[ufs[x]] -= sz[x];
68 | ufs[x] = x;
69 | }
70 |
71 | top = tmp;
72 | }
73 |
74 | map, int> mp;
--------------------------------------------------------------------------------
/src/datastructure/动态KD树.cpp:
--------------------------------------------------------------------------------
1 | int l[2], r[2], x[B + 10][2], w[B + 10];
2 | int n, op, ans = 0, cnt = 0, tmp = 0;
3 | int d;
4 |
5 | struct node {
6 | int x[2], l[2], r[2], w, sum;
7 | node *ch[2];
8 |
9 | bool operator < (const node &a) const {
10 | return x[d] < a.x[d];
11 | }
12 |
13 | void refresh() {
14 | sum = ch[0] -> sum + ch[1] -> sum + w;
15 | l[0] = min(x[0], min(ch[0] -> l[0], ch[1] -> l[0]));
16 | l[1] = min(x[1], min(ch[0] -> l[1], ch[1] -> l[1]));
17 | r[0] = max(x[0], max(ch[0] -> r[0], ch[1] -> r[0]));
18 | r[1] = max(x[1], max(ch[0] -> r[1], ch[1] -> r[1]));
19 | }
20 | } null[maxn], *root = null;
21 |
22 | void build(int l, int r, int k, node *&rt) {
23 | if (l > r) {
24 | rt = null;
25 | return;
26 | }
27 |
28 | int mid = (l + r) / 2;
29 |
30 | d = k;
31 | nth_element(null + l, null + mid, null + r + 1);
32 |
33 | rt = null + mid;
34 | build(l, mid - 1, k ^ 1, rt -> ch[0]);
35 | build(mid + 1, r, k ^ 1, rt -> ch[1]);
36 |
37 | rt -> refresh();
38 | }
39 |
40 | void query(node *rt) {
41 | if (l[0] <= rt -> l[0] && l[1] <= rt -> l[1] && rt -> r[0] <= r[0] && rt -> r[1] <= r[1]) {
42 | ans += rt -> sum;
43 | return;
44 | }
45 | else if (l[0] > rt -> r[0] || l[1] > rt -> r[1] || r[0] < rt -> l[0] || r[1] < rt -> l[1])
46 | return;
47 |
48 | if (l[0] <= rt -> x[0] && l[1] <= rt -> x[1] && rt -> x[0] <= r[0] && rt -> x[1] <= r[1])
49 | ans += rt -> w;
50 |
51 | query(rt -> ch[0]);
52 | query(rt -> ch[1]);
53 | }
54 |
55 | int main() {
56 |
57 | null -> l[0] = null -> l[1] = 10000000;
58 | null -> r[0] = null -> r[1] = -10000000;
59 | null -> sum = 0;
60 | null -> ch[0] = null -> ch[1] = null;
61 | scanf("%*d");
62 |
63 | while (scanf("%d", &op) == 1 && op != 3) {
64 | if (op == 1) {
65 | tmp++;
66 | scanf("%d%d%d", &x[tmp][0], &x[tmp][1], &w[tmp]);
67 | x[tmp][0] ^= ans;
68 | x[tmp][1] ^= ans;
69 | w[tmp] ^= ans;
70 |
71 | if (tmp == B) {
72 | for (int i = 1; i <= tmp; i++) {
73 | null[cnt + i].x[0] = x[i][0];
74 | null[cnt + i].x[1] = x[i][1];
75 | null[cnt + i].w = w[i];
76 | }
77 |
78 | build(1, cnt += tmp, 0, root);
79 | tmp = 0;
80 | }
81 | }
82 | else {
83 | scanf("%d%d%d%d", &l[0], &l[1], &r[0], &r[1]);
84 | l[0] ^= ans;
85 | l[1] ^= ans;
86 | r[0] ^= ans;
87 | r[1] ^= ans;
88 | ans = 0;
89 |
90 | for (int i = 1; i <= tmp; i++)
91 | if (l[0] <= x[i][0] && l[1] <= x[i][1] && x[i][0] <= r[0] && x[i][1] <= r[1])
92 | ans += w[i];
93 |
94 | query(root);
95 | printf("%d\n", ans);
96 | }
97 | }
98 |
99 | return 0;
100 | }
--------------------------------------------------------------------------------
/src/datastructure/动态树分治.cpp:
--------------------------------------------------------------------------------
1 | // 为了减小常数, 这里采用bfs写法, 实测预处理比dfs快将近一半
2 | // 以下以维护一个点到每个黑点的距离之和为例
3 |
4 | // 全局数组定义
5 | vector G[maxn], W[maxn];
6 | int size[maxn], son[maxn], q[maxn];
7 | int p[maxn], depth[maxn], id[maxn][20], d[maxn][20]; // id是对应层所在子树的根
8 | int a[maxn], ca[maxn], b[maxn][20], cb[maxn][20]; // 维护距离和用的
9 | bool vis[maxn], col[maxn];
10 |
11 | // 建树 总计O(n\log n)
12 | // 需要调用找重心和预处理距离, 同时递归调用自身
13 | void build(int x, int k, int s, int pr) { // 结点, 深度, 连通块大小, 点分树上的父亲
14 | x = getcenter(x, s);
15 | vis[x] = true;
16 | depth[x] = k;
17 | p[x] = pr;
18 |
19 | for (int i = 0; i < (int)G[x].size(); i++)
20 | if (!vis[G[x][i]]) {
21 | d[G[x][i]][k] = W[x][i];
22 | p[G[x][i]] = x;
23 |
24 | getdis(G[x][i], k, G[x][i]); // bfs每个子树, 预处理距离
25 | }
26 |
27 | for (int i = 0; i < (int)G[x].size(); i++)
28 | if (!vis[G[x][i]])
29 | build(G[x][i], k + 1, size[G[x][i]], x); // 递归建树
30 | }
31 |
32 | // 找重心 O(n)
33 | int getcenter(int x, int s) {
34 | int head = 0, tail = 0;
35 | q[tail++] = x;
36 |
37 | while (head != tail) {
38 | x = q[head++];
39 | size[x] = 1; // 这里不需要清空, 因为以后要用的话一定会重新赋值
40 | son[x] = 0;
41 |
42 | for (int i = 0; i < (int)G[x].size(); i++)
43 | if (!vis[G[x][i]] && G[x][i] != p[x]) {
44 | p[G[x][i]] = x;
45 | q[tail++] = G[x][i];
46 | }
47 | }
48 |
49 | for (int i = tail - 1; i; i--) {
50 | x = q[i];
51 | size[p[x]] += size[x];
52 |
53 | if (size[x] > size[son[p[x]]])
54 | son[p[x]] = x;
55 | }
56 |
57 | x = q[0];
58 | while (son[x] && size[son[x]] * 2 >= s)
59 | x = son[x];
60 |
61 | return x;
62 | }
63 |
64 | // 预处理距离 O(n)
65 | // 方便起见, 这里直接用了笨一点的方法, O(n\log n)全存下来
66 | void getdis(int x, int k, int rt) {
67 | int head = 0, tail = 0;
68 | q[tail++] = x;
69 |
70 | while (head != tail) {
71 | x = q[head++];
72 | size[x] = 1;
73 | id[x][k] = rt;
74 |
75 | for (int i = 0; i < (int)G[x].size(); i++)
76 | if (!vis[G[x][i]] && G[x][i] != p[x]) {
77 | p[G[x][i]] = x;
78 | d[G[x][i]][k] = d[x][k] + W[x][i];
79 |
80 | q[tail++] = G[x][i];
81 | }
82 | }
83 |
84 | for (int i = tail - 1; i; i--)
85 | size[p[q[i]]] += size[q[i]]; // 后面递归建树要用到子问题大小
86 | }
87 |
88 | // 修改 O(\log n)
89 | void modify(int x) {
90 | if (col[x])
91 | ca[x]--;
92 | else
93 | ca[x]++; // 记得先特判自己作为重心的那层
94 |
95 | for (int u = p[x], k = depth[x] - 1; u; u = p[u], k--) {
96 | if (col[x]) {
97 | a[u] -= d[x][k];
98 | ca[u]--;
99 |
100 | b[id[x][k]][k] -= d[x][k];
101 | cb[id[x][k]][k]--;
102 | }
103 | else {
104 | a[u] += d[x][k];
105 | ca[u]++;
106 |
107 | b[id[x][k]][k] += d[x][k];
108 | cb[id[x][k]][k]++;
109 | }
110 | }
111 |
112 | col[x] ^= true;
113 | }
114 |
115 | // 询问 O(\log n)
116 | int query(int x) {
117 | int ans = a[x]; // 特判自己是重心的那层
118 |
119 | for (int u = p[x], k = depth[x] - 1; u; u = p[u], k--)
120 | ans += a[u] - b[id[x][k]][k] + d[x][k] * (ca[u] - cb[id[x][k]][k]);
121 |
122 | return ans;
123 | }
--------------------------------------------------------------------------------
/src/datastructure/常见根号思路.tex:
--------------------------------------------------------------------------------
1 | \begin{enumerate}
2 |
3 | \item \textbf{通用}
4 |
5 | \begin{itemize}
6 | \item 出现次数大于 $\sqrt n$ 的数不会超过 $\sqrt n$ 个
7 | \item 对于带修改问题,如果不方便分治或者二进制分组,可以考虑对操作分块(定期重建),每次查询时暴力最后的 $\sqrt n$ 个修改并更正答案
8 | \item \textbf{根号分治}:如果分治时每个子问题需要 $O(N)$(N 是全局问题的大小的时间),而规模较小的子问题可以 $O(n^2)$ 解决,则可以使用根号分治
9 | \begin{itemize}
10 | \item 规模大于 $\sqrt n$ 的子问题用 $O(N)$ 的方法解决,规模小于 $\sqrt n$ 的子问题用 $O(n^2)$ 暴力
11 | \item 规模大于 $\sqrt n$ 的子问题最多只有 $\sqrt n$ 个
12 | \item 规模不大于 $\sqrt n$ 的子问题大小的平方和也必定不会超过 $n\sqrt n$
13 | \end{itemize}
14 | \item 如果输入规模之和不大于 $n$(例如给定多个小字符串与大字符串进行询问),那么规模超过 $\sqrt n$ 的问题最多只有 $\sqrt n$ 个
15 | \end{itemize}
16 |
17 | \item \textbf{序列}
18 |
19 | \begin{itemize}
20 | \item 某些维护序列的问题可以用分块/块状链表维护
21 | \item 对于静态区间询问问题,如果可以快速将左/右端点移动一位,可以考虑莫队
22 | \begin{itemize}
23 | \item 如果强制在线可以分块预处理,但是一般空间需要 $n\sqrt n$
24 | \begin{itemize}
25 | \item 例题:询问区间中有几种数出现次数恰好为 $k$,强制在线
26 | \end{itemize}
27 | \item 如果带修改可以试着想一想带修莫队,但是复杂度高达 $n^{\frac 5 3}$
28 | \end{itemize}
29 | \item 线段树可以解决的问题也可以用分块来做到 $O(1)$ 询问或是 $O(1)$ 修改,具体要看哪种操作更多
30 | \end{itemize}
31 |
32 | \item \textbf{树}
33 |
34 | \begin{itemize}
35 | \item 与序列类似,树上也有树分块和树上莫队
36 | \begin{itemize}
37 | \item 树上带修莫队很麻烦,常数也大,最好不要先考虑
38 | \item 树分块不要想当然
39 | \end{itemize}
40 | \item 树分治也可以套根号分治,道理是一样的
41 | \end{itemize}
42 |
43 | \item \textbf{字符串}
44 |
45 | \begin{itemize}
46 | \item 循环节长度大于 $\sqrt n$ 的子串最多只有 $O(n)$ 个,如果是极长子串则只有 $O(\sqrt n)$ 个
47 | \end{itemize}
48 |
49 | \end{enumerate}
50 |
--------------------------------------------------------------------------------
/src/datastructure/整体二分.cpp:
--------------------------------------------------------------------------------
1 | int op[maxn], ql[maxn], qr[maxn]; // 1: modify 2: query
2 | long long qk[maxn]; // 修改和询问可以一起存
3 |
4 | int ans[maxn];
5 |
6 | void solve(int l, int r, vector v) { // 如果想卡常可以用数组, 然后只需要传一个数组的l, r; 递归的时候类似归并反过来, 开两个辅助数组, 处理完再复制回去即可
7 | if (v.empty())
8 | return;
9 |
10 | if (l == r) {
11 | for (int i : v)
12 | if (op[i] == 2)
13 | ans[i] = l;
14 |
15 | return;
16 | }
17 |
18 | int mid = (l + r) / 2;
19 |
20 | vector vl, vr;
21 |
22 | for (int i : v) {
23 | if (op[i] == 1) {
24 | if (qk[i] <= mid)
25 | vl.push_back(i);
26 | else {
27 | update(ql[i], qr[i], 1); // update是区间加
28 | vr.push_back(i);
29 | }
30 | }
31 | else {
32 | long long tmp = query(ql[i], qr[i]);
33 |
34 | if (qk[i] <= tmp) // 因为是k大数查询
35 | vr.push_back(i);
36 | else {
37 | qk[i] -= tmp;
38 | vl.push_back(i);
39 | }
40 | }
41 | }
42 |
43 | for (int i : vr)
44 | if (op[i] == 1)
45 | update(ql[i], qr[i], -1);
46 |
47 | v.clear();
48 |
49 | solve(l, mid, vl);
50 | solve(mid + 1, r, vr);
51 | }
52 |
53 | int main() {
54 | int n, m;
55 | scanf("%d%d", &n, &m);
56 |
57 | M = 1;
58 | while (M < n + 2)
59 | M *= 2;
60 |
61 | for (int i = 1; i <= m; i++)
62 | scanf("%d%d%d%lld", &op[i], &ql[i], &qr[i], &qk[i]);
63 |
64 | vector v;
65 | for (int i = 1; i <= m; i++)
66 | v.push_back(i);
67 |
68 | solve(1, 1e9, v);
69 |
70 | for (int i = 1; i <= m; i++)
71 | if (op[i] == 2)
72 | printf("%d\n", ans[i]);
73 |
74 | return 0;
75 | }
--------------------------------------------------------------------------------
/src/datastructure/文艺平衡树.cpp:
--------------------------------------------------------------------------------
1 | #define dir(x) ((x) == (x) -> p -> ch[1])
2 |
3 | struct node {
4 | int size;
5 | bool rev;
6 | node *ch[2],*p;
7 |
8 | node() : size(1), rev(false) {}
9 |
10 | void pushdown() {
11 | if(!rev)
12 | return;
13 |
14 | ch[0] -> rev ^= true;
15 | ch[1] -> rev ^= true;
16 | swap(ch[0], ch[1]);
17 |
18 | rev=false;
19 | }
20 |
21 | void refresh() {
22 | size = ch[0] -> size + ch[1] -> size + 1;
23 | }
24 | } null[maxn], *root = null;
25 |
26 | void rot(node *x, int d) {
27 | node *y = x -> ch[d ^ 1];
28 |
29 | if ((x -> ch[d ^ 1] = y -> ch[d]) != null)
30 | y -> ch[d] -> p = x;
31 | ((y -> p = x -> p) != null ? x -> p -> ch[dir(x)] : root) = y;
32 | (y -> ch[d] = x) -> p = y;
33 |
34 | x -> refresh();
35 | y -> refresh();
36 | }
37 |
38 | void splay(node *x, node *t) {
39 | while (x -> p != t) {
40 | if (x -> p -> p == t) {
41 | rot(x -> p, dir(x) ^ 1);
42 | break;
43 | }
44 |
45 | if (dir(x) == dir(x -> p))
46 | rot(x -> p -> p, dir(x -> p) ^ 1);
47 | else
48 | rot(x -> p, dir(x) ^ 1);
49 | rot(x -> p, dir(x) ^ 1);
50 | }
51 | }
52 |
53 | node *kth(int k, node *o) {
54 | int d;
55 | k++; // 因为最左边有一个哨兵
56 |
57 | while (o != null) {
58 | o -> pushdown();
59 |
60 | if (k == o -> ch[0] -> size + 1)
61 | return o;
62 |
63 | if ((d = k > o -> ch[0] -> size))
64 | k -= o -> ch[0] -> size + 1;
65 | o = o -> ch[d];
66 | }
67 |
68 | return null;
69 | }
70 |
71 | void reverse(int l, int r) {
72 | splay(kth(l - 1));
73 | splay(kth(r + 1), root);
74 |
75 | root -> ch[1] -> ch[0] -> rev ^= true;
76 | }
77 |
78 | int n, m;
79 |
80 | int main() {
81 | null -> size = 0;
82 | null -> ch[0] = null -> ch[1] = null -> p = null;
83 |
84 | scanf("%d%d", &n, &m);
85 | root = null + n + 1;
86 | root -> ch[0] = root -> ch[1] = root -> p = null;
87 |
88 | for (int i = 1; i <= n; i++) {
89 | null[i].ch[1] = null[i].p = null;
90 | null[i].ch[0] = root;
91 | root -> p = null + i;
92 | (root = null + i) -> refresh();
93 | }
94 |
95 | null[n + 2].ch[1] = null[n + 2].p = null;
96 | null[n + 2].ch[0] = root; // 这里直接建成一条链的, 如果想减少常数也可以递归建一个平衡的树
97 | root -> p = null + n + 2; // 总之记得建两个哨兵, 这样splay起来不需要特判
98 | (root = null + n + 2) -> refresh();
99 |
100 | // Do something
101 |
102 | return 0;
103 | }
--------------------------------------------------------------------------------
/src/datastructure/无旋Treap.cpp:
--------------------------------------------------------------------------------
1 | struct node {
2 | int val, size;
3 | node *ch[2];
4 |
5 | node(int val) : val(val), size(1) {}
6 |
7 | inline void update() {
8 | size = ch[0] -> size + ch[1] -> size;
9 | }
10 |
11 |
12 | } null[maxn];
13 |
14 | node *copied(node *x) { // 如果不用可持久化的话, 直接用就行了
15 | return new node(*x);
16 | }
17 |
18 | node *merge(node *x, node *y) {
19 | if (x == null)
20 | return y;
21 | if (y == null)
22 | return x;
23 |
24 | node *z;
25 | if (rand() % (x -> size + y -> size) < x -> size) {
26 | z = copied(x);
27 | z -> ch[1] = merge(x -> ch[1], y);
28 | }
29 | else {
30 | z = copied(y);
31 | z -> ch[0] = merge(x, y -> ch[0]);
32 | }
33 |
34 | z -> update(); // 因为每次只有一边会递归到儿子, 所以 z 不可能取到 null
35 | return z;
36 | }
37 |
38 | pair split(node *x, int k) { // 左边大小为k
39 | if (x == null)
40 | return make_pair(null, null);
41 |
42 | pair pi(null, null);
43 |
44 | if (k <= x -> ch[0] -> size) {
45 | pi = split(x -> ch[0], k);
46 |
47 | node *z = copied(x);
48 | z -> ch[0] = pi.second;
49 | z -> update();
50 | pi.second = z;
51 | }
52 | else {
53 | pi = split(x -> ch[1], k - x -> ch[0] -> size - 1);
54 |
55 | node *y = copied(x);
56 | y -> ch[1] = pi.first;
57 | y -> update();
58 | pi.first = y;
59 | }
60 |
61 | return pi;
62 | }
63 |
64 | // 记得初始化null
65 | int main() {
66 | for (int i = 0; i <= n; i++)
67 | null[i].ch[0] = null[i].ch[1] = null;
68 | null -> size = 0;
69 |
70 | // do something
71 |
72 | return 0;
73 | }
74 |
--------------------------------------------------------------------------------
/src/datastructure/梯子剖分.cpp:
--------------------------------------------------------------------------------
1 | // 在线求一个点的第k祖先 O(n\log n)-O(1)
2 | // 理论基础: 任意一个点x的k级祖先y所在长链长度一定>=k
3 |
4 | // 全局数组定义
5 | vector G[maxn], v[maxn];
6 | int d[maxn], mxd[maxn], son[maxn], top[maxn], len[maxn];
7 | int f[19][maxn], log_tbl[maxn];
8 |
9 | // 在主函数中两遍dfs之后加上如下预处理
10 | log_tbl[0] = -1;
11 | for (int i = 1; i <= n; i++)
12 | log_tbl[i] = log_tbl[i / 2] + 1;
13 | for (int j = 1; (1 << j) < n; j++)
14 | for (int i = 1; i <= n; i++)
15 | f[j][i] = f[j - 1][f[j - 1][i]];
16 |
17 | // 第一遍dfs, 用于计算深度和找出重儿子
18 | void dfs1(int x) {
19 | mxd[x] = d[x];
20 |
21 | for (int y : G[x])
22 | if (y != f[0][x]){
23 | f[0][y] = x;
24 | d[y] = d[x] + 1;
25 |
26 | dfs1(y);
27 |
28 | mxd[x] = max(mxd[x], mxd[y]);
29 | if (mxd[y] > mxd[son[x]])
30 | son[x] = y;
31 | }
32 | }
33 |
34 | // 第二遍dfs, 用于进行剖分和预处理梯子剖分(每条链向上延伸一倍)数组
35 | void dfs2(int x) {
36 | top[x] = (x == son[f[0][x]] ? top[f[0][x]] : x);
37 |
38 | for (int y : G[x])
39 | if (y != f[0][x])
40 | dfs2(y);
41 |
42 | if (top[x] == x) {
43 | int u = x;
44 | while (top[son[u]] == x)
45 | u = son[u];
46 |
47 | len[x] = d[u] - d[x];
48 | for (int i = 0; i < len[x]; i++, u = f[0][u])
49 | v[x].push_back(u);
50 |
51 | u = x;
52 | for (int i = 0; i < len[x] && u; i++, u = f[0][u])
53 | v[x].push_back(u);
54 | }
55 | }
56 |
57 | // 在线询问x的k级祖先 O(1)
58 | // 不存在时返回0
59 | int query(int x, int k) {
60 | if (!k)
61 | return x;
62 | if (k > d[x])
63 | return 0;
64 |
65 | x = f[log_tbl[k]][x];
66 | k ^= 1 << log_tbl[k];
67 | return v[top[x]][d[top[x]] + len[top[x]] - d[x] + k];
68 | }
--------------------------------------------------------------------------------
/src/datastructure/线段树维护矩形并.cpp:
--------------------------------------------------------------------------------
1 | constexpr int maxn = 100005, maxm = maxn * 70;
2 |
3 | int lc[maxm], rc[maxm], cover[maxm], sum[maxm], root, seg_cnt;
4 | int s, t, d;
5 |
6 | void refresh(int l, int r, int o) {
7 | if (cover[o])
8 | sum[o] = r - l + 1;
9 | else
10 | sum[o] = sum[lc[o]] + sum[rc[o]];
11 | }
12 |
13 | void modify(int l, int r, int &o) {
14 | if (!o)
15 | o = ++seg_cnt;
16 |
17 | if (s <= l && t >= r) {
18 | cover[o] += d;
19 | refresh(l, r, o);
20 |
21 | return;
22 | }
23 |
24 | int mid = (l + r) / 2;
25 |
26 | if (s <= mid)
27 | modify(l, mid, lc[o]);
28 | if (t > mid)
29 | modify(mid + 1, r, rc[o]);
30 |
31 | refresh(l, r, o);
32 | }
33 |
34 | struct modi {
35 | int x, l, r, d;
36 |
37 | bool operator < (const modi &o) {
38 | return x < o.x;
39 | }
40 | } a[maxn * 2];
41 |
42 | int main() {
43 |
44 | int n;
45 | scanf("%d", &n);
46 |
47 | for (int i = 1; i <= n; i++) {
48 | int lx, ly, rx, ry;
49 | scanf("%d%d%d%d", &lx, &ly, &rx, &ry);
50 |
51 | a[i * 2 - 1] = {lx, ly + 1, ry, 1};
52 | a[i * 2] = {rx, ly + 1, ry, -1};
53 | }
54 |
55 | sort(a + 1, a + n * 2 + 1);
56 |
57 | int last = -1;
58 | long long ans = 0;
59 |
60 | for (int i = 1; i <= n * 2; i++) {
61 | if (last != -1)
62 | ans += (long long)(a[i].x - last) * sum[1];
63 | last = a[i].x;
64 |
65 | s = a[i].l;
66 | t = a[i].r;
67 | d = a[i].d;
68 |
69 | modify(1, 1e9, root);
70 | }
71 |
72 | printf("%lld\n", ans);
73 |
74 | return 0;
75 | }
--------------------------------------------------------------------------------
/src/datastructure/莫队.tex:
--------------------------------------------------------------------------------
1 | 注意如果 $n$ 和 $q$ 不平衡,块大小应该设为$\frac n {\sqrt q}$。
2 |
3 | 另外,如果裸的莫队要卡常,可以按块编号奇偶性分别对右端点正序或者倒序排序,期望可以减少一半的移动次数。
4 |
5 | % \subsubsection{回滚莫队(无删除莫队)(待完成)}
6 |
7 | \subsubsection{莫队二次离线}
8 |
9 | 适用范围:询问的是点对相关(或者其它可以枚举每个点和区间算贡献)的信息,并且可以离线;更新时可以使用一些牺牲修改复杂度来改善询问复杂度的数据结构(如单点修改询问区间和)。
10 |
11 | 先按照普通的莫队将区间排序。考虑区间移动的情况,以 $(l, r)$ 向右移动右端点到 $(l, t)$ 为例。
12 |
13 | 对于每个 $i \in (r, t]$ 来说,它都要对区间 $[l, i)$ 算贡献。可以拆成 $[1, i)$ 和 $[1, l)$ 两部分,那么前一部分因为都是 $i$ 和 $[1, i)$ 做贡献的形式,可以直接预处理。
14 |
15 | 考虑后一部分,$i$ 和 $(1, l]$ 做贡献。因为莫队的性质,我们可以保证这样的询问次数不超过 $O((n + m)\sqrt n)$,因此我们可以对每个 $l$ 记录下来哪些 $i$ 要和它询问。并且每次移动时询问的 $i$ 都是连续的,所以对每个 $l$ 开一个 \mintinline{cpp}{vector} 记录下对应的区间和编号就行了。
16 |
17 | 剩余的三种情况(右端点左移或者移动左端点)都是类似的,具体可以看代码。
18 |
19 | 例题:Yuno loves sqrt technology II(询问区间逆序对数)
20 |
21 | \inputminted{cpp}{../src/datastructure/莫队二次离线.cpp}
22 |
23 | \subsubsection{带修莫队在线化 $O(n ^ {\frac 5 3})$}
24 |
25 | 最简单的带修莫队:块大小设成 $n^{\frac 2 3}$,排序时第一关键字是左端点块编号,第二关键字是右端点块编号,第三关键字是时间。(记得把时间压缩成只有修改的时间。)
26 |
27 | 现在要求在线的同时支持修改,仍然以 $B = n^{\frac 2 3}$ 分一块,预处理出两块之间的贡献,那么预处理复杂度就是 $O(n ^ {\frac 5 3})$。
28 |
29 | 修改时最简单的方法是直接把 $n^{\frac 2 3}$ 个区间全更新一遍。嫌慢的话可以给每个区间打一个懒标记,询问的时候如果解了再更新区间的信息。
30 |
31 | 注意如果附加信息是可减的(比如每个数的出现次数),那么就只需要存 $O(n^{\frac 1 3})$ 个。
32 |
33 | 总复杂度仍然是 $O(n^{\frac 5 3})$,如果打懒标记的话是跑不太满的。如果附加信息可减,则空间复杂度是 $O(n^{\frac 4 3})$,否则和时间复杂度同阶。
34 |
35 | \subsubsection{莫队二次离线在线化 $O((n + m)\sqrt n)$}
36 |
37 | 和之前的道理是一样的,$i$ 和 $[1, i)$ 的贡献这部分仍然可以预处理掉,而前后缀对区间的贡献那部分只保存块端点处的信息。
38 |
39 | 按照莫队二次离线的转移方法操作之后,发现只剩两边散块的贡献没有解决。这时可以具体问题具体解决,例如求逆序对的话直接预处理出排序后的数组,询问的时候归并即可。
40 |
41 | 时空复杂度均为 $O(n\sqrt n)$。
42 |
43 | 以下代码以强制在线求区间逆序对为例。(洛谷上被卡常了,正常情况下极限数据应该在 1.5s 内。)
44 |
45 | \inputminted{cpp}{../src/datastructure/莫队二次离线在线化.cpp}
46 |
--------------------------------------------------------------------------------
/src/datastructure/莫队例题.tex:
--------------------------------------------------------------------------------
1 | \subsubsection{例题}
2 |
3 | \subsubsection*{子区间最小值之和}
4 |
5 | (HNOI2016 序列)
6 |
7 | \textbf{待完成。}
8 |
--------------------------------------------------------------------------------
/src/datastructure/长链剖分.cpp:
--------------------------------------------------------------------------------
1 | // 顾名思义, 长链剖分是取最深的儿子作为重儿子
2 |
3 | // O(n)维护以深度为下标的子树信息
4 | vector G[maxn], v[maxn];
5 | int n, p[maxn], h[maxn], son[maxn], ans[maxn];
6 |
7 | // 原题题意: 求每个点的子树中与它距离是几的点最多, 相同的取最大深度
8 | // 由于vector只能在后面加入元素, 为了写代码方便, 这里反过来存
9 | // 或者开一个结构体维护倒过来的vector
10 | void dfs(int x) {
11 | h[x] = 1;
12 |
13 | for (int y : G[x])
14 | if (y != p[x]){
15 | p[y] = x;
16 | dfs(y);
17 |
18 | if (h[y] > h[son[x]])
19 | son[x] = y;
20 | }
21 |
22 | if (!son[x]) {
23 | v[x].push_back(1);
24 | ans[x] = 0;
25 | return;
26 | }
27 |
28 | h[x] = h[son[x]] + 1;
29 | swap(v[x],v[son[x]]);
30 |
31 | if (v[x][ans[son[x]]] == 1)
32 | ans[x] = h[x] - 1;
33 | else
34 | ans[x] = ans[son[x]];
35 |
36 | v[x].push_back(1);
37 |
38 | int mx = v[x][ans[x]];
39 | for (int y : G[x])
40 | if (y != p[x] && y != son[x]) {
41 | for (int j = 1; j <= h[y]; j++) {
42 | v[x][h[x] - j - 1] += v[y][h[y] - j];
43 |
44 | int t = v[x][h[x] - j - 1];
45 | if (t > mx || (t == mx && h[x] - j - 1 > ans[x])) {
46 | mx = t;
47 | ans[x] = h[x] - j - 1;
48 | }
49 | }
50 |
51 | v[y].clear();
52 | }
53 | }
--------------------------------------------------------------------------------
/src/datastructure/非递归线段树.tex:
--------------------------------------------------------------------------------
1 | \sout{让 \textit{fstqwq} 手撕}\ 我队友呢?
2 |
3 | \begin{itemize}
4 | \item 如果 $M = 2^k$,则只能维护 $[1, M - 2]$ 范围
5 | \item 找叶子:$i$ 对应的叶子就是 $i + M$
6 | \item 单点修改:找到叶子然后向上跳
7 | \item 区间查询:左右区间各扩展一位,转换成开区间查询
8 | \inputminted{cpp}{../src/datastructure/非递归线段树单点修改.cpp}
9 |
10 | \end{itemize}
11 |
12 | 区间修改要标记永久化,并且求区间和和求最值的代码不太一样。
13 |
14 | \textbf{区间加,区间求和}
15 | \inputminted{cpp}{../src/datastructure/非递归线段树区间加区间求和.cpp}
16 |
17 | \textbf{区间加,区间求最大值}
18 | \inputminted{cpp}{../src/datastructure/非递归线段树区间加区间求最大值.cpp}
19 |
--------------------------------------------------------------------------------
/src/datastructure/非递归线段树区间加区间求和.cpp:
--------------------------------------------------------------------------------
1 | void update(int l, int r, int d) {
2 | int len = 1, cntl = 0, cntr = 0; // cntl, cntr是左右两边分别实际修改的区间长度
3 | for (l += n - 1, r += n + 1; l ^ r ^ 1; l >>= 1, r >>= 1, len <<= 1) {
4 | tree[l] += cntl * d, tree[r] += cntr * d;
5 | if (~l & 1) tree[l ^ 1] += d * len, mark[l ^ 1] += d, cntl += len;
6 | if (r & 1) tree[r ^ 1] += d * len, mark[r ^ 1] += d, cntr += len;
7 | }
8 |
9 | for (; l; l >>= 1, r >>= 1)
10 | tree[l] += cntl * d, tree[r] += cntr * d;
11 | }
12 |
13 | int query(int l, int r) {
14 | int ans = 0, len = 1, cntl = 0, cntr = 0;
15 | for (l += n - 1, r += n + 1; l ^ r ^ 1; l >>= 1, r >>= 1, len <<= 1) {
16 | ans += cntl * mark[l] + cntr * mark[r];
17 | if (~l & 1) ans += tree[l ^ 1], cntl += len;
18 | if (r & 1) ans += tree[r ^ 1], cntr += len;
19 | }
20 |
21 | for (; l; l >>= 1, r >>= 1)
22 | ans += cntl * mark[l] + cntr * mark[r];
23 |
24 | return ans;
25 | }
--------------------------------------------------------------------------------
/src/datastructure/非递归线段树区间加区间求最大值.cpp:
--------------------------------------------------------------------------------
1 | void update(int l, int r, int d) {
2 | for (l += N - 1, r += N + 1; l ^ r ^ 1; l >>= 1, r >>= 1) {
3 | if (l < N) {
4 | tree[l] = max(tree[l << 1], tree[l << 1 | 1]) + mark[l];
5 | tree[r] = max(tree[r << 1], tree[r << 1 | 1]) + mark[r];
6 | }
7 |
8 | if (~l & 1) {
9 | tree[l ^ 1] += d;
10 | mark[l ^ 1] += d;
11 | }
12 | if (r & 1) {
13 | tree[r ^ 1] += d;
14 | mark[r ^ 1] += d;
15 | }
16 | }
17 |
18 | for (; l; l >>= 1, r >>= 1)
19 | if (l < N) tree[l] = max(tree[l << 1], tree[l << 1 | 1]) + mark[l],
20 | tree[r] = max(tree[r << 1], tree[r << 1 | 1]) + mark[r];
21 | }
22 |
23 | int query(int l, int r) {
24 | int maxl = -INF, maxr = -INF;
25 |
26 | for (l += N - 1, r += N + 1; l ^ r ^ 1; l >>= 1, r >>= 1) {
27 | maxl += mark[l];
28 | maxr += mark[r];
29 |
30 | if (~l & 1)
31 | maxl = max(maxl, tree[l ^ 1]);
32 | if (r & 1)
33 | maxr = max(maxr, tree[r ^ 1]);
34 | }
35 |
36 | while (l) {
37 | maxl += mark[l];
38 | maxr += mark[r];
39 |
40 | l >>= 1;
41 | r >>= 1;
42 | }
43 |
44 | return max(maxl, maxr);
45 | }
--------------------------------------------------------------------------------
/src/datastructure/非递归线段树单点修改.cpp:
--------------------------------------------------------------------------------
1 | int query(int l, int r) {
2 | l += M - 1;
3 | r += M + 1;
4 |
5 | int ans = 0;
6 | while (l ^ r != 1) {
7 | ans += sum[l ^ 1] + sum[r ^ 1];
8 |
9 | l >>= 1;
10 | r >>= 1;
11 | }
12 |
13 | return ans;
14 | }
--------------------------------------------------------------------------------
/src/geometry/最近点对.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/src/geometry/最近点对.png
--------------------------------------------------------------------------------
/src/geometry/最近点对.tex:
--------------------------------------------------------------------------------
1 | 首先分治的做法是众所周知的。
2 |
3 | 有期望 $O(n)$ 的随机增量法:首先将所有点随机打乱,然后每次增加一个点,更新答案。
4 |
5 | 假设当前最近点对距离为 $s$,则把平面划分成 $s \times s$ 的方格,用哈希表存储每个方格有哪些点。
6 |
7 | 加入一个新点时,只需要枚举自身和周围共计 $9$ 个方格中的点,显然枚举到的点最多 $16$ 个。如果加入之后答案变小了,就 $O(n)$ 暴力重构。
8 |
9 | 前 $i$ 个点中 $i$ 是最近点对中的点的概率至多为 $\frac 2 i$,所以每个点的期望贡献都是 $O(1)$,总的复杂度就是期望$O(n)$。
10 |
11 | 如果对每个点都要求出距离最近的点的话,也有随机化的 $O(n)$ 做法:
12 |
13 | \includegraphics[width=0.8\linewidth]{../src/geometry/最近点对.png}
14 |
--------------------------------------------------------------------------------
/src/graph/2sat-tarjan.cpp:
--------------------------------------------------------------------------------
1 | if (!ok)
2 | printf("IMPOSSIBLE\n");
3 | else {
4 | printf("POSSIBLE\n");
5 |
6 | for (int i = 1; i <= n; i++)
7 | printf("%d%c", sccid[i * 2 - 1] > sccid[i * 2], i < n ? ' ' : '\n');
8 | }
--------------------------------------------------------------------------------
/src/graph/2sat.cpp:
--------------------------------------------------------------------------------
1 | bool vis[maxn];
2 | int stk[maxn], top;
3 |
4 | // 主函数
5 | for (int i = 0; i < n; i += 2)
6 | if (!vis[i] && !vis[i ^ 1]) {
7 | top = 0;
8 | if (!dfs(i)) {
9 | while (top)
10 | vis[stk[top--]] = false;
11 |
12 | if (!dfs(i + 1)) {
13 | bad = true;
14 | break;
15 | }
16 | }
17 | }
18 | // 最后stk中的所有元素就是选中的值
19 |
20 | // dfs
21 | bool dfs(int x) {
22 | if (vis[x ^ 1])
23 | return false;
24 |
25 | if (vis[x])
26 | return true;
27 |
28 | vis[x] = true;
29 | stk[++top] = x;
30 |
31 | for (int i = 0; i < (int)G[x].size(); i++)
32 | if (!dfs(G[x][i]))
33 | return false;
34 |
35 | return true;
36 | }
--------------------------------------------------------------------------------
/src/graph/2sat.tex:
--------------------------------------------------------------------------------
1 | 如果限制满足对称性(每个命题的逆否命题对应的边也存在),那么可以使用 Tarjan 算法求 SCC 搞定。
2 |
3 | 具体来说,如果某个变量的两个点在同一 SCC 中则显然无解,否则按拓扑序倒序尝试选择每个SCC即可。
4 |
5 | 由于 Tarjan 算法的特性,找到 SCC 的顺序就是拓扑序 \textbf{倒序},所以判断完是否有解之后,每个变量只需要取 SCC 编号 \textbf{较小}\ 的那个即可。
6 |
7 | \inputminted{cpp}{../src/graph/2sat-tarjan.cpp}
8 |
9 | 如果要字典序最小就用 DFS,注意可以压位优化。另外这份代码是 0-base 的。
10 |
11 | \inputminted{cpp}{../src/graph/2sat.cpp}
12 |
--------------------------------------------------------------------------------
/src/graph/Boruvka.tex:
--------------------------------------------------------------------------------
1 | 思想:每次选择连接每个连通块的最小边,把连通块缩起来。
2 |
3 | 每次连通块个数至少减半,所以迭代 $O(\log n)$ 次即可得到最小生成树。
4 |
5 | 一种比较简单的实现方法:每次迭代遍历所有边,用并查集维护连通性和每个连通块的最小边权。
6 |
7 | 应用:最小异或生成树
8 |
--------------------------------------------------------------------------------
/src/graph/Dinic.cpp:
--------------------------------------------------------------------------------
1 | // 注意Dinic适用于二分图或分层图,对于一般稀疏图ISAP更优,稠密图则HLPP更优
2 |
3 | struct edge {
4 | int to, cap, prev;
5 | } e[maxe * 2];
6 |
7 | int last[maxn], len, d[maxn], cur[maxn], q[maxn];
8 |
9 | // main函数里要初始化
10 | memset(last, -1, sizeof(last));
11 |
12 | void AddEdge(int x, int y, int z) {
13 | e[len].to = y;
14 | e[len].cap = z;
15 | e[len].prev = last[x];
16 | last[x] = len++;
17 | }
18 |
19 | void addedge(int x, int y, int z) {
20 | AddEdge(x, y, z);
21 | AddEdge(y, x, 0);
22 | }
23 |
24 | void bfs() {
25 | int head = 0, tail = 0;
26 | memset(d, -1, sizeof(int) * (t + 5));
27 | q[tail++] = s;
28 | d[s] = 0;
29 |
30 | while (head != tail) {
31 | int x = q[head++];
32 | for (int i = last[x]; ~i; i = e[i].prev)
33 | if (e[i].cap > 0 && d[e[i].to] == -1) {
34 | d[e[i].to] = d[x] + 1;
35 | q[tail++] = e[i].to;
36 | }
37 | }
38 | }
39 |
40 | int dfs(int x, int a) {
41 | if (x == t || !a)
42 | return a;
43 |
44 | int flow = 0, f;
45 | for (int &i = cur[x]; ~i; i = e[i].prev)
46 | if (e[i].cap > 0 && d[e[i].to] == d[x] + 1 && (f = dfs(e[i].to, min(e[i].cap,a)))) {
47 |
48 | e[i].cap -= f;
49 | e[i^1].cap += f;
50 | flow += f;
51 | a -= f;
52 |
53 | if (!a)
54 | break;
55 | }
56 |
57 | return flow;
58 | }
59 |
60 | int Dinic() {
61 | int flow = 0;
62 | while (bfs(), ~d[t]) {
63 | memcpy(cur, last, sizeof(int) * (t + 5));
64 | flow += dfs(s, inf);
65 | }
66 | return flow;
67 | }
--------------------------------------------------------------------------------
/src/graph/HLPP.cpp:
--------------------------------------------------------------------------------
1 | constexpr int maxn = 1205, maxe = 120005;
2 |
3 | struct edge {
4 | int to, cap, prev;
5 | } e[maxe * 2];
6 |
7 | int n, m, s, t;
8 | int last[maxn], cnte;
9 | int h[maxn], gap[maxn * 2];
10 | long long ex[maxn]; // 多余流量
11 | bool inq[maxn];
12 |
13 | struct cmp {
14 | bool operator() (int x, int y) const {
15 | return h[x] < h[y];
16 | }
17 | };
18 |
19 | priority_queue, cmp> heap;
20 |
21 | void adde(int x, int y, int z) {
22 | e[cnte].to = y;
23 | e[cnte].cap = z;
24 | e[cnte].prev = last[x];
25 | last[x] = cnte++;
26 | }
27 |
28 | void addedge(int x, int y, int z) {
29 | adde(x, y, z);
30 | adde(y, x, 0);
31 | }
32 |
33 | bool bfs() {
34 | static int q[maxn];
35 |
36 | fill(h, h + n + 1, 2 * n); // 如果没有全局的n, 记得改这里
37 | int head = 0, tail = 0;
38 | q[tail++] = t;
39 | h[t] = 0;
40 |
41 | while (head < tail) {
42 | int x = q[head++];
43 | for (int i = last[x]; ~i; i = e[i].prev)
44 | if (e[i ^ 1].cap && h[e[i].to] > h[x] + 1) {
45 | h[e[i].to] = h[x] + 1;
46 | q[tail++] = e[i].to;
47 | }
48 | }
49 |
50 | return h[s] < 2 * n;
51 | }
52 |
53 | void push(int x) {
54 | for (int i = last[x]; ~i; i = e[i].prev)
55 | if (e[i].cap && h[x] == h[e[i].to] + 1) {
56 | int d = min(ex[x], (long long)e[i].cap);
57 |
58 | e[i].cap -= d;
59 | e[i ^ 1].cap += d;
60 | ex[x] -= d;
61 | ex[e[i].to] += d;
62 |
63 | if (e[i].to != s && e[i].to != t && !inq[e[i].to]) {
64 | heap.push(e[i].to);
65 | inq[e[i].to] = true;
66 | }
67 |
68 | if (!ex[x])
69 | break;
70 | }
71 | }
72 |
73 | void relabel(int x) {
74 | h[x] = 2 * n;
75 |
76 | for (int i = last[x]; ~i; i = e[i].prev)
77 | if (e[i].cap)
78 | h[x] = min(h[x], h[e[i].to] + 1);
79 | }
80 |
81 | long long hlpp() {
82 | if (!bfs())
83 | return 0;
84 |
85 | // memset(gap, 0, sizeof(int) * 2 * n);
86 | h[s] = n;
87 |
88 | for (int i = 1; i <= n; i++)
89 | gap[h[i]]++;
90 |
91 | for (int i = last[s]; ~i; i = e[i].prev)
92 | if (e[i].cap) {
93 | int d = e[i].cap;
94 |
95 | e[i].cap -= d;
96 | e[i ^ 1].cap += d;
97 | ex[s] -= d;
98 | ex[e[i].to] += d;
99 |
100 | if (e[i].to != s && e[i].to != t && !inq[e[i].to]) {
101 | heap.push(e[i].to);
102 | inq[e[i].to] = true;
103 | }
104 | }
105 |
106 | while (!heap.empty()) {
107 | int x = heap.top();
108 | heap.pop();
109 | inq[x] = false;
110 |
111 | push(x);
112 | if (ex[x]) {
113 | if (!--gap[h[x]]) { // gap
114 | for (int i = 1; i <= n; i++)
115 | if (i != s && i != t && h[i] > h[x])
116 | h[i] = n + 1;
117 | }
118 |
119 | relabel(x);
120 | ++gap[h[x]];
121 | heap.push(x);
122 | inq[x] = true;
123 | }
124 | }
125 |
126 | return ex[t];
127 | }
128 |
129 | //记得初始化
130 | memset(last, -1, sizeof(last));
--------------------------------------------------------------------------------
/src/graph/ISAP.cpp:
--------------------------------------------------------------------------------
1 | // 注意ISAP适用于一般稀疏图, 对于二分图或分层图情况Dinic比较优, 稠密图则HLPP更优
2 |
3 | // 边的定义
4 | // 这里没有记录起点和反向边, 因为反向边即为正向边xor 1, 起点即为反向边的终点
5 | struct edge {
6 | int to, cap, prev;
7 | } e[maxe * 2];
8 |
9 | // 全局变量和数组定义
10 | int last[maxn], cnte = 0, d[maxn], p[maxn], c[maxn], cur[maxn], q[maxn];
11 | int n, m, s, t; // s, t一定要开成全局变量
12 |
13 | void AddEdge(int x, int y, int z) {
14 | e[cnte].to = y;
15 | e[cnte].cap = z;
16 | e[cnte].prev = last[x];
17 | last[x] = cnte++;
18 | }
19 |
20 | void addedge(int x, int y, int z) {
21 | AddEdge(x, y, z);
22 | AddEdge(y, x, 0);
23 | }
24 |
25 | // 预处理到t的距离标号
26 | // 在测试数据组数较少时可以省略,把所有距离标号初始化为0
27 | void bfs() {
28 | memset(d, -1, sizeof(d));
29 |
30 | int head = 0, tail = 0;
31 | d[t] = 0;
32 | q[tail++] = t;
33 |
34 | while (head != tail) {
35 | int x = q[head++];
36 | c[d[x]]++;
37 |
38 | for (int i = last[x]; ~i; i = e[i].prev)
39 | if (e[i ^ 1].cap && d[e[i].to] == -1) {
40 | d[e[i].to] = d[x] + 1;
41 | q[tail++] = e[i].to;
42 | }
43 | }
44 | }
45 |
46 | // augment函数 O(n) 沿增广路增广一次, 返回增广的流量
47 | int augment() {
48 | int a = (~0u) >> 1; // INT_MAX
49 |
50 | for (int x = t; x != s; x = e[p[x] ^ 1].to)
51 | a = min(a, e[p[x]].cap);
52 |
53 | for (int x = t; x != s; x = e[p[x] ^ 1].to) {
54 | e[p[x]].cap -= a;
55 | e[p[x] ^ 1].cap += a;
56 | }
57 |
58 | return a;
59 | }
60 |
61 | // 主过程 O(n^2 m), 返回最大流的流量
62 | // 注意这里的n是编号最大值,在这个值不为n的时候一定要开个变量记录下来并修改代码
63 | int ISAP() {
64 | bfs();
65 |
66 | memcpy(cur, last, sizeof(cur));
67 |
68 | int x = s, flow = 0;
69 |
70 | while (d[s] < n) {
71 | if (x == t) { // 如果走到了t就增广一次, 并返回s重新找增广路
72 | flow += augment();
73 | x = s;
74 | }
75 |
76 | bool ok = false;
77 | for (int &i = cur[x]; ~i; i = e[i].prev)
78 | if (e[i].cap && d[x] == d[e[i].to] + 1) {
79 | p[e[i].to] = i;
80 | x = e[i].to;
81 |
82 | ok = true;
83 | break;
84 | }
85 |
86 | if (!ok) { // 修改距离标号
87 | int tmp = n - 1;
88 | for (int i = last[x]; ~i; i = e[i].prev)
89 | if (e[i].cap)
90 | tmp = min(tmp, d[e[i].to] + 1);
91 |
92 | if (!--c[d[x]])
93 | break; // gap优化, 一定要加上
94 |
95 | c[d[x] = tmp]++;
96 | cur[x] = last[x];
97 |
98 | if(x != s)
99 | x = e[p[x] ^ 1].to;
100 | }
101 | }
102 | return flow;
103 | }
104 |
105 | // 重要! main函数最前面一定要加上如下初始化
106 | memset(last, -1, sizeof(last));
--------------------------------------------------------------------------------
/src/graph/KM二分图最大权匹配.cpp:
--------------------------------------------------------------------------------
1 | const long long INF = 0x3f3f3f3f3f3f3f3f;
2 |
3 | long long w[maxn][maxn], lx[maxn], ly[maxn], slack[maxn];
4 | // 边权 顶标 slack
5 | // 如果要求最大权完美匹配就把不存在的边设为-INF,否则所有边对0取max
6 |
7 | bool visx[maxn], visy[maxn];
8 |
9 | int boy[maxn], girl[maxn], p[maxn], q[maxn], head, tail; // p : pre
10 |
11 | int n, m, N, e;
12 |
13 | // 增广
14 | bool check(int y) {
15 | visy[y] = true;
16 |
17 | if (boy[y]) {
18 | visx[boy[y]] = true;
19 | q[tail++] = boy[y];
20 | return false;
21 | }
22 |
23 | while (y) {
24 | boy[y] = p[y];
25 | swap(y, girl[p[y]]);
26 | }
27 |
28 | return true;
29 | }
30 |
31 | // bfs每个点
32 | void bfs(int x) {
33 | memset(q, 0, sizeof(q));
34 | head = tail = 0;
35 |
36 | q[tail++] = x;
37 | visx[x] = true;
38 |
39 | while (true) {
40 | while (head != tail) {
41 | int x = q[head++];
42 |
43 | for (int y = 1; y <= N; y++)
44 | if (!visy[y]) {
45 | long long d = lx[x] + ly[y] - w[x][y];
46 |
47 | if (d < slack[y]) {
48 | p[y] = x;
49 | slack[y] = d;
50 |
51 | if (!slack[y] && check(y))
52 | return;
53 | }
54 | }
55 | }
56 |
57 | long long d = INF;
58 | for (int i = 1; i <= N; i++)
59 | if (!visy[i])
60 | d = min(d, slack[i]);
61 |
62 | for (int i = 1; i <= N; i++) {
63 | if (visx[i])
64 | lx[i] -= d;
65 |
66 | if (visy[i])
67 | ly[i] += d;
68 | else
69 | slack[i] -= d;
70 | }
71 |
72 | for (int i = 1; i <= N; i++)
73 | if (!visy[i] && !slack[i] && check(i))
74 | return;
75 | }
76 | }
77 |
78 | // 主过程
79 | long long KM() {
80 | for (int i = 1; i <= N; i++) {
81 | // lx[i] = 0;
82 | ly[i] = -INF;
83 | // boy[i] = girl[i] = -1;
84 |
85 | for (int j = 1; j <= N; j++)
86 | ly[i] = max(ly[i], w[j][i]);
87 | }
88 |
89 | for (int i = 1; i <= N; i++) {
90 | memset(slack, 0x3f, sizeof(slack));
91 | memset(visx, 0, sizeof(visx));
92 | memset(visy, 0, sizeof(visy));
93 | bfs(i);
94 | }
95 |
96 | long long ans = 0;
97 | for (int i = 1; i <= N; i++)
98 | ans += w[i][girl[i]];
99 | return ans;
100 | }
101 |
102 | // 为了方便贴上主函数
103 | int main() {
104 |
105 | scanf("%d%d%d", &n, &m, &e);
106 | N = max(n, m);
107 |
108 | while (e--) {
109 | int x, y, c;
110 | scanf("%d%d%d", &x, &y, &c);
111 | w[x][y] = max(c, 0);
112 | }
113 |
114 | printf("%lld\n", KM());
115 |
116 | for (int i = 1; i <= n; i++) {
117 | if (i > 1)
118 | printf(" ");
119 | printf("%d", w[i][girl[i]] > 0 ? girl[i] : 0);
120 | }
121 | printf("\n");
122 |
123 | return 0;
124 | }
--------------------------------------------------------------------------------
/src/graph/SPFA费用流.cpp:
--------------------------------------------------------------------------------
1 | constexpr int maxn = 20005, maxm = 200005;
2 |
3 | struct edge {
4 | int to, prev, cap, w;
5 | } e[maxm * 2];
6 |
7 | int last[maxn], cnte, d[maxn], p[maxn]; // 记得把last初始化成-1, 不然会死循环
8 | bool inq[maxn];
9 |
10 | void spfa(int s) {
11 |
12 | memset(d, -63, sizeof(d));
13 | memset(p, -1, sizeof(p));
14 |
15 | queue q;
16 |
17 | q.push(s);
18 | d[s] = 0;
19 |
20 | while (!q.empty()) {
21 | int x = q.front();
22 | q.pop();
23 | inq[x] = false;
24 |
25 | for (int i = last[x]; ~i; i = e[i].prev)
26 | if (e[i].cap) {
27 | int y = e[i].to;
28 |
29 | if (d[x] + e[i].w > d[y]) {
30 | p[y] = i;
31 | d[y] = d[x] + e[i].w;
32 | if (!inq[y]) {
33 | q.push(y);
34 | inq[y] = true;
35 | }
36 | }
37 | }
38 | }
39 | }
40 |
41 | int mcmf(int s, int t) {
42 | int ans = 0;
43 |
44 | while (spfa(s), d[t] > 0) {
45 | int flow = 0x3f3f3f3f;
46 | for (int x = t; x != s; x = e[p[x] ^ 1].to)
47 | flow = min(flow, e[p[x]].cap);
48 |
49 | ans += flow * d[t];
50 |
51 | for (int x = t; x != s; x = e[p[x] ^ 1].to) {
52 | e[p[x]].cap -= flow;
53 | e[p[x] ^ 1].cap += flow;
54 | }
55 | }
56 |
57 | return ans;
58 | }
59 |
60 | void add(int x, int y, int c, int w) {
61 | e[cnte].to = y;
62 | e[cnte].cap = c;
63 | e[cnte].w = w;
64 |
65 | e[cnte].prev = last[x];
66 | last[x] = cnte++;
67 | }
68 |
69 | void addedge(int x, int y, int c, int w) {
70 | add(x, y, c, w);
71 | add(y, x, 0, -w);
72 | }
73 |
--------------------------------------------------------------------------------
/src/graph/dijkstra费用流.cpp:
--------------------------------------------------------------------------------
1 | struct edge {
2 | int to, cap, prev, w;
3 | } e[maxe * 2];
4 |
5 | int last[maxn], cnte;
6 |
7 | long long d[maxn], h[maxn];
8 | int p[maxn];
9 |
10 | bool vis[maxn];
11 | int s, t;
12 |
13 | void Adde(int x, int y, int z, int w) {
14 | e[cnte].to = y;
15 | e[cnte].cap = z;
16 | e[cnte].w = w;
17 | e[cnte].prev = last[x];
18 | last[x] = cnte++;
19 | }
20 |
21 | void addedge(int x, int y, int z, int w) {
22 | Adde(x, y, z, w);
23 | Adde(y, x, 0, -w);
24 | }
25 |
26 | void dijkstra() {
27 | memset(d, 63, sizeof(d));
28 | memset(vis, 0, sizeof(vis));
29 |
30 | priority_queue > heap;
31 |
32 | d[s] = 0;
33 | heap.push(make_pair(0ll, s));
34 |
35 | while (!heap.empty()) {
36 | int x = heap.top().second;
37 | heap.pop();
38 |
39 | if (vis[x])
40 | continue;
41 |
42 | vis[x] = true;
43 | for (int i = last[x]; ~i; i = e[i].prev)
44 | if (e[i].cap > 0 && d[e[i].to] > d[x] + e[i].w + h[x] - h[e[i].to]) {
45 | d[e[i].to] = d[x] + e[i].w + h[x] - h[e[i].to];
46 | p[e[i].to] = i;
47 | heap.push(make_pair(-d[e[i].to], e[i].to));
48 | }
49 | }
50 | }
51 |
52 | pair mcmf() {
53 | /*
54 | spfa();
55 | for (int i = 1; i <= t; i++)
56 | h[i] = d[i];
57 | // 如果初始有负权就像这样跑一遍SPFA预处理
58 | */
59 |
60 | long long flow = 0, cost = 0;
61 |
62 | while (dijkstra(), d[t] < 0x3f3f3f3f) {
63 | for (int i = 1; i <= t; i++)
64 | h[i] += d[i];
65 |
66 | int a = 0x3f3f3f3f;
67 |
68 | for (int x = t; x != s; x = e[p[x] ^ 1].to)
69 | a = min(a, e[p[x]].cap);
70 |
71 | flow += a;
72 | cost += (long long)a * h[t];
73 |
74 | for (int x = t; x != s; x = e[p[x] ^ 1].to) {
75 | e[p[x]].cap -= a;
76 | e[p[x] ^ 1].cap += a;
77 | }
78 |
79 | }
80 |
81 | return make_pair(flow, cost);
82 | }
83 |
84 | // 记得初始化
85 | memset(last, -1, sizeof(last));
--------------------------------------------------------------------------------
/src/graph/dijkstra费用流.tex:
--------------------------------------------------------------------------------
1 | 也叫原始-对偶费用流。
2 |
3 | 原理和求多源最短路的 Johnson 算法是一样的,都是给每个点维护一个势 $h_u$,使得对任何有向边 $u\to v$ 都满足 $w + h_u - h_v \ge 0$。
4 |
5 | 如果有负费用则从 $s$ 开始跑一遍 SPFA 初始化,否则可以直接初始化 $h_u = 0$。
6 |
7 | 每次增广时得到的路径长度就是 $d_{s, t} + h_t$,增广之后让所有 $h_u = h'_u + d'_{s, u}$,直到 $d_{s, t} = +\infty$(最小费用最大流)或 $d_{s, t} \ge 0$(最小费用流)为止。
8 |
9 | 注意最大费用流要转成取负之后的最小费用流,因为 Dijkstra 求的是最短路。
10 |
11 | \inputminted{cpp}{../src/graph/dijkstra费用流.cpp}
12 |
--------------------------------------------------------------------------------
/src/graph/hopcroft-karp.cpp:
--------------------------------------------------------------------------------
1 | vector G[maxn];
2 |
3 | int girl[maxn], boy[maxn]; // girl: 左边匹配右边 boy: 右边匹配左边
4 |
5 | bool vis[maxn]; // 右半的点是否已被访问
6 | int dx[maxn], dy[maxn];
7 | int q[maxn];
8 |
9 | bool bfs(int n) {
10 | memset(dx, -1, sizeof(int) * (n + 1));
11 | memset(dy, -1, sizeof(int) * (n + 1));
12 |
13 | int head = 0, tail = 0;
14 | for (int i = 1; i <= n; i++)
15 | if (!girl[i]) {
16 | q[tail++] = i;
17 | dx[i] = 0;
18 | }
19 |
20 | bool flag = false;
21 |
22 | while (head != tail) {
23 | int x = q[head++];
24 |
25 | for (auto y : G[x])
26 | if (dy[y] == -1) {
27 | dy[y] = dx[x] + 1;
28 |
29 | if (boy[y]) {
30 | if (dx[boy[y]] == -1) {
31 | dx[boy[y]] = dy[y] + 1;
32 | q[tail++] = boy[y];
33 | }
34 | }
35 | else
36 | flag = true;
37 | }
38 | }
39 |
40 | return flag;
41 | }
42 |
43 | bool dfs(int x) {
44 | for (int y : G[x])
45 | if (!vis[y] && dy[y] == dx[x] + 1) {
46 | vis[y] = true;
47 |
48 | if (boy[y] && !dfs(boy[y]))
49 | continue;
50 |
51 | girl[x] = y;
52 | boy[y] = x;
53 | return true;
54 | }
55 |
56 | return false;
57 | }
58 |
59 | int hopcroft_karp(int n) {
60 | int ans = 0;
61 |
62 | for (int x = 1; x <= n; x++) // 先贪心求出一组初始匹配, 当然不写贪心也行
63 | for (int y : G[x])
64 | if (!boy[y]) {
65 | girl[x] = y;
66 | boy[y] = x;
67 | ans++;
68 | break;
69 | }
70 |
71 | while (bfs(n)) {
72 | memset(vis, 0, sizeof(bool) * (n + 1));
73 |
74 | for (int x = 1; x <= n; x++)
75 | if (!girl[x])
76 | ans += dfs(x);
77 | }
78 |
79 | return ans;
80 | }
--------------------------------------------------------------------------------
/src/graph/hungary.cpp:
--------------------------------------------------------------------------------
1 | vector G[maxn];
2 |
3 | int girl[maxn], boy[maxn]; // 男孩在左边, 女孩在右边
4 | bool vis[maxn];
5 |
6 | bool dfs(int x) {
7 | for (int y : G[x])
8 | if (!vis[y]) {
9 | vis[y] = true;
10 |
11 | if (!boy[y] || dfs(boy[y])) {
12 | girl[x] = y;
13 | boy[y] = x;
14 |
15 | return true;
16 | }
17 | }
18 |
19 | return false;
20 | }
21 |
22 | int hungary() {
23 | int ans = 0;
24 |
25 | for (int i = 1; i <= n; i++)
26 | if (!girl[i]) {
27 | memset(vis, 0, sizeof(vis));
28 | ans += dfs(i);
29 | }
30 |
31 | return ans;
32 | }
--------------------------------------------------------------------------------
/src/graph/johnson.tex:
--------------------------------------------------------------------------------
1 | 首先前提是图没有负环。
2 |
3 | 先任选一个起点 $s$,跑一遍 SPFA,计算每个点的势 $h_u = d_{s, u}$,然后将每条边 $u\to v$ 的权值 $w$ 修改为 $w + h[u] - h[v]$ 即可,由最短路的性质显然修改后边权非负。
4 |
5 | 然后对每个起点跑 Dijkstra,再修正距离 $d_{u, v} = d'_{u, v} - h_u + h_v$ 即可。复杂度$O(nm\log n)$,在稀疏图上是要优于 Floyd 的。
6 |
--------------------------------------------------------------------------------
/src/graph/mdst.cpp:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | using namespace std;
4 |
5 | constexpr int maxn = 505;
6 | constexpr long long inf = 0x3f3f3f3f3f3f3f3fll;
7 |
8 | int g[maxn][maxn], id[maxn][maxn], pr[maxn]; // g是邻接矩阵
9 | long long f[maxn][maxn], d[maxn];
10 | bool vis[maxn];
11 |
12 | vector> minimum_diameter_spanning_tree(int n) { // 1-based
13 | for (int i = 1; i <= n; i++)
14 | for (int j = 1; j <= n; j++)
15 | g[i][j] *= 2; // 输入的边权都要乘2
16 |
17 | memset(f, 63, sizeof(f));
18 |
19 | for (int i = 1; i <= n; i++)
20 | f[i][i] = 0;
21 |
22 | for (int i = 1; i <= n; i++)
23 | for (int j = 1; j <= n; j++)
24 | if (g[i][j])
25 | f[i][j] = g[i][j];
26 |
27 | for (int k = 1; k <= n; k++)
28 | for (int i = 1; i <= n; i++)
29 | for (int j = 1; j <= n; j++)
30 | f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
31 |
32 | for (int i = 1; i <= n; i++) {
33 | for (int j = 1; j <= n; j++)
34 | id[i][j] = j; // 距离i第j近的点
35 |
36 | sort(id[i] + 1, id[i] + n + 1, [&i] (int x, int y) {
37 | return f[i][x] < f[i][y];
38 | });
39 | }
40 |
41 | int o = 0;
42 | long long ansv = inf; // vertex
43 |
44 | for (int i = 1; i <= n; i++)
45 | if (f[i][id[i][n]] * 2 < ansv) {
46 | ansv = f[i][id[i][n]] * 2;
47 | o = i;
48 | }
49 |
50 | int u = 0, v = 0;
51 | long long disu = -inf, disv = -inf, anse = inf;
52 |
53 | for (int x = 1; x <= n; x++)
54 | for (int y = 1; y <= n; y++)
55 | if (g[x][y]) { // 如果g[x][y] = 0说明没有边
56 | int w = g[x][y];
57 |
58 | for (int i = n - 1, j = n; i; i--)
59 | if (f[y][id[x][i]] > f[y][id[x][j]]) {
60 | long long tmp = f[x][id[x][i]] + f[y][id[x][j]] + w;
61 | if (tmp < anse) {
62 | anse = tmp;
63 | u = x;
64 | v = y;
65 |
66 | disu = tmp / 2 - f[x][id[x][i]];
67 | disv = w - disu;
68 | }
69 | j = i;
70 | }
71 | }
72 |
73 | printf("%lld\n", min(ansv, anse) / 2); // 直径
74 |
75 | memset(d, 63, sizeof(d));
76 |
77 | if (ansv <= anse)
78 | d[o] = 0;
79 | else {
80 | d[u] = disu;
81 | d[v] = disv;
82 | }
83 |
84 | for (int k = 1; k <= n; k++) { // Dijkstra
85 | int x = 0;
86 | for (int i = 1; i <= n; i++)
87 | if (!vis[i] && d[i] < d[x])
88 | x = i;
89 |
90 | vis[x] = true;
91 | for (int y = 1; y <= n; y++)
92 | if (g[x][y] && !vis[y]) {
93 | if (d[y] > d[x] + g[x][y]) {
94 | d[y] = d[x] + g[x][y];
95 | pr[y] = x;
96 | }
97 | else if (d[y] == d[x] + g[x][y] && d[pr[y]] < d[x])
98 | pr[y] = x;
99 | }
100 | }
101 |
102 | vector> vec;
103 | for (int i = 1; i <= n; i++)
104 | if (pr[i])
105 | vec.emplace_back(i, pr[i]);
106 |
107 | if (ansv > anse)
108 | vec.emplace_back(u, v);
109 |
110 | return vec;
111 | }
112 |
113 | int main() {
114 |
115 | int n, m;
116 | scanf("%d%d", &n, &m);
117 |
118 | while (m--) {
119 | int x, y, z;
120 | scanf("%d%d%d", &x, &y, &z);
121 |
122 | g[x][y] = g[y][x] = z; // 无向图
123 | }
124 |
125 | auto vec = minimum_diameter_spanning_tree(n);
126 | for (auto [x, y] : vec)
127 | printf("%d %d\n", x, y);
128 |
129 | return 0;
130 | }
--------------------------------------------------------------------------------
/src/graph/prufer.tex:
--------------------------------------------------------------------------------
1 | 对一棵有 $n \ge 2$ 个结点的树,它的 Prufer 编码是一个长为 $n - 2$,且每个数都在 $[1, n]$ 内的序列。
2 |
3 | 构造方法:每次选取编号最小的叶子结点,记录它的父亲,然后把它删掉,直到只剩两个点为止。(并且最后剩的两个点一定有一个是 $n$ 号点。)
4 |
5 | 相应的,由 Prufer 编码重构树的方法:按顺序读入序列,每次选取编号最小的且度数为 $1$ 的结点,把这个点和序列当前点连上,然后两个点剩余度数同时 $-1$。
6 |
7 | \paragraph{性质}
8 |
9 | \begin{itemize}
10 | \item 每个至少 $2$ 个结点的树都唯一对应一个 Prufer 编码。(当然也就可以做无根树哈希。)
11 | \item 每个点在 Prufer 序列中出现的次数恰好是度数 $-1$。所以如果给定某些点的度数然后求方案数,就可以用简单的组合数解决。
12 | \end{itemize}
13 |
14 |
15 | 最后,构造和重构直接写都是 $O(n\log n)$的,想优化成线性需要一些技巧。
16 |
17 | 线性求 Prufer 序列代码:
18 | \begin{minted}{cpp}
19 | // 0-based
20 | vector> adj;
21 | vector parent;
22 |
23 | void dfs(int v) {
24 | for (int u : adj[v]) {
25 | if (u != parent[v]) parent[u] = v, dfs(u);
26 | }
27 | }
28 |
29 | vector pruefer_code() { // pruefer是德语
30 | int n = adj.size();
31 | parent.resize(n), parent[n - 1] = -1;
32 | dfs(n - 1);
33 |
34 | int ptr = -1;
35 | vector degree(n);
36 | for (int i = 0; i < n; i++) {
37 | degree[i] = adj[i].size();
38 | if (degree[i] == 1 && ptr == -1) ptr = i;
39 | }
40 |
41 | vector code(n - 2);
42 | int leaf = ptr;
43 | for (int i = 0; i < n - 2; i++) {
44 | int next = parent[leaf];
45 | code[i] = next;
46 | if (--degree[next] == 1 && next < ptr)
47 | leaf = next;
48 | else {
49 | ptr++;
50 | while (degree[ptr] != 1)
51 | ptr++;
52 | leaf = ptr;
53 | }
54 | }
55 | return code;
56 | }
57 | \end{minted}
58 |
59 | 线性重构树代码:
60 | \begin{minted}{cpp}
61 | // 0-based
62 | vector> pruefer_decode(vector const &code) {
63 | int n = code.size() + 2;
64 | vector degree(n, 1);
65 | for (int i : code) degree[i]++;
66 |
67 | int ptr = 0;
68 | while (degree[ptr] != 1) ptr++;
69 | int leaf = ptr;
70 |
71 | vector> edges;
72 | for (int v : code) {
73 | edges.emplace_back(leaf, v);
74 | if (--degree[v] == 1 && v < ptr)
75 | leaf = v;
76 | else {
77 | ptr++;
78 | while (degree[ptr] != 1)
79 | ptr++;
80 | leaf = ptr;
81 | }
82 | }
83 | edges.emplace_back(leaf, n - 1);
84 | return edges;
85 | }
86 | \end{minted}
87 |
--------------------------------------------------------------------------------
/src/graph/steiner.cpp:
--------------------------------------------------------------------------------
1 | constexpr int maxn = 105, inf = 0x3f3f3f3f;
2 |
3 | int dp[maxn][(1 << 10) + 1];
4 | int g[maxn][maxn], a[15];
5 | bool inq[maxn];
6 |
7 | int main() {
8 |
9 | int n, m, k;
10 | scanf("%d%d%d", &n, &m, &k);
11 |
12 | memset(g, 63, sizeof(g));
13 |
14 | while (m--) {
15 | int u, v, c;
16 | scanf("%d%d%d", &u, &v, &c);
17 |
18 | g[u][v] = g[v][u] = min(g[u][v], c); // 不要忘了是双向边
19 | }
20 |
21 | memset(dp, 63, sizeof(dp));
22 |
23 | for (int i = 0; i < k; i++) {
24 | scanf("%d", &a[i]);
25 |
26 | dp[a[i]][1 << i] = 0;
27 | }
28 |
29 | for (int s = 1; s < (1 << k); s++) {
30 | for (int i = 1; i <= n; i++)
31 | for (int t = (s - 1) & s; t; (--t) &= s)
32 | dp[i][s] = min(dp[i][s], dp[i][t] + dp[i][s ^ t]);
33 |
34 | // SPFA
35 | queue q;
36 | for (int i = 1; i <= n; i++)
37 | if (dp[i][s] < inf) {
38 | q.push(i);
39 | inq[i] = true;
40 | }
41 |
42 | while (!q.empty()) {
43 | int i = q.front();
44 | q.pop();
45 | inq[i] = false; // 最终结束时inq一定全0, 所以不用清空
46 |
47 | for (int j = 1; j <= n; j++)
48 | if (dp[i][s] + g[i][j] < dp[j][s]) {
49 | dp[j][s] = dp[i][s] + g[i][j];
50 | if (!inq[j]) {
51 | q.push(j);
52 | inq[j] = true;
53 | }
54 | }
55 | }
56 | }
57 |
58 | int ans = inf;
59 | for (int i = 1; i <= n; i++)
60 | ans = min(ans, dp[i][(1 << k) - 1]);
61 |
62 | printf("%d\n", ans);
63 |
64 | return 0;
65 | }
--------------------------------------------------------------------------------
/src/graph/steiner.tex:
--------------------------------------------------------------------------------
1 | \paragraph{问题} 一张图上有 $k$ 个关键点,求让关键点两两连通的最小生成树。
2 |
3 | \paragraph{做法} 状压 DP,$f_{i,S}$ 表示以 $i$ 号点为树根,$i$ 与 $S$ 中的所有点连通的最小边权和。
4 |
5 | 转移有两种:
6 |
7 | \begin{enumerate}
8 | \item 枚举子集:
9 | $$ f_{i, S} = \min_{T\subset S} \left\{f_{i, T} + f_{i, S\setminus T}\right\} $$
10 | \item 新加一条边:
11 | $$ f_{i, S} = \min_{(i, j)\in E} \left\{f_{j, S} + w_{i, j}\right\} $$
12 | \end{enumerate}
13 |
14 | 第一种直接枚举子集 DP 就行了,第二种可以用 SPFA 或者 Dijkstra 松弛(显然负边一开始全选就行了,所以只需要处理非负边)。
15 |
16 | 复杂度 $O(n 3^k + 2^k \text{SSSP}(n, m)))$,其中 $\text{SSSP}(n,m)$ 可以是 $nm$(SPFA)或者 $n^2+m$ 或者 $m\log n$(后两种都是 Dijkstra)。
17 |
18 | \inputminted{cpp}{../src/graph/steiner.cpp}
19 |
--------------------------------------------------------------------------------
/src/graph/stoer-wagner.cpp:
--------------------------------------------------------------------------------
1 | const int N = 601;
2 | int fa[N], siz[N], edge[N][N];
3 |
4 | int find(int x) {
5 | return fa[x] == x ? x : fa[x] = find(fa[x]);
6 | }
7 |
8 | int dist[N], vis[N], bin[N];
9 | int n, m;
10 |
11 | int contract(int& s, int& t) { // Find s,t
12 | memset(dist, 0, sizeof(dist));
13 | memset(vis, false, sizeof(vis));
14 |
15 | int i, j, k, mincut, maxc;
16 |
17 | for (i = 1; i <= n; i++) {
18 | k = -1;
19 | maxc = -1;
20 |
21 | for (j = 1; j <= n; j++)
22 | if (!bin[j] && !vis[j] && dist[j] > maxc) {
23 | k = j;
24 | maxc = dist[j];
25 | }
26 |
27 | if (k == -1)
28 | return mincut;
29 |
30 | s = t;
31 | t = k;
32 | mincut = maxc;
33 | vis[k] = true;
34 |
35 | for (j = 1; j <= n; j++)
36 | if (!bin[j] && !vis[j]) dist[j] += edge[k][j];
37 | }
38 |
39 | return mincut;
40 | }
41 |
42 | const int inf = 0x3f3f3f3f;
43 |
44 | int Stoer_Wagner() {
45 | int mincut, i, j, s, t, ans;
46 | for (mincut = inf, i = 1; i < n; i++) {
47 | ans = contract(s, t);
48 | bin[t] = true;
49 |
50 | if (mincut > ans)
51 | mincut = ans;
52 | if (mincut == 0)
53 | return 0;
54 |
55 | for (j = 1; j <= n; j++)
56 | if (!bin[j])
57 | edge[s][j] = (edge[j][s] += edge[j][t]);
58 | }
59 |
60 | return mincut;
61 | }
62 |
63 | int main() {
64 | cin >> n >> m;
65 |
66 | if (m < n - 1) {
67 | cout << 0;
68 | return 0;
69 | }
70 |
71 | for (int i = 1; i <= n; ++i)
72 | fa[i] = i, siz[i] = 1;
73 |
74 | for (int i = 1, u, v, w; i <= m; ++i) {
75 | cin >> u >> v >> w;
76 |
77 | int fu = find(u), fv = find(v);
78 | if (fu != fv) {
79 | if (siz[fu] > siz[fv]) swap(fu, fv);
80 | fa[fu] = fv, siz[fv] += siz[fu];
81 | }
82 |
83 | edge[u][v] += w, edge[v][u] += w;
84 | }
85 |
86 | int fr = find(1);
87 |
88 | if (siz[fr] != n) {
89 | cout << 0;
90 | return 0;
91 | }
92 |
93 | cout << Stoer_Wagner();
94 |
95 | return 0;
96 | }
--------------------------------------------------------------------------------
/src/graph/一般图匹配原理.tex:
--------------------------------------------------------------------------------
1 | 设图 $G$ 的 Tutte 矩阵是 $\tilde A$,首先是最基础的引理:
2 |
3 | \begin{itemize}
4 | \item $G$ 的最大匹配大小是 $\frac 1 2 \text{rank}{\tilde A}$;
5 |
6 | \item $\left({\tilde A} ^{-1}\right) _{i, j} \ne 0 \iff G-\{v_i, v_j\}$ 有完美匹配。
7 | \subitem (考虑到逆矩阵与伴随矩阵的关系,这是显然的。)
8 | \end{itemize}
9 |
10 | 构造最大匹配的方法见板子。对于更一般的问题,可以借助构造方法转化为完美匹配问题。
11 |
12 | 设最大匹配的大小为 $k$,新建 $n - 2k$ 个辅助点,让它们和其他所有点连边,那么如果一个点匹配了一个辅助点,就说明它在原图的匹配中不匹配任何点。
13 |
14 | \paragraph{可行边} 对原图中的任意一条边 $(u, v)$,如果删掉 $u, v$ 后新图仍然有完美匹配(即 ${\tilde A} ^ {-1}_{u, v} \ne 0$),则它是一条可行边。
15 |
16 | \paragraph{必须边} \textbf{待补充}。
17 |
18 | \paragraph{必须点} 可以删掉这个点和一个辅助点,然后判断剩下的图是否还有完美匹配,如果有则说明它不是必须的,否则是必须的。只需要用到逆矩阵即可。
19 |
20 | \paragraph{可行点} 显然对于任意一个点,只要它不是孤立点,就是可行点。
21 |
--------------------------------------------------------------------------------
/src/graph/上下界网络流.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/src/graph/上下界网络流.png
--------------------------------------------------------------------------------
/src/graph/二分图原理.tex:
--------------------------------------------------------------------------------
1 | \begin{itemize}
2 |
3 | \item \textbf{最大匹配的可行边与必须边、关键点}
4 |
5 | 以下的“残量网络”指网络流图的残量网络。
6 |
7 | \paragraph{可行边} 一条边的两个端点在残量网络中处于同一个 SCC,不论是正向边还是反向边。
8 |
9 | \paragraph{必须边} 一条属于当前最大匹配的边,且残量网络中两个端点不在同一个 SCC 中。
10 |
11 | \paragraph{关键点/必须点} 这里不考虑网络流图而只考虑原始的图,将匹配边改成从右到左之后从左边的每个未匹配点进行 floodfill,左边没有被标记的点即为关键点。右边同理。
12 |
13 | \item \textbf{独立集}
14 |
15 | 二分图独立集可以看成最小割问题,割掉最少的点使得 S 和 T 不连通,则剩下的点自然都在独立集中。
16 |
17 | 所以独立集输出方案就是求出不在最小割中的点,独立集的必须点/可行点就是最小割的不可行点/非必须点。
18 |
19 | 割点等价于割掉它与源点或汇点相连的边,可以通过设置中间的边权为无穷以保证不能割掉中间的边,然后按照上面的方法判断即可。
20 |
21 | (由于一个点最多流出一个流量,所以中间的边权其实是可以任取的。)
22 |
23 | \item \textbf{二分图最大权匹配}
24 |
25 | 二分图最大权匹配的对偶问题是 \textbf{最小顶标和}\ 问题,即:为图中的每个顶点赋予一个非负顶标,使得对于任意一条边,两端点的顶标和都要不小于边权,最小化顶标之和。
26 |
27 | KM 算法的原理实际上就是求最小顶标和。
28 |
29 | \end{itemize}
30 |
--------------------------------------------------------------------------------
/src/graph/仙人掌DP.cpp:
--------------------------------------------------------------------------------
1 | struct edge {
2 | int to, w, prev;
3 | } e[maxn * 2];
4 |
5 | vector > v[maxn];
6 | vector d[maxn];
7 | stack stk;
8 |
9 | int p[maxn];
10 | bool vis[maxn], vise[maxn * 2];
11 | int last[maxn], cnte;
12 |
13 | long long f[maxn], g[maxn], sum[maxn];
14 | int n, m, cnt;
15 |
16 | void addedge(int x, int y, int w) {
17 | v[x].push_back(make_pair(y, w));
18 | }
19 |
20 | void dfs(int x) {
21 |
22 | vis[x] = true;
23 |
24 | for (int i = last[x]; ~i; i = e[i].prev) {
25 | if (vise[i ^ 1])
26 | continue;
27 |
28 | int y = e[i].to, w = e[i].w;
29 |
30 | vise[i] = true;
31 |
32 | if (!vis[y]) {
33 | stk.push(i);
34 | p[y] = x;
35 | dfs(y);
36 |
37 | if (!stk.empty() && stk.top() == i) {
38 | stk.pop();
39 | addedge(x, y, w);
40 | }
41 | }
42 |
43 | else {
44 | cnt++;
45 |
46 | long long tmp = w;
47 | while (!stk.empty()) {
48 | int i = stk.top();
49 | stk.pop();
50 |
51 | int yy = e[i].to, ww = e[i].w;
52 |
53 | addedge(cnt, yy, 0);
54 |
55 | d[cnt].push_back(tmp);
56 |
57 | tmp += ww;
58 |
59 | if (e[i ^ 1].to == y)
60 | break;
61 | }
62 |
63 | addedge(y, cnt, 0);
64 |
65 | sum[cnt] = tmp;
66 | }
67 | }
68 | }
69 |
70 | void dp(int x) {
71 |
72 | for (auto o : v[x]) {
73 | int y = o.first, w = o.second;
74 | dp(y);
75 | }
76 |
77 | if (x <= n) {
78 | for (auto o : v[x]) {
79 | int y = o.first, w = o.second;
80 |
81 | f[x] += 2 * w + f[y];
82 | }
83 |
84 | g[x] = f[x];
85 |
86 | for (auto o : v[x]) {
87 | int y = o.first, w = o.second;
88 |
89 | g[x] = min(g[x], f[x] - f[y] - 2 * w + g[y] + w);
90 | }
91 | }
92 | else {
93 | f[x] = sum[x];
94 | for (auto o : v[x]) {
95 | int y = o.first;
96 |
97 | f[x] += f[y];
98 | }
99 |
100 | g[x] = f[x];
101 |
102 | for (int i = 0; i < (int)v[x].size(); i++) {
103 | int y = v[x][i].first;
104 |
105 | g[x] = min(g[x], f[x] - f[y] + g[y] + min(d[x][i], sum[x] - d[x][i]));
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/graph/割点点双.cpp:
--------------------------------------------------------------------------------
1 | vector G[maxn], bcc[maxn];
2 | int dfn[maxn], low[maxn], tim = 0, bccid[maxn], bcc_cnt = 0;
3 | bool iscut[maxn];
4 |
5 | pair stk[maxn];
6 | int stk_cnt = 0;
7 |
8 | void dfs(int x, int pr) {
9 | int child = 0;
10 | dfn[x] = low[x] = ++tim;
11 |
12 | for (int y : G[x]) {
13 | if (!dfn[y]) {
14 | stk[++stk_cnt] = make_pair(x, y);
15 | child++;
16 | dfs(y, x);
17 | low[x] = min(low[x], low[y]);
18 |
19 | if (low[y] >= dfn[x]) {
20 | iscut[x] = true;
21 | bcc_cnt++;
22 |
23 | while (true) {
24 | auto pi = stk[stk_cnt--];
25 |
26 | if (bccid[pi.first] != bcc_cnt) {
27 | bcc[bcc_cnt].push_back(pi.first);
28 | bccid[pi.first] = bcc_cnt;
29 | }
30 | if (bccid[pi.second] != bcc_cnt) {
31 | bcc[bcc_cnt].push_back(pi.second);
32 | bccid[pi.second] = bcc_cnt;
33 | }
34 |
35 | if (pi.first == x && pi.second == y)
36 | break;
37 | }
38 | }
39 | }
40 | else if (dfn[y] < dfn[x] && y != pr) {
41 | stk[++stk_cnt] = make_pair(x, y);
42 | low[x] = min(low[x], dfn[y]);
43 | }
44 | }
45 |
46 | if (!pr && child == 1)
47 | iscut[x] = false;
48 | }
49 |
50 | void Tarjan(int n) {
51 | for (int i = 1; i <= n; i++)
52 | if (!dfn[i])
53 | dfs(i, 0);
54 | }
--------------------------------------------------------------------------------
/src/graph/动态最小生成树.cpp:
--------------------------------------------------------------------------------
1 | struct Edge { int u, v, w; } edges[MAXN];
2 |
3 | int vis[MAXN], stamp = 0; // 因为不好清空,用时间戳标记
4 |
5 | struct Modify { int i, new_w; } op[MAXN]; // 方便起见,只有修改边权一种操作
6 |
7 | int ufs[MAXN], ufs_size[MAXN]; // 因为要撤销,不方便存 rank,所以用 size
8 |
9 | void ufs_init(int n) {
10 | for (int i = 1; i <= n; i++) {
11 | ufs[i] = i;
12 | ufs_size[i] = 1;
13 | }
14 | }
15 |
16 | int findufs(int x) { while (ufs[x] != x) x = ufs[x]; return x; }
17 |
18 | void undo(const vector& stk) {
19 | for (int i = (int)stk.size() - 1; ~i; i--) {
20 | int x = stk[i]; ufs_size[ufs[x]] -= ufs_size[x];
21 | ufs[x] = x; } }
22 |
23 | bool link(int u, int v, vector& stk) {
24 | int x = findufs(u), y = findufs(v);
25 |
26 | if (x != y) {
27 | if (ufs_size[x] > ufs_size[y])
28 | swap(x, y);
29 |
30 | ufs[x] = y;
31 | ufs_size[y] += ufs_size[x];
32 | stk.push_back(x);
33 | return true;
34 | }
35 | return false;
36 | }
37 |
38 | // Returns { unused, used }. e should be already sorted.
39 | pair, vector> kruskal(const vector& e, const vector& must) {
40 | vector stk, used[2];
41 |
42 | for (int i : must)
43 | link(edges[i].u, edges[i].v, stk);
44 |
45 | for (int i : e)
46 | used[link(edges[i].u, edges[i].v, stk)].push_back(i);
47 |
48 | undo(stk);
49 |
50 | return {used[0], used[1]};
51 | }
52 |
53 | ll ans[MAXN];
54 |
55 | void solve(int l, int r, vector e, ll sum) {
56 | if (l == r) {
57 | edges[op[l].i].w = op[l].new_w;
58 |
59 | sort(e.begin(), e.end(), [] (int i, int j) { return edges[i].w < edges[j].w; });
60 | for (int i : kruskal(e, {}).second)
61 | sum += edges[i].w;
62 |
63 | // Did not check if the graph is connected.
64 | ans[l] = sum;
65 |
66 | return;
67 | }
68 |
69 | sort(e.begin(), e.end(), [] (int i, int j) { return edges[i].w < edges[j].w; });
70 |
71 | stamp++;
72 | vector mod;
73 | for (int t = l; t <= r; t++)
74 | if (vis[op[t].i] < stamp) {
75 | vis[op[t].i] = stamp;
76 | mod.push_back(op[t].i);
77 | }
78 |
79 | vector notmod;
80 | for (int i : e)
81 | if (vis[i] < stamp)
82 | notmod.push_back(i);
83 |
84 | // Reduction, unused edges are dismissed.
85 | notmod = get<1>(kruskal(notmod, {}));
86 |
87 | // Contraction, used edges are sure to be in MST.
88 | auto [notsure, must] = kruskal(notmod, mod);
89 |
90 | vector stk;
91 | for (int i : must) {
92 | assert(link(edges[i].u, edges[i].v, stk));
93 | sum += edges[i].w;
94 | }
95 |
96 | for (int i : mod)
97 | notsure.push_back(i);
98 |
99 | int mid = (l + r) / 2;
100 | solve(l, mid, notsure, sum);
101 | solve(mid + 1, r, notsure, sum);
102 |
103 | undo(stk);
104 | }
105 |
106 | int main() { ufs_init(n); }
107 |
--------------------------------------------------------------------------------
/src/graph/动态最小生成树.tex:
--------------------------------------------------------------------------------
1 | % 动态最小生成树的离线算法比较容易,而在线算法通常极为复杂。
2 |
3 | 对时间分治,在每层分治时找出一定在/不在 MST 上的边,只带着不确定边继续递归。
4 |
5 | 过程中的两种重要操作如下:
6 |
7 | \begin{itemize}
8 | \item Reduction:待修改边标为 $+\infty$,跑 MST 后把非树边删掉,减少无用边
9 | \item Contraction:待修改边标为 $-\infty$,跑 MST 后把待修改边之外的所有树边缩点,缩掉必须边
10 | \end{itemize}
11 |
12 | 每轮分治需要 Reduction-Contraction,借此减少不确定边,从而保证复杂度。
13 |
14 | 复杂度证明:假设当前区间有 $k$ 条待修改边,$n$ 和 $m$ 表示点数和边数,那么最坏情况下 R-C 的效果为 $(n, m) \to (n, n + k - 1) \to (k + 1, 2k)$。
15 |
16 | \inputminted{cpp}{../src/graph/动态最小生成树.cpp}
17 |
--------------------------------------------------------------------------------
/src/graph/带花树.cpp:
--------------------------------------------------------------------------------
1 | // 带花树通常比高斯消元快很多, 但在只需要求最大匹配大小的时候并没有高斯消元好写
2 | // 当然输出方案要方便很多
3 |
4 | // 全局数组与变量定义
5 | vector G[maxn];
6 | int girl[maxn], f[maxn], t[maxn], p[maxn], vis[maxn], tim, q[maxn], head, tail;
7 | int n, m;
8 |
9 |
10 | // 封装好的主过程 O(nm)
11 | int blossom() {
12 | int ans = 0;
13 |
14 | for (int i = 1; i <= n; i++)
15 | if (!girl[i])
16 | ans += bfs(i);
17 |
18 | return ans;
19 | }
20 |
21 |
22 | // bfs找增广路 O(m)
23 | bool bfs(int s) {
24 | memset(t, 0, sizeof(t));
25 | memset(p, 0, sizeof(p));
26 |
27 | for (int i = 1; i <= n; i++)
28 | f[i] = i; // 并查集
29 |
30 | head = tail = 0;
31 | q[tail++] = s;
32 | t[s] = 1;
33 |
34 | while (head != tail) {
35 | int x = q[head++];
36 | for (int y : G[x]) {
37 | if (findroot(y) == findroot(x) || t[y] == 2)
38 | continue;
39 |
40 | if (!t[y]) {
41 | t[y] = 2;
42 | p[y] = x;
43 |
44 | if (!girl[y]) {
45 | for (int u = y, t; u; u = t) {
46 | t = girl[p[u]];
47 | girl[p[u]] = u;
48 | girl[u] = p[u];
49 | }
50 | return true;
51 | }
52 |
53 | t[girl[y]] = 1;
54 | q[tail++] = girl[y];
55 | }
56 | else if (t[y] == 1) {
57 | int z = LCA(x, y);
58 |
59 | shrink(x, y, z);
60 | shrink(y, x, z);
61 | }
62 | }
63 | }
64 |
65 | return false;
66 | }
67 |
68 | //缩奇环 O(n)
69 | void shrink(int x, int y, int z) {
70 | while (findroot(x) != z) {
71 | p[x] = y;
72 | y = girl[x];
73 |
74 | if (t[y] == 2) {
75 | t[y] = 1;
76 | q[tail++] = y;
77 | }
78 |
79 | if (findroot(x) == x)
80 | f[x] = z;
81 | if (findroot(y) == y)
82 | f[y] = z;
83 |
84 | x = p[y];
85 | }
86 | }
87 |
88 | //暴力找LCA O(n)
89 | int LCA(int x, int y) {
90 | tim++;
91 | while (true) {
92 | if (x) {
93 | x = findroot(x);
94 |
95 | if (vis[x] == tim)
96 | return x;
97 | else {
98 | vis[x] = tim;
99 | x = p[girl[x]];
100 | }
101 | }
102 | swap(x, y);
103 | }
104 | }
105 |
106 | //并查集的查找 O(1)
107 | int findroot(int x) {
108 | return x == f[x] ? x : (f[x] = findroot(f[x]));
109 | }
--------------------------------------------------------------------------------
/src/graph/弦图相关.tex:
--------------------------------------------------------------------------------
1 | \begin{enumerate}
2 | \item 团数 $\leq$ 色数,弦图团数 = 色数;
3 |
4 | \item 设 $next(v)$ 表示 $N(v)$ 中最前的点,
5 | 令 $w*$ 表示所有满足 $A \in B$ 的 $w$ 中最后的一个点,判断 $v \cup N(v)$ 是否为极大团,
6 | 只需判断是否存在一个 $w$,满足 $Next(w)=v$ 且 $|N(v)| + 1 \leq |N(w)|$ 即可。
7 |
8 | \item 最小染色:完美消除序列 \textbf{从后往前}\ 依次给每个点染色,给每个点染上可以染的最小的颜色。
9 |
10 | \item 最大独立集:完美消除序列 \textbf{从前往后}\ 能选就选。
11 |
12 | \item 弦图最大独立集数 $=$ 最小团覆盖数。
13 |
14 | \item 最小团覆盖:设最大独立集为 $\{p_1,p_2, \dots ,p_t\}$,
15 | 则 $\{p_1\cup N(p_1), \dots , p_t \cup N(p_t)\}$ 为最小团覆盖。
16 | \end{enumerate}
17 |
--------------------------------------------------------------------------------
/src/graph/强连通分量.cpp:
--------------------------------------------------------------------------------
1 | int dfn[maxn], low[maxn], tim = 0;
2 | vector G[maxn], scc[maxn];
3 | int sccid[maxn], scc_cnt = 0, stk[maxn];
4 | bool instk[maxn];
5 |
6 | void dfs(int x) {
7 | dfn[x] = low[x] = ++tim;
8 |
9 | stk[++stk[0]] = x;
10 | instk[x] = true;
11 |
12 | for (int y : G[x]) {
13 | if (!dfn[y]) {
14 | dfs(y);
15 | low[x] = min(low[x], low[y]);
16 | }
17 | else if (instk[y])
18 | low[x] = min(low[x], dfn[y]);
19 | }
20 |
21 | if (dfn[x] == low[x]) {
22 | scc_cnt++;
23 |
24 | int u;
25 | do {
26 | u = stk[stk[0]--];
27 | instk[u] = false;
28 | sccid[u] = scc_cnt;
29 | scc[scc_cnt].push_back(u);
30 | } while (u != x);
31 | }
32 | }
33 |
34 | void tarjan(int n) {
35 | for (int i = 1; i <= n; i++)
36 | if (!dfn[i])
37 | dfs(i);
38 | }
--------------------------------------------------------------------------------
/src/graph/支配树.cpp:
--------------------------------------------------------------------------------
1 | vector G[maxn], R[maxn], son[maxn]; // R是反图, son存的是sdom树上的儿子
2 |
3 | int ufs[maxn];
4 |
5 | int idom[maxn], sdom[maxn], anc[maxn]; // anc: sdom的dfn最小的祖先
6 |
7 | int p[maxn], dfn[maxn], id[maxn], tim;
8 |
9 | int findufs(int x) {
10 | if (ufs[x] == x)
11 | return x;
12 |
13 | int t = ufs[x];
14 | ufs[x] = findufs(ufs[x]);
15 |
16 | if (dfn[sdom[anc[x]]] > dfn[sdom[anc[t]]])
17 | anc[x] = anc[t];
18 |
19 | return ufs[x];
20 | }
21 |
22 | void dfs(int x) {
23 | dfn[x] = ++tim;
24 | id[tim] = x;
25 | sdom[x] = x;
26 |
27 | for (int y : G[x])
28 | if (!dfn[y]) {
29 | p[y] = x;
30 | dfs(y);
31 | }
32 | }
33 |
34 | void get_dominator(int n) {
35 | for (int i = 1; i <= n; i++)
36 | ufs[i] = anc[i] = i;
37 |
38 | dfs(1);
39 |
40 | for (int i = n; i > 1; i--) {
41 | int x = id[i];
42 |
43 | for (int y : R[x])
44 | if (dfn[y]) {
45 | findufs(y);
46 | if (dfn[sdom[x]] > dfn[sdom[anc[y]]])
47 | sdom[x] = sdom[anc[y]];
48 | }
49 |
50 | son[sdom[x]].push_back(x);
51 | ufs[x] = p[x];
52 |
53 | for (int u : son[p[x]]) {
54 | findufs(u);
55 | idom[u] = (sdom[u] == sdom[anc[u]] ? p[x] : anc[u]);
56 | }
57 |
58 | son[p[x]].clear();
59 | }
60 |
61 | for (int i = 2; i <= n; i++) {
62 | int x = id[i];
63 |
64 | if (idom[x] != sdom[x])
65 | idom[x] = idom[idom[x]];
66 |
67 | son[idom[x]].push_back(x);
68 | }
69 | }
--------------------------------------------------------------------------------
/src/graph/整理.md:
--------------------------------------------------------------------------------
1 | ## 最小割
2 |
3 | ### 最小割输出一种方案
4 |
5 | 在残量网络上从源点开始floodfill,如果一条边的起点从源点可达,而终点不可达,那么这条边就在最小割中。
6 |
7 | ### 最小割的可行边与必须边
8 |
9 | #### 可行边
10 |
11 | 满流且残量网络上不存在起点到终点的路径,也就是起点和终点不在同一SCC中。
12 |
13 | #### 必须边
14 |
15 | 满流且残量网络上源点可达起点,终点可达汇点。
16 |
17 | ## 二分图
18 |
19 | ### 最大匹配的可行边与必须边
20 |
21 | #### 可行边
22 |
23 | 一条边的两个端点在同一个SCC中,不论是正边还是反边。
24 |
25 | #### 必须边
26 |
27 | 一条属于当前最大匹配的边,且两个端点不在同一个SCC中。
28 |
29 | ### 独立集
30 |
31 | 二分图独立集可以看成最小割问题,割掉最少的点使得剩下的点都在独立集中。
32 |
33 | (割点等价于割掉它与源点或汇点相连的边)
34 |
35 | 所以独立集输出方案就是求出不在最小割中的点,独立集的必须点/可行点就是最小割的不可行点/非必须点。
36 |
37 | ## 一般图
38 |
39 | 一般图判定必须点/边和可行点/边时一般不会卡常,所以只提供基于Tutte矩阵的做法。
40 |
41 | 设图$G$的Tutte矩阵是$\tilde A$,先提供几个基础的定理:
42 |
43 | - $({\tilde A} ^{-1}) _{i, j} \ne 0$当且仅当$G-\{v_i, v_j\}$有完美匹配。
44 | -
45 |
46 | ### 最大匹配的可行边与必须边
47 |
48 | #### 可行边
49 |
50 |
51 |
52 | #### 必须边
53 |
54 |
--------------------------------------------------------------------------------
/src/graph/最小树形图.cpp:
--------------------------------------------------------------------------------
1 | constexpr int maxn = 105, maxe = 10005, inf = 0x3f3f3f3f;
2 |
3 | struct edge {
4 | int u, v, w;
5 | } e[maxe];
6 |
7 | int mn[maxn], pr[maxn], ufs[maxn], vis[maxn];
8 | bool alive[maxn];
9 |
10 | int edmonds(int n, int m, int rt) {
11 | for (int i = 1; i <= n; i++)
12 | alive[i] = true;
13 |
14 | int ans = 0;
15 |
16 | while (true) {
17 | memset(mn, 63, sizeof(int) * (n + 1));
18 | memset(pr, 0, sizeof(int) * (n + 1));
19 | memset(ufs, 0, sizeof(int) * (n + 1));
20 | memset(vis, 0, sizeof(int) * (n + 1));
21 |
22 | mn[rt] = 0;
23 |
24 | for (int i = 1; i <= m; i++)
25 | if (e[i].u != e[i].v && e[i].w < mn[e[i].v]) {
26 | mn[e[i].v] = e[i].w;
27 | pr[e[i].v] = e[i].u;
28 | }
29 |
30 | for (int i = 1; i <= n; i++)
31 | if (alive[i]) {
32 | if (mn[i] >= inf)
33 | return -1; // 不存在最小树形图
34 |
35 | ans += mn[i];
36 | }
37 |
38 | bool flag = false;
39 |
40 | for (int i = 1; i <= n; i++) {
41 | if (!alive[i])
42 | continue;
43 |
44 | int x = i;
45 | while (x && !vis[x]) {
46 | vis[x] = i;
47 | x = pr[x];
48 | }
49 |
50 | if (x && vis[x] == i) {
51 | flag = true;
52 | for (int u = x; !ufs[u]; u = pr[u])
53 | ufs[u] = x;
54 | }
55 | }
56 |
57 | for (int i = 1; i <= m; i++) {
58 | e[i].w -= mn[e[i].v];
59 |
60 | if (ufs[e[i].u])
61 | e[i].u = ufs[e[i].u];
62 | if (ufs[e[i].v])
63 | e[i].v = ufs[e[i].v];
64 | }
65 |
66 | if (!flag)
67 | return ans;
68 |
69 | for (int i = 1; i <= n; i++)
70 | if (ufs[i] && i != ufs[i])
71 | alive[i] = false;
72 | }
73 | }
--------------------------------------------------------------------------------
/src/graph/最小树形图.tex:
--------------------------------------------------------------------------------
1 | 对每个点找出最小的入边,如果是一个 DAG 那么就已经结束了。
2 |
3 | 否则把环都缩起来,每个点的边权减去环上的边权之后再跑一遍,直到没有环为止。
4 |
5 | 可以用可并堆优化到 $O(m\log n)$,需要写一个带懒标记的左偏树。
6 |
7 | {\large\textbf{$O(nm)$ 版本}}
8 |
9 | \inputminted{cpp}{../src/graph/最小树形图.cpp}
10 |
11 | {\large\textbf{$O(m\log n)$ 版本}}
12 |
13 | (堆优化版本可以参考 \textit{fstqwq} 的模板,在最后没有目录的部分。)
14 |
--------------------------------------------------------------------------------
/src/graph/最小直径生成树.tex:
--------------------------------------------------------------------------------
1 | 首先找到图的绝对中心(可能在点上,也可能在某条边上),然后以绝对中心为起点建最短路树就是最小直径生成树。
2 |
3 | \inputminted{cpp}{../src/graph/mdst.cpp}
4 |
--------------------------------------------------------------------------------
/src/graph/有源汇上下界最大流.cpp:
--------------------------------------------------------------------------------
1 | int ex[maxn], id[maxn];
2 |
3 | int main() {
4 |
5 | memset(last, -1, sizeof(last));
6 |
7 | int n, m, src, sink;
8 | scanf("%d%d%d%d", &n, &m, &src, &sink);
9 | s = n + 1;
10 | t = n + 2;
11 |
12 | while (m--) {
13 | int x, y, b, c;
14 | scanf("%d%d%d%d", &x, &y, &b, &c);
15 |
16 | addedge(x, y, c - b);
17 |
18 | ex[y] += b;
19 | ex[x] -= b;
20 | }
21 |
22 | for (int i = 1; i <= n; i++) {
23 | id[i] = cnte;
24 |
25 | if (ex[i] >= 0)
26 | addedge(s, i, ex[i]);
27 | else
28 | addedge(i, t, -ex[i]);
29 | }
30 |
31 | addedge(sink, src, (~0u) >> 1);
32 |
33 | Dinic();
34 |
35 | if (any_of(id + 1, id + n + 1, [] (int i) {return (bool)e[i].cap;}))
36 | printf("please go home to sleep\n");
37 | else {
38 | int flow = e[cnte - 1].cap;
39 | e[cnte - 1].cap = e[cnte - 2].cap = 0;
40 | s = src;
41 | t = sink;
42 |
43 | printf("%d\n", flow + Dinic());
44 | }
45 |
46 | return 0;
47 | }
--------------------------------------------------------------------------------
/src/graph/欧拉回路.cpp:
--------------------------------------------------------------------------------
1 | vector G[maxn], C[maxn], v[maxn]; // C是边的编号
2 | int cur[maxn];
3 | bool vis[maxn * 2];
4 |
5 | vector > vec;
6 |
7 | int d[maxn];
8 |
9 | void dfs(int x) {
10 | bool bad = false;
11 |
12 | while (!bad) {
13 | bad = true;
14 |
15 | for (int &i = cur[x]; i < (int)G[x].size(); i++)
16 | if (!vis[C[x][i]]) {
17 | vis[C[x][i]] = true;
18 | vec.emplace_back(x, i);
19 | x = G[x][i];
20 | bad = false;
21 |
22 | break;
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/graph/边双.cpp:
--------------------------------------------------------------------------------
1 | int u[maxe], v[maxe];
2 | vector G[maxn]; // 存的是边的编号
3 |
4 | int stk[maxn], top, dfn[maxn], low[maxn], tim, bcc_cnt;
5 | vector bcc[maxn];
6 |
7 | bool isbridge[maxe];
8 |
9 | void dfs(int x, int pr) { // 这里pr是入边的编号
10 | dfn[x] = low[x] = ++tim;
11 | stk[++top] = x;
12 |
13 | for (int i : G[x]) {
14 | int y = (u[i] == x ? v[i] : u[i]);
15 |
16 | if (!dfn[y]) {
17 | dfs(y, i);
18 | low[x] = min(low[x], low[y]);
19 |
20 | if (low[y] > dfn[x])
21 | bridge[i] = true;
22 | }
23 | else if (i != pr)
24 | low[x] = min(low[x], dfn[y]);
25 | }
26 |
27 | if (dfn[x] == low[x]) {
28 | bcc_cnt++;
29 | int y;
30 | do {
31 | y = stk[top--];
32 | bcc[bcc_cnt].push_back(y);
33 | } while (y != x);
34 | }
35 | }
--------------------------------------------------------------------------------
/src/math/Berlekamp-Massey.cpp:
--------------------------------------------------------------------------------
1 | vector berlekamp_massey(const vector &a) {
2 | vector v, last; // v is the answer, 0-based
3 | int k = -1, delta = 0;
4 |
5 | for (int i = 0; i < (int)a.size(); i++) {
6 |
7 | int tmp = 0;
8 | for (int j = 0; j < (int)v.size(); j++)
9 | tmp = (tmp + (long long)a[i - j - 1] * v[j]) % p;
10 |
11 | if (a[i] == tmp)
12 | continue;
13 |
14 | if (k < 0) {
15 | k = i;
16 | delta = (a[i] - tmp + p) % p;
17 | v = vector(i + 1);
18 |
19 | continue;
20 | }
21 |
22 | vector u = v;
23 | int val = (long long)(a[i] - tmp + p) * qpow(delta, p - 2) % p;
24 |
25 | if (v.size() < last.size() + i - k)
26 | v.resize(last.size() + i - k);
27 |
28 | (v[i - k - 1] += val) %= p;
29 |
30 | for (int j = 0; j < (int)last.size(); j++) {
31 | v[i - k + j] = (v[i - k + j] - (long long)val * last[j]) % p;
32 | if (v[i - k + j] < 0)
33 | v[i - k + j] += p;
34 | }
35 |
36 | if ((int)u.size() - i < (int)last.size() - k) {
37 | last = u;
38 | k = i;
39 | delta = a[i] - tmp;
40 | if (delta < 0)
41 | delta += p;
42 | }
43 | }
44 |
45 | for (auto &x : v) // 一般是需要最小递推式的, 所以处理一下
46 | x = (p - x) % p;
47 | v.insert(v.begin(), 1);
48 |
49 | return v; // $\forall i, \sum_{j = 0} ^ m a_{i - j} v_j = 0$
50 | }
--------------------------------------------------------------------------------
/src/math/Berlekamp-Massey.tex:
--------------------------------------------------------------------------------
1 | 如果要求出一个次数为 $k$ 的递推式,则输入的数列需要至少有 $2k$ 项。
2 |
3 | 返回的内容满足 $\sum_{j = 0} ^ {m - 1} a_{i - j} c_j = 0$,并且 $c_0 = 1$。称为最小递推式。
4 |
5 | 如果不加最后的处理的话,代码返回的结果会变成 $a_i = \sum_{j = 0} ^ {m - 1} c_{j - 1} a_{i - j}$,有时候这样会方便接着跑递推,需要的话就删掉最后的处理。
6 |
7 | (实际上 Berlekamp-Massey 是对每个前缀都求出了递推式,不过一般用不到。)
8 |
9 | \inputminted{cpp}{../src/math/Berlekamp-Massey.cpp}
10 |
11 | 如果要求向量序列的递推式,就乘一个随机行向量 $\boldsymbol{v} ^ T$(或者说是把每位乘一个随机权值)变成求数列递推式即可。
12 |
13 | 如果是矩阵序列的话就随机一个行向量 $\boldsymbol{u} ^ T$ 和列向量 $\boldsymbol{v}$,然后把矩阵变成 $\boldsymbol{u} ^ T A \boldsymbol{v}$ 的数列就行了。
14 |
15 | \label{BerlekampMasseyApplication}
16 |
17 | \subsubsection{优化矩阵快速幂DP}
18 |
19 | 如果 $\boldsymbol{f_i}$ 是一个向量,并且转移是一个矩阵,那显然 $\{\boldsymbol{f_i}\}$ 是一个线性递推序列。
20 |
21 | 假设 $\boldsymbol{f_i}$ 有 $n$ 维,先暴力求出 $\boldsymbol{f}_{0 \dots 2n - 1}$,然后跑 Berlekamp-Massey,最后调用 \detailedref{LinearRecurrence} 即可。(快速齐次线性递推的结果是一个序列,某个给定初值的结果就是点乘,所以只需要跑一次。)
22 |
23 | 如果要求 $\boldsymbol{f_m}$,并且矩阵有 $k$ 个非零项的话,复杂度就是 $O(nk + n\log m\log n)$。(因为暴力求前 $2n-1$ 个 $\boldsymbol{f}_i$ 需要 $O(nk)$ 时间。)
24 |
25 | \subsubsection{求矩阵最小多项式}
26 |
27 | 矩阵 $A$ 的最小多项式是次数最小的并且 $f(A) = 0$ 的多项式 $f$。
28 |
29 | 实际上最小多项式就是 $\{A^i\}$ 的最小递推式,所以直接调用 Berlekamp-Massey 就好了。显然它的次数不超过 $n$。
30 |
31 | 瓶颈在于求出 $A^i$,实际上我们只要处理 $A^i \boldsymbol{v}$就行了,每次对向量做递推。
32 |
33 | 设 $A$ 有 $k$ 个非零项,则复杂度为 $O(kn + n ^ 2)$。
34 |
35 | \subsubsection{求稀疏矩阵的行列式}
36 |
37 | 如果能求出特征多项式,则常数项乘上 $(-1)^n$ 就是行列式,但是最小多项式不一定就是特征多项式。
38 |
39 | 把 $A$ 乘上一个随机对角阵 $B$(就是每行分别乘一个随机数),则 $AB$ 的最小多项式有很大概率就是特征多项式,最后再除掉 $\det B$ 就行了。
40 |
41 | 设 $A$ 有 $k$ 个非零项,则复杂度为 $O(kn + n ^ 2)$。
42 |
43 | \subsubsection{求稀疏矩阵的秩}
44 |
45 | 设 $A$ 是一个 $n\times m$ 的矩阵,首先随机一个 $n\times n$ 的对角阵 $P$ 和一个 $m\times m$ 的对角阵 $Q$,然后计算$Q A P A^T Q$ 的最小多项式即可。
46 |
47 | 实际上不用计算这个矩阵,因为求最小多项式时要用它乘一个向量,我们依次把这几个矩阵乘到向量里就行了。答案就是最小多项式除掉所有 $x$ 因子后剩下的次数。
48 |
49 | 设 $A$ 有 $k$ 个非零项,复杂度为 $O(kn + n ^ 2)$。
50 |
51 | \subsubsection{解稀疏方程组}
52 |
53 | \paragraph{问题} $A \boldsymbol{x} = b$,其中 $A$ 是一个 $n \times n$ 的 \textbf{满秩} 稀疏矩阵,$\boldsymbol{b}$ 和 $\boldsymbol{x}$ 是 $n$ 维 \textbf{列} 向量,$A, \boldsymbol{b}$ 已知,需要解出 $\boldsymbol{x}$。
54 |
55 | \paragraph{做法} 显然 $\boldsymbol{x} = A^{-1} \boldsymbol{b}$。如果我们能求出 $\{A^i \boldsymbol{b}\}$($i \ge 0$)的最小递推式 $\{r_{0 \dots m - 1}\}$($m \le n$),那么就有结论
56 |
57 | $$ \boldsymbol{x} = A^{-1} \boldsymbol{b} = -\frac 1 {r_{m - 1}} \sum_{i = 0} ^ {m - 2} r_{m - 2 - i} A^i \boldsymbol{b} $$
58 |
59 | 因为 $A$ 是稀疏矩阵,直接按定义递推出 $\boldsymbol{b} \dots A^{2n - 1} \boldsymbol{b}$即可。设 $A$ 中有 $k$ 个非零项,则复杂度为 $O(kn + n^2)$。
60 |
61 | \inputminted{cpp}{../src/math/解稀疏方程组.cpp}
62 |
--------------------------------------------------------------------------------
/src/math/Berlekamp-Massey应用.tex:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/src/math/Berlekamp-Massey应用.tex
--------------------------------------------------------------------------------
/src/math/Bostan-Mori.cpp:
--------------------------------------------------------------------------------
1 | int bostan_mori(int k, poly a, poly b) {
2 | int n = (int)a.size();
3 |
4 | while (k) {
5 | poly c = b;
6 | for (int i = 1; i < n; i += 2)
7 | c[i] = (p - c[i]) % p;
8 |
9 | a = poly_mul(a, c);
10 | b = poly_mul(b, c);
11 |
12 | for (int i = 0; i < n; i++) {
13 | a[i] = a[i * 2 + k % 2];
14 | b[i] = b[i * 2];
15 | }
16 |
17 | a.resize(n);
18 | b.resize(n);
19 | k /= 2;
20 | }
21 |
22 | return (ll)a[0] * qpow(b[0], p - 2) % p;
23 | }
24 |
--------------------------------------------------------------------------------
/src/math/Bostan-Mori.tex:
--------------------------------------------------------------------------------
1 | \paragraph{问题} 已知两个 $n$ 次多项式 $f(x)$ 与 $g(x)$,求 $\frac {f(x)} {g(x)}$ 的第 $k$ 项系数。
2 |
3 | \paragraph{做法} 上下同乘 $g(-x)$,则底下的 $g(x)g(-x)$ 只有偶数项,所以上面的奇/偶数项乘完之后奇偶性是不变的。然后就可以直接按照 $n$ 的奇偶性分情况只取出奇数项或者偶数项,这样就在 $n$ 不变的情况下把 $k$ 折半了,一直做到 $k = 0$ 然后输出常数项即可。
4 |
5 | $$ [x^k]\dfrac{f(x)}{g(x)}=[x^k]\dfrac{f(x)g(-x)}{g(x)g(-x)}=[x^k]\dfrac{F(x^2)+xG(x^2)}{H(x^2)} $$
6 | $$ =
7 | \begin{cases}
8 | [x^{\lfloor k/2\rfloor}]\dfrac{F(x)}{H(x)}& (k\text{ is even})\\
9 | [x^{\lfloor k/2\rfloor}]\dfrac{G(x)}{H(x)}& (k\text{ is odd})
10 | \end{cases} $$
11 |
12 | 复杂度 $O(n \log n \log k)$。
13 |
14 | \inputminted{cpp}{../src/math/Bostan-Mori.cpp}
15 |
--------------------------------------------------------------------------------
/src/math/FFT.cpp:
--------------------------------------------------------------------------------
1 | using cp = complex;
2 | const double PI = acos(-1.0);
3 |
4 | vector omega[25];
5 |
6 | void fft_init(int n) {
7 | for (int k = 2, d = 0; k <= n; k *= 2, d++) {
8 | omega[d].resize(k + 1);
9 | for (int i = 0; i <= k; i++)
10 | omega[d][i] = polar(1.0, 2 * PI * i / k);
11 | }
12 | }
13 |
14 | void fft(cp* a, int n, int t) {
15 | for (int i = 1, j = 0; i < n - 1; i++) {
16 | int k = n;
17 | do
18 | j ^= (k >>= 1);
19 | while (j < k);
20 |
21 | if (i < j)
22 | swap(a[i], a[j]);
23 | }
24 |
25 | for (int k = 1, d = 0; k < n; k *= 2, d++)
26 | for (int i = 0; i < n; i += k * 2)
27 | for (int j = 0; j < k; j++) {
28 | cp w = omega[d][t > 0 ? j : k * 2 - j];
29 | cp u = a[i + j], v = w * a[i + j + k];
30 | a[i + j] = u + v;
31 | a[i + j + k] = u - v;
32 | }
33 |
34 | if (t < 0)
35 | for (int i = 0; i < n; i++)
36 | a[i] /= n;
37 | }
38 |
--------------------------------------------------------------------------------
/src/math/FWT.cpp:
--------------------------------------------------------------------------------
1 | // 注意 FWT 常数比较小,这点与 FFT/NTT 不同
2 | // 以下代码均以模质数情况为例,其中 n 为变换长度,t 表示正/逆变换
3 |
4 | // 按位或版本
5 | void FWT_or(int *A, int n, int t) {
6 | for (int k = 2; k <= n; k *= 2)
7 | for (int i = 0; i < n; i += k)
8 | for (int j = 0; j < k / 2; j++) {
9 | if (t > 0)
10 | A[i + j + k / 2] = (A[i + j + k / 2] + A[i + j]) % p;
11 | else
12 | A[i + j + k / 2] = (A[i + j + k / 2] - A[i + j] + p) % p;
13 | }
14 | }
15 |
16 | // 按位与版本
17 | void FWT_and(int *A, int n, int t) {
18 | for (int k = 2; k <= n; k *= 2)
19 | for (int i = 0; i < n; i += k)
20 | for (int j = 0; j < k / 2; j++) {
21 | if (t > 0)
22 | A[i + j] = (A[i + j] + A[i + j + k / 2]) % p;
23 | else
24 | A[i + j] = (A[i + j] - A[i + j + k / 2] + p) % p;
25 | }
26 | }
27 |
28 | // 按位异或版本
29 | void FWT_xor(int *A, int n, int t) {
30 | for (int k = 2; k <= n; k *= 2)
31 | for (int i = 0; i < n; i += k)
32 | for (int j = 0; j < k / 2; j++) {
33 | int a = A[i + j], b = A[i + j + k / 2];
34 | A[i + j] = (a + b) % p;
35 | A[i + j + k / 2] = (a - b + p) % p;
36 | }
37 |
38 | if (t < 0) {
39 | int inv = qpow(n % p, p - 2); // n 的逆元,在不取模时需要用每层除以 2 代替
40 | for (int i = 0; i < n; i++)
41 | A[i] = A[i] * inv % p;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/math/MTT.cpp:
--------------------------------------------------------------------------------
1 | void dft(cp* a, cp* b, int n) {
2 | static cp c[MAXN];
3 | for (int i = 0; i < n; i++)
4 | c[i] = cp(a[i].real(), b[i].real());
5 |
6 | fft(c, n, 1);
7 | for (int i = 0; i < n; i++) {
8 | int j = (n - i) & (n - 1);
9 | a[i] = (c[i] + conj(c[j])) * 0.5;
10 | b[i] = (c[i] - conj(c[j])) * -0.5i;
11 | }
12 | }
13 |
14 | void idft(cp* a, cp* b, int n) {
15 | static cp c[MAXN];
16 | for (int i = 0; i < n; i++)
17 | c[i] = a[i] + 1i * b[i];
18 |
19 | fft(c, n, -1);
20 | for (int i = 0; i < n; i++) {
21 | a[i] = c[i].real();
22 | b[i] = c[i].imag();
23 | }
24 | }
25 |
26 | vector multiply(const vector& u, const vector& v, int mod) {
27 | static cp a[2][MAXN], b[2][MAXN], c[3][MAXN];
28 |
29 | int base = ceil(sqrt(mod));
30 | int n = (int)u.size(), m = (int)v.size();
31 |
32 | int fft_n = 1;
33 | while (fft_n < n + m - 1)
34 | fft_n *= 2;
35 |
36 | for (int i = 0; i < 2; i++) {
37 | fill(a[i], a[i] + fft_n, 0);
38 | fill(b[i], b[i] + fft_n, 0);
39 | }
40 | for (int i = 0; i < 3; i++)
41 | fill(c[i], c[i] + fft_n, 0);
42 |
43 | for (int i = 0; i < n; i++) {
44 | a[0][i] = (u[i] % mod) % base;
45 | a[1][i] = (u[i] % mod) / base;
46 | }
47 |
48 | for (int i = 0; i < m; i++) {
49 | b[0][i] = (v[i] % mod) % base;
50 | b[1][i] = (v[i] % mod) / base;
51 | }
52 |
53 | dft(a[0], a[1], fft_n);
54 | dft(b[0], b[1], fft_n);
55 |
56 | for (int i = 0; i < fft_n; i++) {
57 | c[0][i] = a[0][i] * b[0][i];
58 | c[1][i] = a[0][i] * b[1][i] + a[1][i] * b[0][i];
59 | c[2][i] = a[1][i] * b[1][i];
60 | }
61 |
62 | fft(c[1], fft_n, -1);
63 | idft(c[0], c[2], fft_n);
64 |
65 | int base2 = base * base % mod;
66 | vector ans(n + m - 1);
67 |
68 | for (int i = 0; i < n + m - 1; i++)
69 | ans[i] = ((ll)(c[0][i].real() + 0.5) +
70 | (ll)(c[1][i].real() + 0.5) % mod * base +
71 | (ll)(c[2][i].real() + 0.5) % mod * base2) % mod;
72 |
73 | return ans;
74 | }
75 |
--------------------------------------------------------------------------------
/src/math/NTT.cpp:
--------------------------------------------------------------------------------
1 | using ll = long long;
2 | using ull = unsigned long long;
3 |
4 | int inv[MAXN]; // 逆元,如果需要积分就顺便预处理出来
5 | poly omega[25]; // 单位根
6 |
7 | // n 是 DFT 的最大长度
8 | // 例如如果最多有两个长为 k 的多项式相乘,或者求逆的长度为 k,那么 n 需要 >= 2k
9 |
10 | void ntt_init(int n) {
11 | for (int k = 2, d = 0; k <= n; k *= 2, d++) {
12 | omega[d].resize(k + 1);
13 |
14 | int wn = qpow(3, (p - 1) / k), tmp = 1;
15 | for (int i = 0; i <= k; i++) {
16 | omega[d][i] = tmp;
17 | tmp = (ll)tmp * wn % p;
18 | }
19 | }
20 |
21 | inv[1] = 1;
22 | for (int i = 2; i < n; i++)
23 | inv[i] = (ll)(p - p / i) * inv[p % i] % p;
24 | }
25 |
26 | // 传入的必须是 [0, p) 范围内,不能有负的,不然会溢出
27 | // 不能保证就把 d == 16 改成 d % 8 == 0 之类
28 | void ntt(int *c, int n, int t) {
29 | static ull a[MAXN];
30 | for (int i = 0; i < n; i++) a[i] = c[i];
31 |
32 | for (int i = 1, j = 0; i < n - 1; i++) {
33 | int k = n;
34 | do
35 | j ^= (k >>= 1);
36 | while (j < k);
37 |
38 | if (i < j)
39 | swap(a[i], a[j]);
40 | }
41 |
42 | for (int k = 1, d = 0; k < n; k *= 2, d++) {
43 | if (d == 16)
44 | for (int i = 0; i < n; i++)
45 | a[i] %= p;
46 |
47 | for (int i = 0; i < n; i += k * 2)
48 | for (int j = 0; j < k; j++) {
49 | int w = omega[d][t > 0 ? j : k * 2 - j];
50 | ull u = a[i + j], v = w * a[i + j + k] % p;
51 | a[i + j] = u + v;
52 | a[i + j + k] = u - v + p;
53 | }
54 | }
55 |
56 | if (t > 0) {
57 | for (int i = 0; i < n; i++)
58 | c[i] = a[i] % p;
59 | }
60 | else {
61 | int inv = qpow(n, p - 2);
62 | for (int i = 0; i < n; i++)
63 | c[i] = a[i] * inv % p;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/math/SG定理.tex:
--------------------------------------------------------------------------------
1 | 对于一个 \textbf{平等}\ 游戏,可以为每个状态定义一个 SG 函数。
2 |
3 | 一个状态的 SG 函数等于所有它能一步到达的状态的 SG 函数的 $\text{mex}$,也就是最小的没有出现过的自然数。
4 |
5 | 那么所有先手必败态的 SG 函数为 $0$,先手必胜态的SG函数非 $0$。
6 |
7 | 如果有一个游戏,它由若干个独立的子游戏组成,且每次行动时 \textbf{只能选一个}\ 子游戏进行操作,则这个游戏的 SG 函数就是所有子游戏的SG函数的异或和。比如最经典的 Nim 游戏,每次只能选一堆取若干个石子。
8 |
9 | 同时操作多个子游戏的结论参见 \detailedref{classicgame}。
10 |
--------------------------------------------------------------------------------
/src/math/fwt3.cpp:
--------------------------------------------------------------------------------
1 | void fwt_or(int *a, int n, int tp) {
2 | for (int j = 0; (1 << j) < n; j++)
3 | for (int i = 0; i < n; i++)
4 | if (i >> j & 1) {
5 | if (tp > 0)
6 | a[i] += a[i ^ (1 << j)];
7 | else
8 | a[i] -= a[i ^ (1 << j)];
9 | }
10 | }
11 |
12 | // and自然就是or反过来
13 | void fwt_and(int *a, int n, int tp) {
14 | for (int j = 0; (1 << j) < n; j++)
15 | for (int i = 0; i < n; i++)
16 | if (!(i >> j & 1)) {
17 | if (tp > 0)
18 | a[i] += a[i | (1 << j)];
19 | else
20 | a[i] -= a[i | (1 << j)];
21 | }
22 | }
23 |
24 | // xor同理
--------------------------------------------------------------------------------
/src/math/gauss_jordan.cpp:
--------------------------------------------------------------------------------
1 | void Gauss_Jordan(int A[][maxn], int n) {
2 | for (int i = 1; i <= n; i++) {
3 | int ii = i;
4 | for (int j = i + 1; j <= n; j++)
5 | if (fabs(A[j][i]) > fabs(A[ii][i]))
6 | ii = j;
7 |
8 | if (ii != i) // 这里没有判是否无解,如果有可能无解的话要判一下
9 | for (int j = i; j <= n + 1; j++)
10 | swap(A[i][j], A[ii][j]);
11 |
12 | for (int j = 1; j <= n; j++)
13 | if (j != i) // 消成对角
14 | for (int k = n + 1; k >= i; k--)
15 | A[j][k] -= A[j][i] / A[i][i] * A[i][k];
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/math/minmax.tex:
--------------------------------------------------------------------------------
1 | $$ \begin{aligned} \max(S)=\sum_{T \subset S}(-1)^{|T|+1}\min(T) \\ \min(S)=\sum_{T \subset S}(-1)^{|T|+1}\max(T) \end{aligned} $$
2 |
3 | \paragraph{推广} 求第 $k$ 大:
4 |
5 | $$ k\mhyphen\max(S) = \sum_{T \subset S} (-1)^{|T|-k} {|T|-1 \choose k-1} \min(T) $$
6 |
7 | 显然只有大小至少为 $k$ 的子集会被枚举到。
8 |
--------------------------------------------------------------------------------
/src/math/simpson.cpp:
--------------------------------------------------------------------------------
1 | // Adaptive Simpson's method : double simpson::solve (double (*f) (double), double l, double r, double eps) : integrates f over (l, r) with error eps.
2 |
3 | double area (double (*f) (double), double l, double r) {
4 | double m = l + (r - l) / 2;
5 | return (f(l) + 4 * f(m) + f(r)) * (r - l) / 6;
6 | }
7 |
8 | double solve (double (*f) (double), double l, double r, double eps, double a) {
9 | double m = l + (r - l) / 2;
10 | double left = area(f, l, m), right = area(f, m, r);
11 | if (fabs(left + right - a) <= 15 * eps)
12 | return left + right + (left + right - a) / 15.0;
13 | return solve(f, l, m, eps / 2, left) + solve(f, m, r, eps / 2, right);
14 | }
15 |
16 | double solve (double (*f) (double), double l, double r, double eps) {
17 | return solve(f, l, r, eps, area (f, l, r));
18 | }
--------------------------------------------------------------------------------
/src/math/三模数NTT.cpp:
--------------------------------------------------------------------------------
1 | //以下为三模数NTT, 原理是选取三个乘积大于结果的NTT模数, 最后中国剩余定理合并
2 | //以对23333333(不是质数)取模为例
3 | constexpr int maxn = 262200, Mod = 23333333, g = 3, m[] = {998244353, 1004535809, 1045430273}, m0_inv = 669690699, m1_inv = 332747959, M_inv = 942377029; // 这三个模数最小原根都是3
4 | constexpr long long M = (long long)m[0] * m[1];
5 |
6 | // 主函数(当然更多时候包装一下比较好)
7 | // 用来卷积的是A和B
8 | // 需要调用mul
9 | int n, N = 1, A[maxn], B[maxn], C[maxn], D[maxn], ans[3][maxn];
10 | int main() {
11 | scanf("%d", &n);
12 |
13 | while (N < n * 2)
14 | N *= 2;
15 |
16 | for (int i = 0; i < n; i++)
17 | scanf("%d", &A[i]);
18 | for (int i = 0; i < n; i++)
19 | scanf("%d", &B[i]);
20 |
21 | for (int i = 0; i < 3; i++)
22 | mul(m[i], ans[i]);
23 |
24 | for (int i = 0; i < n; i++)
25 | printf("%d ", China(ans[0][i], ans[1][i], ans[2][i]));
26 |
27 | return 0;
28 | }
29 |
30 | // mul O(n\log n)
31 | // 包装了模NTT模数的卷积
32 | // 需要调用NTT
33 | void mul(int p, int *ans) {
34 | copy(A, A + N, C);
35 | copy(B, B + N, D);
36 |
37 | NTT(C, N, 1, p);
38 | NTT(D, N, 1, p);
39 |
40 | for (int i = 0; i < N; i++)
41 | ans[i] = (long long)C[i] * D[i] % p;
42 |
43 | NTT(ans, N, -1, p);
44 | }
45 |
46 | // 中国剩余定理 O(1)
47 | // 由于直接合并会爆long long, 采用神奇的方法合并
48 | // 需要调用O(1)快速乘
49 | inline int China(int a0, int a1, int a2) {
50 | long long A = (mul((long long)a0 * m1_inv, m[1], M) + mul((long long)a1 * m0_inv, m[0], M)) % M;
51 | int k = ((a2 - A) % m[2] + m[2]) % m[2] * M_inv % m[2];
52 | return (k % Mod * (M % Mod) % Mod + A % Mod) % Mod;
53 | }
--------------------------------------------------------------------------------
/src/math/任意模数卷积.tex:
--------------------------------------------------------------------------------
1 | 三模数 NTT 和直接拆系数 FFT 都太慢了,不要用。
2 |
3 | MTT 的原理就是拆系数 FFT,只不过优化了做变换的次数。
4 |
5 | 考虑要对 $A(x), B(x)$ 两个多项式做 DFT,可以构造两个复多项式
6 |
7 | $$ P(x) = A(x) + iB(x) \quad Q(x) = A(x) - iB(x) $$
8 |
9 | 只需要 DFT 一个,另一个 DFT 实际上就是前者反转再取共轭,再利用
10 |
11 | $$ A(x) = \frac {P(x) + Q(x)} 2 \quad B(x) = \frac {P(x) - Q(x)} {2i} $$
12 |
13 | 即可还原出 $A(x), B(x)$。
14 |
15 | IDFT 的道理更简单,如果要对 $A(x)$ 和 $B(x)$ 做 IDFT,只需要对 $A(x) + i B(x)$ 做 IDFT 即可,因为 IDFT 的结果必定为实数,所以结果的实部和虚部就分别是 $A(x)$ 和 $B(x)$。
16 |
17 | \textbf{实际上任何同时对两个实序列进行 DFT,或者同时对结果为实序列的 DFT 进行逆变换时都可以按照上面的方法优化,可以减少一半的 DFT 次数。}
18 |
19 | \inputminted{cpp}{../src/math/MTT.cpp}
20 |
--------------------------------------------------------------------------------
/src/math/伯努利数.tex:
--------------------------------------------------------------------------------
1 | \paragraph{指数生成函数} $\begin{aligned} B(x)=\sum_{i\ge 0}\frac{B_i x^i}{i!}=\frac x{e^x-1} \end{aligned}$
2 |
3 | $$ \begin{aligned}B_n=[n=0]-\sum_{i=0}^{n-1}{n\choose i}\frac{B_i}{n-k+1}\end{aligned} $$
4 |
5 | $$ \begin{aligned}\sum_{i=0}^n{n+1\choose i}B_i=0\end{aligned} $$
6 |
7 | $$ \begin{aligned}S_n(m)=\sum_{i=0}^{m-1}i^n=\sum_{i=0}^n{n\choose i}B_{n-i}\frac{m^{i+1}}{i+1}\end{aligned} $$
8 |
9 | $$ B_0 = 1,\, B_1 = -\frac 1 2,\, B_4 = -\frac 1 {30},\, B_6 = \frac 1 {42},\, B_8 = -\frac 1{30},\, \dots $$
10 |
11 | (除了 $B_1 = -\frac 1 2$ 以外,伯努利数的奇数项都是 $0$。)
12 |
13 | 自然数幂次和关于次数的 EGF:
14 |
15 | $$ \begin{aligned} F(x)=&\sum_{k=0}^\infty \frac{\sum_{i=0}^n i^k}{k!}x^k\\ =&\sum_{i=0}^n e^{ix}\\ =&\frac{e^{(n+1)x-1}}{e^x-1} \end{aligned} $$
16 |
--------------------------------------------------------------------------------
/src/math/分拆数.cpp:
--------------------------------------------------------------------------------
1 | int b = sqrt(n);
2 | ans[0] = tmp[0] = 1;
3 |
4 | for (int i = 1; i <= b; ++i) {
5 | for (int rep = 0; rep < 2; ++rep)
6 | for (int j = i; j <= n - i * i; ++j)
7 | add(tmp[j], tmp[j - i]);
8 |
9 | for (int j = i * i; j <= n; ++j)
10 | add(ans[j], tmp[j - i * i]);
11 | }
12 |
13 | // -----
14 |
15 | long long a[100010];
16 | long long p[50005]; // 欧拉五边形数定理
17 |
18 | int main() {
19 | p[0] = 1;
20 | p[1] = 1;
21 | p[2] = 2;
22 | int i;
23 | for (i = 1; i < 50005; i++) { // 递推式系数1,2,5,7,12,15,22,26...i*(3*i-1)/2,i*(3*i+1)/2
24 | a[2 * i] = i * (i * 3 - 1) / 2; // 五边形数为1,5,12,22...i*(3*i-1)/2
25 | a[2 * i + 1] = i * (i * 3 + 1) / 2;
26 | }
27 | for (i = 3; i < 50005; i++) { // p[n]=p[n-1]+p[n-2]-p[n-5]-p[n-7]+p[12]+p[15]-...+p[n-i*[3i-1]/2]+p[n-i*[3i+1]/2]
28 | p[i] = 0;
29 | int j;
30 | for (j = 2; a[j] <= i; j++) { //有可能为负数, 式中加1000007
31 | if (j & 2)
32 | p[i] = (p[i] + p[i - a[j]] + 1000007) % 1000007;
33 | else
34 | p[i] = (p[i] - p[i - a[j]] + 1000007) % 1000007;
35 | }
36 | }
37 | int n;
38 | while (~scanf("%d", &n))
39 | printf("%lld\n", p[n]);
40 | }
--------------------------------------------------------------------------------
/src/math/分治FFT.cpp:
--------------------------------------------------------------------------------
1 | void solve(int l,int r) {
2 | if (l == r)
3 | return;
4 |
5 | int mid = (l + r) / 2;
6 |
7 | solve(l, mid);
8 |
9 | int N = 1;
10 | while (N <= r - l + 1)
11 | N *= 2;
12 |
13 | for (int i = l; i <= mid; i++)
14 | B[i - l] = (long long)A[i] * fac_inv[i] % p;
15 | fill(B + mid - l + 1, B + N, 0);
16 | for (int i = 0; i < N; i++)
17 | C[i] = fac_inv[i];
18 |
19 | NTT(B, N, 1);
20 | NTT(C, N, 1);
21 |
22 | for (int i = 0; i < N; i++)
23 | B[i] = (long long)B[i] * C[i] % p;
24 |
25 | NTT(B, N, -1);
26 |
27 | for (int i = mid + 1; i <= r; i++)
28 | A[i] = (A[i] + B[i - l] * 2 % p * (long long)fac[i] % p) % p;
29 |
30 | solve(mid + 1, r);
31 | }
--------------------------------------------------------------------------------
/src/math/半在线卷积.cpp:
--------------------------------------------------------------------------------
1 | void solve(int l, int r) {
2 | if (r <= m)
3 | return;
4 |
5 | if (r - l == 1) {
6 | if (l == m)
7 | f[l] = a[m];
8 | else
9 | f[l] = (long long)f[l] * inv[l - m] % p;
10 |
11 | for (int i = l, t = (long long)l * f[l] % p; i <= n; i += l)
12 | g[i] = (g[i] + t) % p;
13 |
14 | return;
15 | }
16 |
17 | int mid = (l + r) / 2;
18 |
19 | solve(l, mid);
20 |
21 | if (l == 0) {
22 | for (int i = 1; i < mid; i++) {
23 | A[i] = f[i];
24 | B[i] = (c[i] + g[i]) % p;
25 | }
26 | NTT(A, r, 1);
27 | NTT(B, r, 1);
28 | for (int i = 0; i < r; i++)
29 | A[i] = (long long)A[i] * B[i] % p;
30 | NTT(A, r, -1);
31 |
32 | for (int i = mid; i < r; i++)
33 | f[i] = (f[i] + A[i]) % p;
34 | }
35 | else {
36 | for (int i = 0; i < r - l; i++)
37 | A[i] = f[i];
38 | for (int i = l; i < mid; i++)
39 | B[i - l] = (c[i] + g[i]) % p;
40 | NTT(A, r - l, 1);
41 | NTT(B, r - l, 1);
42 | for (int i = 0; i < r - l; i++)
43 | A[i] = (long long)A[i] * B[i] %p;
44 | NTT(A, r - l, -1);
45 |
46 | for (int i = mid; i < r; i++)
47 | f[i] = (f[i] + A[i - l]) % p;
48 |
49 | memset(A, 0, sizeof(int) * (r - l));
50 | memset(B, 0, sizeof(int) * (r - l));
51 |
52 | for (int i = l; i < mid; i++)
53 | A[i - l] = f[i];
54 | for (int i = 0; i < r - l; i++)
55 | B[i] = (c[i] + g[i]) % p;
56 | NTT(A, r - l, 1);
57 | NTT(B, r - l, 1);
58 | for (int i = 0; i < r - l; i++)
59 | A[i] = (long long)A[i] * B[i] % p;
60 | NTT(A, r - l, -1);
61 |
62 | for (int i = mid; i < r; i++)
63 | f[i] = (f[i] + A[i - l]) % p;
64 | }
65 |
66 | memset(A, 0, sizeof(int) * (r - l));
67 | memset(B, 0, sizeof(int) * (r - l));
68 |
69 | solve(mid, r);
70 | }
71 |
--------------------------------------------------------------------------------
/src/math/单位根反演.tex:
--------------------------------------------------------------------------------
1 | $$ [k|n] = \frac{1}{k} \sum_{i = 0} ^ {k-1}\omega_k^{in} $$
2 |
3 | $$ \sum_{i \ge 0} [x^{ik}]f(x) = \frac{1}{k}\sum_{j=0}^{k-1}f(\omega_{k}^j) $$
4 |
5 | 如果要求 $k \equiv r \bmod n$,就等价于 $\left[k | (n - r)\right]$,类似地推导一下即可。
6 |
--------------------------------------------------------------------------------
/src/math/单纯形.cpp:
--------------------------------------------------------------------------------
1 | const double eps = 1e-10;
2 |
3 | double A[maxn][maxn], x[maxn];
4 | int n, m, t, id[maxn * 2];
5 |
6 | // 方便起见,这里附上主函数
7 | int main() {
8 | scanf("%d%d%d", &n, &m, &t);
9 |
10 | for (int i = 1; i <= n; i++) {
11 | scanf("%lf", &A[0][i]);
12 | id[i] = i;
13 | }
14 |
15 | for (int i = 1; i <= m; i++) {
16 | for (int j = 1; j <= n; j++)
17 | scanf("%lf", &A[i][j]);
18 |
19 | scanf("%lf", &A[i][0]);
20 | }
21 |
22 | if (!initalize())
23 | printf("Infeasible"); // 无解
24 | else if (!simplex())
25 | printf("Unbounded"); // 最优解无限大
26 |
27 | else {
28 | printf("%.15lf\n", -A[0][0]);
29 | if (t) {
30 | for (int i = 1; i <= m; i++)
31 | x[id[i + n]] = A[i][0];
32 | for (int i = 1; i <= n; i++)
33 | printf("%.15lf ",x[i]);
34 | }
35 | }
36 | return 0;
37 | }
38 |
39 | //初始化
40 | //对于初始解可行的问题,可以把初始化省略掉
41 | bool initalize() {
42 | while (true) {
43 | double t = 0.0;
44 | int l = 0, e = 0;
45 |
46 | for (int i = 1; i <= m; i++)
47 | if (A[i][0] + eps < t) {
48 | t = A[i][0];
49 | l = i;
50 | }
51 |
52 | if (!l)
53 | return true;
54 |
55 | for (int i = 1; i <= n; i++)
56 | if (A[l][i] < -eps && (!e || id[i] < id[e]))
57 | e = i;
58 |
59 | if (!e)
60 | return false;
61 |
62 | pivot(l, e);
63 | }
64 | }
65 |
66 | //求解
67 | bool simplex() {
68 | while (true) {
69 | int l = 0, e = 0;
70 | for (int i = 1; i <= n; i++)
71 | if (A[0][i] > eps && (!e || id[i] < id[e]))
72 | e = i;
73 |
74 | if (!e)
75 | return true;
76 |
77 | double t = 1e50;
78 | for (int i = 1; i <= m; i++)
79 | if (A[i][e] > eps && A[i][0] / A[i][e] < t) {
80 | l = i;
81 | t = A[i][0]/A[i][e];
82 | }
83 |
84 | if (!l)
85 | return false;
86 |
87 | pivot(l, e);
88 | }
89 | }
90 |
91 | //转轴操作,本质是在凸包上沿着一条棱移动
92 | void pivot(int l, int e) {
93 | swap(id[e], id[n + l]);
94 | double t = A[l][e];
95 | A[l][e] = 1.0;
96 |
97 | for (int i = 0; i <= n; i++)
98 | A[l][i] /= t;
99 |
100 | for (int i = 0; i <= m; i++)
101 | if (i != l) {
102 | t = A[i][e];
103 | A[i][e] = 0.0;
104 | for (int j = 0; j <= n; j++)
105 | A[i][j] -= t * A[l][j];
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/math/博弈论例题.tex:
--------------------------------------------------------------------------------
1 | \begin{enumerate}
2 | \item \textbf{黑白棋游戏}
3 |
4 | 一些棋子排成一列,棋子两面分别是黑色和白色。两个人轮流行动,每次可以选择一个白色朝上的棋子,把它和它左边的所有棋子都翻转,不能行动的输。
5 |
6 | \paragraph{结论} 只需要看最左边的棋子即可,因为每次操作最左边的棋子都一定会被翻转。
7 |
8 | 二维情况同理,如果每次是把左上角的棋子全部翻转,那么就只需要看左上角的那个棋子。
9 |
10 | \end{enumerate}
11 |
--------------------------------------------------------------------------------
/src/math/卡特兰数.tex:
--------------------------------------------------------------------------------
1 | \begin{enumerate}
2 |
3 | \item \textbf{卡特兰数}
4 |
5 | $$C_n = \frac 1 {n + 1}{2n\choose n} = {2n \choose n} - {2n \choose n - 1}$$
6 |
7 | \begin{itemize}
8 | \item $n$ 个元素按顺序入栈,出栈序列方案数
9 | \item $n$ 对括号的合法括号序列数
10 | \item $n + 1$ 个叶子的满二叉树个数
11 | \end{itemize}
12 |
13 | \paragraph{递推式}
14 | $$ C_n = \sum_{i = 0} ^ {n - 1} C_i C_{n - i - 1} = C_{n - 1} \frac {4n - 2} {n + 1} $$
15 |
16 | \paragraph{普通生成函数} $C(x) = \frac {1 - \sqrt {1 - 4 x}} {2 x}$
17 |
18 | \paragraph{扩展} 如果有 $n$ 个 \texttt{(} 和 $m$ 个 \texttt{)},方案数为 ${n + m \choose n} - {n + m \choose m - 1}$。
19 |
20 | \item \textbf{施罗德数}
21 |
22 | $$ S_n = S_{n-1} + \sum_{i = 0} ^ {n - 1} S_i S_{n - i - 1} $$
23 | $$ (n + 1)s_n = (6n - 3)s_{n - 1} - (n - 2) s_{n - 2} $$
24 |
25 | 其中 $S_n$ 是(大)施罗德数,$s_n$是小施罗德数(也叫超级卡特兰数)。
26 |
27 | 除了 $S_0 = s_0 = 1$ 以外,都有 $S_i = 2s_i$。
28 |
29 | 施罗德数的组合意义:
30 |
31 | \begin{itemize}
32 | \item 从 $(0, 0)$ 走到 $(n, n)$,每次可以走右、上或者右上一步,并且不能超过 $y=x$ 这条线的方案数
33 | \item 可以有空位,并且括号对数和空位置数加起来等于 $n$ 的合法括号序列数
34 | \item 凸 $n$ 边形的 \textbf{任意}\ 剖分方案数
35 | \end{itemize}
36 |
37 | (有些人会把大(而不是小)施罗德数叫做超级卡特兰数。)
38 |
39 | \item \textbf{默慈金数}
40 |
41 | $$ M_{n + 1} = M_n + \sum_{i = 0} ^ {n - 1} M_i M_{n - 1 - i} = \frac {(2n + 3)M_n + 3n M_{n - 1}} {n + 3} $$
42 | $$ M_n = \sum_{i = 0} ^ {\frac n 2} {n \choose 2i} C_i $$
43 |
44 | \begin{itemize}
45 | \item 从 $(0, 0)$ 走到 $(n, 0)$,每次可以走右上、右下或者正右方一步,且不能走到 $y<0$ 的位置的方案数
46 | \item 可以有空位,长为 $n$ 的合法括号序列数
47 | \item 在圆上的 $n$ 个 \textbf{不同的}\ 点之间画任意条不相交(\textbf{包括端点})的弦的方案数
48 | \end{itemize}
49 |
50 | \paragraph{扩展} 默慈金数画的弦不可以共享端点。如果可以共享端点的话是 A054726,参见 \detailedref{oeis}。
51 |
52 | \end{enumerate}
53 |
--------------------------------------------------------------------------------
/src/math/多项式复合.cpp:
--------------------------------------------------------------------------------
1 | // b.size() = n + 1
2 | vector bostan_mori_comp(int n, const poly& a, vector b) {
3 | if (!n) { // a 的长度一定是最初的 n + 1
4 | int n0 = (int)a.size(), m = (int)b[0].size();
5 |
6 | b[0].insert(b[0].begin(), 1);
7 | b[0].resize(get_ntt_n(n0));
8 | poly b_inv = poly_inv(b[0]);
9 | b_inv.resize(a.size());
10 |
11 | poly t = poly_auto_mul(a, b_inv);
12 | poly res(m);
13 | for (int i = 0; i < n0; i++)
14 | res[i + m - n0] = t[i];
15 |
16 | return {res};
17 | }
18 |
19 | int ntt_n = get_ntt_n(n * 2 + 1);
20 | int m = (int)b[0].size();
21 |
22 | vector q = b;
23 | dft_2d(b, ntt_n, m * 2);
24 |
25 | vector c(ntt_n);
26 | for (int i = 0; i < ntt_n; i++) // Q(-x, y) 的 DFT 直接从 Q(x, y) 的 DFT 转化过来就行了
27 | c[i] = b[(i + ntt_n / 2) & (ntt_n - 1)];
28 |
29 | b.resize(ntt_n / 2);
30 |
31 | for (int i = 0; i < ntt_n / 2; i++)
32 | for (int j = 0; j < m * 2; j++)
33 | b[i][j] = (ll)b[i][j] * c[i][j] % p;
34 |
35 | idft_2d(b, n / 2 + 1, [] (int i) { return true; });
36 |
37 | for (int i = 0; i <= n / 2; i++) {
38 | for (int j = m * 2 - 1; j; j--)
39 | b[i][j] = b[i][j - 1];
40 | b[i][0] = 0;
41 |
42 | for (int j = 0; j < m; j++)
43 | b[i][j] = (b[i][j] + q[i * 2][j] * 2 % p) % p;
44 | }
45 |
46 | b.resize(n / 2 + 1);
47 |
48 | vector res = bostan_mori_comp(n / 2, a, move(b));
49 |
50 | vector t = res;
51 | dft_2d(t, ntt_n / 2, m * 2);
52 |
53 | for (int i = 0; i < ntt_n; i++) // 因为 V(x^2, y) 只有 x^2k 项,所以只需做一半
54 | for (int j = 0; j < m * 2; j++) // DFT 的后一半一定和前一半一样
55 | c[i][j] = (ll)t[i & (ntt_n / 2 - 1)][j] * c[i][j] % p;
56 |
57 | idft_2d(c, n + 1, [](int i) { return true; });
58 |
59 | t = vector(n + 1);
60 | for (int i = 0; i <= n; i++) {
61 | t[i].resize(m);
62 |
63 | for (int j = 0; j < m; j++)
64 | t[i][j] = c[i][j + m - 1];
65 |
66 | if (i % 2 == 0)
67 | for (int j = 0; j < m; j++)
68 | (t[i][j] += res[i / 2][j + m]) %= p;
69 | }
70 |
71 | return t;
72 | }
73 |
74 | // 求 F(G(x)), 长度要相同
75 | poly poly_composition(const poly& f, const poly& g) {
76 | int n = (int)f.size() - 1;
77 |
78 | poly a(n + 1);
79 | for (int i = 0; i <= n; i++)
80 | a[i] = f[n - i];
81 |
82 | vector b(n + 1);
83 | for (int i = 0; i <= n; i++)
84 | b[i] = {(p - g[i]) % p};
85 |
86 | vector t = bostan_mori_comp(n, a, b);
87 |
88 | poly res(n + 1);
89 | for (int i = 0; i <= n; i++)
90 | res[i] = t[i][0];
91 | return res;
92 | }
93 |
--------------------------------------------------------------------------------
/src/math/多项式复合.tex:
--------------------------------------------------------------------------------
1 | \paragraph{问题} 给出两个最高次数均为 $x^n$ 的多项式 $F(x)$ 和 $G(x)$,求 $F(G(x))$ 的 $0 \dots n$ 项系数。
2 |
3 | 首先要注意形式幂级数的复合只有在 $G(x)$ 没有常数项时才是良定义的,不然 $F(G(0))$ 不一定收敛。不过如果只是多项式复合就无所谓了。
4 |
5 | \paragraph{做法} 设 $H(x) = F(G(x))$,也就是
6 |
7 | $$ H(x) = \sum_{i = 0} ^ n f_i G(x) ^ i $$
8 |
9 | 点积是不好做的,不过可以把 $F(x)$ 的系数反过来变成卷积。设 $r_i = f_{n - i}$,那么
10 |
11 | $$ \begin{aligned}
12 | H(x) \equiv & \sum_{i = 0} ^ n r_{n - i} G(x) ^ i \\
13 | \equiv & \sum_{i = 0} ^ n r_{n - i} \left[ y ^ i \right] \frac 1 {1 - y G(x)} \\
14 | \equiv & \left[ y ^ n \right] \frac {R(y)} {1 - y G(x)} \pmod {x ^ {n + 1}}
15 | \end{aligned} $$
16 |
17 | 发现这个形式和多项式复合逆的子问题很像,不过这里要求的是 $y^n$ 项的系数。仍然按照 Bostan-Mori 算法的思路,上下同乘 $Q(-x, y)$:
18 |
19 | $$ \begin{aligned}
20 | \left[ y ^ n \right] \frac {R(y)} {Q(x, y)} \equiv & \left[ y ^ n \right] \frac {R(y)} {Q(x, y) Q(-x, y)} Q(-x, y) \\
21 | \equiv & \left[ y ^ n \right] \frac {R(y)} {V(x ^ 2, y)} Q(-x, y) \pmod {x ^ {n + 1}}
22 | \end{aligned} $$
23 |
24 | 那么 $\frac {R(y)} {V(x ^ 2, y)} \pmod {x ^ {n + 1}}$ 就变成了一个 $x$ 的最高次数减半的子问题,这时后面的 $\pmod {x ^ {n + 1}}$ 就用得上了。因为乘了一次所以 $y$ 的最高次数会翻倍,一直递归到底才会达到 $y^n$。注意递归的时候 $R(y)$ 是完全不变的,因此为了保证复杂度需要先递归地做下去,返回的时候再乘 $Q(-x, y)$。边界是 $\frac {R(y)} {Q(x, y)} \equiv R(y) Q(0, y) ^ {-1} \pmod {x ^ 1}$。
25 |
26 | 返回的时候 $x$ 的最高次数要翻一倍,不需要处理,最多把超过当前层 $n$ 的部分截掉就行了。而因为我们最终只需要 $y^n$ 项,假设当前 $Q(-x, y)$ 的 $y$ 次数是 $2^k$,那么当前层返回的结果的 $y^i$ 项最多会影响到最终的第 $i + 1 + 2 + 4 + \dots + 2^{k - 1} = i + 2^k - 1$ 项,因此每层只需要返回最高的 $2^k$ 项。每层的总项数都是 $O(n)$ 的,总复杂度是 $O(n \log^2 n)$。
27 |
28 | 类似多项式复合逆,这里也有一个优化:在递归的时候 $y$ 的最高次数是 $2^k$,项数就是 $2^k + 1$,做卷积的时候会多做一倍。但可以注意到 $Q(x, y)$ 的 $y_0$ 项始终是 $1$,所以可以把它提出来,这样项数就刚好是 $2^k$ 了,每步按照 $(Ay + 1) (By + 1) = (ABy + A + B) y + 1$ 计算即可。
29 |
30 | 这样优化的话,返回时会变成取一个 $2^{k + 1}$ 项多项式与一个 $2^k$ 项多项式的乘积的较高 $2^k$ 项。实际上按照循环卷积的原理,直接做长为 $2^{k + 1}$ 的 FFT 就行了,这时较高的 $2^k$ 项在循环卷积下是不受影响的。
31 |
32 | 另外因为返回时还涉及到 $\frac {R(y)} {V(x ^ 2, y)}$ 的 FFT,因为 FFT 本质就是单位根处的点值,所以可以发现 $x$ 这一维的 DFT 实际上就是 $\frac {R(y)} {V(x, y)}$ 的 DFT 再重复一次。这样返回时可以少做一半 FFT,减少一些常数。
33 |
34 | 代码里的 \mintinline{cpp}|dft_2d| 和 \mintinline{cpp}|idft_2d| 和多项式复合逆里的是一样的,去抄一下就行了。
35 |
36 | \inputminted{cpp}{../src/math/多项式复合.cpp}
37 |
--------------------------------------------------------------------------------
/src/math/多项式复合逆.tex:
--------------------------------------------------------------------------------
1 | \paragraph{拉格朗日反演} 如果 $f(x)$ 与 $g(x)$ 互为复合逆,则有
2 |
3 | $$ \begin{aligned}\relax \left[x^n\right]f(x)=&\frac{1}{n}\left[x^{n-1}\right]\left(\frac{x}{g(x)}\right)^n \\
4 | \relax \left[x^n\right]h(f(x))=&\frac{1}{n}\left[x^{n-1}\right]h'(x)\left(\frac{x}{g(x)}\right)^n\end{aligned} $$
5 |
6 | 这样可以得到复合逆的一项。如果需要 $0 \dots n$ 项的所有系数,就麻烦一些。
7 |
8 | 推导过程省略,直接上结论:
9 |
10 | $$ \begin{aligned}
11 | f(x) \equiv & x \left( \sum_{k = 1} ^ n x^{n - k} \frac n k \left[ x^n \right] g^k(x) \right) ^ {- 1 / n} \pmod {x^{n + 1}} \\
12 | \equiv & \frac x {g_1} \left( \sum_{k = 1} ^ n \frac n k \left( \frac x {g_1} \right) ^ {n - k} \left[ z^n \right] \left( \frac {g(z)} {g_1} \right) ^k \right) ^ {- 1 / n}
13 | \end{aligned} $$
14 |
15 | $g_1$ 是 $g(x)$ 的一次项。把它提出来是为了把要开根的式子常数项化成 $1$,避免考虑 $n \bmod \varphi(p)$ 的逆元。
16 |
17 | 现在唯一的难点就在于求 $[x^n] \left( \frac {g(x)} {g_1} \right) ^ k$。
18 |
19 | 考虑更一般的问题,即 \textbf{对所有 $k \in [1, n]$,如何分别求出 $[x^n] A^k(x)$}。
20 |
21 | 引入另一个自变量 $y$,代表 $A(x)$ 的次数这一维,得到一个二元生成函数:
22 |
23 | $$ \sum_{i} x^i \sum_{j} y^j [x^i] A^j(x) = \sum_{j} y^j A^j(x) = \frac 1 {1 - y A(x)} $$
24 |
25 | $x^n$ 项的系数即为所求。
26 |
27 | 针对这个子问题,再考虑更一般的问题,即求 $[x^n] \frac {P(x, y)} {Q(x, y)}$。
28 |
29 | 按照 Bostan-Mori (\ref{BostanMori},第 \pageref{BostanMori} 页) 一样的思路,上下同乘 $Q(-x, y)$,又会发现分母 $Q(x, y) Q(-x, y)$ 里 $x$ 只有偶数项,只取出奇数项或者偶数项就可以把 $n$ 折半,然后递归下去即可。边界是 $\frac {P(0, y)} {Q(0, y)}$。
30 |
31 | 在这里初始时 $x$ 最高项是 $n$ 次,而 $y$ 只有一次。每步 $x$ 的最大次数都会减半,而 $y$ 的最大次数会翻倍,所以总的项数一直是 $O(n)$ 的。总的复杂度是 $O(n \log^2 n)$。
32 |
33 | 这里有一个优化:做完 $k$ 次迭代之后 $y$ 的最高次数是 $2^k$,也就是说有 $2^k + 1$ 项,在做卷积的时候就会白白多做一倍的长度。实际上可以注意到,$P(x, 0)$ 和 $Q(x, 0)$ 的 $y^0$ 项只能是 $0$ 或者 $1$,不会包含 $x$。所以可以把它提出来,这样 $y$ 这一维的长度就刚好是 $2^k$ 了。
34 |
35 | $Q(x, 0)$ 因为每次都保留偶数项,所以常数项一直都是 $1$。$P(x, 0)$ 初始常数项也是 $1$,但在保留一次奇数项之后就一直是 $0$ 了。
36 |
37 | 设 $P(x, 0)$ 的常数项是 $k$,按照 $(Py + k) (Qy + 1) = (PQy + P + kQ)y + k$ 计算即可。
38 |
39 | 解决上面的所有子问题之后别忘了还要有一个多项式开 $n$ 次根,总代码量还是很大的。
40 |
41 | \inputminted{cpp}{../src/math/多项式复合逆.cpp}
42 |
--------------------------------------------------------------------------------
/src/math/多项式快速插值.cpp:
--------------------------------------------------------------------------------
1 | int qx[maxn], qy[maxn];
2 | int th[25][maxn * 2], ansf[maxn]; // th存的是各阶段的M(x)
3 |
4 | void pretreat2(int l, int r, int k) { // 预处理
5 | static int A[maxn], B[maxn];
6 | int *h = th[k] + l * 2;
7 |
8 | if (l == r) {
9 | h[0] = p - qx[l];
10 | h[1] = 1;
11 | return;
12 | }
13 |
14 | int mid = (l + r) / 2;
15 |
16 | pretreat2(l, mid, k + 1);
17 | pretreat2(mid + 1, r, k + 1);
18 |
19 | int N = 1;
20 | while (N <= r - l + 1)
21 | N *= 2;
22 |
23 | int *hl = th[k + 1] + l * 2, *hr = th[k + 1] + (mid + 1) * 2;
24 |
25 | memset(A, 0, sizeof(int) * N);
26 | memset(B, 0, sizeof(int) * N);
27 |
28 | memcpy(A, hl, sizeof(int) * (mid - l + 2));
29 | memcpy(B, hr, sizeof(int) * (r - mid + 1));
30 |
31 | NTT(A, N, 1);
32 | NTT(B, N, 1);
33 |
34 | for (int i = 0; i < N; i++)
35 | A[i] = (long long)A[i] * B[i] % p;
36 |
37 | NTT(A, N, -1);
38 |
39 | for (int i = 0; i <= r - l + 1; i++)
40 | h[i] = A[i];
41 | }
42 |
43 | void solve2(int l, int r, int k) { // 分治
44 | static int A[maxn], B[maxn], t[maxn];
45 |
46 | if (l == r)
47 | return;
48 |
49 | int mid = (l + r) / 2;
50 |
51 | solve2(l, mid, k + 1);
52 | solve2(mid + 1, r, k + 1);
53 |
54 | int *hl = th[k + 1] + l * 2, *hr = th[k + 1] + (mid + 1) * 2;
55 |
56 | int N = 1;
57 |
58 | while (N < r - l + 1)
59 | N *= 2;
60 |
61 | memset(A, 0, sizeof(int) * N);
62 | memset(B, 0, sizeof(int) * N);
63 |
64 | memcpy(A, ansf + l, sizeof(int) * (mid - l + 1));
65 | memcpy(B, hr, sizeof(int) * (r - mid + 1));
66 |
67 | NTT(A, N, 1);
68 | NTT(B, N, 1);
69 |
70 | for (int i = 0; i < N; i++)
71 | t[i] = (long long)A[i] * B[i] % p;
72 |
73 | memset(A, 0, sizeof(int) * N);
74 | memset(B, 0, sizeof(int) * N);
75 |
76 | memcpy(A, ansf + mid + 1, sizeof(int) * (r - mid));
77 | memcpy(B, hl, sizeof(int) * (mid - l + 2));
78 |
79 | NTT(A, N, 1);
80 | NTT(B, N, 1);
81 |
82 | for (int i = 0; i < N; i++)
83 | t[i] = (t[i] + (long long)A[i] * B[i]) % p;
84 |
85 | NTT(t, N, -1);
86 |
87 | memcpy(ansf + l, t, sizeof(int) * (r - l + 1));
88 | }
89 |
90 | // 主过程
91 | // 如果x, y传nullptr表示询问已经存在了qx, qy里
92 | void interpolation(int *x, int *y, int n, int *f = nullptr) {
93 | static int d[maxn];
94 |
95 | if (x)
96 | memcpy(qx, x, sizeof(int) * n);
97 | if (y)
98 | memcpy(qy, y, sizeof(int) * n);
99 |
100 | pretreat2(0, n - 1, 0);
101 |
102 | get_derivative(th[0], d, n + 1);
103 |
104 | multipoint_eval(d, qx, nullptr, n, n);
105 |
106 | for (int i = 0; i < n; i++)
107 | ansf[i] = (long long)qy[i] * qpow(ans[i], p - 2) % p;
108 |
109 | solve2(0, n - 1, 0);
110 |
111 | if (f)
112 | memcpy(f, ansf, sizeof(int) * n);
113 | }
--------------------------------------------------------------------------------
/src/math/多项式快速插值.tex:
--------------------------------------------------------------------------------
1 | \paragraph{问题} 给出 $n$ 个 $x_i$ 与 $y_i$, 求一个 $(n - 1)$ 次多项式 $F(x)$,满足 $F(x_i) = y_i$。
2 |
3 | 考虑拉格朗日插值:
4 |
5 | $$ F(x) = \sum_{i = 1} ^ n \frac {\prod_{i \neq j} (x - x_j)} {\prod_{i \neq j} (x_i - x_j)} y_i $$
6 |
7 | 第一步要先对每个 $i$ 求出
8 |
9 | $$ \prod_{i \neq j} (x_i - x_j) $$
10 |
11 | 设
12 |
13 | $$ M(x) = \prod_{i = 1} ^ n (x - x_i) $$
14 |
15 | 那么想要的是
16 |
17 | $$ \frac {M(x)} {x - x_i} $$
18 |
19 | 取 $x=x_i$ 时,上下都为 0,使用洛必达法则,则原式化为 $M'(x)$。
20 |
21 | 使用分治算出 $M(x)$,然后使用多点求值即可算出每个
22 |
23 | $$ \prod_{i \neq j} (x_i - x_j) = M'(x_i) $$
24 |
25 | 记
26 |
27 | $$ v_i = \frac {y_i} {\prod_{i \neq j} (x_i - x_j)} $$
28 |
29 | 那么第二步要求出
30 |
31 | $$ \sum_{i = 1} ^ n v_i \prod_{i \neq j} (x - x_j) $$
32 |
33 | 使用分治,设
34 |
35 | $$ L(x) = \prod_{i = 1} ^ {\lfloor n / 2 \rfloor} (x - x_i), \; R(x) = \prod_{i = \lfloor n / 2 \rfloor + 1} ^ n (x - x_i)$$
36 |
37 | 则原式化为
38 |
39 | $$ \left( \sum_{i = 1} ^ {\lfloor n / 2 \rfloor} v_i \prod_{i \neq j ,\, j \leq \lfloor n / 2 \rfloor} (x - x_j) \right) R(x) + $$
40 |
41 | $$ \left( \sum_{i = \lfloor n / 2 \rfloor + 1} ^ n v_i \prod_{i \neq j ,\, j > \lfloor n / 2 \rfloor} (x - x_j) \right) L(x) $$
42 |
43 | 递归计算,复杂度 $O(n\log^2n)$。
44 |
45 | 注意由于整体和局部的 $M(x)$ 都要用到,要预处理一下。
46 |
47 | \inputminted{cpp}{../src/math/多项式快速插值.cpp}
48 |
--------------------------------------------------------------------------------
/src/math/常用NTT素数及原根.md:
--------------------------------------------------------------------------------
1 | (只整理了可能有用的,太大或者太小的没加进去)
2 |
3 | (比较重要的模数和不寻常的原根用粗体标注)
4 |
5 | | $p=r\times 2^k+1$ | $r$ | $k$ | 最小原根 |
6 | | :---------------: | :--: | :--: | :------: |
7 | | 104857601 | 25 | 22 | 3 |
8 | | 167772161 | 5 | 25 | 3 |
9 | | 469762049 | 7 | 26 | 3 |
10 | | 985661441 | 235 | 22 | 3 |
11 | | **998244353** | 119 | 23 | 3 |
12 | | **1004535809** | 479 | 21 | 3 |
13 | | *1005060097* | 1917 | 19 | **5** |
14 | | **2013265921** | 15 | 27 | **31** |
15 | | 2281701377 | 17 | 27 | 3 |
16 | | 31525197391593473 | 7 | 52 | 3 |
17 | | 180143985094819841 | 5 | 55 | **6** |
18 | | 1945555039024054273 | 27 | 56 | **5** |
19 | | 4179340454199820289 | 29 | 57 | 3 |
20 |
21 | 注:
22 |
23 | 1005060097有点危险,在变换长度大于524288时不可使用。
24 |
25 |
--------------------------------------------------------------------------------
/src/math/常见数列打表.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/src/math/常见数列打表.md
--------------------------------------------------------------------------------
/src/math/常见生成函数.tex:
--------------------------------------------------------------------------------
1 | $$ \frac x {(1 - x) ^ 2} = \sum_{i \ge 0} i x ^ i $$
2 |
3 | $$ \frac 1 {(1 - x) ^ k} = \sum_{i \ge 0} {i + k - 1 \choose i} x ^ i = \sum_{i \ge 0} {i + k - 1 \choose k - 1}x^i, \; k > 0 $$
4 |
5 | $$ \begin{aligned}
6 | \sum_{i = 0} ^ \infty i^n x^i = \sum_{k = 0} ^ n {n \brace k} k! \frac {x^k} {(1-x) ^ {k + 1}} = \sum_{k = 0} ^ n {n \brace k} k! \frac {x^k (1-x) ^ {n – k}} {(1-x) ^ {n + 1}} \\
7 | = \frac 1 {(1-x) ^ {n + 1}} \sum_{i = 0} ^ n \frac {x^i} {(n-i)!} \sum_{k = 0} ^ i {n \brace k}k!(n-k)! \frac {(-1)^{i-k}} {(i-k)!}
8 | \end{aligned} $$
9 |
10 | (用上面的方法可以把分子化成一个 $n$ 次以内的多项式,并且可以用一次卷积求出来。)
11 |
12 | 如果把 $i^n$ 换成任意的一个 $n$ 次多项式,那么我们可以求出它的下降幂表示形式(或者说是牛顿插值)的系数 $r_i$,发现用 $r_k$ 替换掉上面的 ${n \brace k}k!$ 之后其余过程完全相同。
13 |
--------------------------------------------------------------------------------
/src/math/康托展开.tex:
--------------------------------------------------------------------------------
1 | \paragraph{求排列的排名} 先对每个数都求出它后面有几个数比它小(可以用树状数组),记为 $c_i$,则排列的排名就是
2 |
3 | $$ \sum_{i = 1} ^ n c_i (n - i)! $$
4 |
5 | \paragraph{已知排名构造排列} 从前到后先分别求出 $c_i$,有了 $c_i$ 之后再用一个平衡树(需要维护排名)倒序处理即可。
6 |
--------------------------------------------------------------------------------
/src/math/快速线性递推-BostanMori.cpp:
--------------------------------------------------------------------------------
1 | // $a_n = \sum_{i = 1} ^ m f_i a_{n - i}\quad (f_0 = 0)$
2 | // f.size() = a.size() + 1
3 | int linear_recurrance(int n, poly f, poly a) {
4 | int m = (int)a.size();
5 |
6 | int ntt_n = 1;
7 | while (ntt_n <= m)
8 | ntt_n *= 2;
9 |
10 | ntt_init(ntt_n * 2);
11 |
12 | f.resize(ntt_n);
13 | a.resize(ntt_n);
14 |
15 | for (int i = 1; i <= m; i++)
16 | f[i] = (p - f[i]) % p;
17 | f[0] = 1;
18 |
19 | a = poly_mul(a, f);
20 | a.resize(ntt_n);
21 | fill(a.data() + m, a.data() + ntt_n, 0);
22 |
23 | return bostan_mori(n, a, f);
24 | }
25 |
--------------------------------------------------------------------------------
/src/math/快速线性递推-多项式除法.cpp:
--------------------------------------------------------------------------------
1 | poly poly_power_mod(ll k, const poly& m) { // x^k mod m
2 | poly ans{1}, a{0, 1};
3 |
4 | while (k) {
5 | if (k & 1)
6 | ans = poly_mod(poly_auto_mul(ans, a), m).first;
7 | a = poly_mod(poly_auto_mul(a, a), m).first;
8 | k /= 2;
9 | }
10 |
11 | return ans;
12 | }
13 |
14 | // $a_n = \sum_{i = 1} ^ m c_i a_{n - i}\quad (c_0 = 0)$
15 | struct linear_recurrence {
16 | poly f; // f是预处理结果
17 |
18 | linear_recurrence(const poly& c, ll n) {
19 | assert(c[0] == 0); // c[0] 是没有用的
20 | int m = (int)c.size() - 1;
21 |
22 | int ntt_n = 1;
23 | while (ntt_n < m * 2)
24 | ntt_n *= 2;
25 | ntt_init(ntt_n); // 图省事就直接 ntt_init(1 << 18)
26 |
27 | poly t(m + 1);
28 | t[m] = 1;
29 |
30 | for (int i = 0; i < m; i++)
31 | t[i] = (p - c[m - i]) % p;
32 | f = poly_power_mod(n, t);
33 | }
34 |
35 | int operator()(const vector& a) { // 0~m-1项初始值
36 | assert(a.size() == f.size()); int ans = 0;
37 | for (int i = 0; i < (int)a.size(); i++)
38 | ans = (ans + (ll)f[i] * a[i]) % p;
39 | return ans;
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/src/math/快速线性递推.tex:
--------------------------------------------------------------------------------
1 | \paragraph{多项式除法} 需要的代码参见 \ref{PolyOperation}.\nameref{PolyOperation} (第 \pageref{PolyOperation} 页)。
2 |
3 | \inputminted{cpp}{../src/math/快速线性递推-多项式除法.cpp}
4 |
5 | \paragraph{Bostan-Mori} 具体参见 \ref{BostanMori}.Bostan-Mori (第 \pageref{BostanMori} 页)。
6 |
7 | 这个做法只需要抄多项式乘法,并且常数更小。
8 |
9 | \inputminted{cpp}{../src/math/快速线性递推-BostanMori.cpp}
10 |
--------------------------------------------------------------------------------
/src/math/快速阶乘算法.tex:
--------------------------------------------------------------------------------
1 | \paragraph{问题} 求 $n! \pmod p$,$n < p$,$p$ 是 NTT 模数。
2 |
3 | 考虑令 $m = \left\lfloor \sqrt n \right\rfloor$,那么我们可以写出连续 $m$ 个数相乘的多项式:
4 |
5 | $$ f(x) = \prod_{i = 1} ^ m (x + i) $$
6 |
7 | 那么显然就有
8 |
9 | $$ n! = \left( \prod_{k = 0} ^ {m - 1} f(k m) \right) \prod_{i = m ^ 2 + 1} ^ n i $$
10 |
11 | $f(x)$ 的系数可以用倍增求(或者懒一点直接分治 FFT),然后 $f(km)$ 可以用多点求值求出,所以总复杂度就是$O(\sqrt n \log^2 n)$。
12 |
13 | 当然如果 $p$ 不变并且多次询问的话,我们只需要取一个 $m$,也就是预处理 $O(\sqrt p \log^2 p)$,询问 $O(\sqrt p)$。
14 |
--------------------------------------------------------------------------------
/src/math/拆系数FFT.cpp:
--------------------------------------------------------------------------------
1 | constexpr int maxn = 262200, p = 23333333, M = 4830; // M取值要使得结果不超过10^14
2 |
3 | // 需要开的数组
4 | struct Complex {
5 | // 内容略
6 | } w[maxn], w_inv[maxn], A[maxn], B[maxn], C[maxn], D[maxn], F[maxn], G[maxn], H[maxn];
7 |
8 | // 主函数(当然更多时候包装一下比较好)
9 | // 需要调用FFT初始化, FFT
10 | int main() {
11 | scanf("%d", &n);
12 |
13 | int N = 1;
14 | while (N < n * 2)
15 | N *= 2;
16 |
17 | for (int i = 0, x; i < n; i++) {
18 | scanf("%d", &x);
19 | A[i] = x / M;
20 | B[i] = x % M;
21 | }
22 |
23 | for (int i = 0, x; i < n; i++) {
24 | scanf("%d", &x);
25 | C[i] = x / M;
26 | D[i] = x % M;
27 | }
28 |
29 | FFT_init(N);
30 |
31 | FFT(A, N, 1);
32 | FFT(B, N, 1);
33 | FFT(C, N, 1);
34 | FFT(D, N, 1);
35 |
36 | for (int i = 0; i < N; i++) {
37 | F[i] = A[i] * C[i];
38 | G[i] = A[i] * D[i] + B[i] * C[i];
39 | H[i] = B[i] * D[i];
40 | }
41 |
42 | FFT(F, N, -1);
43 | FFT(G, N, -1);
44 | FFT(H, N, -1);
45 |
46 | for (int i = 0; i < n; i++)
47 | printf("%d ", (int)((M * M * ((long long)(F[i].a + 0.5) % p) % p + M * ((long long)(G[i].a + 0.5) % p) % p + (long long)(H[i].a + 0.5) % p) % p));
48 |
49 | return 0;
50 | }
51 |
--------------------------------------------------------------------------------
/src/math/拉格朗日插值.tex:
--------------------------------------------------------------------------------
1 | $$ f(x) = \sum_i f(x_i) \prod_{j \neq i} \frac {x - x_j} {x_i - x_j} $$
2 |
3 | 快速插值参见 \detailedref{PolyFastInterpolation}。
4 |
--------------------------------------------------------------------------------
/src/math/斐波那契数.tex:
--------------------------------------------------------------------------------
1 | \paragraph{斐波那契数} $F_0 = 0, \, F_1 = 1, \, F_n = F_{n - 1} + F_{n - 2}$
2 |
3 | $0, \, 1, \, 1, \, 2, \, 3, \, 5, \, 8, \, 13, \, 21, \, 34, \, 55, \, 89, \, \dots$
4 |
5 | \paragraph{卢卡斯数} $L_0 = 2, \, L_1 = 1$
6 |
7 | $2, \, 1, \, 3, \, 4, \, 7, \, 11, \, 18, \, 29, \, 47, \, 76, \, 123, \, 199, \, \dots$
8 |
9 | \paragraph{通项公式}
10 |
11 | $\phi = \frac {1 + \sqrt 5} 2, \; \hat\phi = \frac {1 - \sqrt 5} 2$
12 |
13 | $F_n = \frac {\phi^n - {\hat\phi} ^ n} {\sqrt 5}, \; L_n = \phi ^ n + {\hat\phi} ^ n$
14 |
15 | 实际上有
16 | $$\frac {L_n + F_n \sqrt 5} 2 = \left( \frac {1 + \sqrt 5} 2 \right) ^ n$$
17 | 所以求通项的话写一个类然后快速幂就可以同时得到两者。
18 |
19 | \paragraph{快速倍增法}
20 | $F_{2k} = F_k \left( 2 F_{k + 1} - F_k \right), \; F_{2k + 1} = F_{k + 1} ^ 2 + F_k ^ 2$
21 |
22 | \begin{minted} {cpp}
23 | pair fib(int n) { // 返回F(n)和F(n + 1)
24 | if (n == 0)
25 | return {0, 1};
26 | auto p = fib(n >> 1);
27 | int c = p.first * (2 * p.second - p.first);
28 | int d = p.first * p.first + p.second * p.second;
29 | if (n & 1)
30 | return {d, c + d};
31 | else
32 | return {c, d};
33 | }
34 | \end{minted}
35 |
--------------------------------------------------------------------------------
/src/math/斯特林数.tex:
--------------------------------------------------------------------------------
1 | \begin{enumerate}
2 |
3 | \item \textbf{第一类斯特林数}
4 |
5 | $n\brack k$ 表示 $n$ 个元素划分成 $k$ 个 \textbf{轮换}\ 的方案数。
6 |
7 | \paragraph{递推式} ${n \brack k} = {n-1 \brack k-1} + (n-1){n-1 \brack k}$
8 |
9 | \paragraph{求同一行} 分治 FFT $O(n\log ^2 n)$,或者倍增 $O(n\log n)$(每次都是 $f(x) = g(x) g(x + d)$ 的形式)。
10 |
11 | $$ \begin{aligned} \sum_{k = 0} ^ n {n \brack k} x^k = \prod_{i = 0} ^ {n - 1} (x + i) \end{aligned} $$
12 |
13 | \paragraph{求同一列} 用一个轮换的 EGF 做 $k$ 次幂。
14 |
15 | $$ \sum_{n = 0} ^ \infty {n \brack k} \frac {x ^ n} {n!} = \frac {\left(\ln (1 - x)\right) ^ k} {k!} = \frac {x ^ k} {k!} \left( \frac {\ln (1 - x)} x \right) ^ k $$
16 |
17 | \item \textbf{第二类斯特林数}
18 |
19 | $n\brace k$ 表示 $n$ 个元素划分成 $k$ 个子集的方案数。
20 |
21 | \paragraph{递推式} ${n \brace k} = {n-1 \brace k-1} + k{n-1 \brace k}$
22 |
23 | \paragraph{求某一项} 容斥,狗都会做。
24 |
25 | $$ {n \brace k} = \frac 1 {k!} \sum_{i = 0} ^ k (-1) ^ i {k \choose i} (k - i) ^ n = \sum_{i = 0} ^ k \frac {(-1) ^ i} {i!} \frac {(k - i) ^ n} {(k - i)!} $$
26 |
27 | \paragraph{求同一行} FFT,狗都会做。
28 |
29 | \paragraph{求同一列} EGF:
30 |
31 | $$ \sum_{n = 0} ^ \infty {n \brace k} \frac {x ^ n} {n!} = \frac {\left(e ^ x - 1\right) ^ k} {k!} = \frac {x ^ k} {k!} \left( \frac {e ^ x - 1} x \right) ^ k $$
32 |
33 | OGF:
34 |
35 | $$ \sum_{n = 0} ^ \infty {n \brace k} x ^ n = x ^ k \left(\prod_{i = 1} ^ k (1 - i x)\right) ^ {-1} $$
36 |
37 | \item \textbf{斯特林反演}
38 |
39 | $$ f(n) = \sum_{k = 0} ^ n {n \brace k} g(k) \iff g(n) = \sum_{k = 0} ^ n (-1) ^ {n - k} {n \brack k} f(k) $$
40 |
41 | \item \textbf{幂的转换}
42 |
43 | \paragraph{上升幂与普通幂的转换}
44 |
45 | $$ x^{\overline{n}}=\sum_{k} {n \brack k} x^k $$
46 |
47 | $$ x^n=\sum_{k} {n \brace k} (-1)^{n-k} x^{\overline{k}} $$
48 |
49 | \paragraph{下降幂与普通幂的转换}
50 |
51 | $$ x^n=\sum_{k} {n \brace k} x^{\underline{k}} = \sum_{k} {x \choose k} {n \brace k} k! $$
52 |
53 | $$ x^{\underline{n}}=\sum_{k} {n \brack k} (-1)^{n-k} x^k $$
54 |
55 | 另外,多项式的 \textbf{点值}\ 表示的每项除以阶乘,卷上 $e^{-x}$ 再乘上阶乘之后是 \textbf{牛顿插值}\ 表示,或者不乘阶乘就是 \textbf{下降幂}\ 系数表示。反过来的转换当然卷上 $e^x$ 就行了。原理是每次差分等价于乘以 $(1 - x)$,展开之后用一次卷积取代多次差分。(参见 \detailedref{NewtonInterpolation}。)
56 |
57 | \item \textbf{斯特林多项式(斯特林数关于斜线的性质)}
58 |
59 | \paragraph{定义}
60 |
61 | $$ \sigma_n(x) = \frac {{x\brack n}} {x(x-1)\dots(x-n)} $$
62 |
63 | $\sigma_n(x)$ 的最高次数是 $x^{n - 1}$。(所以作为唯一的特例, $\sigma_0(x) = \frac 1 x$ 不是多项式。)
64 |
65 | 斯特林多项式实际上非常神奇,它与两类斯特林数都有关系:
66 |
67 | $$ {n \brack n-k} = n^{\underline{k+1}} \sigma_k(n) $$
68 |
69 | $$ {n \brace n-k} = (-1)^{k+1} n^{\underline{k+1}} \sigma_k(-(n-k)) $$
70 |
71 | 不过它并不好求。可以 $O(k^2)$ 直接计算前几个点值然后插值,或者如果要推式子的话,可以用后面提到的二阶欧拉数(\detailedref{EulerianNumber})。
72 |
73 | \end{enumerate}
74 |
--------------------------------------------------------------------------------
/src/math/方差.tex:
--------------------------------------------------------------------------------
1 | $m$ 个数的方差:
2 |
3 | $$ s^2 = \frac{\sum_{i=1}^m x_i^2}m - \overline x^2$$
4 |
5 | 随机变量的方差:$D^2(x)=E(x^2)-E^2(x)$
6 |
--------------------------------------------------------------------------------
/src/math/欧拉数.tex:
--------------------------------------------------------------------------------
1 | \def \bangle{ \atopwithdelims \langle \rangle}
2 |
3 | \begin{enumerate}
4 |
5 | \item \textbf{欧拉数}
6 |
7 | ${n\bangle k}$ 表示 $n$ 个数的排列,有 $k$ 个上升的方案数。
8 |
9 | $$ {n\bangle k} = (n - k){n - 1 \bangle k - 1} + (k + 1){n - 1 \bangle k} $$
10 |
11 | $$ {n\bangle k} = \sum_{i = 0} ^ {k + 1} (-1)^i {n + 1\choose i} (k + 1 - i)^n $$
12 |
13 | $$ \sum_{k = 0} ^ {n - 1} {n\bangle k} = n! $$
14 |
15 | $$ x^n = \sum_{k = 0} ^ {n - 1} {n\bangle k} {x+k \choose n} $$
16 |
17 | $$ k!{n\brace k} = \sum_{i = 0} ^ {n - 1} {n\bangle i} {i\choose n - k} $$
18 |
19 | \item \textbf{二阶欧拉数}
20 |
21 | $\left\langle\!\!{n\bangle k}\!\!\right\rangle$ 表示每个数都出现 \textbf{两次}\ 的多重排列,并且每个数两次出现之间的数都比它要大。在此前提下有 $k$ 个上升的方案数。
22 |
23 | $$ \left\langle\!\!{n\bangle k}\!\!\right\rangle = (2n-k-1)\left\langle\!\!{n-1\bangle k-1}\!\!\right\rangle + (k+1)\left\langle\!\!{n-1 \bangle k}\!\!\right\rangle $$
24 |
25 | $$ \sum_{k = 0} ^ {n - 1} \left\langle\!\!{n\bangle k}\!\!\right\rangle = (2n-1)!! = \frac{(2n)^{\underline n}} {2^n} $$
26 |
27 | \item \textbf{二阶欧拉数与斯特林数的关系}
28 |
29 | $$ {x \brace x-n} = \sum_{k = 0} ^ {n - 1} \left\langle\!\!{n\bangle k}\!\!\right\rangle {x + n - k - 1 \choose 2n} $$
30 |
31 | $$ {x \brack x-n} = \sum_{k = 0} ^ {n - 1} \left\langle\!\!{n\bangle k}\!\!\right\rangle {x + k \choose 2n} $$
32 |
33 | \end{enumerate}
34 |
--------------------------------------------------------------------------------
/src/math/牛顿插值.cpp:
--------------------------------------------------------------------------------
1 | for (int i = 0; i <= k; i++)
2 | r[i] = f(i);
3 | for (int j = 0; j < k; j++)
4 | for (int i = k; i > j; i--)
5 | r[i] -= r[i - 1];
6 |
--------------------------------------------------------------------------------
/src/math/牛顿插值.tex:
--------------------------------------------------------------------------------
1 | 牛顿插值的原理是 \textbf{二项式反演}。
2 |
3 | \paragraph{二项式反演}
4 |
5 | $$ f(n) = \sum_{k = 0} ^ n {n \choose k} g(k) \; \iff \; g(n) = \sum_{k = 0} ^ n \left( -1 \right) ^ {n - k} {n \choose k} f(k) $$
6 |
7 | 可以用 $e^x$ 和 $e^{-x}$ 的麦克劳林展开式证明。
8 |
9 | 套用二项式反演的结论即可得到牛顿插值:
10 |
11 | $$ f(n) = \sum_{i = 0} ^ k {n \choose i} r_i , \; \text{where} \; r_i = \sum_{j = 0} ^ i (-1) ^ {i - j} {i \choose j} f(j) $$
12 |
13 | 其中 $k$ 表示 $f(n)$ 的最高次项系数。
14 |
15 | 实现时右边的式子等价于 $k$ 次差分:
16 |
17 | \inputminted{cpp}{../src/math/牛顿插值.cpp}
18 |
19 | 注意到预处理 $r_i$ 的式子满足卷积形式,必要时可以用 FFT 优化至 $O(k\log k)$。
20 |
--------------------------------------------------------------------------------
/src/math/矩阵乘法.cpp:
--------------------------------------------------------------------------------
1 | for (int i = 1; i <= n; i++)
2 | for (int k = 1; k <= n; k++)
3 | for (int j = 1; j <= n; j++)
4 | a[i][j] += b[i][k] * c[k][j];
5 | // 通过改善内存访问连续性,显著提升速度
6 |
--------------------------------------------------------------------------------
/src/math/矩阵树定理.tex:
--------------------------------------------------------------------------------
1 | \paragraph{无向图} 设图 $G$ 的基尔霍夫矩阵 $L(G)$ 等于度数矩阵减去邻接矩阵,则 $G$ 的生成树个数等于 $L(G)$ 的任意一个代数余子式的值。
2 |
3 | \paragraph{有向图} 类似地定义 $L_{in}(G)$ 等于 \textbf{入度}\ 矩阵减去邻接矩阵($i$ 指向 $j$ 有边,则 $A_{i, j} = 1$),$L_{out}(G)$ 等于 \textbf{出度}\ 矩阵减去邻接矩阵。
4 |
5 | 则以 $i$ 为根的内向树个数即为 $L_{out}$ 的第 $i$ 个主子式(关于第 $i$ 行第 $i$ 列的余子式),外向树个数即为 $L_{in}$ 的第 $i$ 个主子式。
6 |
7 | (可以看出,只有无向图才满足$L(G)$的所有代数余子式都相等。)
8 |
9 | \paragraph{BEST定理(有向图欧拉回路计数)} 如果 $G$ 是有向欧拉图,则 $G$ 的欧拉回路的个数等于以一个任意点为根的内/外向树个数乘以 $\prod_v (\deg(v) - 1) !$。
10 |
11 | (在欧拉图里,无论以哪个结点为根,也无论内向树还是外向树,生成树数量都是一样的。)
12 |
13 | 另外无向图欧拉回路计数是挑战 NPC。
14 |
--------------------------------------------------------------------------------
/src/math/纳什均衡.tex:
--------------------------------------------------------------------------------
1 | \paragraph{纯策略\,混合策略} 纯策略是指一定会选择某个选项,混合策略是指对每个选项都有一个概率分布 $p_i$,以相应的概率选择这个选项。
2 |
3 | 考虑这样的游戏:有几个人(当然可以是两个)各自独立地做决定,然后同时公布每个人的决定,而每个人的收益和所有人的选择有关。
4 |
5 | 那么纳什均衡就是每个人都决定一个混合策略,使得在其他人都是纯策略的情况下,这个人最坏情况下(也就是说其他人的纯策略最针对他的时候)的收益是最大的。也就是说,收益函数对这个人的混合策略求一个偏导,结果是 0(因为是极大值)。
6 |
7 | 纳什均衡点可能存在多个,不过在一个双人 \textbf{零和}\ 游戏中,纳什均衡点一定唯一存在。
8 |
--------------------------------------------------------------------------------
/src/math/线性代数.tex:
--------------------------------------------------------------------------------
1 | \paragraph{行列式}
2 | $$ \det A = \sum_{\sigma} \text{sgn}(\sigma) \prod_{i}a_{i, \sigma_i} $$
3 |
4 | \paragraph{逆矩阵}
5 | $$ B = A^{-1} \iff AB = 1 $$
6 |
7 | \paragraph{代数余子式}
8 | $$ C_{i, j} = (-1) ^ {i + j} M_{i, j} = (-1) ^ {i + j} \left| A ^ {i, j} \right| $$
9 |
10 | 也就是 $A$ 去掉一行一列之后的行列式。
11 |
12 | \paragraph{伴随矩阵}
13 | $$ A^{*} = C^T $$
14 |
15 | 即代数余子式矩阵的转置。
16 |
17 | 同时我们有 $$ A^{*} = |A| A^{-1}$$
18 |
19 | \paragraph{特征多项式}
20 | $$ P_A(x) = \det \left(Ix - A\right) $$
21 |
22 | 特征根:特征多项式的所有 $n$ 个根(可能有重根)。
23 |
--------------------------------------------------------------------------------
/src/math/线性基.cpp:
--------------------------------------------------------------------------------
1 | void add(unsigned long long x) {
2 | for (int i = 63; i >= 0; i--)
3 | if (x >> i & 1) {
4 | if (b[i])
5 | x ^= b[i];
6 | else {
7 | b[i] = x;
8 |
9 | for (int j = i - 1; j >= 0; j--)
10 | if (b[j] && (b[i] >> j & 1))
11 | b[i] ^= b[j];
12 |
13 | for (int j = i + 1; j < 64; j++)
14 | if (b[j] >> i & 1)
15 | b[j] ^= b[i];
16 |
17 | break;
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/src/math/线性基.md:
--------------------------------------------------------------------------------
1 | # 线性基
2 |
3 | 线性基是向量空间的一组基,通常可以解决有关异或的一些题目。
4 |
5 | 通俗一点的讲法就是由一个集合构造出来的另一个集合,它有以下几个性质:
6 |
7 | - 线性基的元素能相互异或得到原集合的元素的所有相互异或得到的值。
8 | - 线性基是满足性质 1 的最小的集合。
9 | - 线性基没有异或和为 0 的子集。
10 | - 线性基中每个元素的异或方案唯一,也就是说,线性基中不同的异或组合异或出的数都是不一样的。
11 | - 线性基中每个元素的二进制最高位互不相同。
12 |
13 | 构造线性基的方法如下:
14 |
15 | 对原集合的每个数 p 转为二进制,从高位向低位扫,对于第 位是 1 的,如果 不存在,那么令 并结束扫描,如果存在,令 。
16 |
17 | 代码:
18 |
19 | ```c++
20 | inline void insert(long long x) {
21 | for (int i = 55; i + 1; i--) {
22 | if (!(x >> i)) // x的第i位是0
23 | continue;
24 | if (!p[i]) {
25 | p[i] = x;
26 | break;
27 | }
28 | x ^= p[i];
29 | }
30 | }
31 | ```
32 |
33 | 查询原集合内任意几个元素 xor 的最大值,就可以用线性基解决。
34 |
35 | 将线性基从高位向低位扫,若 xor 上当前扫到的 答案变大,就把答案异或上 。
36 |
37 | 为什么能行呢?因为从高往低位扫,若当前扫到第 位,意味着可以保证答案的第 位为 1,且后面没有机会改变第 位。
38 |
39 | 查询原集合内任意几个元素 xor 的最小值,就是线性基集合所有元素中最小的那个。
40 |
41 | 查询某个数是否能被异或出来,类似于插入,如果最后插入的数 被异或成了 0,则能被异或出来。
--------------------------------------------------------------------------------
/src/math/线性规划对偶原理.tex:
--------------------------------------------------------------------------------
1 | 给定一个原始线性规划:
2 |
3 | $$
4 | \begin{aligned}
5 | \text{Minimize}&&\sum_{j=1}^n c_j x_j\\
6 | \text{Subject to}&&\sum_{j=1}^n a_{ij} x_j\ge b_i,\\
7 | &&x_j\ge 0
8 | \end{aligned}
9 | $$
10 |
11 | 定义它的对偶线性规划为:
12 |
13 | $$
14 | \begin{aligned}
15 | \text{Maximize}&&\sum_{i=1}^m b_i y_i\\
16 | \text{Subject to}&&\sum_{i=1}^m a_{ij} y_i\le c_j,\\
17 | &&y_i\ge 0
18 | \end{aligned}
19 | $$
20 |
21 | 用矩阵可以更形象地表示为:
22 | $$
23 | \begin{aligned}
24 | \text{Minimize}&& \ c^T \boldsymbol x &&&& \text{Maximize} && \boldsymbol b^{T}\boldsymbol y\\
25 | \text{Subject to}&& A\boldsymbol x \ge \boldsymbol b, && \Longleftrightarrow && \text{Subject to} && A^T\boldsymbol y \le \boldsymbol c,\\
26 | && \boldsymbol x\ge 0 &&&&&& \boldsymbol y\ge 0
27 | \end{aligned}
28 | $$
29 |
--------------------------------------------------------------------------------
/src/math/线性齐次线性常系数递推.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/src/math/线性齐次线性常系数递推.png
--------------------------------------------------------------------------------
/src/math/经典博弈.tex:
--------------------------------------------------------------------------------
1 | \begin{enumerate}
2 |
3 | \item \textbf{阶梯博弈}
4 |
5 | 台阶的每层都有一些石子,每次可以选一层(但不能是第 $0$ 层),把任意个石子移到低一层。
6 |
7 | \paragraph{结论} 奇数层的石子数量进行异或和即可。
8 |
9 | 实际上只要路径长度唯一就可以,比如在树上博弈,然后石子向根节点方向移动,那么就是奇数深度的石子数量进行异或和。
10 |
11 | \item \textbf{可以同时操作多个子游戏}
12 |
13 | 如果某个游戏由若干个独立的子游戏组成,并且每次可以 \textbf{任意选几个} (当然至少一个)子游戏进行操作,那么结论是:所有子游戏都必败时先手才会必败,否则先手必胜。
14 |
15 | \item \textbf{每次最多操作 $k$ 个子游戏 (Nim-K)}
16 |
17 | 如果每次最多操作 $k$ 个子游戏,结论是:把所有子游戏的 SG 函数写成二进制表示,如果每一位上的 $1$ 个数都是 $(k+1)$ 的倍数,则先手必败,否则先手必胜。
18 |
19 | (实际上前一条可以看做 $k = \infty$ 的情况,也就是所有 SG 值都是 $0$ 时才会先手必败。)
20 |
21 | 如果要求整个游戏的 SG 函数,就按照上面的方法每个二进制位相加后 $\bmod (k+1)$,视为 $(k+1)$ 进制数求值即可。(\textbf{未验证})
22 |
23 | \item \textbf{反 Nim 游戏 (Anti-Nim)}
24 |
25 | 和 Nim 游戏差不多,唯一的不同是取走最后一个石子的输。
26 |
27 | 分两种情况:
28 |
29 | \begin{itemize}
30 | \item 所有堆石子个数都是 $1$:有偶数堆时先手必胜,否则先手必败。
31 | \item 存在某个堆石子数多于 $1$:异或和不为 $0$ 则先手必胜,否则先手必败。
32 | \end{itemize}
33 |
34 | 当然石子个数实际上就是 SG 函数,所以判别条件全都改成 SG 函数也是一样的。(\textbf{未验证})
35 |
36 | \item \textbf{威佐夫博弈}
37 |
38 | 有两堆石子,每次要么从一堆中取任意个,要么从两堆中都取走相同数量。也等价于两个人移动一个只能向左上方走的皇后,不能动的输。
39 |
40 | \paragraph{结论}设两堆石子分别有 $a$ 个和 $b$ 个,且 $a < b$,则先手必败当且仅当 $a = \left\lfloor (b - a) \frac {1 + \sqrt 5} 2 \right\rfloor$。
41 |
42 | \item \textbf{删子树博弈}
43 |
44 | 有一棵有根树,两个人轮流操作,每次可以选一个点(除了根节点)然后把它的子树都删掉,不能操作的输。
45 |
46 | \paragraph{结论}
47 |
48 | $$ SG(u) = \text{XOR} _{v \in son_u} \left( SG(v) + 1 \right) $$
49 |
50 | \item \textbf{无向图游戏}
51 |
52 | 在一个无向图上的某个点上摆一个棋子,两个人轮流把棋子移动到相邻的点,并且每个点只能走一次,不能操作的输。
53 |
54 | \paragraph{结论} 如果某个点一定在最大匹配中,则先手必胜,否则先手必败。
55 |
56 | \end{enumerate}
57 |
--------------------------------------------------------------------------------
/src/math/行列式取模.cpp:
--------------------------------------------------------------------------------
1 | int p;
2 |
3 | int Gauss(int A[maxn][maxn], int n) {
4 | int det = 1;
5 |
6 | for (int i = 1; i <= n; i++) {
7 | for (int j = i + 1; j <= n; j++)
8 | while (A[j][i]) {
9 | int t = (p - A[i][i] / A[j][i]) % p;
10 | for (int k = i; k <= n; k++)
11 | A[i][k] = (A[i][k] + (long long)A[j][k] * t) % p;
12 |
13 | swap(A[i], A[j]);
14 | det = (p - det) % p; // 交换一次之后行列式取负
15 | }
16 |
17 | if (!A[i][i])
18 | return 0;
19 |
20 | det = (long long)det * A[i][i] % p;
21 | }
22 |
23 | return det;
24 | }
--------------------------------------------------------------------------------
/src/math/解稀疏方程组.cpp:
--------------------------------------------------------------------------------
1 | vector solve_sparse_equations(const vector > &A, const vector &b) {
2 | int n = (int)b.size(); // 0-based
3 |
4 | vector > f({b});
5 |
6 | for (int i = 1; i < 2 * n; i++) {
7 | vector v(n);
8 | auto &u = f.back();
9 |
10 | for (auto [x, y, z] : A) // [x, y, value]
11 | v[x] = (v[x] + (long long)u[y] * z) % p;
12 |
13 | f.push_back(v);
14 | }
15 |
16 | vector w(n);
17 | mt19937 gen;
18 | for (auto &x : w)
19 | x = uniform_int_distribution(1, p - 1)(gen);
20 |
21 | vector a(2 * n);
22 | for (int i = 0; i < 2 * n; i++)
23 | for (int j = 0; j < n; j++)
24 | a[i] = (a[i] + (long long)f[i][j] * w[j]) % p;
25 |
26 | auto c = berlekamp_massey(a);
27 | int m = (int)c.size();
28 |
29 | vector ans(n);
30 |
31 | for (int i = 0; i < m - 1; i++)
32 | for (int j = 0; j < n; j++)
33 | ans[j] = (ans[j] + (long long)c[m - 2 - i] * f[i][j]) % p;
34 |
35 | int inv = qpow(p - c[m - 1], p - 2);
36 |
37 | for (int i = 0; i < n; i++)
38 | ans[i] = (long long)ans[i] * inv % p;
39 |
40 | return ans;
41 | }
--------------------------------------------------------------------------------
/src/math/贝尔数.tex:
--------------------------------------------------------------------------------
1 | $$B_0 = 1,\, B_1 = 1,\, B_2 = 2,\, B_3 = 5,\,$$
2 | $$B_4 = 15,\, B_5 = 52,\, B_6 = 203, \dots$$
3 |
4 | $$\begin{aligned}B_n = \sum_{k = 0} ^ n {n\brace k}\end{aligned}$$
5 |
6 | \paragraph{递推式}
7 | $$ B_{n + 1} = \sum_{k = 0} ^n {n\choose k} B_k $$
8 |
9 | \paragraph{指数生成函数} $B(x) = e^{e^x - 1}$
10 |
11 | \paragraph{Touchard 同余} 如果 $p$ 是素数,那么:
12 | $$ B_{n + p^m} \equiv (m B_n + B_{n + 1}) \pmod p $$
13 |
--------------------------------------------------------------------------------
/src/math/连通图计数.tex:
--------------------------------------------------------------------------------
1 | 设大小为$n$的满足一个限制$P$的简单无向图数量为$g_n$, 满足限制$P$且连通的简单无向图数量为$f_n$, 如果已知$g_{1\dots n}$求$f_n$, 可以得到递推式
2 |
3 | $$\begin{aligned}f_n=g_n-\sum_{k=1}^{n-1}{n-1\choose k-1}f_k g_{n-k}\end{aligned}$$
4 |
5 | 这个递推式的意义就是用任意图的数量减掉不连通的数量, 而不连通的数量可以通过枚举$1$号点所在连通块大小来计算.
6 |
7 | 注意, 由于$f_0=0$, 因此递推式的枚举下界取$0$和$1$都是可以的.
8 |
9 | 推一推式子会发现得到一个多项式求逆, 再仔细看看, 其实就是一个多项式$\ln$.
--------------------------------------------------------------------------------
/src/math/高斯消元.tex:
--------------------------------------------------------------------------------
1 | \paragraph{高斯-约当消元法 Gauss-Jordan}
2 | 每次选取当前行绝对值最大的数作为代表元,在做浮点数消元时可以很好地保证精度。
3 |
4 | \inputminted{cpp}{../src/math/gauss_jordan.cpp}
5 |
6 | \paragraph{解线性方程组}
7 | 在矩阵的右边加上一列表示系数即可,如果消成上三角的话最后要倒序回代。
8 |
9 | \paragraph{求逆矩阵}
10 | 维护一个矩阵 $B$,初始设为 $n$ 阶单位矩阵。在消元的同时对 $B$ 进行一样的操作,那么把 $A$ 消成单位矩阵时 $B$ 就是逆矩阵。
11 |
12 | \paragraph{行列式}
13 | 消成对角之后把代表元乘起来。如果是任意模数,要注意消元时每交换一次行列要取反一次。
14 |
--------------------------------------------------------------------------------
/src/math/高斯消元与自由元搜索.cpp:
--------------------------------------------------------------------------------
1 |
2 | void gauss() {
3 | for (int i = 63; ~i; i--) {
4 | if (!(a[i] >> i & 1)) {
5 | for (int j = i - 1; ~j; j--)
6 | if (a[j] >> i & 1) {
7 | swap(a[i], a[j]);
8 | swap(b[i], b[j]);
9 | break;
10 | }
11 | }
12 |
13 | if (!(a[i] >> i & 1))
14 | continue;
15 |
16 | for (int j = 63; ~j; j--)
17 | if (j != i && (a[j] >> i & 1)) {
18 | a[j] ^= a[i];
19 | b[j] ^= b[i];
20 | }
21 | }
22 | }
23 |
24 | bool check() {
25 | unsigned long long x = 0;
26 | for (int i = 0; i < 64; i++)
27 | if (ans[i])
28 | x |= 1ull << i;
29 |
30 | for (int i = 1; i <= n; i++) {
31 | x ^= x << 13;
32 | x ^= x >> 7;
33 | x ^= x << 17;
34 |
35 | if (x % i != val[i])
36 | return false;
37 | }
38 | return true;
39 | }
40 |
41 | bool dfs(int i) {
42 | if (i == 64)
43 | return check();
44 |
45 | if (a[i] >> i & 1) {
46 | ans[i] = b[i];
47 | for (int j = 0; j < i; j++)
48 | if (a[i] >> j & 1)
49 | ans[i] ^= ans[j];
50 |
51 | return dfs(i + 1);
52 | }
53 |
54 | ans[i] = 0;
55 | if (dfs(i + 1))
56 | return true;
57 |
58 | ans[i] = 1;
59 | return dfs(i + 1);
60 |
61 | }
--------------------------------------------------------------------------------
/src/math/齐次线性递推.cpp:
--------------------------------------------------------------------------------
1 | struct LinearRecurrence {
2 | vector first, trans;
3 | vector > bin;
4 |
5 | vector multi(const vector &a, const vector &b) {
6 | int n = (int)a.size() - 1;
7 |
8 | vector c(n * 2 + 1);
9 |
10 | for (int i = 0; i <= n; i++)
11 | for (int j = 0; j <= n; j++)
12 | c[i + j] = (c[i + j] + (long long)a[i] * b[j]) % p;
13 |
14 | for (int i = n * 2; i > n; i--) {
15 | for (int j = 0; j < n; j++)
16 | c[i - 1 - j] = (c[i - 1 - j] + (long long)c[i] * trans[j]) % p;
17 |
18 | c[i] = 0;
19 | }
20 |
21 | c.resize(n + 1);
22 | return c;
23 | }
24 |
25 | LinearRecurrence(vector &first, vector &trans) : first(first), trans(trans) {
26 | int n = (int)first.size();
27 |
28 | vector a(n + 1);
29 | a[1] = 1;
30 | bin.push_back(a);
31 |
32 | for (int i = 1; i < 64; i++)
33 | bin.push_back(multi(bin[i - 1], bin[i - 1]));
34 | }
35 |
36 | int calc(long long k) {
37 | int n = (int)first.size();
38 |
39 | vector a(n + 1);
40 | a[0] = 1;
41 |
42 | for (int i = 0; i < 64; i++)
43 | if (k >> i & 1)
44 | a = multi(a, bin[i]);
45 |
46 | int ans = 0;
47 | for (int i = 0; i < n; i++)
48 | ans = (ans + (long long)a[i + 1] * first[i]) % p;
49 |
50 | return ans;
51 | }
52 | };
--------------------------------------------------------------------------------
/src/misc/O(1)快速乘.cpp:
--------------------------------------------------------------------------------
1 | // long double 快速乘
2 | // 在两数直接相乘会爆long long时才有必要使用
3 | // 常数比直接long long乘法 + 取模大很多, 非必要时不建议使用
4 | long long mul(long long a, long long b, long long p) {
5 | a %= p;
6 | b %= p;
7 | return ((a * b - p * (long long)((long double)a / p * b + 0.5)) % p + p) % p;
8 | }
9 |
10 | // 指令集快速乘
11 | // 试机记得测试能不能过编译
12 | inline long long mul(const long long a, const long long b, const long long p) {
13 | long long ans;
14 | __asm__ __volatile__ ("\tmulq %%rbx\n\tdivq %%rcx\n" : "=d"(ans) : "a"(a), "b"(b), "c"(p));
15 | return ans;
16 | }
17 |
18 | // int乘法取模, 大概比直接做快一倍
19 | inline int mul_mod(int a, int b, int p) {
20 | int ans;
21 | __asm__ __volatile__ ("\tmull %%ebx\n\tdivl %%ecx\n" : "=d"(ans) : "a"(a), "b"(b), "c"(p));
22 | return ans;
23 | }
--------------------------------------------------------------------------------
/src/misc/STL.tex:
--------------------------------------------------------------------------------
1 | \subsubsection{vector}
2 | \begin{itemize}
3 | % \item \mintinline{cpp}{vector(int nSize)}: 创建一个vector, 元素个数为nSize
4 | \item \mintinline{cpp}{vector(int n, const T &value)}:创建一个 \mintinline{cpp}{vector},元素个数为 n,且值均为 \mintinline{cpp}{value}。
5 | % \item \mintinline{cpp}{vector(begin, end)}:复制[begin, end)区间内另一个数组的元素到vector中
6 | % \item \mintinline{cpp}{void assign(int n, const T &x)}:设置向量中前 n 个元素的值为 \mintinline{cpp}{x}
7 | % \item \mintinline{cpp}{void assign(const_iterator first, const_iterator last)}:向量中 \mintinline{cpp}{[first, last)} 中元素设置成当前向量元素。
8 | \item \mintinline{cpp}{void emplace_back(Args&&... args)}:自动构造并 \mintinline{cpp}{push_back} 一个元素,例如对一个存储 \mintinline{cpp}{pair} 的 \mintinline{cpp}{vector} 可以 \mintinline{cpp}{v.emplace_back(x, y)}。
9 | \end{itemize}
10 |
11 | \subsubsection{list}
12 | \begin{itemize}
13 | \item \mintinline{cpp}{assign()} 给list赋值
14 | \item \mintinline{cpp}{back()} 返回最后一个元素
15 | \item \mintinline{cpp}{begin()} 返回指向第一个元素的迭代器
16 | \item \mintinline{cpp}{clear()} 删除所有元素
17 | \item \mintinline{cpp}{empty()} 如果list是空的则返回true
18 | \item \mintinline{cpp}{end()} 返回末尾的迭代器
19 | \item \mintinline{cpp}{erase()} 删除一个元素
20 | \item \mintinline{cpp}{front()} 返回第一个元素
21 | \item \mintinline{cpp}{insert()} 插入一个元素到list中
22 | \item \mintinline{cpp}{max_size()} 返回list能容纳的最大元素数量
23 | \item \mintinline{cpp}{merge()} 合并两个list
24 | \item \mintinline{cpp}{pop_back()} 删除最后一个元素
25 | \item \mintinline{cpp}{pop_front()} 删除第一个元素
26 | \item \mintinline{cpp}{push_back()} 在list的末尾添加一个元素
27 | \item \mintinline{cpp}{push_front()} 在list的头部添加一个元素
28 | \item \mintinline{cpp}{rbegin()} 返回指向第一个元素的逆向迭代器
29 | \item \mintinline{cpp}{remove()} 从list删除元素
30 | \item \mintinline{cpp}{remove_if()} 按指定条件删除元素
31 | \item \mintinline{cpp}{rend()} 指向list末尾的逆向迭代器
32 | \item \mintinline{cpp}{resize()} 改变list的大小
33 | \item \mintinline{cpp}{reverse()} 把list的元素倒转
34 | \item \mintinline{cpp}{size()} 返回list中的元素个数
35 | \item \mintinline{cpp}{sort()} 给list排序
36 | \item \mintinline{cpp}{splice()} 合并两个list
37 | \item \mintinline{cpp}{swap()} 交换两个list
38 | \item \mintinline{cpp}{unique()} 删除list中重复的元
39 | \end{itemize}
40 |
41 | \subsubsection{unordered\_set/map}
42 |
43 | \begin{itemize}
44 | \item \mintinline{cpp}{unordered_map}:自定义哈希函数, 其中 \mintinline{cpp}{hash} 是一个带重载括号的类。
45 | \end{itemize}
46 |
47 | \subsubsection{自定义 Hash}
48 | \inputminted{cpp}{../src/misc/hash.cpp}
49 |
--------------------------------------------------------------------------------
/src/misc/cheat.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/src/misc/cheat.pdf
--------------------------------------------------------------------------------
/src/misc/cpp.tex:
--------------------------------------------------------------------------------
1 | \subsubsection[cmath]{\texttt{cmath}}
2 |
3 | \begin{itemize}
4 | \item \mintinline{cpp}{std::log1p(x)}:(\textbf{注意是数字 1})返回 $\ln(1 + x)$ 的值,$x$ 非常接近 $0$ 时比直接 exp 精确得多。
5 | \item \mintinline{cpp}{std::hypot(x, y[, z])}:返回平方和的平方根,或者说到原点的欧几里德距离。
6 | \end{itemize}
7 |
8 | \subsubsection[algorithm]{\texttt{algorithm}}
9 |
10 | \begin{itemize}
11 | \item \mintinline{cpp}{std::all_of(begin, end, f)}:检查范围内元素调用函数 \mintinline{cpp}{f} 后是否全返回真。类似地还有 \mintinline{cpp}{std::any_of} 和 \mintinline{cpp}{std::none_of}。
12 | \item \mintinline{cpp}{std::for_each(begin, end, f)}:对范围内所有元素调用一次 \mintinline{cpp}{f}。如果传入的是引用,也可以用 \mintinline{cpp}{f} 修改。(例如 \mintinline{cpp}{for_each(a, a + n, [](int &x){ cout << ++x << "\n"; })})
13 | \item \mintinline{cpp}{std::for_each_n(begin, n, f)}:同上,只不过范围改成了从 \mintinline{cpp}{begin} 开始的 n 个元素。
14 | \item \mintinline{cpp}{std::copy(), std::copy_n()}:用法谁都会,但标准里说如果元素是可平凡复制的(比如 \mintinline{cpp}{int}),那么它会避免批量赋值,并且调用 \mintinline{cpp}{std::memmove()} 之类的快速复制函数。(一句话总结:它跑得快)
15 | \item \mintinline{cpp}{std::rotate(begin, mid, end)}:\textbf{向前} 循环移动,移动后 \mintinline{cpp}{mid} 位置的元素会跑到 \mintinline{cpp}{begin} 位置。C++11 起会返回 \mintinline{cpp}{begin} 位置的元素移动后的位置。
16 | \item \mintinline{cpp}{std::unique(begin, end)}:去重,返回去重后的 \mintinline{cpp}{end}。
17 | \item \mintinline{cpp}{std::partition(begin, end, f)}:把 \mintinline{cpp}{f} 为 \mintinline{cpp}{true} 的放在前面,\mintinline{cpp}{false} 的放在后面,返回值是第二部分的开头,\textbf{不保持相对顺序}。如果要保留相对顺序可以用 \mintinline{cpp}{std::stable_partition()},比如写整体二分。
18 | \item \mintinline{cpp}{std::partition_copy(begin, end, begin_t, begin_f, f)}:不修改原数组,把 \mintinline{cpp}{true} 的扔到 \mintinline{cpp}{begin_t},\mintinline{cpp}{false} 的扔到 \mintinline{cpp}{begin_f}。返回值是两部分结尾的迭代器的 \mintinline{cpp}{pair}。
19 | \item \mintinline{cpp}{std::equal_range(begin, end, x)}:在已经排好序的数组里找到等于 \mintinline{cpp}{x} 的范围。
20 | \item \mintinline{cpp}{std::minmax(a, b)}:返回 \mintinline{cpp}{pair(min(a, b), max(a, b))}。但是要注意 \textbf{返回的是引用},所以不能直接用来交换 $l, r$。
21 | \end{itemize}
22 |
23 | \subsubsection[std::tuple]{\texttt{std::tuple}}
24 |
25 | \begin{itemize}
26 | % \item \mintinline{cpp}{std::make_tuple(...)}:返回构造好的 \mintinline{cpp}{tuple}。
27 | \item \mintinline{cpp}{std::get(tup)}:返回 \mintinline{cpp}{tuple} 的第 \mintinline{cpp}{i} 项。
28 | \item \mintinline{cpp}{std::tuple_cat(...)}:传入几个 \mintinline{cpp}{tuple},返回按顺序连起来的 \mintinline{cpp}{tuple}。
29 | \item \mintinline{cpp}{std::tie(x, y, z, ...)}:把传入的变量的 \textbf{左值引用} 绑起来作为 \mintinline{cpp}{tuple} 返回,例如可以 \mintinline{cpp}{std::tie(x, y, z) = std::tuple(a, b, c)}。
30 | \end{itemize}
31 |
32 | \subsubsection[complex]{\texttt{complex}}
33 |
34 | \begin{itemize}
35 | \item \mintinline{cpp}{complex imaginary = 1i, x = 2 + 3i}:语法糖,可以这样直接构造复数。
36 | \item \mintinline{cpp}{real/imag(x)}:返回实部/虚部。
37 | \item \mintinline{cpp}{conj(x)}:返回共轭复数。
38 | \item \mintinline{cpp}{arg(x)}:返回辐角。
39 | \item \mintinline{cpp}{norm(x)}:返回模的平方。(直接求模用 \mintinline{cpp}{abs(x)} 就行。)
40 | \item \mintinline{cpp}{polar(len, theta)}:用模长和辐角构造复数。
41 | \end{itemize}
42 |
--------------------------------------------------------------------------------
/src/misc/decimal.py:
--------------------------------------------------------------------------------
1 | import decimal
2 |
3 | decimal.getcontext().prec = 1234 # 有效数字位数
4 |
5 | x = decimal.Decimal(2)
6 | x = decimal.Decimal('50.5679') # 不要用float, 因为float本身就不准确
7 |
8 | x = decimal.Decimal('50.5679'). \
9 | quantize(decimal.Decimal('0.00')) # 保留两位小数, 50.57
10 | x = decimal.Decimal('50.5679'). \
11 | quantize(decimal.Decimal('0.00'), decimal.ROUND_HALF_UP) # 四舍五入
12 | # 第二个参数可选如下:
13 | # ROUND_HALF_UP 四舍五入
14 | # ROUND_HALF_DOWN 五舍六入
15 | # ROUND_HALF_EVEN 银行家舍入法, 舍入到最近的偶数
16 | # ROUND_UP 向绝对值大的取整
17 | # ROUND_DOWN 向绝对值小的取整
18 | # ROUND_CEILING 向正无穷取整
19 | # ROUND_FLOOR 向负无穷取整
20 | # ROUND_05UP (away from zero if last digit after rounding towards zero would have been 0 or 5; otherwise towards zero)
21 |
22 | print('%f' % x) # 这样做只有float的精度
23 | s = str(x)
24 |
25 | decimal.is_finate(x) # x是否有穷(NaN也算)
26 | decimal.is_infinate(x)
27 | decimal.is_nan(x)
28 | decimal.is_normal(x) # x是否正常
29 | decimal.is_signed(x) # 是否为负数
30 |
31 | decimal.fma(a, b, c) # a * b + c, 精度更高
32 |
33 | x.exp(), x.ln(), x.sqrt(), x.log10()
34 |
35 | # 可以转复数, 前提是要import complex
--------------------------------------------------------------------------------
/src/misc/garsiawachs.tex:
--------------------------------------------------------------------------------
1 | 设序列是 $\{a_i\}$,从左往右,找到一个最小的且满足 $a_{k-1} \le a_{k+1}$ 的 $k$,找到后合并 $a_k$ 和 $a_{k-1}$,再从当前位置开始向左找最大的 $j$ 满足 $a_j \ge a_k + a_{k-1}$(当然是指合并前的),然后把 $a_k + a_{k-1}$ 插到 $j$ 的后面就行。一直重复,直到只剩下一堆石子就可以了。
2 |
3 | 另外在这个过程中,可以假设 $a_{-1}$ 和 $a_n$ 是正无穷的,可省略边界的判别。把 $a_0$ 设为 INF,$a_{n+1}$ 设为 INF-1,可实现剩余一堆石子时自动结束。
4 |
--------------------------------------------------------------------------------
/src/misc/hash.cpp:
--------------------------------------------------------------------------------
1 | struct fuck_hash {
2 | fuck_hash() = default;
3 |
4 | size_t operator() (const fuck &f) const {
5 | return (size_t)f[0] ^ ((size_t)f[1] << 7) ^
6 | ((size_t)f[2] << 15) ^ ((size_t)f[3] << 23);
7 | }
8 | };
9 |
10 | unordered_map cnt, sum;
--------------------------------------------------------------------------------
/src/misc/kahan.cpp:
--------------------------------------------------------------------------------
1 | double kahanSum(vector vec) {
2 | double sum = 0, c = 0;
3 | for (auto x : vec) {
4 | double y = x - c;
5 | double t = sum + y;
6 | c = (t - sum) - y;
7 | sum = t;
8 | }
9 | return sum;
10 | }
--------------------------------------------------------------------------------
/src/misc/keyboard-shortcuts-linux.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/src/misc/keyboard-shortcuts-linux.pdf
--------------------------------------------------------------------------------
/src/misc/keyboard-shortcuts-windows.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/src/misc/keyboard-shortcuts-windows.pdf
--------------------------------------------------------------------------------
/src/misc/pbds.tex:
--------------------------------------------------------------------------------
1 | \subsubsection{哈希表}
2 |
3 | \begin{minted}{cpp}
4 | #include
5 | #include
6 | using namespace __gnu_pbds;
7 |
8 | cc_hash_table mp1; // 拉链法
9 | gp_hash_table mp2; // 查探法(快一些)
10 | \end{minted}
11 |
12 | \subsubsection{堆}
13 |
14 | 默认也是大根堆,和 \mintinline{cpp}{std::priority_queue} 保持一致。
15 |
16 | \begin{minted}{cpp}
17 | #include
18 | using namespace __gnu_pbds;
19 |
20 | __gnu_pbds::priority_queue q;
21 | __gnu_pbds::priority_queue, pairing_heap_tag> pq;
22 | \end{minted}
23 |
24 | 效率参考:
25 |
26 | \includegraphics[scale = 0.385]{../src/misc/pbds_heap.png}
27 |
28 | 常用操作:
29 |
30 | \begin{itemize}
31 |
32 | \item \mintinline{cpp}{push()}:向堆中压入一个元素,\textbf{返回迭代器}。
33 | % \item \mintinline{cpp}{pop()}:将堆顶元素弹出。
34 | % \item \mintinline{cpp}{top()}:返回堆顶元素。
35 | % \item \mintinline{cpp}{size()}:返回元素个数。
36 | % \item \mintinline{cpp}{empty()}:返回是否非空。
37 | \item \mintinline{cpp}{modify(point_iterator, const key)}:把迭代器位置的 \mintinline{cpp}{key} 修改为传入的 \mintinline{cpp}{key}。
38 | \item \mintinline{cpp}{erase(point_iterator)}:把迭代器位置的键值从堆中删除。
39 | \item \mintinline{cpp}{join(__gnu_pbds::priority_queue &other)}:把 \mintinline{cpp}{other} 合并到 \mintinline{cpp}{*this},并把 \mintinline{cpp}{other} 清空
40 | \end{itemize}
41 |
42 | \subsubsection{平衡树}
43 |
44 | \begin{minted}{cpp}
45 | #include
46 | #include
47 | using namespace __gnu_pbds;
48 |
49 | tree, rb_tree_tag, tree_order_statistics_node_update> t;
50 |
51 | // rb_tree_tag 红黑树(还有splay_tree_tag和ov_tree_tag,后者不知道是什么)
52 | \end{minted}
53 |
54 | 注意第五个参数要填 \mintinline{cpp}{tree_order_statistics_node_update} 才能使用排名操作。
55 |
56 | \begin{itemize}
57 | \item \mintinline{cpp}{insert(x)}:向树中插入一个元素 \mintinline{cpp}{x},返回\mintinline{cpp}{pair}。
58 | \item \mintinline{cpp}{erase(x)}:从树中删除一个元素/迭代器 \mintinline{cpp}{x},返回一个 \mintinline{cpp}{bool} 表明是否删除成功。
59 | \item \mintinline{cpp}{order_of_key(x)}:返回 \mintinline{cpp}{x} 的排名,\textbf{0-based}。
60 | \item \mintinline{cpp}{find_by_order(x)}:返回排名(\textbf{0-based})所对应元素的迭代器。
61 | % \item \mintinline{cpp}{lower_bound(x) / upper_bound(x)}:返回第一个$\ge$或者>x的元素的迭代器
62 | \item \mintinline{cpp}{join(other)}:将 \mintinline{cpp}{other} 并入当前树,前提是两棵树的类型一样,并且二者值域不能重叠。\mintinline{cpp}{other} 树会被删除。
63 | \item \mintinline{cpp}{split(x, other)}:分裂成两部分,小于等于 \mintinline{cpp}{x} 的属于当前树,其余的属于 \mintinline{cpp}{other}。
64 | % \item \mintinline{cpp}{empty()}:返回是否为空
65 | % \item \mintinline{cpp}{size()}:返回大小
66 | \end{itemize}
67 |
68 | (注意 \textbf{平衡树不支持多重值},如果需要多重值,可以再开一个 \mintinline{cpp}{unordered_map} 来记录值出现的次数,将\mintinline{cpp}{x << 32} 后加上出现的次数后插入。注意此时应该为 \mintinline{cpp}{long long} 类型。)
69 |
--------------------------------------------------------------------------------
/src/misc/pbds_heap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antileaf/Standard-Code-Library/60724af003aa56518ad0f750ee3f6f7dc4128f9a/src/misc/pbds_heap.png
--------------------------------------------------------------------------------
/src/misc/rope.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | using namespace __gnu_cxx;
3 |
4 | push_back(x); // 在末尾添加x
5 | insert(pos, x); // 在pos插入x, 自然支持整个char数组的一次插入
6 | erase(pos, x); // 从pos开始删除x个, 不要只传一个参数, 有毒
7 | copy(pos, len, x); // 从pos开始到pos + len为止的部分, 赋值给x
8 | replace(pos, x); // 从pos开始换成x
9 | substr(pos, x); // 提取pos开始x个
10 | at(x) / [x]; // 访问第x个元素
--------------------------------------------------------------------------------
/src/misc/vscode.tex:
--------------------------------------------------------------------------------
1 | \subsubsection{插件}
2 |
3 | \begin{itemize}
4 | \item Chinese (Simplified)(简体中文语言包)
5 | \item C/C++
6 | \item C++ Intellisense (前提是让用)
7 | \item Better C++ Syntax
8 | \item Python
9 | \item Pylance(前提是让用)
10 | \end{itemize}
11 |
12 | \subsubsection{设置选项}
13 |
14 | \begin{itemize}
15 | \item Editor: Insert Spaces (取消勾选,改为 tab 缩进)
16 | \item Editor: Line Warp (开启折行)
17 | \item 改配色,“深色+:默认深色”
18 | \item 自动保存(F1 $\rightarrow$ ``auto'')
19 | \item Terminal $\to$ Integrated: Cursor Style(修改终端光标形状)
20 | \item Terminal $\to$ Integrated: Cursor Blinking(终端光标闪烁)
21 | \item 字体改为 Cascadia Code/MonoWindows 可用)
22 | \end{itemize}
23 |
24 | \subsubsection{快捷键}
25 |
26 | \begin{itemize}
27 | \item \texttt{Ctrl + \text{`}}(键盘左上角的倒引号):显示终端
28 | \item \texttt{Ctrl + 1/2/3}:切到对应栏
29 | \item \texttt{F1 / Ctrl + Shift + P}:万能键, 打开命令面板
30 | \item \texttt{F8}:下一个 Error \quad Shift+F8:上一个 Error
31 | \item \texttt{Ctrl + [ or ]}:当前行向左/右缩进
32 | \item \texttt{Alt + F12}:查看定义的缩略图(显示小窗, 不跳过去)
33 | \item \texttt{Ctrl + H}:查找替换
34 | \item \texttt{Ctrl + D}:下一个匹配的也被选中(用于配合 Ctrl+F)
35 | \item \texttt{Ctrl + U}:回退上一个光标操作(防止光标飞了找不回去)
36 | \item \texttt{Ctrl +} $\backslash$:水平分栏,最多 3 栏
37 | \end{itemize}
38 |
39 | 更多快捷键参见最后两页,分别是 Windows 和 Linux 下的快捷键列表。
40 |
--------------------------------------------------------------------------------
/src/misc/xorshift.cpp:
--------------------------------------------------------------------------------
1 | ull k1, k2;
2 | const int mod = 10000000;
3 | ull xorShift128Plus() {
4 | ull k3 = k1, k4 = k2;
5 | k1 = k4;
6 | k3 ^= (k3 << 23);
7 | k2 = k3 ^ k4 ^ (k3 >> 17) ^ (k4 >> 26);
8 | return k2 + k4;
9 | }
10 | void gen(ull _k1, ull _k2) {
11 | k1 = _k1, k2 = _k2;
12 | int x = xorShift128Plus() % threshold + 1;
13 | // do sth
14 | }
15 |
16 |
17 | uint32_t xor128(void) {
18 | static uint32_t x = 123456789;
19 | static uint32_t y = 362436069;
20 | static uint32_t z = 521288629;
21 | static uint32_t w = 88675123;
22 | uint32_t t;
23 |
24 | t = x ^ (x << 11);
25 | x = y; y = z; z = w;
26 | return w = w ^ (w >> 19) ^ (t ^ (t >> 8));
27 | }
--------------------------------------------------------------------------------
/src/misc/常用NTT素数及原根.tex:
--------------------------------------------------------------------------------
1 | \begin{tabular}{|c|c|c|c|}
2 | \hline $p = r \times 2 ^ k + 1$ & $r$ & $k$ & 最小原根 \\
3 | \hline $104857601$ & $25$ & $22$ & $3$ \\
4 | \hline $167772161$ & $5$ & $25$ & $3$ \\
5 | \hline $469762049$ & $7$ & $26$ & $3$ \\
6 | \hline $\mathbf{985661441}$ & $235$ & $22$ & $3$ \\
7 | \hline $\mathbf{998244353}$ & $119$ & $23$ & $3$ \\
8 | \hline $1004535809$ & $479$ & $21$ & $3$ \\
9 | \hline $1005060097 ^ *$ & $1917$ & $19$ & $\emph{5}$ \\
10 | \hline $\emph{2013265921}$ & $15$ & $27$ & $\emph{31}$ \\
11 | \hline $2281701377$ & $17$ & $27$ & $3$ \\
12 | \hline $31525197391593473$ & $7$ & $52$ & $3$ \\
13 | \hline $180143985094819841$ & $5$ & $55$ & $\emph{6}$ \\
14 | \hline $1945555039024054273$ & $27$ & $56$ & $\emph{5}$ \\
15 | \hline $4179340454199820289$ & $29$ & $57$ & $3$ \\
16 | \hline
17 | \end{tabular}
18 |
19 | *注:$1005060097$ 有点危险,在变化长度大于 $524288 = 2 ^ {19}$ 时不可用。
20 |
--------------------------------------------------------------------------------
/src/misc/德扑.tex:
--------------------------------------------------------------------------------
1 | 一般来说德扑里 Ace 都是最大的,所以把 Ace 的点数规定为 $14$ 会好写许多。
2 |
3 | 附一个高低奥马哈的参考代码,除了有四张底牌和需要比低之外和德扑区别不大。
4 |
5 | \inputminted{cpp}{../src/misc/高低奥马哈.cpp}
6 |
--------------------------------------------------------------------------------
/src/misc/枚举子集.tex:
--------------------------------------------------------------------------------
1 | (注意这是 $t\ne 0$ 的写法,如果可以等于 $0$ 需要在循环里手动 \mintinline{cpp}{break}。)
2 | \begin{minted}{cpp}
3 | for (int t = s; t; (--t) &= s) {
4 | // do something
5 | }
6 | \end{minted}
7 |
--------------------------------------------------------------------------------
/src/misc/炉石.tex:
--------------------------------------------------------------------------------
1 | 两个随从 $(a_i, h_i)$ 和 $(a_j, h_j)$ 皇城 PK,最后只有 $a_i\times h_i$ 较大的一方才有可能活下来,当然也有可能一起死。
2 |
--------------------------------------------------------------------------------
/src/misc/笛卡尔树.cpp:
--------------------------------------------------------------------------------
1 | int s[maxn], root, lc[maxn], rc[maxn];
2 |
3 | int top = 0;
4 | s[++top] = root = 1;
5 | for (int i = 2; i <= n; i++) {
6 | s[top + 1] = 0;
7 | while (top && a[i] < a[s[top]]) // 小根笛卡尔树
8 | top--;
9 |
10 | if (top)
11 | rc[s[top]] = i;
12 | else
13 | root = i;
14 |
15 | lc[i] = s[top + 1];
16 | s[++top] = i;
17 | }
--------------------------------------------------------------------------------
/src/misc/编译选项.tex:
--------------------------------------------------------------------------------
1 | \begin{itemize}
2 | \item \texttt{-O2 -g -std=c++20}:狗都知道
3 | \item \texttt{-Wall -Wextra -Wshadow -Wconversion}:更多警告
4 | \subitem --\; \texttt{-Werror}:强制将所有 Warning 变成 Error
5 | \item \texttt{-fsanitize=(address/undefined/ftrapv)}:检查数组越界/有符号整数溢出(算 UB)
6 | \subitem --\; 调试神器,在遇到错误时会输出信息。
7 | \subitem --\; 注意无符号类型溢出不算 UB。
8 | % \item \mintinline{cpp}{-fno-ms-extensions}:关闭一些和 msvc 保持一致的特性,例如不标返回值类型的函数会报 CE 而不是默认为 \mintinline{cpp}{int}。
9 | % \subitem --\;但是不写 \texttt{return} 的话它还是管不了。
10 | \item \mintinline{cpp}{#define debug(x) cout << #x << " = " << x << endl}
11 | \end{itemize}
12 |
--------------------------------------------------------------------------------
/src/misc/骂人的艺术.tex:
--------------------------------------------------------------------------------
1 |
2 | \hspace{2em}古今中外没有一个不骂人的人。骂人就是有道德观念的意思,因为在骂人的时候,至少在骂人者自己总觉得那人有该骂的地方。何者该骂,何者不该骂,这个抉择的标准,是极道德的。所以根本不骂人,大可不必。骂人是一种发泄感情的方法,尤其是那一种怨怒的感情。想骂人的时候而不骂,时常在身体上弄出毛病,所以想骂人时,骂骂何妨?
3 |
4 | \hspace{2em}但是,骂人是一种高深的学问,不是人人都可以随便试的。有因为骂人挨嘴巴的,有因为骂人吃官司的,有因为骂人反被人骂的,这都是不会骂人的原故。今以研究所得,公诸同好,或可为骂人时之一助乎?
5 |
6 | \begin{enumerate}
7 |
8 | \item 知己知彼
9 |
10 | \hspace{1.8em}骂人是和动手打架一样的,你如其敢打人一拳,你先要自己忖度下,你吃得起别人的一拳否。这叫做知己知彼。骂人也是一样。譬如你骂他是“屈死”,你先要反省,自己和“屈死”有无分别。你骂别人荒唐,你自己想想曾否吃喝嫖赌。否则别人回敬你一二句,你就受不了。所以别人有着某种短处,而足下也正有同病,那么你在骂他的时候只得割爱。
11 |
12 | \item 无骂不如己者
13 |
14 | \hspace{1.8em}要骂人须要挑比你大一点的人物,比你漂亮一点的或者比你坏得万倍而比你得势的人物,总之,你要骂人,那人无论在好的一方面或坏的一方面都要能胜过你,你才不吃亏。你骂大人物,就怕他不理你,他一回骂,你就算骂着了。因为身份相同的人才肯对骂。在坏的一方面胜过你的,你骂他就如教训一般,他既便回骂,一般人仍不会理会他的。假如你骂一个无关痛痒的人,你越骂他他越得意,时常可以把一个无名小卒骂出名了,你看冤与不冤?
15 |
16 | \item 适可而止
17 |
18 | \hspace{1.8em}骂大人物骂到他回骂的时候,便不可再骂;再骂则一般人对你必无同情,以为你是无理取闹。骂小人物骂到他不能回骂的时候,便不可再骂;再骂下去则一般人对你也必无同情,以为你是欺负弱者。
19 |
20 | \item 旁敲侧击
21 |
22 | \hspace{1.8em}他偷东西,你骂他是贼;他抢东西,你骂他是盗,这是笨伯。骂人必须先明虚实掩映之法,须要烘托旁衬,旁敲侧击,于要紧处只一语便得,所谓杀人于咽喉处着刀。越要骂他你越要原谅他,即便说些恭维话亦不为过,这样的骂法才能显得你所骂的句句是真实确凿,让旁人看起来也可见得你的度量。
23 |
24 | \item 态度镇定
25 |
26 | \hspace{1.8em}骂人最忌浮躁。一语不合,面红筋跳,暴躁如雷,此灌夫骂座,泼妇骂街之术,不足以言骂人。善骂者必须态度镇静,行若无事。普通一般骂人,谁的声音高便算谁占理,谁的来势猛便算谁骂赢,惟真善骂人者,乃能避其锋而击其懈。你等他骂得疲倦的时候,你只消轻轻的回敬他一句,让他再狂吼一阵。在他暴躁不堪的时候,你不妨对他冷笑几声,包管你不费力气,把他气得死去活来,骂得他针针见血。
27 |
28 | \item 出言典雅
29 |
30 | \hspace{1.8em}骂人要骂得微妙含蓄,你骂他一句要使他不甚觉得是骂,等到想过一遍才慢慢觉悟这句话不是好话,让他笑着的面孔由白而红,由红而紫,由紫而灰,这才是骂人的上乘。欲达到此种目的,深刻之用意固不可少,而典雅之言词则尤为重要。言词典雅可使听者不致刺耳。如要骂人骂得典雅,则首先要在骂时万万别提起女人身上的某一部分,万万不要涉及生理学范围。骂人一骂到生理学范围以内,底下再有什么话都不好说了。譬如你骂某甲,千万别提起他的令堂令妹。因为那样一来,便无是非可言,并且你自己也不免有令堂令妹,他若回敬起来,岂非势均力敌,半斤八两?再者骂人的时候,最好不要加人以种种难堪的名词,称呼起来总要客气,即使他是极卑鄙的小人,你也不妨称他先生,越客气,越骂得有力量。骂得时节最好引用他自己的词句,这不但可以使他难堪,还可以减轻他对你骂的力量。俗话少用,因为俗话一览无遗,不若典雅古文曲折含蓄。
31 |
32 | \item 以退为进
33 |
34 | \hspace{1.8em}两人对骂,而自己亦有理屈之处,则处于开骂伊始,特宜注意,最好是毅然将自己理屈之处完全承认下来,即使道歉认错均不妨事。先把自己理屈之处轻轻遮掩过去,然后你再重整旗鼓,着着逼人,方可无后顾之忧。即使自己没有理屈的地方,也绝不可自行夸张,务必要谦逊不遑,把自己的位置降到一个不可再降的位置,然后骂起人来,自有一种公正光明的态度。否则你骂他一两句,他便以你个人的事反唇相讥,一场对骂,会变成两人私下口角,是非曲直,无从判断。所以骂人者自己要低声下气,此所谓以退为进。
35 |
36 | \item 预设埋伏
37 |
38 | \hspace{1.8em}你把这句话骂过去,你便要想想看,他将用什么话骂回来。有眼光的骂人者,便处处留神,或是先将他要骂你的话替他说出来,或是预先安设埋伏,令他骂回来的话失去效力。他骂你的话,你替他说出来,这便等于缴了他的械一般。预设埋伏,便是在要攻击你的地方,你先轻轻的安下话根,然后他骂过来就等于枪弹打在沙包上,不能中伤。
39 |
40 | \item 小题大做
41 |
42 | \hspace{1.8em}如对方有该骂之处,而题目身小,不值一骂,或你所知不多,不足一骂,那时节你便可用小题大做的方法,来扩大题目。先用诚恳而怀疑的态度引申对方的意思,由不紧要之点引到大题目上去,处处用严谨的逻辑逼他说出不逻辑的话来,或是逼他说出合于逻辑但不合乎理的话来,然后你再大举骂他,骂到体无完肤为止,而原来惹动你的小题目,轻轻一提便了。
43 |
44 | \item 远交近攻
45 |
46 | \hspace{1.8em}一个时候,只能骂一个人,或一种人,或一派人。决不宜多树敌。所以骂人的时候,万勿连累旁人,即使必须牵涉多人,你也要表示好意,否则回骂之声纷至沓来,使你无从应付。
47 |
48 | \end{enumerate}
49 |
50 | \hspace{2em}骂人的艺术,一时所能想起来的有上面十条,信手拈来,并无条理。我做此文的用意,是助人骂人。同时也是想把骂人的技术揭破一点,供爱骂人者参考。挨骂的人看看,骂人的心理原来是这样的,也算是揭破一张黑幕给你瞧瞧!
51 |
--------------------------------------------------------------------------------
/src/numbertheory/Miller-Rabin.cpp:
--------------------------------------------------------------------------------
1 | // 复杂度可以认为是常数
2 |
3 | // 用一个数检测
4 | // 需要调用long long快速幂和O(1)快速乘
5 | bool check(long long n, long long b) { // b: base
6 | long long a = n - 1;
7 | int k = 0;
8 |
9 | while (a % 2 == 0) {
10 | a /= 2;
11 | k++;
12 | }
13 |
14 | long long t = qpow(b, a, n); // 这里的快速幂函数需要写O(1)快速乘
15 | if (t == 1 || t == n - 1)
16 | return true;
17 |
18 | while (k--) {
19 | t = mul(t, t, n); // mul是O(1)快速乘函数
20 | if(t == n - 1)
21 | return true;
22 | }
23 |
24 | return false;
25 | }
26 |
27 |
28 | // 封装好的函数体
29 | // 需要调用check
30 | bool Miller_Rabin(long long n) {
31 | if (n == 1)
32 | return false;
33 | if (n == 2)
34 | return true;
35 | if (n % 2 == 0)
36 | return false;
37 |
38 | // int范围内只需要检查 {2, 7, 61}
39 | // long long范围内只需要检查 {2, 325, 9375, 28178, 450775, 9780504, 1795265022}
40 |
41 | for (int i : {2, 325, 9375, 28178, 450775, 9780504, 1795265022}) {
42 | if (i >= n)
43 | break;
44 | if (!check(n, i))
45 | return false;
46 | }
47 |
48 | return true;
49 | }
50 |
--------------------------------------------------------------------------------
/src/numbertheory/O(n)求逆元.cpp:
--------------------------------------------------------------------------------
1 | inv[1] = 1; // 如果 p 是素数,那 p % i 必不可能是 0
2 | for (int i = 2; i <= n; i++)
3 | inv[i] = (long long)(p - (p / i)) * inv[p % i] % p;
4 |
--------------------------------------------------------------------------------
/src/numbertheory/PN.tex:
--------------------------------------------------------------------------------
1 | 注意 Powerful Number 筛只能求一些 \textbf{特殊}\ 的 \textbf{积性函数}\ 的前缀和。
2 |
3 | 本质上就是构造一个方便求前缀和的函数,然后做类似杜教筛的操作。
4 |
5 | 定义 Powerful Number 表示每个质因子幂次都大于 $1$ 的数,显然最多有 $\sqrt n$ 个。
6 |
7 | 设我们要求和的函数是 $f(n)$,构造一个 \textit{方便求前缀和的}\ \textbf{积性}\ 函数 $g(n)$ 使得 $g(p) = f(p)$。
8 |
9 | 那么就存在一个积性函数 $h = f * g ^ {-1}$,也就是 $f = g *h$。可以证明 $h(p) = 0$,所以只有 Powerful Number 的 $h$ 值不为 $0$。
10 |
11 | $$ _f(i) = \sum_{d = 1} ^ n h(d) S_g \left( \left\lfloor \frac n d \right\rfloor \right) $$
12 |
13 | 只需要枚举每个 Powerful Number 作为 $d$,然后用杜教筛计算 $g$ 的前缀和即可。
14 |
15 | 求 $h(d)$ 时要先预处理 $h(p^k)$,显然有
16 |
17 | $$ h \left(p ^ k \right) = f \left(p ^ k \right) - \sum_{i = 1} ^ k g \left( p ^ i \right) h \left( p ^ {k - i} \right) $$
18 |
19 | 处理完之后 DFS 就行了。(显然只需要筛 $\sqrt n$ 以内的质数。)
20 |
21 | 复杂度取决于杜教筛的复杂度,特殊题目构造的好也可以做到 $O \left( \sqrt n \right)$。
22 |
23 | 例题:
24 |
25 | \begin{itemize}
26 | \item $f \left( p ^ k \right) = p ^ k \left( p ^ k - 1 \right)$:$g(n) = \text{id}(n) \varphi(n)$。
27 | \item $f \left( p ^ k \right) = p \, \text{xor} \, k$:$n$ 为偶数时 $g(n) = 3 \varphi(n)$,否则 $g(n) = \varphi(n)$。
28 | \end{itemize}
29 |
--------------------------------------------------------------------------------
/src/numbertheory/Pollard-Rho.cpp:
--------------------------------------------------------------------------------
1 | // 注意,虽然Pollard's Rho的理论复杂度是O(n ^ {1 / 4})的,
2 | // 但实际跑起来比较慢, 一般用于做long long范围内的质因数分解
3 |
4 |
5 | // 封装好的函数体
6 | // 需要调用solve
7 | void factorize(long long n, vector &v) { // v用于存分解出来的质因子, 重复的会放多个
8 | for (int i : {2, 3, 5, 7, 11, 13, 17, 19})
9 | while (n % i == 0) {
10 | v.push_back(i);
11 | n /= i;
12 | }
13 |
14 | solve(n, v);
15 | sort(v.begin(), v.end()); // 从小到大排序后返回
16 | }
17 |
18 | // 递归过程
19 | // 需要调用Pollard's Rho主过程, 同时递归调用自身
20 | void solve(long long n, vector &v) {
21 | if (n == 1)
22 | return;
23 |
24 | long long p;
25 | do
26 | p = Pollards_Rho(n);
27 | while (!p); // p是任意一个非平凡因子
28 |
29 | if (p == n) {
30 | v.push_back(p); // 说明n本身就是质数
31 | return;
32 | }
33 |
34 | solve(p, v); // 递归分解两半
35 | solve(n / p, v);
36 | }
37 |
38 | // Pollard's Rho主过程
39 | // 需要使用Miller-Rabin作为子算法
40 | // 同时需要调用O(1)快速乘和gcd函数
41 | long long Pollards_Rho(long long n) {
42 | // assert(n > 1);
43 |
44 | if (Miller_Rabin(n))
45 | return n;
46 |
47 | long long c = rand() % (n - 2) + 1, i = 1, k = 2, x = rand() % (n - 3) + 2, u = 2; // 注意这里rand函数需要重定义一下
48 | while (true) {
49 | i++;
50 | x = (mul(x, x, n) + c) % n; // mul是O(1)快速乘函数
51 |
52 | long long g = gcd((u - x + n) % n, n);
53 | if (g > 1 && g < n)
54 | return g;
55 |
56 | if (u == x)
57 | return 0; // 失败, 需要重新调用
58 |
59 | if (i == k) {
60 | u = x;
61 | k *= 2;
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/src/numbertheory/crt.tex:
--------------------------------------------------------------------------------
1 | $$ x \equiv a_i \pmod {m_i} $$
2 |
3 | $$ M = \prod_i m_i,\; M_i = \frac M {m_i} $$
4 |
5 | $$ M_i' \equiv M_i^{-1} \pmod {m_i} $$
6 |
7 | $$ x \equiv \sum_{i} a_i M_i M_i' \pmod M $$
--------------------------------------------------------------------------------
/src/numbertheory/excrt.tex:
--------------------------------------------------------------------------------
1 | 设两个方程分别是 $x\equiv a_1 \pmod {m_1}$ 和 $x\equiv a_2 \pmod {m_2}$。
2 |
3 | 将它们转化为不定方程 $x = m_1 p + a_1 = m_2 q + a_2$,其中 $p, q$ 是整数,则有 $m_1 p - m_2 q = a_2 - a_1$。
4 |
5 | 当 $a_2 - a_1$ 不能被 $\gcd(m_1, m_2)$ 整除时无解,否则可以通过扩展欧几里德解出来一组可行解 $(p, q)$。
6 |
7 | 则原来的两方程组成的模方程组的解为 $x\equiv b\pmod M$,其中 $b = m_1 p + a_1$,$M = \text{lcm}(m_1, m_2)$。
8 |
--------------------------------------------------------------------------------
/src/numbertheory/exgcd.cpp:
--------------------------------------------------------------------------------
1 | void exgcd(LL a, LL b, LL &c, LL &x, LL &y) {
2 | if (b == 0) {
3 | c = a;
4 | x = 1;
5 | y = 0;
6 | return;
7 | }
8 |
9 | exgcd(b, a % b, c, x, y);
10 |
11 | LL tmp = x;
12 | x = y;
13 | y = tmp - (a / b) * y;
14 | }
--------------------------------------------------------------------------------
/src/numbertheory/exgcd.tex:
--------------------------------------------------------------------------------
1 | \subsubsection{求通解的方法}
2 |
3 | 假设我们已经找到了一组解 $(p_0, q_0)$ 满足 $a p_0 + b q_0 = \gcd(a,b)$,那么其他的解都满足
4 |
5 | $$p = p_0 + \frac b {\gcd(p, q)} \times t\quad\quad q = q_0 - \frac a {\gcd(p, q)} \times t$$
6 |
7 | 其中 $t$ 为任意整数。
8 |
--------------------------------------------------------------------------------
/src/numbertheory/min25.cpp:
--------------------------------------------------------------------------------
1 | constexpr int MAXN = 200005, p = (int)1e9 + 7;
2 |
3 | bool notp[MAXN];
4 | int prime[MAXN / 5], prime_cnt;
5 |
6 | // 线性筛省略
7 |
8 | long long divs[MAXN]; // n 的所有整除结果
9 | int sqrtn;
10 |
11 | void get_divs(long long n) {
12 | for (int i = 1; i <= sqrtn; i++)
13 | divs[++divs[0]] = i;
14 |
15 | for (int i = sqrtn; i; i--)
16 | divs[++divs[0]] = n / i;
17 |
18 | assert(is_sorted(divs + 1, divs + divs[0] + 1));
19 | divs[0] = unique(divs + 1, divs + divs[0] + 1) - divs - 1;
20 | }
21 |
22 | int getid(long long x) { // divs[divs[0]] 就是 n
23 | return x <= sqrtn ? x : (divs[0] - divs[divs[0]] / x + 1);
24 | }
25 |
26 | int f(long long n) {
27 | n %= p;
28 | return n * (n - 1) % p;
29 | }
30 |
31 | int g[MAXN];
32 | int dp[MAXN], prime_sum[MAXN];
33 |
34 | void get_dp(function pw, function sum) {
35 | memset(dp, 0, sizeof(dp));
36 | memset(prime_sum, 0, sizeof(prime_sum));
37 |
38 | for (int i = 1; i <= prime_cnt; i++)
39 | prime_sum[i] = (prime_sum[i - 1] + pw(prime[i])) % p;
40 |
41 | for (int i = 1; i <= divs[0]; i++)
42 | dp[i] = (sum(divs[i]) + p - 1) % p;
43 |
44 | for (int i = 1; i <= prime_cnt; i++) {
45 | int pi = prime[i];
46 |
47 | for (int j = divs[0]; (long long)pi * pi <= divs[j]; j--) {
48 | int k = getid(divs[j] / pi);
49 |
50 | dp[j] = (dp[j] + (long long)pw(pi) * (prime_sum[i - 1] - dp[k])) % p;
51 | if (dp[j] < 0)
52 | dp[j] += p;
53 | }
54 | }
55 | }
56 |
57 | void calc(long long n) {
58 | get_divs(n);
59 |
60 | get_dp([] (long long x) {
61 | x %= p;
62 | return x * x % p;
63 | }, [] (long long n) {
64 | n %= p;
65 | return n * (n + 1) % p * (2 * n + 1) % p * ((p + 1) / 6) % p;
66 | }); // x ^ 2
67 |
68 | for (int i = 1; i <= divs[0]; i++)
69 | g[i] = dp[i];
70 |
71 | get_dp([] (long long x) {
72 | return x % p;
73 | }, [] (long long n) {
74 | n %= p;
75 | return n * (n + 1) / 2 % p;
76 | }); // x
77 |
78 | for (int i = 1; i <= divs[0]; i++)
79 | g[i] = (g[i] - dp[i] + p) % p;
80 |
81 | memset(prime_sum, 0, sizeof(prime_sum));
82 | for (int i = 1; i <= prime_cnt; i++)
83 | prime_sum[i] = (prime_sum[i - 1] + f(prime[i])) % p;
84 | }
85 |
86 | int S(int i, long long n) {
87 | if (prime[i] > n || n < 1)
88 | return 0;
89 |
90 | int sq = sqrt(n + 0.5);
91 | int tmp = (g[getid(n)] - prime_sum[i - 1] + p) % p;
92 |
93 | for (int k = i; k <= prime_cnt && prime[k] <= sq; k++) {
94 | int pk = prime[k];
95 | long long pw = pk;
96 |
97 | for (int c = 1; pw * pk <= n; c++, pw *= pk)
98 | tmp = (tmp + (long long)S(k + 1, n / pw) * f(pw) + f(pw * pk)) % p;
99 | }
100 |
101 | return tmp;
102 | }
103 |
104 | int main() {
105 |
106 | long long n;
107 | cin >> n;
108 |
109 | sqrtn = 1; // sqrtn 是全局的
110 | while ((long long)(sqrtn + 1) * (sqrtn + 1) <= n)
111 | sqrtn++;
112 |
113 | get_table(sqrtn);
114 |
115 | calc(n);
116 |
117 | int ans = (S(1, n) + 1) % p;
118 |
119 | cout << ans << endl;
120 |
121 | return 0;
122 | }
123 |
--------------------------------------------------------------------------------
/src/numbertheory/min25.tex:
--------------------------------------------------------------------------------
1 | 问题仍然是计算 \textbf{积性函数}\ $f(n)$ 的前 $n$ 项和。
2 |
3 | 设 $\sqrt n$ 以内的质数为 $p_1 \dots p_{\pi\left(\sqrt n\right)}$,记
4 |
5 | $$ g(n) = \sum_{i = 1} ^ n \left[i \in \mathbb{P}\right] f(i) $$
6 |
7 | 也就是只考虑质数项的和。
8 |
9 | 为了方便求出 $g$,构造一个多项式函数 $F(x) = \sum a_i x^i$,满足 $F(p) = f(p)$,这样每个次数就可以分开算贡献。($f(p^c)$ 的形式是无所谓的,只需要能直接求就行。)
10 |
11 | 再令
12 |
13 | $$ h_k(i, n) = \sum_{x = 2} ^ n \left[ x \in \mathbb{P}\ \text{或}\ x\ \text{与前}\ i\ \text{个质数互质} \right] x^k $$
14 |
15 | 显然 $g(n) = \sum_k a_k h_k\left( \pi\left(\sqrt n\right), n \right)$,递推求出所有 $h_k$ 即可得到 $g$。
16 |
17 | 考虑 $h$ 的转移,当 $p_i > \sqrt n$ 时显然有 $h_k(i, n) = h_k(i - 1, n)$,否则有
18 |
19 | $$ h_k(i, n) = h_k(i - 1, n) - p_i ^ k h_k\left( i - 1, \left\lfloor \frac n {p_i} \right\rfloor \right) + p_i ^ k \sum_{j = 1} ^ {i - 1} p_j ^ k $$
20 |
21 | 边界为 $h_k(0, n) = \sum_{i = 2} ^ n i^k$。
22 |
23 | 求出 $g$ 之后,为了得到所有 $f(i)$ 之和还需要一次递推。设
24 |
25 | $$ S(i, n) = \sum_{k = 2} ^ n \left[ k\ \text{与前}\ (i - 1)\ \text{个质数互质} \right] f(k) $$
26 |
27 | 则
28 |
29 | $$ \begin{aligned} S(i, n) = & g(n) - \sum_{k = 1} ^ {i - 1} f(p_k) \\
30 | + & \sum_{k = i} ^ {p_k \le \sqrt n} \sum_{c = 1} ^ {p_k ^ {c + 1} \le n} \left( S\left( k + 1, \left\lfloor \frac n {p_k ^ c} \right\rfloor \right) f\left( p_k ^ c \right) + f\left( p_k ^ {c + 1} \right) \right) \end{aligned} $$
31 |
32 | 这里直接递归即可,注意边界应设为 $p_i > n$ 或 $n < 1$ 时 $S(i, n) = 0$。最后的答案即为 $\text{ans} = S(1, n) + f(1)$。
33 |
34 | 也可以从大到小枚举 $i$ 递推,考虑到只有 $p_i \le \sqrt n$ 时才有递推,可以后缀和优化。不优化的复杂度是 $O(n^{1 - \epsilon})$,优化之后是 $O\left( \frac {n^{\frac 3 4}} {\log n} \right)$,不过一般是不优化更快。
35 |
36 | \inputminted{cpp}{../src/numbertheory/min25.cpp}
37 |
--------------------------------------------------------------------------------
/src/numbertheory/二次剩余.cpp:
--------------------------------------------------------------------------------
1 | int p, w;
2 |
3 | struct pi {
4 | int a, b; // a + b * sqrt(w)
5 |
6 | pi(int a = 0, int b = 0) : a(a), b(b) {}
7 |
8 | friend pi operator * (const pi &u, const pi &v) {
9 | return pi(((long long)u.a * v.a + (long long)u.b * v.b % p * w) % p,
10 | ((long long)u.a * v.b + (long long)u.b * v.a) % p);
11 | }
12 | };
13 |
14 | pi qpow(pi a, int b) {
15 | pi ans(1, 0);
16 |
17 | while (b) {
18 | if (b & 1)
19 | ans = ans * a;
20 |
21 | b >>= 1;
22 | a = a * a;
23 | }
24 |
25 | return ans;
26 | }
27 |
28 | int qpow(int a, int b) {
29 | int ans = 1;
30 |
31 | while (b) {
32 | if (b & 1)
33 | ans = (long long)ans * a % p;
34 |
35 | b >>= 1;
36 | a = (long long)a * a % p;
37 | }
38 |
39 | return ans;
40 | }
41 |
42 | int my_legendre(int a) { // std有同名函数, 最好换个名字, 不然传了两个数都查不出来
43 | return qpow(a, (p - 1) / 2);
44 | }
45 |
46 | int quadratic_residual(int b, int mod) {
47 | p = mod;
48 |
49 | if (p == 2)
50 | return 1;
51 |
52 | if (my_legendre(b) == p - 1)
53 | return -1; // 无解
54 |
55 | int a;
56 | do {
57 | a = rand() % p;
58 | w = ((long long)a * a - b) % p;
59 | if (w < 0)
60 | w += p;
61 | } while (my_legendre(w) != p - 1);
62 |
63 | return qpow(pi(a, 1), (p + 1) / 2).a;
64 | }
--------------------------------------------------------------------------------
/src/numbertheory/公式.tex:
--------------------------------------------------------------------------------
1 | \subsubsection[狄利克雷(Dirichlet)卷积]{Dirichlet 卷积}
2 | $$ (f * g)(n) = \sum_{d | n} f(d) g\left( \frac n d \right) $$
3 |
4 | $$\mu * I = e$$
5 |
6 | 其中 $e$ 为单位元,满足 $e(n) = [n = 1]$。
7 |
8 | $$\varphi * I = id $$
9 |
10 | $$\mu * id = \varphi $$
11 |
12 | $$\sigma_0 = I * I ,\, \sigma_1 = id * I ,\, \sigma_k = id^{k - 1} * I$$
13 |
14 | \subsubsection{莫比乌斯反演}
15 | $$ \begin{aligned}
16 | f(n) = & \sum_{d | n} g(d) & \, \iff \quad & g(n) = \sum_{d | n} \mu\left( \frac n d \right) f(d) \\
17 | f(d) = & \sum_{d | k} g(k) & \, \iff \quad & g(d) = \sum_{d | k} \mu\left( \frac k d \right) f(k)
18 | \end{aligned} $$
19 |
20 | $$\sum_{i = 1} ^ n \sum_{j = 1} ^ m \left[(i, j) = d\right] = \sum_{d | k} \mu\left( \frac k d \right) \left\lfloor \frac n k \right\rfloor \left\lfloor \frac m k \right\rfloor$$
21 |
22 | \subsubsection{降幂公式}
23 |
24 | $$ a^k \equiv a ^ {k \bmod \varphi(p) + \varphi(p)} \pmod p,\; k \ge \varphi(p) $$
25 |
26 | 和费马小定理的区别在于不需要 $a$ 与 $p$ 互质,但要注意 $k < \varphi(p)$ 时不成立。
27 |
28 | \subsubsection{其他常用公式}
29 |
30 | $$\sum_{i = 1} ^ n \left[(i, n) = 1\right] i = n \frac {\varphi(n) + e(n)} 2$$
31 |
32 | $$\sum_{i = 1} ^ n \sum_{j = 1} ^ i \left[(i, j) = d\right] = S_\varphi \left( \left\lfloor \frac n d \right\rfloor \right)$$
33 |
34 | $$ \sum_{i = 1} ^ n f(i) \sum_{j = 1} ^ {\left\lfloor \frac n i \right\rfloor} g(j) = \sum_{i = 1} ^ n g(i) \sum_{j = 1} ^ {\left\lfloor \frac n i \right\rfloor} f(j) $$
35 |
--------------------------------------------------------------------------------
/src/numbertheory/原根.tex:
--------------------------------------------------------------------------------
1 | \paragraph{阶} 最小的整数 $k$ 使得 $a ^ k \equiv 1 \pmod m$,记为 $\delta_m(a)$。
2 |
3 | 显然 $a$ 在阶以下次数的幂是两两不同的。
4 |
5 | 一个性质:如果 $a, b$ 均与 $m$ 互质,则
6 | $$\delta_m(ab)=\delta_m(a)\delta_m(b) \iff \gcd\left(\delta_m(a),\delta_p(b)\right) = 1$$
7 |
8 | 另外,如果 $a$ 与 $m$ 互质,则有 $\delta_p(a^k)=\dfrac{\delta_p(a)}{\gcd\left(\delta_p(a),k\right)}$。(也就是长为 $\delta_m(a)$ 的环上一次跳 $k$ 步的周期。)
9 |
10 | \paragraph{原根} 所有阶等于 $\varphi(m)$ 的数。
11 |
12 | 只有形如$2,\, 4,\, p^k,\, 2 p^k$($p$是奇素数)的数才有原根。并且如果 $m$ 有原根,那么原根的个数一定是 $\varphi\left(\varphi(m)\right)$ 个。
13 |
14 | 暴力找原根代码:
15 |
16 | \begin{minted}{python}
17 | def split(n): # 分解质因数
18 | i = 2
19 | a = []
20 | while i * i <= n:
21 | if n % i == 0:
22 | a.append(i)
23 |
24 | while n % i == 0:
25 | n /= i
26 |
27 | i += 1
28 |
29 | if n > 1:
30 | a.append(n)
31 |
32 | return a
33 |
34 | def getg(p): # 找原根
35 | def judge(g):
36 | for i in d:
37 | if pow(g, (p - 1) / i, p) == 1:
38 | return False
39 | return True
40 |
41 | d = split(p - 1)
42 | g = 2
43 |
44 | while not judge(g):
45 | g += 1
46 |
47 | return g
48 |
49 | print(getg(int(input())))
50 | \end{minted}
51 |
--------------------------------------------------------------------------------
/src/numbertheory/扩展线性筛.cpp:
--------------------------------------------------------------------------------
1 | constexpr int p = 1000000007;
2 |
3 | int prime[MAXN / 10], sigma_one[MAXN], f[MAXN], g[MAXN];
4 | // f:除掉最小质因子后剩下的部分
5 | // g:最小质因子的幂次,在 f(p^k) 比较复杂时很有用,但 f(p^k) 可以递推时就可以省略了
6 | // 这里没有记录最小质因子,但根据线性筛的性质,每个合数只会被它最小的质因子筛掉
7 | bool notp[MAXN]; // 顾名思义
8 |
9 | void get_table(int n) {
10 | sigma_one[1] = 1; // 积性函数必有f(1) = 1
11 | for (int i = 2; i <= n; i++) {
12 | if (!notp[i]) { // 质数情况
13 | prime[++prime[0]] = i;
14 | sigma_one[i] = i + 1;
15 | f[i] = g[i] = 1;
16 | }
17 |
18 | for (int j = 1; j <= prime[0] && i * prime[j] <= n; j++) {
19 | notp[i * prime[j]] = true;
20 |
21 | if (i % prime[j]) { // 加入一个新的质因子,这种情况很简单
22 | sigma_one[i * prime[j]] = (long long)sigma_one[i] * (prime[j] + 1) % p;
23 | f[i * prime[j]] = i;
24 | g[i * prime[j]] = 1;
25 | }
26 | else { // 再加入一次最小质因子,需要再进行分类讨论
27 | f[i * prime[j]] = f[i];
28 | g[i * prime[j]] = g[i] + 1;
29 | // 对于 f(p^k) 可以直接递推的函数,这里的判断可以改成
30 | // i / prime[j] % prime[j] != 0, 这样可以省下 f[] 的空间,
31 | // 但常数很可能会稍大一些
32 |
33 | if (f[i] == 1) // 质数的幂次,这里 $\sigma_1$ 可以递推
34 | sigma_one[i * prime[j]] = (sigma_one[i] + i * prime[j]) % p;
35 | // 对于更一般的情况,可以借助 g[] 计算 f(p^k)
36 | else sigma_one[i * prime[j]] = // 否则直接利用积性,两半乘起来
37 | (long long)sigma_one[i * prime[j] / f[i]] * sigma_one[f[i]] % p;
38 | break;
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/numbertheory/杜教筛.cpp:
--------------------------------------------------------------------------------
1 | // 用于求可以用狄利克雷卷积构造出好求和的东西的函数的前缀和(有点绕)
2 | // 有些题只要求n <= 10 ^ 9, 这时就没必要开long long了, 但记得乘法时强转
3 |
4 | //常量/全局变量/数组定义
5 | const int maxn = 5000005, table_size = 5000000, p = 1000000007, inv_2 = (p + 1) / 2;
6 | bool notp[maxn];
7 | int prime[maxn / 20], phi[maxn], tbl[100005];
8 | // tbl用来顶替哈希表, 其实开到n ^ {1 / 3}就够了, 不过保险起见开成\sqrt n比较好
9 | long long N;
10 |
11 | // 主函数前面加上这么一句
12 | memset(tbl, -1, sizeof(tbl));
13 |
14 | // 线性筛预处理部分略去
15 |
16 | // 杜教筛主过程 总计O(n ^ {2 / 3})
17 | // 递归调用自身
18 | // 递推式还需具体情况具体分析,这里以求欧拉函数前缀和(mod 10 ^ 9 + 7)为例
19 | int S(long long n) {
20 | if (n <= table_size)
21 | return phi[n];
22 | else if (~tbl[N / n])
23 | return tbl[N / n];
24 | // 原理: n除以所有可能的数的结果一定互不相同
25 |
26 | int ans = 0;
27 | for (long long i = 2, last; i <= n; i = last + 1) {
28 | last = n / (n / i);
29 | ans = (ans + (last - i + 1) % p * S(n / i)) % p; // 如果n是int范围的话记得强转
30 | }
31 |
32 | ans = (n % p * ((n + 1) % p) % p * inv_2 - ans + p) % p; // 同上
33 | return tbl[N / n] = ans;
34 | }
--------------------------------------------------------------------------------
/src/numbertheory/杜教筛.tex:
--------------------------------------------------------------------------------
1 | $$ S_{\varphi}(n) = \frac {n(n + 1)} 2 - \sum_{d = 2} ^ n S_{\varphi} \left( \left\lfloor \frac n d \right\rfloor \right) $$
2 |
3 | $$ S_\mu (n) = 1 - \sum_{d = 2} ^ n S_\mu \left( \left\lfloor \frac n d \right\rfloor \right) $$
4 |
5 | \inputminted{cpp}{../src/numbertheory/杜教筛.cpp}
6 |
--------------------------------------------------------------------------------
/src/numbertheory/洲阁筛.cpp:
--------------------------------------------------------------------------------
1 | constexpr int MAXN = 200005, p = 1000000007;
2 |
3 | long long N, divs[MAXN]; // 询问的 n 和存储所有整除结果的表
4 | int sqrtn;
5 |
6 | inline int getid(long long x) {
7 | if (x <= sqrtn)
8 | return x;
9 |
10 | return divs[0] - N / x + 1;
11 | }
12 |
13 | bool notp[MAXN];
14 | int prime[MAXN], prime_cnt, rem[MAXN]; // 线性筛用数组
15 |
16 | int f[MAXN], pr[MAXN], g[2][MAXN], dp[MAXN];
17 | int l[MAXN], r[MAXN];
18 |
19 | // 线性筛省略
20 |
21 | inline int get_sum(long long n, int k) {
22 | n %= p;
23 |
24 | if (k == 1)
25 | return n * (n + 1) % p * ((p + 1) / 2) % p;
26 |
27 | else
28 | return n * (n + 1) % p * (2 * n + 1) % p * ((p + 1) / 6) % p;
29 | }
30 |
31 | void get_dp(long long n, int k, int *dp) {
32 | for (int j = 1; j <= divs[0]; j++)
33 | dp[j] = get_sum(divs[j], k);
34 |
35 | for (int i = 1; i <= prime_cnt; i++) {
36 | long long lb = (long long)prime[i] * prime[i];
37 | int pw = (k == 1 ? prime[i] : (int)(lb % p));
38 |
39 | pr[i] = (pr[i - 1] + pw) % p;
40 |
41 | for (int j = divs[0]; j && divs[j] >= lb; j--) {
42 | int t = getid(divs[j] / prime[i]);
43 |
44 | int tmp = dp[t];
45 | if (l[t] < i)
46 | tmp = (tmp - pr[min(i - 1, r[t])] + pr[l[t]]) % p;
47 |
48 | dp[j] = (dp[j] - (long long)pw * tmp) % p;
49 | if (dp[j] < 0)
50 | dp[j] += p;
51 | }
52 | }
53 |
54 | for (int j = 1; j <= divs[0]; j++) {
55 | dp[j] = (dp[j] - pr[r[j]] + pr[l[j]]) % p;
56 |
57 | dp[j] = (dp[j] + p - 1) % p; // 因为DP数组是有1的, 但后面计算不应该有1
58 | }
59 | }
60 |
61 | int calc1(long long n) {
62 | get_dp(n, 1, g[0]);
63 | get_dp(n, 2, g[1]);
64 |
65 | int ans = 0;
66 |
67 | for (int i = 1; i <= sqrtn; i++)
68 | ans = (ans + (long long)f[i] * (g[1][getid(N / i)] - g[0][getid(N / i)])) % p;
69 |
70 | if (ans < 0)
71 | ans += p;
72 |
73 | return ans;
74 | }
75 |
76 | int calc2(long long n) {
77 | for (int j = 1; j <= divs[0]; j++)
78 | dp[j] = 1;
79 |
80 | for (int i = 1; i <= prime_cnt; i++)
81 | pr[i] = (pr[i - 1] + f[prime[i]]) % p;
82 |
83 | for (int i = prime_cnt; i; i--) {
84 | long long lb = (long long)prime[i] * prime[i];
85 |
86 | for (int j = divs[0]; j && divs[j] >= lb; j--)
87 | for (long long pc = prime[i]; pc <= divs[j]; pc *= prime[i]) {
88 | int t = getid(divs[j] / pc);
89 |
90 | int tmp = dp[t];
91 | if (r[t] > i)
92 | tmp = (tmp + pr[r[t]] - pr[max(i, l[t])]) % p;
93 |
94 | dp[j] = (dp[j] + pc % p * ((pc - 1) % p) % p * tmp) % p;
95 | }
96 | }
97 |
98 | return (long long)(dp[divs[0]] + pr[r[divs[0]]] - pr[l[divs[0]]] + p) % p;
99 | }
100 |
101 | int main() {
102 | // ios::sync_with_stdio(false);
103 |
104 | cin >> N;
105 |
106 | sqrtn = (int)sqrt(N);
107 |
108 | get_table(sqrtn);
109 |
110 | for (int i = 1; i <= sqrtn; i++)
111 | divs[++divs[0]] = i;
112 |
113 | for (int i = 1; i <= sqrtn; i++)
114 | divs[++divs[0]] = N / i;
115 |
116 | sort(divs + 1, divs + divs[0] + 1);
117 |
118 | divs[0] = unique(divs + 1, divs + divs[0] + 1) - divs - 1;
119 |
120 | int li = 0, ri = 0;
121 | for (int j = 1; j <= divs[0]; j++) {
122 | while (ri < prime_cnt && prime[ri + 1] <= divs[j])
123 | ri++;
124 |
125 | while (li <= prime_cnt && (long long)prime[li] * prime[li] <= divs[j])
126 | li++;
127 |
128 | l[j] = li - 1;
129 | r[j] = ri;
130 | }
131 |
132 | cout << (calc1(N) + calc2(N)) % p << endl;
133 |
134 | return 0;
135 | }
136 |
--------------------------------------------------------------------------------
/src/numbertheory/洲阁筛.tex:
--------------------------------------------------------------------------------
1 | (\textbf{比较难写,不建议用。最好用后面的 min25 筛。})
2 |
3 | 计算 \textbf{积性函数}\ $f(n)$ 的前 $n$ 项之和时,我们可以把所有项按照是否有 $> \sqrt n$ 的质因子分两类讨论,最后将两部分的贡献加起来即可。
4 |
5 | \begin{enumerate}
6 |
7 | \item \textbf{有 $> \sqrt n$ 的质因子}
8 |
9 | 显然 $> \sqrt n$ 的质因子幂次最多为 $1$,所以这一部分的贡献就是
10 |
11 | $$ \sum_{i = 1} ^ {\sqrt n} f(i) \sum_{d = \sqrt n + 1} ^ {\left\lfloor \frac n i \right\rfloor} \left[ d \in \mathbb{P} \right] f(d) $$
12 |
13 | 我们可以 DP 后面的和式。由于 $f(p)$ 是一个关于 $p$ 的低次多项式,我们可以对每个次数分别 DP:设 $g_{i, j}$ 表示 $[1, j]$ 中和前 $i$ 个质数都互质的数的 $k$ 次方之和。设 $\sqrt n$ 以内的质数总共有 $m$ 个,显然贡献就转换成了
14 |
15 | $$ \sum_{i = 1} ^ {\sqrt n} i ^ k g_{m, \left\lfloor \frac n i \right\rfloor} $$
16 |
17 | 边界显然就是自然数幂次和,转移是
18 |
19 | $$ g_{i, j} = g_{i - 1, j} - p_i ^ k g_{i - 1, \left\lfloor \frac j {p_i} \right\rfloor} $$
20 |
21 | 也就是减掉和第 $i$ 个质数不互质的贡献。
22 |
23 | 在滚动数组的基础上再优化一下:首先如果 $j < p_i$ 那肯定就只有 $1$ 一个数;另外,如果 $p_i \le j < p_i ^ 2$,显然就有 $g_{i, j} = g_{i - 1, j} - p_i ^ k$。所以对每个 $j$ 记下最大的 $i$ 使得 $p_i ^ 2 \le j$,那么比这个还大的情况就不需要递推了,用到的时候再加上一个前缀和即可。
24 |
25 | \item \textbf{所有质因子都 $\le \sqrt n$}
26 |
27 | 类似的道理,我们继续 DP:$h_{i, j}$ 表示只含有第 $i$ 到 $m$($m = \pi\left(\sqrt n\right)$)个质数作为质因子的所有数的 $f(i)$ 之和。(这里不需要对每个次幂单独 DP 了,另外倒着 DP 是为了方便卡上限。)
28 |
29 | 边界显然是 $h_{m + 1, j} = 1$,转移是
30 |
31 | $$ h_{i, j} = h_{i + 1, j} + \sum_{c} f(p_i ^ c) h_{i + 1, \left\lfloor \frac j {p_i ^ c} \right\rfloor} $$
32 |
33 | 跟上面一样的道理优化,分成三段: $j < p_i$时$h_{i, j} = 1$;$j < p_i ^ 2$ 时 $h_{i, j} = h_{i + 1, j} + f(p_i)$(同样用前缀和解决);再小的部分就老实递推。
34 |
35 | \end{enumerate}
36 |
37 | 预处理 $\sqrt n$ 以内的部分之后跑两次DP,最后把两部分的贡献加起来就行了。两部分的复杂度都是 $\Theta \left( \frac {n ^ {\frac 3 4}} {\log n} \right)$。
38 |
39 | 以下代码以洛谷 P5325($f(p^k) = p^k (p^k - 1)$)为例。
40 |
41 | \inputminted{cpp}{../src/numbertheory/洲阁筛.cpp}
42 |
--------------------------------------------------------------------------------
/src/numbertheory/类欧.cpp:
--------------------------------------------------------------------------------
1 | int solve(int n, int a, int b, int m) {
2 | if (!b)
3 | return n * (a / m);
4 | if (a >= m)
5 | return n * (a / m) + solve(n, a % m, b, m);
6 | if (b >= m)
7 | return (n - 1) * n / 2 * (b / m) + solve(n, a, b % m, m);
8 |
9 | return solve((a + b * n) / m, (a + b * n) % m, m, b);
10 | }
--------------------------------------------------------------------------------
/src/string/AC自动机.cpp:
--------------------------------------------------------------------------------
1 | int ch[maxm][26], f[maxm][26], q[maxm], sum[maxm], cnt = 0;
2 |
3 | // 在字典树中插入一个字符串 O(n)
4 | int insert(const char *c) {
5 | int x = 0;
6 | while (*c) {
7 | if (!ch[x][*c - 'a'])
8 | ch[x][*c - 'a'] = ++cnt;
9 | x = ch[x][*c++ - 'a'];
10 | }
11 | return x;
12 | }
13 |
14 | // 建AC自动机 O(n * sigma)
15 | void getfail() {
16 | int x, head = 0, tail = 0;
17 |
18 | for (int c = 0; c < 26; c++)
19 | if (ch[0][c])
20 | q[tail++] = ch[0][c]; // 把根节点的儿子加入队列
21 |
22 | while (head != tail) {
23 | x = q[head++];
24 |
25 | G[f[x][0]].push_back(x);
26 | fill(f[x] + 1, f[x] + 26, cnt + 1);
27 |
28 | for (int c = 0; c < 26; c++) {
29 | if (ch[x][c]) {
30 | int y = f[x][0];
31 |
32 | f[ch[x][c]][0] = ch[y][c];
33 | q[tail++] = ch[x][c];
34 | }
35 | else
36 | ch[x][c] = ch[f[x][0]][c];
37 | }
38 | }
39 | fill(f[0], f[0] + 26, cnt + 1);
40 | }
--------------------------------------------------------------------------------
/src/string/KMP.cpp:
--------------------------------------------------------------------------------
1 | int fail[maxn];
2 |
3 | void kmp(const char *s, int n) {
4 | fail[0] = fail[1] = 0;
5 |
6 | for (int i = 1; i < n; i++) {
7 | int j = fail[i];
8 |
9 | while (j && s[i + 1] != s[j + 1])
10 | j = fail[j];
11 |
12 | if (s[i + 1] == s[j + 1])
13 | fail[i + 1] = j + 1;
14 | else
15 | fail[i + 1] = 0;
16 | }
17 | }
--------------------------------------------------------------------------------
/src/string/SAMSA.cpp:
--------------------------------------------------------------------------------
1 | bool vis[maxn * 2];
2 | char s[maxn];
3 | int n, id[maxn * 2], ch[maxn * 2][26], height[maxn], tim = 0;
4 |
5 | void dfs(int x) {
6 | if (id[x]) {
7 | height[tim++] = val[last];
8 | sa[tim] = id[x];
9 |
10 | last = x;
11 | }
12 |
13 | for (int c = 0; c < 26; c++)
14 | if (ch[x][c])
15 | dfs(ch[x][c]);
16 |
17 | last = par[x];
18 | }
19 |
20 | int main() {
21 | last = ++cnt;
22 |
23 | scanf("%s", s + 1);
24 | n = strlen(s + 1);
25 |
26 | for (int i = n; i; i--) {
27 | expand(s[i] - 'a');
28 | id[last] = i;
29 | }
30 |
31 | vis[1] = true;
32 | for (int i = 1; i <= cnt; i++)
33 | if (id[i])
34 | for (int x = i, pos = n; x && !vis[x]; x = par[x]) {
35 | vis[x] = true;
36 | pos -= val[x] - val[par[x]];
37 | ch[par[x]][s[pos + 1] - 'a'] = x;
38 | }
39 |
40 | dfs(1);
41 |
42 | for (int i = 1; i <= n; i++) {
43 | if (i > 1)
44 | printf(" ");
45 | printf("%d", sa[i]); // 1-based
46 | }
47 | printf("\n");
48 |
49 | for (int i = 1; i < n; i++) {
50 | if (i > 1)
51 | printf(" ");
52 | printf("%d", height[i]);
53 | }
54 | printf("\n");
55 |
56 | return 0;
57 | }
--------------------------------------------------------------------------------
/src/string/exKMP.cpp:
--------------------------------------------------------------------------------
1 | void exkmp(const char *s, int *a, int n) {
2 | int l = 0, r = 0;
3 | a[0] = n;
4 |
5 | for (int i = 1; i <= n; i++) {
6 | a[i] = i > r ? 0 : min(r - i + 1, a[i - l]);
7 |
8 | while (i + a[i] < n && s[a[i]] == s[i + a[i]])
9 | a[i]++;
10 |
11 | if (i + a[i] - 1 > r) {
12 | l = i;
13 | r = i + a[i] - 1;
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/src/string/manacher.cpp:
--------------------------------------------------------------------------------
1 | // n 为串长,回文半径输出到 p 数组中
2 | // 数组要开串长的两倍
3 | void manacher(const char* t, int n) {
4 | static char s[maxn * 2];
5 |
6 | for (int i = n; i; i--)
7 | s[i * 2] = t[i];
8 | for (int i = 0; i <= n; i++)
9 | s[i * 2 + 1] = '#';
10 |
11 | s[0] = '$';
12 | s[(n + 1) * 2] = '\0';
13 | n = n * 2 + 1;
14 |
15 | int mx = 0, j = 0;
16 |
17 | for (int i = 1; i <= n; i++) {
18 | p[i] = (mx > i ? min(p[j * 2 - i], mx - i) : 1);
19 | while (s[i - p[i]] == s[i + p[i]])
20 | p[i]++;
21 |
22 | if (i + p[i] > mx) {
23 | mx = i + p[i];
24 | j = i;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/string/sa.cpp:
--------------------------------------------------------------------------------
1 | constexpr int MAXN = 1000005;
2 |
3 | void get_sa(char *s, int n, int *sa, int *rnk, int *height) { // 1-based
4 | static int buc[MAXN], id[MAXN], p[MAXN], t[MAXN];
5 |
6 | int m = 300;
7 |
8 | for (int i = 1; i <= n; i++)
9 | buc[rnk[i] = s[i]]++;
10 | for (int i = 1; i <= m; i++)
11 | buc[i] += buc[i - 1];
12 | for (int i = n; i; i--)
13 | sa[buc[rnk[i]]--] = i;
14 |
15 | memset(buc, 0, sizeof(int) * (m + 1));
16 |
17 | for (int k = 1, cnt = 0; cnt != n; k *= 2, m = cnt) {
18 | cnt = 0;
19 |
20 | for (int i = n; i > n - k; i--)
21 | id[++cnt] = i;
22 | for (int i = 1; i <= n; i++)
23 | if (sa[i] > k)
24 | id[++cnt] = sa[i] - k;
25 |
26 | for (int i = 1; i <= n; i++)
27 | buc[p[i] = rnk[id[i]]]++;
28 | for (int i = 1; i <= m; i++)
29 | buc[i] += buc[i - 1];
30 | for (int i = n; i; i--)
31 | sa[buc[p[i]]--] = id[i];
32 |
33 | memset(buc, 0, sizeof(int) * (m + 1));
34 |
35 | memcpy(t, rnk, sizeof(int) * (n + 1));
36 | t[n + 1] = 0; // 记得清空 n + 1
37 |
38 | cnt = 0;
39 | for (int i = 1; i <= n; i++) {
40 | if (t[sa[i]] != t[sa[i - 1]] || t[sa[i] + k] != t[sa[i - 1] + k])
41 | cnt++;
42 |
43 | rnk[sa[i]] = cnt;
44 | }
45 | }
46 |
47 | for (int i = 1; i <= n; i++)
48 | sa[rnk[i]] = i;
49 |
50 | for (int i = 1, k = 0; i <= n; i++) {
51 | if (k)
52 | k--;
53 |
54 | if (rnk[i] > 1) // 两个都要判,否则会左/右越界
55 | while (sa[rnk[i] - 1] + k <= n && s[i + k] == s[sa[rnk[i] - 1] + k])
56 | k++;
57 |
58 | height[rnk[i]] = k;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/string/samlct.cpp:
--------------------------------------------------------------------------------
1 | int tim; // tim实际上就是当前的右端点
2 |
3 | node *access(node *x) {
4 | node *y = null;
5 |
6 | while (x != null) {
7 | splay(x);
8 |
9 | x -> ch[1] = null;
10 | x -> refresh();
11 |
12 | if (x -> val) // val记录的是上次访问时间, 也就是right集合最大值
13 | update(x -> val - val[x -> r] + 1, x -> val - val[par[x -> l]], -1);
14 |
15 | x -> val = tim;
16 | x -> lazy = true;
17 |
18 | update(x -> val - val[x -> r] + 1, x -> val - val[par[x -> l]], 1);
19 |
20 | x -> ch[1] = y;
21 |
22 | (y = x) -> refresh();
23 |
24 | x = x -> p;
25 | }
26 |
27 | return y;
28 | }
29 |
30 | // 以下是main函数中的用法
31 | for (int i = 1; i <= n; i++) {
32 | tim++;
33 | access(null + id[i]);
34 |
35 | if (i >= m) // 例题询问长度是固定的, 如果不固定的话就按照右端点离线即可
36 | ans[i - m + 1] = query(i - m + 1, i);
37 | }
--------------------------------------------------------------------------------
/src/string/区间本质不同子串统计.tex:
--------------------------------------------------------------------------------
1 | \paragraph{问题} 给定一个字符串 $s$,多次询问 $[l, r]$ 区间的本质不同的子串个数,可能强制在线。
2 |
3 | \paragraph{做法} 考虑建出后缀自动机,然后枚举右端点,用线段树维护每个左端点的答案。
4 |
5 | 显然只有 \texttt{right} 集合在 $[l, r]$ 中的串才有可能有贡献,所以我们可以只考虑每个串最大的 \texttt{right}。
6 |
7 | 每次右端点 + 1 时找到它对应的结点 $u$,则 $u$ 到根节点路径上的每个点,它的 \texttt{right} 集合都会被 $r$ 更新。
8 |
9 | 对于某个特定的左端点 $l$,我们需要保证本质不同的子串左端点不能越过它;因此对于一个结点 $p$,我们知道它对应的子串长度 $(val_{par_p}, val_p]$ 之后,在 $p$ 的 \texttt{right} 集合最大值减去对应长度,这样对应的 $l$ 内全部 + 1 即可。这样询问时就只需要查询 $r$ 对应的线段树中 $[l, r]$ 的区间和了。(当然旧的 \texttt{right} 对应的区间也要减掉)
10 |
11 | 实际上可以发现更新时都是把路径分成若干个整段更新 \texttt{right} 集合,因此可以用 LCT 维护这个过程。
12 |
13 | 时间复杂度 $O(n\log ^ 2 n)$,空间$O(n)$。当然如果强制在线的话,就把线段树改成主席树,空间复杂度就和时间复杂度同阶了。
14 |
15 | \inputminted{cpp}{../src/string/samlct.cpp}
16 |
17 | 还有一份完整的代码,因为写起来确实细节挺多的。这份代码支持在尾部加一个字符或者询问区间有多少子串至少出现了 \textbf{两次},并且强制在线。
18 |
19 | \inputminted{cpp}{../src/string/samlct2.cpp}
20 |
--------------------------------------------------------------------------------
/src/string/后缀平衡树.tex:
--------------------------------------------------------------------------------
1 | 如果不需要查询排名,只需要维护前驱后继关系的题目,可以直接用二分哈希 + \texttt{set} 去做。
2 |
3 | 一般的题目需要查询排名,这时候就需要写替罪羊树或者 Treap 维护 \texttt{tag}。插入后缀时,如果首字母相同,只需比较各自删除首字母后的 \texttt{tag} 大小即可。
4 |
5 | (Treap 也具有重量平衡树的性质,每次插入后影响到的子树大小期望是 $O(\log n)$ 的,所以每次做完插入操作之后直接暴力重构子树内tag就行了。)
6 |
--------------------------------------------------------------------------------
/src/string/后缀自动机.cpp:
--------------------------------------------------------------------------------
1 | // 在字符集比较小的时候可以直接开go数组, 否则需要用map或者哈希表替换
2 | // 注意!!!结点数要开成串长的两倍
3 |
4 | // 全局变量与数组定义
5 | int last, val[maxn], par[maxn], go[maxn][26], sam_cnt;
6 | int c[maxn], q[maxn]; // 用来桶排序
7 |
8 | // 在主函数开头加上这句初始化
9 | last = sam_cnt = 1;
10 |
11 | // 以下是按val进行桶排序的代码
12 | for (int i = 1; i <= sam_cnt; i++)
13 | c[val[i] + 1]++;
14 | for (int i = 1; i <= n; i++)
15 | c[i] += c[i - 1]; // 这里n是串长
16 | for (int i = 1; i <= sam_cnt; i++)
17 | q[++c[val[i]]] = i;
18 |
19 | //加入一个字符 均摊O(1)
20 | void extend(int c) {
21 | int p = last, np = ++sam_cnt;
22 | val[np] = val[p] + 1;
23 |
24 | while (p && !go[p][c]) {
25 | go[p][c] = np;
26 | p = par[p];
27 | }
28 |
29 | if (!p)
30 | par[np] = 1;
31 | else {
32 | int q = go[p][c];
33 |
34 | if (val[q] == val[p] + 1)
35 | par[np] = q;
36 | else {
37 | int nq = ++sam_cnt;
38 | val[nq] = val[p] + 1;
39 | memcpy(go[nq], go[q], sizeof(go[q]));
40 |
41 | par[nq] = par[q];
42 | par[np] = par[q] = nq;
43 |
44 | while (p && go[p][c] == q){
45 | go[p][c] = nq;
46 | p = par[p];
47 | }
48 | }
49 | }
50 |
51 | last = np;
52 | }
--------------------------------------------------------------------------------
/src/string/回文树.cpp:
--------------------------------------------------------------------------------
1 | // 定理: 一个字符串本质不同的回文子串个数是O(n)的
2 | // 注意回文树只需要开一倍结点, 另外结点编号也是一个可用的bfs序
3 |
4 | // 全局数组定义
5 | int val[maxn], par[maxn], go[maxn][26], last, cnt;
6 | char s[maxn];
7 |
8 | // 重要!在主函数最前面一定要加上以下初始化
9 | par[0] = cnt = 1;
10 | val[1] = -1;
11 | // 这个初始化和广义回文树不一样, 写普通题可以用, 广义回文树就不要乱搞了
12 |
13 | // extend函数 均摊O(1)
14 | // 向后扩展一个字符
15 | // 传入对应下标
16 | void extend(int n) {
17 | int p = last, c = s[n] - 'a';
18 | while (s[n - val[p] - 1] != s[n])
19 | p = par[p];
20 |
21 | if (!go[p][c]) {
22 | int q = ++cnt, now = p;
23 | val[q] = val[p] + 2;
24 |
25 | do
26 | p = par[p];
27 | while (s[n - val[p] - 1] != s[n]);
28 |
29 | par[q] = go[p][c];
30 | last = go[now][c] = q;
31 | }
32 | else
33 | last = go[p][c];
34 |
35 | // a[last]++;
36 | }
37 |
--------------------------------------------------------------------------------
/src/string/字符串原理.tex:
--------------------------------------------------------------------------------
1 | KMP 和 AC 自动机的 \texttt{fail} 指针存储的都是它在串或者字典树上的最长后缀,因此要判断两个前缀是否互为后缀时可以直接用 \texttt{fail} 指针判断。当然它不能做最长公共后缀,不过可以用一个树链的并来做子串问题。
2 |
3 | 后缀数组利用的主要是 LCP 长度可以按照字典序做 RMQ 的性质,与某个串的 LCP 长度 $\ge$ 某个值的后缀形成一个区间。另外一个比较好用的性质是本质不同的子串个数 = 所有子串数 - 字典序相邻的串的 \texttt{height}。
4 |
5 | 后缀自动机实际上可以接受的是所有后缀, 如果把中间状态也算上的话就是所有子串. 它的 \text{fail} 指针代表的也是当前串的后缀,不过注意每个状态可以代表很多状态,只要右端点在 \texttt{right} 集合中且长度处在 $(val_{par_p}, val_p]$ 中的串都被它代表。
6 |
7 | 后缀自动机的 \texttt{fail} 树也就是 \textbf{反串} 的后缀树。每个结点代表的串和后缀自动机同理,两个串的 LCP 也就是他们在后缀树上的 LCA。
8 |
--------------------------------------------------------------------------------
/src/string/广义后缀自动机.cpp:
--------------------------------------------------------------------------------
1 | int extend(int p, int c) {
2 | int np = 0;
3 |
4 | if (!go[p][c]) {
5 | np = ++sam_cnt;
6 | val[np] = val[p] + 1;
7 | while (p && !go[p][c]) {
8 | go[p][c] = np;
9 | p = par[p];
10 | }
11 | }
12 |
13 | if (!p)
14 | par[np] = 1;
15 | else {
16 | int q = go[p][c];
17 |
18 | if (val[q] == val[p] + 1) {
19 | if (np)
20 | par[np] = q;
21 | else
22 | return q;
23 | }
24 | else {
25 | int nq = ++sam_cnt;
26 | val[nq] = val[p] + 1;
27 | memcpy(go[nq], go[q], sizeof(go[q]));
28 |
29 | par[nq] = par[q];
30 | par[q] = nq;
31 | if (np)
32 | par[np] = nq;
33 |
34 | while (p && go[p][c] == q){
35 | go[p][c] = nq;
36 | p = par[p];
37 | }
38 |
39 | if (!np)
40 | return nq;
41 | }
42 | }
43 |
44 | return np;
45 | }
46 | // 调用的时候直接last = 1然后一路调用last = extend(last, c)就行了
--------------------------------------------------------------------------------
/备忘.md:
--------------------------------------------------------------------------------
1 | # 备忘
2 |
3 | Tags: 乱搞
4 |
5 | ---
6 |
7 | [好东西合集](http://codeforces.com/blog/entry/57282)
8 | [带花树&HK笔记](http://www.csie.ntnu.edu.tw/~u91029/Matching.html#5)
9 | [可持久化可并堆求k短路](https://blog.bill.moe/kth-shortest-path-notes/)
10 | [长链剖分学习笔记](https://blog.bill.moe/long-chain-subdivision-notes/)
11 | [扩展埃氏筛法线性的证明(逃](https://www.cnblogs.com/zbh2047/p/8552551.html)
12 | [扩展埃氏筛法讲解](https://www.spoj.com/problems/TEES/)
13 | [Public Based Data Structures](https://gcc.gnu.org/onlinedocs/libstdc%2B%2B/ext/pb_ds/)
14 | [KaTeX语法参考手册](https://khan.github.io/KaTeX/docs/supported.html)
15 | [凹函数DP的O(n)做法](https://dl.acm.org/citation.cfm?id=79800)
16 |
17 | 我的emacs设置
18 | ```lisp
19 | (ido-mode t)
20 | (setq-default cursor-type 'bar)
21 | (setq c-basic-offset 4) ; c c++ 缩进4个空格
22 | (setq default-tab-width 4)
23 | (defun compile-cpp ()
24 | (interactive)
25 | (delete-other-windows)
26 | (split-window-right)
27 | (compile compile-command))
28 | (defun run-shell ()
29 | (interactive)
30 | (delete-other-windows)
31 | (split-window-right)
32 | (other-window 1)
33 | (shell))
34 | (defun my-up ()
35 | (interactive)
36 | (scroll-up 3))
37 | (defun my-down ()
38 | (interactive)
39 | (scroll-down 3))
40 | (global-set-key (kbd "") 'compile-cpp)
41 | (global-set-key (kbd "") 'run-shell)
42 | (global-set-key (kbd "") 'my-up)
43 | (global-set-key (kbd "") 'my-down)
44 | (global-set-key (kbd "C-a") 'mark-whole-buffer)
45 | (global-set-key (kbd "") 'newline-and-indent)
46 | (global-unset-key (kbd "C-x C-z"))
47 | (fset 'yes-or-no-p 'y-or-n-p)
48 | (custom-set-variables
49 | ;; custom-set-variables was added by Custom.
50 | ;; If you edit it by hand, you could mess it up, so be careful.
51 | ;; Your init file should contain only one such instance.
52 | ;; If there is more than one, they won't work right.
53 | '(column-number-mode t)
54 | '(cua-mode t nil (cua-base))
55 | '(custom-enabled-themes (quote (whiteboard)))
56 | '(inhibit-startup-screen t)
57 | '(show-paren-mode t)
58 | '(size-indication-mode t)
59 | '(tool-bar-mode nil))
60 | (custom-set-faces
61 | ;; custom-set-faces was added by Custom.
62 | ;; If you edit it by hand, you could mess it up, so be careful.
63 | ;; Your init file should contain only one such instance.
64 | ;; If there is more than one, they won't work right.
65 | '(default ((t (:family "Courier 10 Pitch" :foundry "bitstream" :slant normal :weight normal :height 113 :width normal)))))
66 |
67 | (defun myc++()
68 | (linum-mode t)
69 | ;;(c-toggle-hungry-state t)
70 | (setq-default tab-width 4)
71 | (set (make-local-variable 'compile-command)
72 | (format "g++ %s.cpp -Wall -Wextra -O2 -std=c++11 -o %s"
73 | (setq file-name (file-name-sans-extension (file-name-nondirectory buffer-file-name)))
74 | file-name file-name)))
75 | (add-hook 'c++-mode-hook 'myc++)
76 |
77 | ;;(setq c-default-style "str"); 没有这个 { } 就会瞎搞
78 | ```
79 |
80 | #### 选项清单:
81 |
82 | - 主题(whiteboard)、字体(Courier 10 Pitch)、Cua Keys(这些在菜单栏里都有)、去掉工具栏、显示文件大小、显示列号(这三项都在菜单栏里的Options里面)
83 | - 光标大小、tab宽度(tab-width)、提示模式(ido-mode)
84 | - 注意括号匹配可以从Options的第二项设置,不必打到文件里了
85 | - compile-cpp(编译cpp)和run-shell(在另一个窗口打开shell)函数
86 | - 改一下滑轮滚动
87 | - 配置编译(f9)、shell(f5)快捷键,调整C-a为全选,重设回车为换行+缩进
88 | - 把"yes/no"改为"y/n"
--------------------------------------------------------------------------------