常用函数 STL算法 1 next_permutation (a.begin (), a.end ())
进制运算的内置函数 (GCC/Clang)1 2 3 __builtin_clz(unsigned int x) __builtin_ctz(unsigned int x) __builtin_popcount(unsigned int x)
C++ 匿名函数(Lambda 表达式) 1 2 3 4 5 6 7 8 9 10 [捕获列表](参数列表) -> 返回类型 { 函数体 } auto add = [](int a, int b) -> int { return a + b; };function<返回类型(传入类型)> func = [&](参数) -> 返回类型 { };
注意 :有的递归lambda不能自动推导函数类型,需手写,例如:function<int(int, int)> 返回int,传入两个int。
基础算法模板 二分查找模板 1 2 3 4 5 6 7 8 9 10 11 int l = 0 , r = INT_MAX;while (l <= r) { int mid = (l + r) / 2 ; if (judge (mid)) { l = mid + 1 ; } else { r = mid - 1 ; } }
二分答案适用场景 :
答案满足单调性(前半部分符合,后半部分不符合)
求最大/最小符合的值
小数精度处理 :
题目要求保留3位小数时,可对每个数乘10000(多一位用于四舍五入)
计算完答案后除以10000.0
三分查找模板 用于单峰函数 (先减后增 或 先增后减)寻找极值点。
1. 实数域(求极小值)
1 2 3 4 5 6 7 8 9 10 double l = 0 , r = 1e9 ;while (r - l > 1e-9 ) { double m1 = (2 * l + r) / 3 ; double m2 = (l + 2 * r) / 3 ; if (f (m1) < f (m2)) r = m2; else l = m1; } double ans = f (l);
2. 整数域(求极小值)
1 2 3 4 5 6 7 8 9 10 11 12 long long l = 0 , r = 1e9 ;while (r - l > 5 ) { long long m1 = (2 * l + r) / 3 ; long long m2 = (l + 2 * r) / 3 ; if (f (m1) < f (m2)) r = m2; else l = m1; } long long ans = f (l);for (long long i = l + 1 ; i <= r; ++i) ans = min (ans, f (i));
3. 求极大值(凹函数)
将比较符号反转即可:
1 if (f (m1) > f (m2)) r = m2; else l = m1;
注意事项 :
函数必须在给定区间内严格单峰
实数域精度 1e-9 可根据题目调整
整数域通常把小区间长度设为3~10,最后暴力枚举
滑动窗口(双端队列) 1 2 3 4 5 6 7 8 9 10 deque<int > dq; for (int i = 0 ; i < n; i++) { while (!dq.empty () && a[dq.back ()] < a[i]) dq.pop_back (); dq.push_back (i); }
核心思路 :维护左边界还是右边界是个重要问题,一般维护右边界的较多。
数学公式与技巧 运算符优先级(从高到低)
() 括号
* / % 乘除取余
+ - 加减
<< >> 移位
< > <= >= 比较运算符
== != 相等判断
& 按位与
^ 按位异或
| 按位或
&& 逻辑与
|| 逻辑或
= 赋值
注意 :位运算符(&, ^, |)的优先级低于 比较运算符!遇到不确定的就加括号。
求和化简 $$i \bmod j = i - [i/j] \times j$$
交集计算 设两不等式 $l_1 \leq x \leq r_1$ 和 $l_2 \leq x \leq r_2$ 的交集:
1 L = max(min(r₁, r₂), max(l₁, l₂), 0)
需要严格保证 $l \leq r$。
整数取整
向下取整:x / t
向上去整:(x + t - 1) / t
一些复杂度 调和级数 : $$\sum_{i=1}^{m} \frac{1}{i} = O(\log m)$$
正方形旋转坐标变换 设子矩阵左上角为 $(x_0, y_0)$,边长为 len。相对坐标:
$i_{rel} = x - x_0 + 1$
$j_{rel} = y - y_0 + 1$
顺时针旋转90°正变换 : $$(i’, j’) = (j, len - i + 1)$$
逆时针旋转90°正变换 : $$(i’, j’) = (len - j + 1, i)$$
逆变换(逆向查询) :
顺时针旋转(op=1):$i = j’, j = len - i’ + 1$(逆时针旋转)
逆时针旋转(op=2):$i = len - j’ + 1, j = i’$(顺时针旋转)
1 2 3 4 5 6 7 if (op.op == 1 ) { new_i_rel = len - j_rel + 1 ; new_j_rel = i_rel; } else { new_i_rel = j_rel; new_j_rel = len - i_rel + 1 ; }
数论 素数相关 埃氏筛 1 2 3 4 5 6 7 8 vector<bool > vis (N + 1 , false ) ;for (int i = 2 ; i <= N; i++) { if (!vis[i]) { for (int j = i * i; j <= N; j += i) { vis[j] = true ; } } }
判断素数 1 2 3 4 5 6 7 8 9 bool isprime (int n) { if (n <= 1 ) return false ; if (n == 2 ) return true ; if (n % 2 == 0 ) return false ; for (int i = 3 ; i * i <= n; i += 2 ) { if (n % i == 0 ) return false ; } return true ; }
Miller-Rabin 素数测试 适用于 $10^{18}$ 范围内的大数素数判断,使用以下12个基底 {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37} 可得到100%准确结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 long long power (long long base, long long exp, long long mod) { long long res = 1 ; base %= mod; while (exp > 0 ) { if (exp % 2 == 1 ) res = (__int128)res * base % mod; base = (__int128)base * base % mod; exp /= 2 ; } return res; } bool miller_rabin_check (long long d, long long n) { long long a = 2 + rand () % (n - 4 ); long long x = power (a, d, n); if (x == 1 || x == n - 1 ) return true ; while (d != n - 1 ) { x = (__int128)x * x % n; d *= 2 ; if (x == 1 ) return false ; if (x == n - 1 ) return true ; } return false ; } bool is_prime (long long n) { if (n < 2 ) return false ; if (n == 2 || n == 3 ) return true ; if (n % 2 == 0 ) return false ; long long bases[] = {2 , 3 , 5 , 7 , 11 , 13 , 17 , 19 , 23 , 29 , 31 , 37 }; long long d = n - 1 ; while (d % 2 == 0 ) d /= 2 ; for (long long a : bases) { if (n == a) return true ; if (power (a, d, n) != 1 ) { long long t = d; bool prime = false ; while (t < n - 1 ) { if (power (a, t, n) == n - 1 ) { prime = true ; break ; } t *= 2 ; } if (!prime) return false ; } } return true ; }
GCD与LCM GCD性质 $$\gcd(a+c, b+c) = \gcd(a+c, b-a)$$
性质解释 : 对于任意整数 $x, y$,有: $$\gcd(x, y) = \gcd(x, y-x)$$
这是因为 $\gcd(x, y)$ 整除 $x$ 和 $y$,所以也整除 $y-x$;反过来,任何整除 $x$ 和 $y-x$ 的数也能整除 $x + (y-x) = y$。因此两个数对的公约数集合完全相同。
常见应用 :
欧几里得算法(辗转相除法) :不断用大数减小数(或取模)
化简GCD问题 :当两个数同时加上同一个数 $c$ 时,它们的差保持不变
求解线性丢番图方程 :若 $\gcd(a, b) = d$,则存在整数 $x, y$ 使 $ax + by = d$
判断互质 :若 $|a-b| = 1$,则两数一定互质
快速幂与逆元 快速幂模板 1 2 3 4 5 6 7 8 9 10 int qpow (int base, int exp, int mod) { int res = 1 ; base = base % mod; while (exp > 0 ) { if (exp & 1 ) res = (long long )res * base % mod; base = (long long )base * base % mod; exp >>= 1 ; } return res; }
乘法逆元 1 2 3 4 5 6 int inv (int n, int mod) { return qpow (n, mod - 2 , mod); }
扩展欧几里得求逆元(mod为非质数) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ll exgcd (ll a, ll b, ll &x, ll &y) { if (!b) { x = 1 ; y = 0 ; return a; } ll d = exgcd (b, a % b, x, y); ll t = x; x = y; y = t - (a / b) * y; return d; } void solve () { ll a, m; cin >> a >> m; ll inv, tmp; exgcd (a, m, inv, tmp); inv = (inv % m + m) % m; cout << inv << '\n' ; }
组合数 阶乘与组合数(结合快速幂逆元取模) 1 2 3 4 5 6 7 8 9 vector<int > fac (n + 1 , 1 ) ;for (int i = 1 ; i <= n; i++) { fac[i] = (long long )fac[i - 1 ] * i % mod; } auto C = [&](int a, int b) -> int { if (a < 0 || b < 0 || b > a) return 0 ; return (long long )fac[a] * inv (fac[b]) % mod * inv (fac[a - b]) % mod; };
隔板法 part(n, k) 的含义
定义:将 $n$ 个相同的物品分成 $k$ 个非空连续组的方法数。
直观理解 :
有 $n$ 个物品排成一行:○ ○ ○ ○ ○(5个)
要分成 $k$ 组,需要在 $n-1$ 个空隙中放 $k-1$ 个隔板。
从 $n-1$ 个空隙选 $k-1$ 个放隔板。
公式推导 :
n个物品 → n-1个空隙
分成k组 → 需要k-1个隔板
从n-1个空隙选k-1个放隔板
选法数 = C(n-1, k-1)
用公式表示:
1 2 3 int part (int n, int k) { return C (n - 1 , k - 1 ); }
线性基 作用
判断一个数能否被异或出来 :通过不断消去高位,看最后是否能得到0
求最大异或和 :贪心从高位到低位,如果异或后变大就异或
求最小异或和 :最小的非0 basis元素
求第k小异或和 :需要重构线性基
创建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 vector<int > a (n + 1 ) ;vector<int > basis (61 , 0 ) ; void insert (int x) { for (int i = 60 ; i >= 0 ; i--) { if (!(x & (1LL << i))) continue ; if (!basis[i]) { basis[i] = x; return ; } x ^= basis[i]; } } for (int i = 1 ; i <= n; i++) { insert (a[i]); }
查询(是否可以异或出某数) 1 2 3 4 5 6 7 8 9 bool query (int x) { for (int i = 60 ; i >= 0 ; i--) { if ((x >> i) & 1 ) { if (!basis[i]) return false ; x ^= basis[i]; } } return x == 0 ; }
得到最大异或 1 2 3 4 5 6 7 int getmax () { int ans = 0 ; for (int i = 60 ; i >= 0 ; i--) { if ((ans ^ basis[i]) > ans) ans ^= basis[i]; } return ans; }
带掩码的线性基 用于记录每个基向量由哪些原向量异或而成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 vector<int > bas (35 ) , msk (35 ) , idd (70 ) ;for (int i = 1 ; i <= n; i++) { int v = (a[i] ^ b[i]); if (!v) continue ; int cur = v; unsigned long long curm = (1ULL << cnt); for (int j = 31 ; j >= 0 ; j--) { if ((cur >> j) & 1 ) { if (!bas[j]) { bas[j] = cur; msk[j] = curm; idd[cnt] = i; cnt++; break ; } cur ^= bas[j]; curm ^= msk[j]; } } } unsigned long long pick = 0 ;int cur = X;for (int j = 31 ; j >= 0 ; j--) { if ((cur >> j) & 1 ) { if (!bas[j]) { cout << -1 << endl; return ; } cur ^= bas[j]; pick ^= msk[j]; } } vector<bool > use (n + 1 ) ;for (int i = 0 ; i < cnt; i++) if ((pick >> i) & 1 ) use[idd[i]] = 1 ; for (int i = 1 ; i <= n; i++) cout << (use[i] ? b[i] : a[i]) << " \n" [i == n];
其他数论算法 秦九韶算法 一种高效求多项式值的算法,将多项式 $$P(x) = a_nx^n + a_{n-1}x^{n-1} + \cdots + a_1x + a_0$$
改写为嵌套形式: $$P(x) = (\cdots((a_nx + a_{n-1})x + a_{n-2})x + \cdots + a_1)x + a_0$$
只需 $n$ 次乘法和 $n$ 次加法,避免直接计算高次幂。
取模版本 :
1 ans = (ans * x + a_i) mod M
大数/小数近似 对于一个数 $N = M \cdot 10^L$,其中 $L = \log_{10}(n)$。
那么对于 $\lfloor N/n \rfloor$ 的前几位与 $M$ 相同。
快速找到某数组中有几个该数的约数 类似线性筛的思想:
1 2 3 4 5 6 7 8 int maxnum = 2e6 ;vector<int > c (maxn + 1 ) ;for (int i = 1 ; i <= maxnum; i++) { if (cnt[i] == 0 ) continue ; for (int j = i; j <= n; j += i) { c[j] += cnt[i]; } }
注意 :先预处理数组中数的出现次数,要求数的范围不大。
图论 最短路 Floyd-Warshall 算法 寻找加权图中所有顶点对之间最短路径 的动态规划算法。
特点 :
允许负权边(但不能有负权环)
时间复杂度:$O(V^3)$
空间复杂度:$O(V^2)$
核心思想 : 逐步允许每个顶点作为”中转点”,更新任意两点之间的距离:
1 dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
算法步骤 :
初始化距离矩阵 dist
对于 $k = 1$ 到 $n$(中转点)
对于 $i = 1$ 到 $n$
对于 $j = 1$ 到 $n$
更新最短路径
检测负权环 :若任意 dist[i][i] < 0,则存在负权环。
图的连通性 判环 - 三色DFS 用于有向图检测环。
颜色
数值
含义
白色(White)
0
未访问
灰色(Gray)
1
正在访问(在递归栈中)
黑色(Black)
2
访问完成
判环条件 :遇到灰色 邻居节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <bits/stdc++.h> using namespace std;class DirectedGraphCycle {private : vector<vector<int >> adj; vector<int > color; int n; bool dfs (int u) { color[u] = 1 ; for (int v : adj[u]) { if (color[v] == 0 ) { if (dfs (v)) return true ; } else if (color[v] == 1 ) { return true ; } } color[u] = 2 ; return false ; } public : DirectedGraphCycle (int vertices) : n (vertices) { adj.resize (n); color.assign (n, 0 ); } void addEdge (int u, int v) { adj[u].push_back (v); } bool hasCycle () { color.assign (n, 0 ); for (int i = 0 ; i < n; i++) { if (color[i] == 0 ) { if (dfs (i)) return true ; } } return false ; } };
强联通分量 SCC(Tarjan算法) 基本概念 :
dfn[u]:节点u的时间戳
low[u]:u及其子树,最多走一条回边能到达的最早节点
belong[u]:节点u所属的SCC编号
stk:收集遍历的节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 const int maxn = 1e5 + 10 ;vector<int > edge[maxn], belong (maxn); vector<int > dfn (maxn) , low (maxn) ;vector<int > num (maxn) ;vector<bool > vis (maxn) ;stack<int > st; int scc = 0 , tim = 0 ;void tarjan (int u) { dfn[u] = low[u] = ++tim; vis[u] = true ; st.push (u); for (int v : edge[u]) { if (!dfn[v]) { tarjan (v); low[u] = min (low[u], low[v]); } else if (vis[v]) { low[u] = min (low[u], dfn[v]); } } if (dfn[u] == low[u]) { scc++; while (!st.empty ()) { int cur = st.top (); st.pop (); belong[cur] = scc; num[scc]++; vis[cur] = false ; if (cur == u) break ; } } }
Kosaraju算法 算法流程 :
在原图执行DFS ,记录每个顶点的完成时间,按完成时间降序排序 (拓扑排序)
在转置图 中,按排序顺序依次DFS,找到所有连通分量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 int n, m;cin >> n >> m; vector<int > g[n], r[n]; for (int i = 1 ; i <= m; i++) { int u, v; cin >> u >> v; g[u].push_back (v); r[v].push_back (u); } vector<int > topo; vector<int > vis (n + 1 , 0 ) ;auto dfs = [&](this auto const &dfs, int u, int type) -> void { vis[u] = 1 ; vector<int > &p = type ? r[u] : g[u]; for (auto v : p) { if (!vis[v]) dfs (v, 0 ); } if (!type) topo.push_back (u); }; for (int i = 1 ; i <= n; i++) { if (!vis[i]) dfs (i, 0 ); } vis.assign (n + 1 , 0 ); int SCC = 0 ;for (auto it = topo.rbegin (); it != topo.rend (); it++) { if (!vis[*it]) { SCC++; dfs (*it, 1 ); } } cout << SCC << endl;
二分图与匹配 最大匹配(匈牙利算法) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 vector<int > a[n]; vector<int > b; vector<int > vis (b.size()) , match (b.size(), -1 ) ;int t = 0 ; function<bool (int )> dfs = [&](int u) -> bool { for (auto v : a[u]) { if (vis[v] == t) continue ; vis[v] = t; if (match[v] == -1 || dfs (match[v])) { match[v] = u; return true ; } } return false ; }; int cnt = 0 ;for (int i = 0 ; i < n; i++) { t++; if (dfs (i)) cnt++; } cout << cnt << endl;
时间复杂度 :$O(n \cdot E)$,最坏 $O(VE)$
拓扑排序 对有向无环图(DAG)的顶点进行线性排序,若存在边 $A \to B$,则 $A$ 排在 $B$ 前面。
核心思想 :不断移除入度为0 的节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 vector<int > p (n + 1 ) ; vector<vector<int >> mp (n + 1 ); for (int i = 1 ; i <= m; i++) { int u, v; cin >> u >> v; mp[u].push_back (v); p[v]++; } queue<int > q; vector<int > ans; for (int i = 1 ; i <= n; i++) { if (p[i] == 0 ) q.push (i); } while (!q.empty ()) { int t = q.front (); q.pop (); ans.push_back (t); for (auto x : mp[t]) { p[x]--; if (p[x] == 0 ) q.push (x); } }
最小生成树 Prim算法 贪心算法,针对 $n-1$ 条边。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 vector<pair<int , int >> p[n + 1 ]; vector<int > vis (n + 1 , 0 ) ;vector<long long > dis (n + 1 , LLONG_MAX) ;dis[1 ] = 0 ; for (int i = 1 ; i < n; i++) { int cur = -1 ; long long minnum = LLONG_MAX; for (int j = 1 ; j <= n; j++) { if (!vis[j] && minnum > dis[j]) { cur = j; minnum = dis[j]; } } vis[cur] = 1 ; for (auto [x, t] : p[cur]) { if (!vis[x] && t < dis[x]) { dis[x] = t; } } }
Kruskal算法 按边贪心,用并查集 维护连通性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct Edge { int u, v, w; bool operator <(const Edge &other) const { return w < other.w; } }; vector<Edge> edges; int ans = 0 , cnt = 0 ;sort (edges.begin (), edges.end ());for (auto &e : edges) { int u = e.u, v = e.v, w = e.w; if (unio (u, v)) { ans += w; cnt++; if (cnt == n - 1 ) break ; } } if (cnt != n - 1 ) ans = -1 ;
并查集模板 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 vector<int > par (n + 1 ) , sz (n + 1 ) ;void init (int n) { for (int i = 1 ; i <= n; i++) { par[i] = i; sz[i] = 1 ; } } int find (int x) { if (par[x] == x) return x; return par[x] = find (par[x]); } bool unio (int x, int y) { x = find (x); y = find (y); if (x == y) return false ; if (sz[x] < sz[y]) swap (x, y); par[y] = x; sz[x] += sz[y]; return true ; }
字符串 KMP算法 核心作用 :字符串查找(在文本中查找模式串)
核心思想 :保证文本串的指针永不回溯,利用已经匹配成功的部分信息,跳过绝不可能匹配成功的中间位置。
四大作用
作用
说明
对应函数
精准定位
返回模式串在文本中所有出现位置
searchAll()
重叠计数
统计重叠出现的次数
countOccurrences()
最长前缀匹配
计算文本与模式串的最长匹配长度
longestPrefixMatch()
寻找循环节
判断字符串是否由重复子串组成
利用前缀函数
C++ KMP通用模板 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include <bits/stdc++.h> using namespace std;class KMP {private : string pattern; vector<int > pi; int m; void buildPrefix () { pi.resize (m); int j = 0 ; for (int i = 1 ; i < m; ++i) { while (j && pattern[i] != pattern[j]) { j = pi[j - 1 ]; } if (pattern[i] == pattern[j]) ++j; pi[i] = j; } } public : KMP (const string& p) : pattern (p), m (p.size ()) { if (m > 0 ) buildPrefix (); } vector<int > searchAll (const string& text) const { vector<int > positions; if (m == 0 ) return positions; int j = 0 ; for (int i = 0 ; i < (int )text.size (); ++i) { while (j > 0 && text[i] != pattern[j]) { j = pi[j - 1 ]; } if (text[i] == pattern[j]) ++j; if (j == m) { positions.push_back (i - m + 1 ); j = pi[j - 1 ]; } } return positions; } int countOccurrences (const string& text) const { return (int )searchAll (text).size (); } int longestPrefixMatch (const string& text) const { if (m == 0 ) return 0 ; int j = 0 , res = 0 ; for (char c : text) { while (j > 0 && c != pattern[j]) { j = pi[j - 1 ]; } if (c == pattern[j]) { ++j; res = max (res, j); if (j == m) j = pi[j - 1 ]; } } return res; } };
字符串哈希 哈希公式 对于字符串 $S = s_1s_2…s_n$:
基数(base) :通常取质数,如 131, 13331, 137
模数(mod) :通常取大质数,如 $10^9+7$, $10^9+9$, 998244353
$$hash(S) = (s_1 \times base^{n-1} + s_2 \times base^{n-2} + \cdots + s_n \times base^0) \bmod mod$$
创建哈希数组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const int mod = 1e9 + 7 ;const int BASE = 131 ;const int N = 1e6 + 5 ;string str; int n;int Hash[N], P[N];void initHASH () { P[0 ] = 1 ; for (int i = 1 ; i <= n; i++) { Hash[i] = (Hash[i - 1 ] * BASE + str[i]) % mod; P[i] = (long long )P[i - 1 ] * BASE % mod; } } int getHash (int l, int r) { return ((Hash[r] - (long long )Hash[l - 1 ] * P[r - l + 1 ] % mod + mod) % mod); }
结构体包装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct StringHash { vector<int > p, h; StringHash (const string& s) { int n = s.size (); p.resize (n + 1 , 1 ); h.resize (n + 1 , 0 ); for (int i = 0 ; i < n; i++) { p[i + 1 ] = (long long )p[i] * BASE % MOD; h[i + 1 ] = ((long long )h[i] * BASE + s[i]) % MOD; } } int get_hash (int l, int r) { long long res = (h[r] - (long long )h[l - 1 ] * p[r - l + 1 ] % MOD + MOD) % MOD; return (int )res; } };
哈希判断回文 时间复杂度:$O(2n)$
同时维护正推和倒推的哈希数组,通过比较区间哈希值判断回文。
循环移位判断 子串 $S[0..k-1]$ 循环移位后的哈希:
1 2 3 4 5 6 7 8 9 10 11 12 ll rotate_hash (const string &s, int k) { int n = s.length (); k %= n; if (k == 0 ) return h[n]; ll prefix = get_hash (0 , k); ll total = h[n]; ll suffix_hash = (total - (long long )prefix * p[n - k] % mod + mod) % mod; ll rotated = ((long long )suffix_hash * p[k] % mod + prefix) % mod; return rotated; }
数据结构 线段树 模板一:区间求和(支持区间加法) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 class SegmentTreeSumAdd {private : int n; vector<long long > st; vector<long long > lazy; void build (int p, int L, int R, const vector<int >& A) { if (L == R) { st[p] = A[L]; return ; } int m = (L + R) / 2 ; build (p * 2 , L, m, A); build (p * 2 + 1 , m + 1 , R, A); st[p] = st[p * 2 ] + st[p * 2 + 1 ]; } void propagate (int p, int L, int R) { if (lazy[p] == 0 ) return ; st[p] += lazy[p] * (R - L + 1 ); if (L != R) { lazy[p * 2 ] += lazy[p]; lazy[p * 2 + 1 ] += lazy[p]; } lazy[p] = 0 ; } long long query (int p, int L, int R, int i, int j) { propagate (p, L, R); if (i > j) return 0 ; if (L >= i && R <= j) return st[p]; int m = (L + R) / 2 ; return query (p * 2 , L, m, i, min (m, j)) + query (p * 2 + 1 , m + 1 , R, max (i, m + 1 ), j); } void update (int p, int L, int R, int i, int j, long long val) { propagate (p, L, R); if (i > j) return ; if (L >= i && R <= j) { st[p] += val * (R - L + 1 ); if (L != R) { lazy[p * 2 ] += val; lazy[p * 2 + 1 ] += val; } return ; } int m = (L + R) / 2 ; update (p * 2 , L, m, i, min (m, j), val); update (p * 2 + 1 , m + 1 , R, max (i, m + 1 ), j, val); st[p] = st[p * 2 ] + st[p * 2 + 1 ]; } public : SegmentTreeSumAdd (int sz) : n (sz) { st.assign (4 * n + 1 , 0 ); lazy.assign (4 * n + 1 , 0 ); } SegmentTreeSumAdd (const vector<int >& init) : n (init.size ()) { st.assign (4 * n + 1 , 0 ); lazy.assign (4 * n + 1 , 0 ); build (1 , 1 , n, init); } long long query (int i, int j) { return query (1 , 1 , n, i, j); } void add (int i, int j, long long val) { update (1 , 1 , n, i, j, val); } };
模板二:区间最值(支持区间赋值) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 class SegmentTreeMaxAssign {private : int n; vector<int > A; vector<int > st; vector<int > lazy; int l (int p) { return p << 1 ; } int r (int p) { return (p << 1 ) + 1 ; } int conquer (int a, int b) { if (a == -1 ) return b; if (b == -1 ) return a; return max (a, b); } void build (int p, int L, int R) { if (L == R) { st[p] = A[L]; return ; } int m = (L + R) / 2 ; build (l (p), L, m); build (r (p), m + 1 , R); st[p] = conquer (st[l (p)], st[r (p)]); } void propagate (int p, int L, int R) { if (lazy[p] == -1 ) return ; st[p] = lazy[p]; if (L != R) { lazy[l (p)] = lazy[p]; lazy[r (p)] = lazy[p]; } lazy[p] = -1 ; } int query (int p, int L, int R, int i, int j) { propagate (p, L, R); if (i > j) return -1 ; if (L >= i && R <= j) return st[p]; int m = (L + R) / 2 ; return conquer ( query (l (p), L, m, i, min (m, j)), query (r (p), m + 1 , R, max (i, m + 1 ), j) ); } void update (int p, int L, int R, int i, int j, int val) { propagate (p, L, R); if (i > j) return ; if (L >= i && R <= j) { st[p] = val; lazy[p] = val; return ; } int m = (L + R) / 2 ; update (l (p), L, m, i, min (m, j), val); update (r (p), m + 1 , R, max (i, m + 1 ), j, val); st[p] = conquer (st[l (p)], st[r (p)]); } public : SegmentTreeMaxAssign (int sz) : n (sz), A (n + 1 ), st (4 * n + 1 , -1 ), lazy (4 * n + 1 , -1 ) {} int query (int i, int j) { return query (1 , 1 , n, i, j); } void assign (int i, int j, int val) { update (1 , 1 , n, i, j, val); } };
模板三:区间求和(支持区间赋值 + 区间加法) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 class SegmentTreeSumBoth {private : int n; vector<long long > st; vector<long long > lazy_set; vector<long long > lazy_add; void apply_set (int p, int L, int R, long long val) { st[p] = val * (R - L + 1 ); lazy_set[p] = val; lazy_add[p] = 0 ; } void apply_add (int p, int L, int R, long long val) { st[p] += val * (R - L + 1 ); if (lazy_set[p] != LLONG_MIN) { lazy_set[p] += val; } else { lazy_add[p] += val; } } void propagate (int p, int L, int R) { if (L == R) return ; int m = (L + R) / 2 ; if (lazy_set[p] != LLONG_MIN) { apply_set (p * 2 , L, m, lazy_set[p]); apply_set (p * 2 + 1 , m + 1 , R, lazy_set[p]); lazy_set[p] = LLONG_MIN; } if (lazy_add[p] != 0 ) { apply_add (p * 2 , L, m, lazy_add[p]); apply_add (p * 2 + 1 , m + 1 , R, lazy_add[p]); lazy_add[p] = 0 ; } } public : SegmentTreeSumBoth (int sz) : n (sz), st (4 * n + 1 , 0 ), lazy_set (4 * n + 1 , LLONG_MIN), lazy_add (4 * n + 1 , 0 ) {} long long query (int i, int j) { return query (1 , 1 , n, i, j); } void assign (int i, int j, long long val) { update_set (1 , 1 , n, i, j, val); } void add (int i, int j, long long val) { update_add (1 , 1 , n, i, j, val); } };
三种模板对比 :
特性
模板一
模板二
模板三
查询
最值
区间和
区间和
更新
区间赋值
区间加法
赋值+加法
懒标记数
1个
1个
2个
优先级
-
-
赋值 > 加法
树状数组(二叉索引树) 普通版 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 template <class T >struct BIT { int n; vector<T> a; BIT (int sz = 0 ) : n (sz), a (sz + 1 ) {} void add (int p, T v) { for (; p <= n; p += p & -p) a[p] += v; } T ask (int p) { T r = 0 ; for (; p; p -= p & -p) r += a[p]; return r; } T ASK (int l, int r) { return ask (r) - ask (l - 1 ); } void ADD (int l, int r, T v) { add (l, v); add (r + 1 , -v); } int kth (T k) { int idx = 0 ; int bitmax = (1 << (int )log2 (n)); while (bitmax > 0 ) { int nextidx = idx + bitmax; if (nextidx <= n && a[nextidx] < k) { k -= a[nextidx]; idx = nextidx; } bitmax >>= 1 ; } return idx + 1 ; } };
进阶版(带权差分) 维护两个数组 c1(存d[i])和 c2(存带权差分 $(i-1) \cdot d[i]$)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 template <class T >struct BIT { int n; vector<T> c1, c2; BIT (int sz = 0 ) : n (sz), c1 (sz + 5 ), c2 (sz + 5 ) {} void _add(int p, T v, vector<T>& c) { for (; p <= n; p += p & -p) c[p] += v; } T _ask(int p, vector<T>& c) { T r = 0 ; for (; p; p -= p & -p) r += c[p]; return r; } void range_add (int l, int r, T v) { _add(l, v, c1); _add(r + 1 , -v, c1); _add(l, (l - 1 ) * v, c2); _add(r + 1 , r * (-v), c2); } T prefix_sum (int x) { T sum1 = _ask(x, c1); T sum2 = _ask(x, c2); return x * sum1 - sum2; } T range_sum (int l, int r) { return prefix_sum (r) - prefix_sum (l - 1 ); } };
珂朵莉树(区间赋值操作) 又称老司机树,用 set 维护区间,复杂度近似 $O(m \log n)$。
节点定义与split操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 struct Node { int l, r; mutable int v; Node (int L, int R = -1 , int V = 0 ) : l (L), r (R), v (V) {} bool operator <(const Node &other) const { return l < other.l; } }; set<Node> s; set<Node>::iterator split (int pos) { auto it = s.lower_bound (pos); if (it != s.end () && it->l == pos) return it; --it; if (pos > it->r) return s.end (); int L = it->l, R = it->r, V = it->v; s.erase (it); s.insert (Node (L, pos - 1 , V)); return s.insert (Node (pos, R, V)).first; }
区间操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 void add (int l, int r, int val = 1 ) { auto itr = split (r + 1 ), itl = split (l); for (; itl != itr; itl++) itl->v += val; } void assign (int l, int r, int val = 0 ) { auto itr = split (r + 1 ), itl = split (l); s.erase (itl, itr); s.insert (Node (l, r, val)); } int sum (int l, int r, int ex, int mod) { auto itr = split (r + 1 ), itl = split (l); int res = 0 ; for (; itl != itr; itl++) { res = (res + (long long )(itl->r - itl->l + 1 ) * qpow (itl->v, ex, mod)) % mod; } return res; } int rank (int l, int r, int k, bool reversed = false ) { if (reversed) k = r - l + 2 - k; auto itr = split (r + 1 ), itl = split (l); vector<pair<int , int >> vp; for (; itl != itr; itl++) { vp.push_back ({itl->v, itl->r - itl->l + 1 }); } sort (vp.begin (), vp.end ()); for (auto &i : vp) { k -= i.second; if (k <= 0 ) return i.first; } return -1 ; }
单调栈 原理 单调栈是一种栈内元素保持单调性 的数据结构。
核心思想 :
入栈时,如果破坏单调性,就弹出栈顶元素,直到满足单调性
每个元素最多入栈一次、出栈一次 ,时间复杂度 $O(n)$
应用场景
栈类型
寻找方向
寻找目标
单调递增栈
左/右
第一个比当前小 的元素
单调递减栈
左/右
第一个比当前大 的元素
常见口诀 :
找右边第一个更大的 → 从右向左遍历,维护单调递减栈
找左边第一个更小的 → 从左向右遍历,维护单调递增栈
手写栈模板 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 vector<int > stk (n) ;int top = 0 ;stk[++top] = x; cout << stk[top]; cout << top << endl; if (top) { }for (int i = 1 ; i <= top; i++) { }
寻找下一个更大元素 1 2 3 4 5 6 7 8 9 10 11 12 13 14 vector<int > a (n + 1 ) ;vector<int > st (n + 1 ) ;int top = 0 ;vector<int > ans (n + 1 ) ;for (int i = 1 ; i <= n; i++) { while (top && a[i] > a[st[top]]) { ans[st[top]] = i; top--; } st[++top] = i; }
单调栈上二分 因为单调栈中存的都是下标,满足二分的单调性,可以找某个下标。
动态规划 LIS最长递增子序列 $O(n \log n)$ 优化版本:
1 2 3 4 5 6 7 8 9 10 vector<int > LIS; for (int i = 1 ; i <= n; i++) { auto idx = lower_bound (LIS.begin (), LIS.end (), a[i]); if (idx == LIS.end ()) { LIS.push_back (a[i]); } else { *idx = a[i]; } } cout << LIS.size () << endl;
背包问题 多重背包 - 二进制优化 时间复杂度:$O(T \times m \times \sum \log(s_i))$
1 2 3 4 5 6 7 8 9 10 11 12 13 int amount = s[i];for (int k = 1 ; amount > 0 ; k <<= 1 ) { int num = min (k, amount); int volume = num * v[i]; int value = num * w[i]; for (int j = m; j >= volume; j--) { dp[j] = max (dp[j], dp[j - volume] + value); } amount -= num; }
多重背包 - 单调队列优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 const int maxn = 1e6 + 10 ;vector<int > dp (maxn) , backdp (maxn) ;for (int i = 1 ; i <= n; i++) { backdp = dp; int volume = v[i]; int value = w[i]; int amount = s[i]; for (int r = 0 ; r < volume; r++) { deque<int > dq; for (int k = 0 ; r + k * volume <= m; k++) { int j = r + k * volume; while (!dq.empty () && backdp[j] - k * value >= backdp[dq.back ()] - (dq.back () - r) / volume * value) { dq.pop_back (); } dq.push_back (j); while (!dq.empty () && (j - dq.front ()) / volume > amount) { dq.pop_front (); } dp[j] = max (dp[j], backdp[dq.front ()] + (j - dq.front ()) / volume * value); } } }
杂项 高精度 模板一:非负大整数(基数1e9) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 struct BigInt { static const int BASE = 1000000000 ; vector<int > d; BigInt (long long x = 0 ) { *this = x; } BigInt& operator =(long long x) { d.clear (); if (x == 0 ) { d.push_back (0 ); return *this ; } while (x) { d.push_back (x % BASE); x /= BASE; } return *this ; } void trim () { while (d.size () > 1 && d.back () == 0 ) d.pop_back (); } string toString () const { if (d.empty ()) return "0" ; string s = to_string (d.back ()); for (int i = (int )d.size () - 2 ; i >= 0 ; --i) { string t = to_string (d[i]); s += string (9 - t.size (), '0' ) + t; } return s; } }; static inline bool operator <(const BigInt& a, const BigInt& b) { if (a.d.size () != b.d.size ()) return a.d.size () < b.d.size (); for (int i = (int )a.d.size () - 1 ; i >= 0 ; --i) { if (a.d[i] != b.d[i]) return a.d[i] < b.d[i]; } return false ; } static inline void mul_int (BigInt& a, int m) { if (m == 0 ) { a = 0 ; return ; } long long carry = 0 ; for (size_t i = 0 ; i < a.d.size (); ++i) { long long cur = carry + 1LL * a.d[i] * m; a.d[i] = cur % BigInt::BASE; carry = cur / BigInt::BASE; } while (carry) { a.d.push_back (carry % BigInt::BASE); carry /= BigInt::BASE; } } static inline BigInt div_int (const BigInt& a, int v) { BigInt q; q.d.assign (a.d.size (), 0 ); long long rem = 0 ; for (int i = (int )a.d.size () - 1 ; i >= 0 ; --i) { long long cur = a.d[i] + rem * BigInt::BASE; q.d[i] = cur / v; rem = cur % v; } q.trim (); return q; }
模板二:支持负数(字符串存储) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 class BigInt {private : string num; void trim () { if (num.empty ()) { num = "0" ; return ; } bool negative = (num[0 ] == '-' ); string digits = negative ? num.substr (1 ) : num; size_t pos = digits.find_first_not_of ('0' ); if (pos == string::npos) { num = "0" ; return ; } digits = digits.substr (pos); num = negative ? "-" + digits : digits; } public : BigInt () : num ("0" ) {} BigInt (const long long &n) : num (to_string (n)) {} BigInt (const string &n) : num (n) { trim (); } BigInt multiply (const BigInt &rhs) const { string a = num, b = rhs.num; int sign = 1 ; if (a[0 ] == '-' ) { sign *= -1 ; a = a.substr (1 ); } if (b[0 ] == '-' ) { sign *= -1 ; b = b.substr (1 ); } a = a.substr (a.find_first_not_of ('0' )); b = b.substr (b.find_first_not_of ('0' )); if (a.empty () || b.empty ()) return BigInt (0 ); vector<int > c (a.length() + b.length(), 0 ) ; for (int i = a.length () - 1 ; i >= 0 ; i--) { for (int j = b.length () - 1 ; j >= 0 ; j--) { int mul = (a[i] - '0' ) * (b[j] - '0' ); int sum = mul + c[i + j + 1 ]; c[i + j + 1 ] = sum % 10 ; c[i + j] += sum / 10 ; } } string result; for (int digit : c) { if (!(result.empty () && digit == 0 )) { result.push_back (digit + '0' ); } } if (result.empty ()) result = "0" ; if (sign == -1 && result != "0" ) result = "-" + result; return BigInt (result); } friend BigInt operator *(const BigInt &a, const BigInt &b) { return a.multiply (b); } friend ostream& operator <<(ostream &os, const BigInt &n) { os << n.num; return os; } friend bool operator <(const BigInt &lhs, const BigInt &rhs) { string a = lhs.num, b = rhs.num; bool na = (a[0 ] == '-' ), nb = (b[0 ] == '-' ); if (na && !nb) return true ; if (!na && nb) return false ; if (na) a = a.substr (1 ); if (nb) b = b.substr (1 ); if (!na && !nb) { if (a.length () != b.length ()) return a.length () < b.length (); return a < b; } if (a.length () != b.length ()) return a.length () > b.length (); return a > b; } friend bool operator >(const BigInt &lhs, const BigInt &rhs) { return rhs < lhs; } friend bool operator ==(const BigInt &lhs, const BigInt &rhs) { return lhs.num == rhs.num; } };
位运算与格雷码 位运算操作 1 2 3 4 5 ^ 按位异或 & 按位与 | 按位或 << 左移(乘以2^n) >> 右移(整除2^n)
常用技巧 :
操作
代码
说明
判断奇偶
n & 1
结果为1是奇数
获取最低8位
n & 0xFF
清零第k位
n & (~(1 << k))
第k位置1
n | (1 << k)
取反
n ^ 1
对带符号整数:1 ^ A = -A - 1
异或性质 :
$x \oplus 0 = x$
$x \oplus x = 0$
满足交换律和结合律
两次异或同一个数会还原:$a \oplus b \oplus b = a$
格雷码 性质 :
单位距离性 :相邻数字有且仅有一位不同
循环性 :首尾也仅一位不同
反射构造 :可通过镜像对称前一位格雷码并加前缀递归生成
转换公式 :
二进制转格雷码 : $$G = B \oplus (B >> 1)$$
格雷码转二进制 :
$b_{n-1} = g_{n-1}$
$b_i = g_i \oplus b_{i+1}$ 对 $i = n-2 \ldots 0$
其他技巧 __int128的使用 用于处理超大整数运算(超过64位)。
输入输出 (因为标准流不支持):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 __int128 read () { char arr[30 ]; __int128 res = 0 ; scanf ("%s" , arr); for (int i = 1 ; i <= strlen (arr); i++) { res = res * 10 + (arr[i] - '0' ); } return res; } void print (__int128 num) { if (num > 9 ) print (num / 10 ); putchar (num % 10 + '0' ); }
注意事项 :
在GCC、Linux系统、大多数OJ上支持
在VS2017等可能不支持
不能关闭同步流
判断回文数 1 2 3 4 5 6 7 8 9 10 11 12 bool isreverse (int x) { if (x < 0 ) return false ; if (x != 0 && x % 10 == 0 ) return false ; int reversedHalf = 0 ; while (x > reversedHalf) { reversedHalf = reversedHalf * 10 + x % 10 ; x /= 10 ; } return x == reversedHalf || x == reversedHalf / 10 ; }
构造回文数 1 2 3 4 5 6 7 8 9 10 11 12 for (int i = 1 ; i <= 1000000 ; i++) { string ss = to_string (i), tt; tt = ss; reverse (tt.begin (), tt.end ()); string s1 = ss + tt; string s2 = ss; for (int j = 1 ; j < tt.size (); j++) s2 += tt[j]; }
暴力寻找MEX 1 2 3 4 5 6 7 8 vector<int > vis (n) ;int mex = 0 ;for (int i = 0 ; i < n; i++) { vis[a[i]]++; } while (max < n && vis[mex]) mex++;
只使用/2和-1变为0的最小操作次数 1 2 3 4 5 6 7 8 int minSteps (int a) { if (a == 0 ) return 0 ; int bits = 32 - __builtin_clz(a); int ones = __builtin_popcount(a); return bits + ones - 1 ; }
差分 普通差分
O(1)区间修改 :d[l] += k; d[r+1] -= k;
O(n)区间查询
带权差分 引入两个数组:
c1:存 $d[i] = a[i] - a[i-1]$
c2:存带权差分 $(i-1) \cdot d[i]$
区间修改(O(1)) :
1 2 c1[l] += k; c1[r+1] -= k; c2[l] += (l-1) * k; c2[r+1] -= r * k;
区间查询(O(1)) :
1 2 pre[x] = x * sum(c1[1..x]) - sum(c2[1..x]) pre[l..r] = pre[r] - pre[l-1]
前缀和 1 2 3 4 5 6 vector<int > pre (n + 1 , 0 ) ;for (int i = 1 ; i <= n; i++) { pre[i] = pre[i - 1 ] + a[i]; } int sum = pre[r] - pre[l - 1 ];
二维差分 格子点坐标 ($x_1, y_1$ 到 $x_2, y_2$):
1 2 3 4 dif[x1][y1]++ dif[x2 + 1 ][y1]-- dif[x1][y2 + 1 ]-- dif[x2 + 1 ][y2 + 1 ]++
边的交点 ($x_1, y_1$ 到 $x_2, y_2$):
1 2 3 4 dif[x1][y1]++ dif[x2][y1]-- dif[x1][y2]-- dif[x2][y2]++
容斥原理 待补充完整内容。
错误与经验总结 常见错误
计算连续段的初始长度 :当条件不符时,初始长度应该为1
map vs unordered_map :尽量使用map,unordered_map可能因为创建太多而超时
变量命名 :避免重复变量名,因为有时变量会定义在全局中
运算符优先级 :特别注意优先级问题
取模运算 :注意取模的细节
BFS/DFS标记 :应在入队前 标记,而不是出队后
环的处理 :遇到环、前后相连并要删去元素时,向链表思考
代码注释 :把dill注释掉要记得恢复
标记问题 :注意标记的正确性
思维缜密性 :代码错误往往在想不到的地方
手模的重要性 :手模是很重要的
附录 常用小知识 行号构成的集合 这些行号构成的集合是 ${1 + k \cdot a \bmod n \mid k = 0,1,2,\ldots}$,等价于1加上由 $a$ 生成的加法子群。
在模 $n$ 的整数环中,由 $a$ 生成的循环子群的大小为 $n / \gcd(n, a)$。
手动模拟的重要性 手模是很重要的,可以帮助发现逻辑错误。
文件信息
整理日期:2026-07-16
总行数:3437行
分类数:7个主要分类
文档持续更新中…