-
计算几何
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E8%AE%A1%E7%AE%97%E5%87%A0%E4%BD%95/
人一定要有梦想。 精度处理 精度设置 精度的最小误差 `eps` 通常在 $10^{-8}$ 以内,视题目要求改变。 ``` cpp using var = double; const var eps = 1e-8; ``` 符号函数 `sign` ``` latex \DeclareMathOperator{\sign}{sign} \DeclareMathOperator{\eps }{eps} \sign(x)= \begin{cases} -1,&x<-\eps\\ 0,&-\eps\le x\le \eps\\ 1,&x>\eps \end{cases} ``` ``` cpp inline int sign(var x) { return x < -eps ? -1 : (x > eps ? 1 : 0); } ``` 绝对值函数 `absolute` ``` latex \DeclareMathOperator{\absolute}{absolute} \absolute(x)=x\cdot\sign(x) ``` ``` cpp inline var absolute(var x) { return x * sign(x); } ``` 符号约定 点 `Point` 二维平面点由 $x,y$ 坐标组成。 ``` cpp struct Point { var x, y; Point(var x = 0, var y = 0) : x(x), y(y) {} }; ``` 在精度范围内判断相等。 ``` cpp bool operator == (const Point& a, const Point& b) { return ! sign(a.x - b.x) && ! sign(a.y - b.y); } ``` 标准输入输出支持。 ``` cpp istream& operator >> (istream& in, Point& p) { in >> p.x >> p.y; return in; } ostream& operator << (ostream& out, const Point& p) { out << "(" << p.x << ", " << p.y << ")"; return out; } ``` 向量 `Vector` 向量 `Vector` 的定义沿用点 `Point`的定义。 ``` cpp using Vector = Point; ``` 多边形 `Poly` 多边形 `Poly` 由一组点 `Point`构成。 ``` cpp using Poly = vector<Point>; ``` 标准输入输出支持。 ``` cpp istream& operator >> (istream& in, Poly& poly) { for (auto& p : poly) { in >> p; } return in; } ostream& operator << (ostream& out, Poly& poly) { for (const auto& p : poly) { out << p << ","; } return out; } ``` 向量运算 加减 `operator +/-` ``` latex (x_1,y_1)\pm(x_2,y_2)=(x_1\pm x_2,y_1\pm y_2) ``` ``` cpp Vector operator + (const Vector& a, const Vector& b) { return { a.x + b.x, a.y + b.y }; } Vector operator - (const Vector& a, const Vector& b) { return { a.x - b.x, a.y - b.y }; } ``` 数乘 `operator *` ``` latex (x,y)\cdot k=(x\cdot k,y\cdot k) ``` ``` cpp Vector operator * (const Vector& p, var k) { return { p.x * k, p.y * k }; } ``` 点乘 `dot` ``` latex (x_1,y_1)\cdot(x_2,y_2)=x_1\cdot x_2 + y_1\cdot y_2 ``` ``` cpp var dot(const Vector& a, const Vector& b) { return a.x * b.x + a.y * b.y; } ``` 叉乘 `cross` ``` latex (x_1,y_1)\times(x_2,y_2)=x_1\cdot y_2 - x_2\cdot y_1 ``` ``` cpp var cross(const Vector& a, const Vector& b) { return a.x * b.y - a.y * b.x; } ``` 取模 `module` ``` latex \left\Vert(x, y)\right\Vert=\sqrt{x^2+y^2} ``` ``` latex \left\Vert\vec{p}\right\Vert=\sqrt{\vec{p}\cdot\vec{p}} ``` ``` cpp var module(const Vector& p) { return sqrt(dot(p, p)); } ``` 旋转 `rotate` 向量 $\vec{p}$ 逆时针旋转 $\theta$ 弧度。 ``` latex (x, y)\cdot \begin{pmatrix} \cos\theta & \sin\theta \\ -\sin\theta & \cos\theta \end{pmatrix} ``` ``` cpp Vector rotate(const Vector& p, var theta) { return { p.x * cos(theta) - p.y * sin(theta), p.x * sin(theta) + p.y * cos(theta) }; } ``` 向量 `vec` 从点 $A$ 指向点 $B$ 的向量。 ``` cpp Vector vec(const Point& a, const Point& b) { return b - a; } ``` 点|点 距离 `to_point` ``` cpp var to_point(const Point& a, const Point& b) { return module(a - b); } ``` 旋转 `rotate` 点 $P$ 绕点 $C$ 逆时针旋转 $\theta$ 弧度。 ``` cpp Point rotate(const Point& p, const Point& c, var theta) { return c + rotate(p - c, theta); } ``` 点|线段 相交 `on_seg` 点 $P$ 是否在线段 $AB$ 上。 ``` cpp bool on_seg(const Point& p, const Point& a, const Point& b) { return ! sign(cross(p - a, b - a)) and sign(dot(p - a, p - b)) <= 0; } ``` 距离 `to_seg` 点 $P$ 到线段 $AB$ 的距离。 ``` cpp var to_seg(const Point& p, const Point& a, const Point& b) { if (a == b) return to_point(p, a); Vector x = p - a, y = p - b, z = b - a; if (sign(dot(x, z)) < 0) return module(x); if (sign(dot(y, z)) > 0) return module(y); return absolute(cross(x, z) / module(z)); } ``` 点|直线 相交 `on_line` 点 $P$ 是否在直线 $AB$ 上。 ``` cpp bool on_line(const Point& p, const Point& a, const Point& b) { return ! sign(cross(p - a, b - a)); } ``` 距离 `to_line` 点 $P$ 到直线 $AB$ 的距离。 ``` cpp var to_line(const Point& p, const Point& a, const Point& b) { Vector x = p - a, z = b - a; return absolute(cross(x, z) / module(z)); } ``` 垂足 `foot_point` 点 $P$ 到直线 $AB$ 的垂足。 ``` cpp Point foot_point(const Point& p, const Point& a, const Point& b) { Vector x = p - a, y = p - b, z = b - a; var prj1 = dot(x, z) / module(z); var prj2 = dot(y, z) / module(z); return a + z * (prj1 / (prj1 - prj2)); } ``` 对称点 `symmetry_point` 点 $P$ 关于直线 $AB$ 的对称点。 ``` cpp Point symmetry_point(const Point& p, const Point& a, const Point& b) { return foot_point(p, a, b) * 2 - p; } ``` 线|线 直线|直线 交点 `cross_point` 直线 $AB$ 和直线 $CD$ 的交点。 ``` cpp Point cross_point(const Point& a, const Point& b, const Point& c, const Point& d) { Vector x = b - a, y = d - c, z = a - c; return a + x * (cross(y, z) / cross(x, y)); } ``` 直线|线段 相交 `inter_line_seg` 直线 $AB$ 和线段 $CD$ 是否相交。 ``` cpp bool inter_line_seg(const Point& a, const Point& b, const Point& c, const Point& d) { return on_seg(cross_point(a, b, c, d), c, d); } ``` 线段|线段 相交 `inter_seg_seg` 线段 $AB$ 和线段 $CD$ 是否相交。 ``` cpp bool inter_seg_seg(const Point& a, const Point& b, const Point& c, const Point& d) { var c1 = cross(b - a, c - a), c2 = cross(b - a, d - a); var d1 = cross(d - c, a - c), d2 = cross(d - c, b - c); return sign(c1) * sign(c2) < 0 and sign(d1) * sign(d2) < 0; } ``` 多边形 有向面积 `area` 逆时针为正向。 ``` cpp var area(const Poly& poly) { var s = 0; int n = poly.size(); for (int i = 0; i < n; i ++) s += cross(polyi], poly[(i + 1) % n]); return s / 2.0; } ``` 点|多边形 相交 `on_poly` 点 $P$ 与多边形 $Poly$ 的关系。 - 在外部:返回 $0$; - 在内部:返回 $1$; - 在边界:返回 $2$。 === 射线法 ``` cpp int on_poly(const Point& p, const Poly& poly) { int counter = 0; int n = poly.size(); for (int i = 0; i < n; i ++) { const Point& a = poly[i]; const Point& b = poly[(i + 1) % n]; if (on_seg(p, a, b)) return 2; if (p.y >= min(a.y, b.y) and p.y < max(a.y, b.y)) { var product = a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x); counter += sign(product - p.x) > 0; } } return counter % 2; } ``` === 二分法 ``` cpp int on_poly_binary(const Point& p, const Poly& poly) { static auto check = [&{ return sign(cross(l - a, r - a)) > 0; }; int n = poly.size(); if (check(poly[0], p, poly[1]) or check(poly[0], poly[n-1], p)) return 0; if (on_seg(p, poly[0], poly[1]) or on_seg(p, poly[0], poly[n-1])) return 2; int l = -1, r = n; while (l + 1 < r) { int m = (l + r) / 2; if (check(poly[0], poly[m], p)) l = m; else r = m; } if (check(poly[l], p, poly[l+1])) return 0; if (on_seg(p, poly[l], poly[l+1])) return 2; return 1; } ```
-
二分图最大匹配
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E5%9B%BE%E8%AE%BA/%E4%BA%8C%E5%88%86%E5%9B%BE%E6%9C%80%E5%A4%A7%E5%8C%B9%E9%85%8D/
跟二分算法没有任何关系。 二分图 图 $G$ 是「二分图」,当且仅当 $G$ 的点可以被分成两部分,且所有边的两个端点都不在同一部分。我们把这两部分称为「左部」和「右部」。 「二分图匹配」是一种特殊的「二分图」,其「一个部分的每个节点」最多只与一个「另一部分的节点」相连。以下是一个例子: || :-: 「二分图最大匹配」是所有「二分图匹配」中,边最多的情况。 二分图最大匹配算法 !!! question 给定二分图 $G$,其左部有 $n$ 个点,右部有 $m$ 个点,边数为 $e$。要使 $G$ 成为「二分图最大匹配」,最多可以选出几条边? 匈牙利算法 常见的二分图匹配算法是匈牙利算法,其正确性基于 Hall 定理。但该定理比较复杂,在此略去。 匈牙利算法的核心思想可以用一个比喻来描述: 假设有一群男生🚹和一群女生🚺,想要相互配对成为情侣💑。我们的目标是让尽可能多的人配对。 算法依次从每个单身男生🚹出发,去寻找可以配对的单身女生🚺: - 如果这个女生🚺还没被配对,就配对给他(配对成功✔️); - 如果这个女生🚺已经被配对了,就把她抢过来,让她的原男友🚹单身。接下来看看原男友🚹能否再配对一个别的女生🚺 - 如果可以,就保持这样的配对关系(配对成功✔️) - 否则就把这个女生🚺还回去(配对失败❌) - 如果全都配对失败了❌,那么这个男生🚹就只能永远单身了😭。 根据以上的描述,容易写出以下 `dfs` 函数: ``` cpp // match[v]: 是与 v 匹配的男朋友的编号 int match[N]; // bool dfs(u): 男生 u 是否可以成功匹配到女生 bool dfs(const int u) { for (const auto& v : g[u]) { // 如果这个女生「未配对」或「其原男友可配对别的女生」 if ((!match[v]) || dfs(match[v])) { // 配对成功 match[v] = u; return true; } } // 全都配对失败了 return false; } ``` 然后在主函数中: ``` cpp int ans = 0; for (int i = 1; i <= n; i ++) { if (dfs(i)) { ans ++; } } ``` 但这么写可能会导致一个问题: - A🚹抢走了 B🚹的女朋友🚺,接下来看看 B🚹能否再配对一个别的女生🚺 - B🚹抢回了 A🚹的女朋友🚺,接下来看看 A🚹能否再配对一个别的女生🚺 - A🚹抢回了 B🚹的女朋友🚺,接下来看看 B🚹能否再配对一个别的女生🚺 - ... 所以另外规定:在讨论同一个男生🚹的配对情况时,同一个女生🚺不允许访问多次。这样 B🚹就无法抢回 A🚹的女朋友🚺,不会造成死循环。 另增 `bool` 数组 `vis`,`vis[v]` 表示 $v$ 是否被访问过,初始都为 `false`。相应地,要将 `dfs` 改成: ``` cpp bool dfs(const int u) { for (const auto& v : g[u]) { if (vis[v]) // 已经访问过了,不许访问 continue; vis[v] = true; // 标记为已访问 if ((!match[v]) || dfs(match[v])) { match[v] = u; return true; } } return false; } ``` 主函数也需要修改: ``` cpp int ans = 0; for (int i = 1; i <= n; i ++) { memset(vis, 0, sizeof vis); // 初始化为 false if (dfs(i)) { ans ++; } } ``` 模板 ``` cpp include <bits/stdc++.h> using namespace std; const int N = 1001; int n, m, e; vector<int> g[N]; int match[N]; bool vis[N]; bool dfs(const int u) { for (const auto& v : g[u]) { if (vis[v]) continue; vis[v] = true; if ((!match[v]) || dfs(match[v])) { match[v] = u; return true; } } return false; } int main() { scanf("%d%d%d", &n, &m, &e); for (int i = 1; i <= e; i ++) { static int u, v; scanf("%d%d", &u, &v); g[u].push_back(v); } int ans = 0; for (int i = 1; i <= n; i ++) { memset(vis, 0, sizeof vis); ans += dfs(i); } printf("%d\n", ans); return 0; } ```
-
拉格朗日插值法
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E6%95%B0%E5%80%BC%E5%88%86%E6%9E%90/%E6%8B%89%E6%A0%BC%E6%9C%97%E6%97%A5%E6%8F%92%E5%80%BC%E6%B3%95/
任意一个有穷数列都有通项公式。 给出 $n$ 个点 $(x_1,y_1),(x_2,y_2),\cdots,(x_n,y_n)$,怎么找到一个函数 $f(x)$ 同时过这 $n$ 个点? 拉格朗日的想法是这样的:构造一个开关函数 ``` latex L_k(t)=\begin{cases} 1,&t=x_k\\ \\ 0,&t=\text{other}\ x \end{cases} ``` 这个开关函数在学术上称为「插值基函数」。只有代入 $x_k$ 时开关才打开,代入其它 $x$ 时开关关闭。 进而构造 ``` latex f(x)=y_1L_1(x)+y_2L_2(x)+\cdots+y_nL_n(x) ``` 代入 $x_k$ 时,仅 $y_k$ 后面的开关是开着的,其余皆关闭,所以函数值为 $y_k$,即 $\forall k,f(x_k)=y_k$。这就是我们想要的函数。 问题就变成了如何构造开关函数。 ---- 容易发现 $\forall k,t=x_k,$ ``` latex \prod (t-x_i)=(t-x_1)(t-x_2)\cdots(t-x_n)=0 ``` 把 $(t-x_k)$ 这一项拿走,得到 ``` latex \prod_{i\not=k}(t-x_i)=\begin{cases} \not=0,&t=x_k\\ \\ 0,&t=\text{other}\ x \end{cases} ``` 为使 $t=x_k$ 时得到 $1$,再作以下改变 ``` latex \prod_{i\not=k}\frac{t-x_i}{x_k-x_i}=\begin{cases} 1,&t=x_k\\ \\ 0,&t=\text{other}\ x \end{cases} ``` 于是我们成功地构造出了开关函数 ``` latex L_k(t)=\prod_{i\not=k}\frac{t-x_i}{x_k-x_i} ``` 模板 ``` cpp include <iostream> include <vector> using namespace std; double f(vector<double>& x, vector<double>& y, double t) { double result = 0.0; int n = x.size(); for (int i = 0; i < n; i ++) { double term = y[i]; for (int j = 0; j < n; j ++) { if (j != i) { term *= (t - x[j]) / (x[i] - x[j]); } } result += term; } return result; } int main() { vector<double> x = {1, 2, 3, 4, 5}; vector<double> y = {1, 4, 9, 16, 25}; double t; while (cin >> t) { cout << f(x, y, t) << endl; } return 0; } ```
-
Manacher 算法
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%AE%97%E6%B3%95/manacher/
马拉车算法。 Manacher 算法可以在 $O(n)$ 的时间复杂度下找到字符串的最长回文子串[^1]。 [^1]:回文串是指正读和反读都相同的字符串。 例如 `"banana"` 的最长回文子串是 `"anana"`。 预处理 「奇数长度的回文子串」和「偶数长度的回文子串」其实是有所区别的。例如 `"aba"` 的中心是一个字符,但 `"abba"` 的中心位于两个字符之间。 在探测回文子串的长度时,我们所采用的是「中心扩展法」,即确定中心并向两侧逐步扩展,直到两端的字符不同为止。 那么针对奇数长度的回文子串,要以字符为中心扩展;针对偶数长度的回文子串,要以字符间隙为中心扩展。这意味着算法需要分别处理这两种情况,想想就头大。 Manacher 采用了一个很聪明的方法:在字符间隙(包括字符串首尾)各插入一个特殊字符(比如 ``),这么一来,就只需要考虑长度为奇数的情况。 ::: > `"aba"` → `"aba"` ::: > `"abba"` → `"abba"` 另一个预处理优化是,当回文子串扩展到字符串的开头和结尾时,需要额外的边界检查来防止数组越界,也比较麻烦。我们可以再在字符串的开头和结尾添加不同的特殊字符,这样一旦扩展到边界就会自动停止。 ::: > `"aba"` → `"^aba$"` ::: > `"abba"` → `"^abba$"` 注:不得使用原字符串中存在的字符作为特殊字符。 算法主体 以下是一些变量的定义: - 创建数组 $p$,其中 $p[i]$ 表示以第 $i$ 个字符为中心,能扩展的最大次数。 > 例如对 `"abacab"`,以第 $3$ 个字符 `c` 为中心,最多可以向左右扩展 $2$ 次,就记 $p[3]=2$。 - 使用两个变量 $M$ 和 $R$ 描述当前已知的最靠右的回文串的信息。 - $M$ 是其中心位置,初始为 $0$; - $R$ 是其右边界位置,初始为 $0$。 ---- **Manacher 算法的主要策略是:从左至右依次计算 $p[i]$,并实时地更新 $M$ 和 $R$ 的值。** 假设当前要计算 $p[i]$,则此时应已经算出了 $p[1],p[2],\cdots,p[i-1]$ 以及当前的 $M$ 和 $R$。 - **`Case 1:`** $i$ 在 $R$ 之内: 可以找到 $i$ 关于 $M$ 的对称点 $2M-i$。:-: 由于蓝色区域是一个回文串,这意味着我们可以通过其对称性获取到一些信息。 - **`Case 1.1:`** 若 $p[2M-i]\le R-i$: 我们知道 $R-i$ 也是 $2M-i$ 到蓝区左端的距离。这种情况实际上是在说:以 $2M-i$ 为中心扩展,扩不出蓝区的范围。 由蓝区的对称性可知,以 $i$ 为中心的「可扩展程度」和以 $2M-i$ 为中心的「可扩展程度」是相同的,于是有 $p[i]=p[2M-i]$。:-: - **`Case 1.2:`** 若 $p[2M-i]>R-i$: 以 $2M-i$ 为中心扩展,可以扩展到蓝区外面。 由于蓝区以外的情况我们并不知晓,这种情况下 $p[i]$ 只能有一个 $R-i$ 的保底。我们直接从 $R$ 开始继续暴力扩展,从而得到 $p[i]$ 的实际值。:-: 扩展完成后,我们得到了一个更靠右的回文串,需要相应地更新 $M$ 和 $R$ 的值。:-: - **`Case 2:`** $i$ 在 $R$ 之外: 这种情况就只能直接暴力扩展了。扩展完成后同样要更新 $M$ 和 $R$。:-: 进一步简化算法的过程: - 若 $i> R$,则令 $p[i]=0$; - 否则令 $\DeclareMathOperator{\min}{min} p[i]=\min(p[2M-i],R-i)$。 在此基础上,继续暴力扩展,得到 $p[i]$ 的实际值,并更新 $M$ 和 $R$。 最后遍历数组 $p$,$p$ 中的最大值即为最长回文子串的半径。最后要记得把特殊字符去除掉。 ---- 每次暴力扩展的起点都是 $R$,扩展完成后 $R$ 又更新了过去,作为下一次暴力扩展的起点。在扩展的过程中,每个字符都只被访问了一次。故时间复杂度为 $O(n)$。 模板 ``` cpp include <bits/stdc++.h> using namespace std; string manacher(const string &s) { string t = "^"; for (char c : s) { t += c; t += ""; } t += '$'; vector<int> p(t.size()); int M = 0, R = 0; for (int i = 1; i < t.size() - 1; i ++) { p[i] = (i > R) ? 0 : min(p[2 * M - i], R - i); while (t[i + p[i] + 1] == t[i - p[i] - 1]) p[i] ++; if (i + p[i] > R) { M = i; R = i + p[i]; } } int maxM = 0, maxP = 0; for (int i = 1; i < t.size() - 1; i ++) { if (p[i] > maxP) { maxM = i; maxP = p[i]; } } int start = (maxM - maxP) / 2; return s.substr(start, maxP); } int main() { string s; cin >> s; cout << manacher(s) << endl; return 0; } ```
-
快速沃尔什变换(FWT)
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E5%A4%9A%E9%A1%B9%E5%BC%8F/%E5%BF%AB%E9%80%9F%E6%B2%83%E5%B0%94%E4%BB%80%E5%8F%98%E6%8D%A2/
一个很邪门的算法。 二进制卷积快速傅里叶变换 (`FFT`)可以高效地计算两个多项式的乘积,进而加速了卷积运算。 ???+ info 序列 $A$ 和序列 $B$ 的卷积记作 $C=A*B$,其中 $$C_k=\sum_{i+j=k}A_iB_j$$ 构造多项式 ``` latex A(x)=A_0+A_1x+A_2x^2+\cdots\\ B(x)=B_0+B_1x+B_2x^2+\cdots ``` 我们知道 $A(x)\cdot B(x)$ 中 $x^k$ 前的系数就是 $C_k$,而 $A(x)\cdot B(x)$ 的系数序列可由 `FFT` 得到。因此 `FFT` 也被认为是快速卷积算法。 ::: 现在考虑一类奇怪的卷积: ``` latex \DeclareMathOperator{\or}{or} \DeclareMathOperator{\and}{and} \DeclareMathOperator{\xor}{xor} \sum_{i \or j=k}A_iB_j \qquad \sum_{i \and j=k}A_iB_j \qquad \sum_{i \xor j=k}A_iB_j ``` 其中 $\or,\and,\xor$ 是二进制按位或、与、异或。右(下)图附上了一位二进制运算的真值表。 面对这类奇葩的卷积时,`FFT` 就束手无策了。 ::: $A$|$B$|$A\or B$|$A\and B$|$A\xor B$ :-:|:-:|:-:|:-:|:-: 0|0|0|0|0 0|1|1|0|1 1|0|1|0|1 1|1|1|1|0 还有高手? 快速沃尔什变换(Fast Walsh-Hadamard Transform,`FWT`)是加速二进制卷积运算的算法,其大方向与 `FFT` 非常相似。 回忆一下 `FFT` 如何加速多项式乘法: ???+ example FFT 1. 对 $A(x)$ 和 $B(x)$ 使用 `DFT`,得到两个点集: ``` latex \left\{\big(x_i,A(x_i)\big)\mid 0\le i< N\right\} \\ \left\{\big(x_i,B(x_i)\big)\mid 0\le i< N\right\} ``` 2. 计算出 $C(x)=A(x)\cdot B(x)$ 的点值表示法: ``` latex \left\{\big(x_i,A(x_i)\cdot B(x_i)\big)\mid 0\le i< N\right\} ``` 3. 使用 `IDFT` 将其转化为系数表示法。 我们在 `DFT` 和 `IDFT` 中使用 `FFT` 的思想来加速这个过程。 那么其实 `FWT` 用到的也是类似的思路: ???+ example FWT 1. 对序列 $A,B$ 使用 `FWT`,得到两个中间态(中间态也是序列): ``` latex \text{FWT}(A),\text{FWT}(B) ``` 2. 使用点对点乘法,计算出 $C$ 的中间态: ``` latex \text{FWT}(C)=\left\{\text{FWT}(A)_i\times\text{FWT}(B)_i \ \Big| \ i=0,1,2,\cdots\right\} ``` 3. 使用 `UFWT`(也称 `IFWT`)将其还原为序列 $C$。 只不过 `FWT` 的中间态略显抽象,并且 $\or,\and,\xor$ 卷积所对应的中间态还各不相同。 OR 卷积 在 $\or$ 卷积中,序列 $X$ 对应的中间态为与之等长的序列 $\text{FWT}(X)$,其中 ``` latex \text{FWT}(X)_k=\sum_{i\or k=k}X_i ``` > 🤔~~沃尔什究竟是怎么想出来的?~~ 设有两个序列 $A,B$,待求的序列是 $C$,其中 $C_k=\sum_{i \or j=k}A_iB_j$。 可以证明,对 $A$ 和 $B$ 的中间态进行点对点乘法可以得出 $C$ 的中间态。 ???+ note proof 容易发现 $$a\or c=c,b\or c=c\intro (a\or b)\or c=c$$ 可以从集合意义理解:若 $a\in c$ 且 $b\in c$,则 $a$ 和 $c$ 的并集也是 $c$ 的子集。 接下来的证明步骤会用到该结论。 ``` latex \begin{aligned} & \ \text{FWT}(A)_ k\times\text{FWT}(B)_ k\\ =& \ \left(\sum_{i\or k=k}A_i\right)\left(\sum_{j\or k=k}B_j\right)\\ =& \ \sum_{i\or k=k,j\or k=k}A_iB_j\\ =& \ \sum_{(i\or j)\or k=k}A_iB_j\\ =&\!\!\!\overset{x=i\or j}{=\!=\!=\!=}\!\!\!=\sum_{x\or k=k} \ \sum_{i\or j=x}A_iB_j\\ =& \ \sum_{x\or k=k}C_x\\ =& \ \text{FWT}(C)_k\\ \end{aligned} ``` 证毕。 现在推导一下中间态的求法。 类似于 `FFT`,此处也规定所有序列的长度 $n$ 都是 $2$ 的幂次。 我们将序列 $A=\\{A_0,A_1,\cdots,A_{n-1}\\}$ 均分为两段 ``` latex A_\text{prev}=\left\{A_0,A_1,\cdots,A_{\frac{n}{2}-1}\right\} \quad A_\text{back}=\left\{A_\frac{n}{2},A_{\frac{n}{2}+1},\cdots,A_{n-1}\right\} ``` 然后仔细盯真 $A$ 的中间态: $$\text{FWT}(A)_ k=\sum_{i\or k=k}A_i$$ - 当 $\textstyle 0\le k\le \frac{n}{2}-1$ 时,由 $\or$ 运算的性质可知,$i\or k=k$ 中 $i$ 的大小不可能超过 $k$。因此 $i$ 也落在 $\textstyle 0\cdots \frac{n}{2}-1$ 的范围内,也就是说 $A_i$ 只能来自 $A_\text{prev}$,即 $\sum_{i\or k=k}A_i=\sum_{i\or k=k}(A_\text{prev})_i$,即 ``` latex \text{FWT}(A)_ k=\text{FWT}\left(A_\text{prev}\right)_k ``` - 当 $\textstyle\frac{n}{2}\le k\le n-1$ 时,$A_i$ 既可以来自 $A_\text{prev}$,又可以来自 $A_\text{back}$。由于 $\text{FWT}(A)_k$ 是一个求和式,我们可以顺理成章地把它拆成两个部分的和 ``` latex \text{FWT}(A)_ k=\text{FWT}\left(A_\text{prev}\right)_{k-\frac{n}{2}}+\text{FWT}\left(A_\text{back}\right)_{k-\frac{n}{2}} ``` 注:此处对 $k$ 减去 $\textstyle\frac{n}{2}$ 是为了调整下标,使其适应相应的分段序列。或者说,$A_\text{prev}$ 和 $A_\text{back}$ 的长度都只有 $\textstyle\frac{n}{2}$,作此调整可以防止数组访问越界。 即 ``` latex \text{FWT}(A)_k=\begin{cases} \text{FWT}\left(A_\text{prev}\right)_k,&0\le k\le \frac{n}{2}-1\\ \\ \text{FWT}\left(A_\text{prev}\right)_{k-\frac{n}{2}}+\text{FWT}\left(A_\text{back}\right)_{k-\frac{n}{2}},&\frac{n}{2}\le k\le n-1 \end{cases} ``` 更进一步地,以序列为基本单位看问题: ``` latex \DeclareMathOperator{\merge}{merge} \text{FWT}(A)=\begin{cases} A,&n=1\\ \\ \merge\left(\text{FWT}\left(A_\text{prev}\right),\text{FWT}\left(A_\text{back}\right) \oplus \text{FWT}\left(A_\text{prev}\right)\right),&n>1 \end{cases} \tag{1} ``` 其中 $\merge$ 是拼接两个序列的操作,$\oplus$ 表示两个序列的点对点加法。 ---- 上面我们已经剖析了 `FWT` 的算法,现在还需要讨论其反演 `UFWT`。 其实只要把点对点加法 $\oplus$ 改为点对点减法 $\ominus$ 就可以得到 `UFWT` 的递推式。 ``` latex \text{UFWT}(A)=\begin{cases} A,&n=1\\ \\ \merge\left(\text{UFWT}\left(A_\text{prev}\right),\text{UFWT}\left(A_\text{back}\right) \ominus \text{UFWT}\left(A_\text{prev}\right)\right),&n>1 \end{cases} \tag{2} ``` 容易证明 $(2)$ 式是 $(1)$ 式的逆变换。 ??? note proof 此论断等价于 $\text{UFWT}\big(\text{FWT}(A)\big)=A$,这里采用数学归纳法证明。 设序列 $A$ 的长度为 $n$。当 $n=1$ 时命题显然成立。 由于 $n$ 始终是 $2$ 的幂次,即只需证明当 $n>1$ 时 $$\textstyle 命题对 \ \frac{n}{2} \ 成立 \intro 命题对 \ n \ 成立$$ 现假设命题对 $\textstyle\frac{n}{2}$ 成立。由于 $A_\text{prev}$ 和 $A_\text{back}$ 都是长为 $\textstyle\frac{n}{2}$ 的序列,因此有 ``` latex \text{UFWT}\big(\text{FWT}(A_\text{prev})\big)=A_\text{prev}, \text{UFWT}\big(\text{FWT}(A_\text{back})\big)=A_\text{back} ``` 由式 $(1)$ $(2)$ 得(将 $\merge$ 拆开): ``` latex \begin{aligned} &\begin{cases} \text{FWT}(A)_\text{prev}=\text{FWT}(A_\text{prev}) \\ \text{FWT}(A)_\text{back}=\text{FWT}(A_\text{back})\oplus\text{FWT}(A_\text{prev}) \end{cases} \\ \\ &\begin{cases} \text{UFWT}(A)_\text{prev}=\text{UFWT}(A_\text{prev}) \\ \text{UFWT}(A)_\text{back}=\text{UFWT}(A_\text{back})\ominus\text{UFWT}(A_\text{prev}) \end{cases} \end{aligned} ``` 因此 ``` latex \begin{aligned} \text{UFWT}\big(\text{FWT}(A)\big)_\text{prev}&=\text{UFWT}\big(\text{FWT}(A)_\text{prev}\big)=\text{UFWT}\big(\text{FWT}(A_\text{prev})\big)=A_\text{prev}\\ \\ \text{UFWT}\big(\text{FWT}(A)\big)_\text{back}&=\text{UFWT}\big(\text{FWT}(A)_\text{back}\big)\ominus\text{UFWT}\big(\text{FWT}(A)_\text{prev}\big)\\ &=\text{UFWT}\big(\text{FWT}(A_\text{back}) \oplus \text{FWT}(A_\text{prev})\big)\ominus \text{UFWT}\big(\text{FWT}(A_\text{prev})\big)\\ &=\text{UFWT}\big(\text{FWT}(A_\text{back})\big) \oplus \text{UFWT}\big(\text{FWT}(A_\text{prev})\big)\ominus \text{UFWT}\big(\text{FWT}(A_\text{prev})\big)\\ &=A_\text{back}\oplus A_\text{prev}\ominus A_\text{prev}\\ &=A_\text{back} \end{aligned} ``` 因此 ``` latex \text{UFWT}\big(\text{FWT}(A)\big)=\merge(A_\text{prev},A_\text{back})=A ``` 即命题对 $n$ 成立。证毕。 模板 === 递归 ``` cpp void FWT_OR(vector<int>& a, int l, int r, bool inv) { if (l == r) return; int mid = (l + r) >> 1; FWT_OR(a, l, mid, inv); FWT_OR(a, mid + 1, r, inv); for (int i = 0; i <= mid - l; i ++) { if (!inv) amid + 1 + i] += a[l + i]; else a[mid + 1 + i] -= a[l + i]; } } ``` === 递推 ``` cpp void FWT_OR(vector<int>& a, bool inv) { int n = a.size(); for (int d = 1; d < n; d <<= 1) { for (int m = d << 1, i = 0; i < n; i += m) { for (int j = 0; j < d; j ++) { if (!inv) a[i + j + d] += a[i + j]; else a[i + j + d] -= a[i + j]; } } } } ``` AND 卷积 $\and$ 卷积和 $\or$ 卷积在本质上是类似的。 ``` latex \text{FWT}(X)_k=\sum_{i\and k=k}X_i ``` 类似可证 $\text{FWT}(A)_k\times\text{FWT}(B)_k=\text{FWT}(C)_k$。 由于 $\and$ 运算的性质,递推式的点对点加减法操作都集中在前面。 ``` latex \begin{aligned} \text{FWT}(A)&=\begin{cases} A,&n=1\\ \\ \merge\left(\text{FWT}\left(A_\text{prev}\right) \oplus \text{FWT}\left(A_\text{back}\right),\text{FWT}\left(A_\text{back}\right)\right),&n>1 \end{cases} \newline \newline \text{UFWT}(A)&=\begin{cases} A,&n=1\\ \\ \merge\left(\text{UFWT}\left(A_\text{prev}\right) \ominus \text{UFWT}\left(A_\text{back}\right),\text{UFWT}\left(A_\text{back}\right)\right),&n>1 \end{cases} \end{aligned} ``` 模板 === 递归 ``` cpp void FWT_AND(vector<int>& a, int l, int r, bool inv) { if (l == r) return; int mid = (l + r) >> 1; FWT_AND(a, l, mid, inv); FWT_AND(a, mid + 1, r, inv); for (int i = 0; i <= mid - l; i ++) { if (!inv) a[l + i] += a[mid + 1 + i]; else a[l + i] -= a[mid + 1 + i]; } } ``` === 递推 ``` cpp void FWT_AND(vector<int>& a, bool inv) { int n = a.size(); for (int d = 1; d < n; d <<= 1) { for (int m = d << 1, i = 0; i < n; i += m) { for (int j = 0; j < d; ++j) { if (!inv) a[i + j] += a[i + j + d]; else a[i + j] -= a[i + j + d]; } } } } ``` XOR 卷积 $\xor$ 卷积的构造相比前两个更加麻烦。 设序列 $X$(长为 $n$)的中间态为 $\text{FWT}(X)$,其中 $$\text{FWT}(X) _ k=\sum_{i=0}^{n-1}f(i,k)X_i$$ 其中 $f(x,y)$ 是待确定的函数,需要通过以下过程反推出来。 ---- 要使 $\text{FWT}(A)_k\times\text{FWT}(B)_k=\text{FWT}(C)_k$,即 $$\sum_{i=0}^{n-1}f(i,k)A_i \times \sum_{j=0}^{n-1}f(j,k)B_j = \sum_{x=0}^{n-1}f(x,k)C_x$$ 将 $C_x=\sum_{i\xor j=x}A_iB_j$ 代入 $$\sum_{i=0}^{n-1}f(i,k)A_i \times \sum_{j=0}^{n-1}f(j,k)B_j = \sum_{x=0}^{n-1}f(x,k) \sum_{i\xor j=x}A_iB_j$$ $$\sum_{i=0}^{n-1}\sum_{j=0}^{n-1} f(i,k) f(j,k) A_iB_j = \sum_{x=0}^{n-1} \sum_{i\xor j=x} f(x,k) A_iB_j$$ 在 $0\cdots n-1$ 范围内,对于每个可能的 $i,j$ 组合,总有一个 $x$ 满足 $i\xor j=x$,所以 $\sum_{x=0}^{n-1} \sum_{i\xor j=x}$ 实际上遍历了所有的 $i$ 和 $j$,因此 $$\sum_{i=0}^{n-1}\sum_{j=0}^{n-1} f(i,k) f(j,k) A_iB_j = \sum_{i=0}^{n-1} \sum_{j=0}^{n-1} f(i\xor j,k) A_iB_j$$ 即得 $$f(i,k)f(j,k)=f(i\xor j,k)\tag{i}$$ ---- 注意到 ``` latex \DeclareMathOperator{\bitcount}{bitcount} a\xor b=c\intro(-1)^{\large\bitcount(a)}(-1)^{\large\bitcount(b)}=(-1)^{\large\bitcount(c)}\tag{ii} ``` 其中 $\bitcount(x)$ 表示 $x$ 的二进制中 $1$ 的个数。 > 这句话实际是在说:当 $a\xor b=c$ 时 $\bitcount(a)+\bitcount(b)$ 的奇偶性和 $\bitcount(c)$ 相同。 > > 这个结论是显然的。二进制按位异或实际上是一位一位地异或: > - 若 $a,b$ 的当前位都是 $1$,则 $c$ 的该位为 $0$,相当于少了两个 $1$,$1$ 的个数的奇偶性没有变; > - 若一个是 $1$,另一个是 $0$,则 $c$ 的该位为 $1$,相当于没差; > - 若都是 $0$ 也没差。 > > 既然奇偶性相同,那么 $(-1)^{\large\bitcount(a)+\bitcount(b)}=(-1)^{\large\bitcount(c)}$,变形可得 $(\text{ii})$ 式。 又注意到 $$(i\and k)\xor(j\and k)=(i\xor j)\and k$$ 代入 $(\text{ii})$ 式得 $$(-1)^{\large\bitcount(i\and k)}(-1)^{\large\bitcount(j\and k)}=(-1)^{\large\bitcount((i\xor j)\and k)}$$ 于是便可以确定函数 $f$ 的形式: $$f(x,y)=(-1)^{\large\bitcount(x \and y)}$$ 该形式完美符合 $(\text i)$ 式的结论。 从而序列 $X$ 的中间态的全貌应该是这样的: $$\text{FWT}(X) _ k=\sum_{i=0}^{n-1}(-1)^{\large\bitcount(i\and k)}X_i$$ ---- 现对 $\text{FWT}(A)_k$ 进行分治。 - 当 $\textstyle 0\le k\le \frac{n}{2}-1$ 时,直接把求和式拆分成两半 ``` latex \begin{aligned} \text{FWT}(A) _ k&=\sum_{i=0}^{n-1}(-1)^{\large\bitcount(i\and k)}A_i\\ &=\sum_{i=0}^{\frac{n}{2}-1}(-1)^{\large\bitcount(i\and k)}A_i+\sum_{i=\frac{n}{2}}^{n-1}(-1)^{\large\bitcount(i\and k)}A_i\\ &=\sum_{i=0}^{\frac{n}{2}-1}(-1)^{\large\bitcount(i\and k)}(A_\text{prev})_i+\sum_{i=0}^{\frac{n}{2}-1}(-1)^{\large\bitcount(i\and k)}(A_\text{back})_i\\ &=\text{FWT}(A_\text{prev})_k+\text{FWT}(A_\text{back})_k \end{aligned} ``` - 当 $\textstyle \frac{n}{2}\le k\le n-1$ 时,需要对下标 $k$ 进行偏移,偏移量为 $\textstyle\frac{n}{2}$,理由同 [OR 卷积。 需要注意的是,当 $i$ 取 $\textstyle\frac{n}{2}\cdots n-1$ 时,$\textstyle\bitcount\left(i\and \left(k-\frac{n}{2}\right)\right)=-\bitcount(i\and k)$,下标偏移导致了此处符号的改变。 ``` latex \begin{aligned} \text{FWT}(A) _ k&=\sum_{i=0}^{n-1}(-1)^{\large\bitcount(i\and k)}A_i\\ &=\sum_{i=0}^{\frac{n}{2}-1}(-1)^{\large\bitcount\left(i\and \left(k-\frac{n}{2}\right)\right)}A_i-\sum_{i=\frac{n}{2}}^{n-1}(-1)^{\large\bitcount\left(i\and \left(k-\frac{n}{2}\right)\right)}A_i\\ &=\sum_{i=0}^{\frac{n}{2}-1}(-1)^{\large\bitcount\left(i\and \left(k-\frac{n}{2}\right)\right)}(A_\text{prev})_i-\sum_{i=0}^{\frac{n}{2}-1}(-1)^{\large\bitcount\left(i\and \left(k-\frac{n}{2}\right)\right)}(A_\text{back})_i\\ &=\text{FWT}(A_\text{prev})_{k-\frac{n}{2}}-\text{FWT}(A_\text{back})_{k-\frac{n}{2}} \end{aligned} ``` 即 ``` latex \text{FWT}(A)_k=\begin{cases} \text{FWT}(A_\text{prev})_k+\text{FWT}(A_\text{back})_k, &0\le k\le \frac{n}{2}-1\\ \\ \text{FWT}(A_\text{prev})_{k-\frac{n}{2}}-\text{FWT}(A_\text{back})_{k-\frac{n}{2}}, &\frac{n}{2}\le k\le n-1 \end{cases} ``` 进一步地 ``` latex \text{FWT}(A)=\begin{cases} A,&n=1\\ \merge(\text{FWT}(A_\text{prev})\oplus \text{FWT}(A_\text{back}),\text{FWT}(A_\text{prev})\ominus \text{FWT}(A_\text{back})),&n>1 \end{cases} ``` 对应地 ``` latex \text{UFWT}(A)=\begin{cases} A,&n=1\\ \merge\left(\displaystyle\frac{\text{UFWT}(A_\text{prev})\oplus \text{UFWT}(A_\text{back})}{2},\frac{\text{UFWT}(A_\text{prev})\ominus \text{UFWT}(A_\text{back})}{2}\right),&n>1 \end{cases} ``` 其中对序列的除法运算也是点对点除法。 模板 === 递归 ``` cpp void FWT_XOR(vector<int>& a, int l, int r, bool inv) { if (l == r) return; int mid = (l + r) >> 1; FWT_XOR(a, l, mid, inv); FWT_XOR(a, mid + 1, r, inv); for (int i = 0; i <= mid - l; i ++) { int u = a[l + i], v = a[mid + 1 + i]; a[l + i] = u + v; a[mid + 1 + i] = u - v; } if (inv) { for (int i = l; i <= r; i ++) { a[i] /= 2; } } } ``` === 递推 ``` cpp void FWT_XOR(vector<int>& a, bool inv) { int n = a.size(); for (int d = 1; d < n; d <<= 1) { for (int i = 0; i < n; i += (d << 1)) { for (int j = 0; j < d; j ++) { int u = a[i + j], v = a[i + j + d]; a[i + j] = u + v; a[i + j + d] = u - v; } } } if (inv) { for (int i = 0; i < n; i ++) { a[i] /= n; } } } ```
-
快速数论变换(FNTT)
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E5%A4%9A%E9%A1%B9%E5%BC%8F/%E5%BF%AB%E9%80%9F%E6%95%B0%E8%AE%BA%E5%8F%98%E6%8D%A2/
快速傅里叶变换的变体。 在快速傅里叶变换 (`FFT`)中,单位复根用于多项式的分治过程,作为变量 $x$ 的取值。然而,使用单位复根有几个缺点: 1. 单位复根定义在三角函数之上 $$\omega_N=\cos\frac{2\pi}{N}+i\sin\frac{2\pi}{N}$$ 在运算过程中,会频繁地跟浮点复数 `complex<double>` 打交道,导致精度损失; 2. 不适用于模环境(复数无法取模); 3. 算法常数较大,影响效率。 幸运的是,在整数域中,我们找到了可替代单位复根的数值,它们**仅在模环境中有效**。这就是快速数论变换(Fast Number-Theoretic Transform, `FNTT`)的核心。 原根 对于质数 $p$,如果存在整数 $g$ 使 $$g^0\bmod p,g^1\bmod p,g^2\bmod p,\cdots,g^{p-2}\bmod p$$ 这 $p-1$ 个数互不相同,则称 $g$ 是 $p$ 的**原根**。 ---- 根据费马小定理: > 对任意质数 $p\in\mathbb{P}$ 和任意整数 $a\in\mathbb{Z}$,都有 $a^{p-1}\equiv 1 \pmod{p}$。 因此 $g^{p-1}\equiv 1=g^0\pmod{p}$。进一步地: ``` latex \begin{aligned} g^{p}\equiv g^{1}\pmod{p}\\ g^{p+1}\equiv g^{2}\pmod{p}\\ g^{p+2}\equiv g^{3}\pmod{p} \end{aligned} ``` 这意味着,计算 $g$ 的更高幂次会重复之前的模 $p$ 序列,形成一个轮回。 我们可以将此轮回用环状结构表示。 ::::-: 模意义下原根的轮回 ::::-: 单位复根的轮回 是不是跟单位复根如出一辙。 基于原根的该性质,我们可以构造与单位复根同构的表达式。 现对质数 $p$ 作进一步的约束:令 $p$ 满足 $p=1+N$ 的形式,其中 $N=2^\xi$ 是 $2$ 的幂次。从而构造 $$G_N=g^{\frac{p-1}{N}}\bmod p$$ 可以发现,$G_N$ 具有单位复根 $\omega_N$ 的所有性质: 1. **周期性**:$G_N^N=1$ 证明:$G_N^N=g^{p-1}\bmod p=1$ 2. **消去性**:$G_{2N}^{2k}=G_N^k$ 证明:$G_{2N}^{2k}=g^{\frac{p-1}{2N}\cdot 2k}\bmod p=g^{\frac{p-1}{N}\cdot k}\bmod p=G_N^k$ 3. **对称性**:$G_{N}^{k+N/2}=-G_{N}^k$ 证明: $$G_{N}^{k+N/2}=g^{\frac{p-1}{N}\left(k+\frac{N}{2}\right)} \bmod p=g^{\frac{p-1}{N}\cdot k}\cdot g^{\frac{p-1}{2}}\bmod p=G_N^{k}\cdot g^{\frac{p-1}{2}}\bmod p$$ 由于 $\left(g^{\frac{p-1}{2}}\right)^2=g^{p-1}\equiv 1\pmod{p}$,故 $g^{\frac{p-1}{2}}\equiv\pm 1\pmod{p}$。 根据原根的定义,$g^0,g^1,\cdots,g^{p-2}$ 在模 $p$ 意义下互不相同,因此 $g^{\frac{p-1}{2}}\not\equiv g^0= 1\pmod{p}$,即 $g^{\frac{p-1}{2}}\bmod p$ 只能是 $-1$。代入得 $G_{N}^{k+N/2}=-G_{N}^k$。 因此 $G_N$ 可以完全替代单位复根,参与 `FFT` 的运算过程。 原根表 > 注:一个质数可能对应多个原根。例如 $998244353$ 的原根可以是 $3$,也可以是 $114514$。 模板 ``` cpp include <bits/stdc++.h> using namespace std; typedef long long LL; const LL Mod = 998244353; const LL G = 114514; const LL Gi = 137043501; LL powmod(LL a, LL b) { LL res = 1; a %= Mod; while (b > 0) { if (b % 2 == 1) res = res * a % Mod; a = a * a % Mod; b /= 2; } return res; } vector<LL> NTT(vector<LL> a, bool invert) { int n = a.size(); if (n == 1) return a; vector<LL> a0(n / 2), a1(n / 2); for (int i = 0; i < n / 2; i ++) { a0[i] = a[2 * i]; a1[i] = a[2 * i + 1]; } vector<LL> y0 = NTT(a0, invert); vector<LL> y1 = NTT(a1, invert); vector<LL> y(n); LL w = 1; LL root = powmod(!invert ? Gi : G, (Mod - 1) / n); for (int i = 0; i < n / 2; i ++) { LL u = y0[i]; LL v = y1[i] * w % Mod; y[i] = (u + v) % Mod; y[i + n / 2] = (u - v + Mod) % Mod; w = w * root % Mod; } return y; } vector<LL> multiply(vector<LL> A, vector<LL> B) { int n = 1; while (n < A.size() + B.size()) n *= 2; A.resize(n); B.resize(n); vector<LL> yA = NTT(A, false); vector<LL> yB = NTT(B, false); vector<LL> yC(n); for (int i = 0; i < n; i ++) yC[i] = yA[i] * yB[i] % Mod; vector<LL> C = NTT(yC, true); LL inv_n = powmod(n, Mod - 2); for (LL &x : C) x = x * inv_n % Mod; while (C.size() && ! C.back()) C.pop_back(); return C; } int main() { vector<LL> A = {1, 2, 3}; vector<LL> B = {4, 5, 6}; vector<LL> res = multiply(A, B); for (LL x : res) cout << x << " "; cout << endl; return 0; } ```
-
快速傅里叶变换:应用
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E5%A4%9A%E9%A1%B9%E5%BC%8F/%E5%BF%AB%E9%80%9F%E5%82%85%E9%87%8C%E5%8F%B6%E5%8F%98%E6%8D%A2%E5%BA%94%E7%94%A8/
FFT 的所有应用都围绕着它的本质功能:计算两个多项式的乘积。 大整数乘法(高精度乘法) 假设有两个整数 $a=537,b=721$。$a,b$ 的每一位可以视为多项式 $A(x),B(x)$ 的系数,就像这样 $$A(x)=5x^2+3x+7, B(x)=7x^2+2x+1$$ 计算 $C(x)=A(x)\cdot B(x)$: $$C(x)=35x^4+31x^3+60x^2+17x+7$$ 提取 $C(x)$ 的系数: $$\\{35,31,60,17,7\\}$$ 从右往左,逢 $10$ 进 $1$: $$\\{3,8,7,1,7,7\\}$$ 这样就能得到 $537\times721$ 的结果是 $387177$。 中间的多项式乘法由 $\text{FFT}$ 进行。如果 $a$ 和 $b$ 是 $n$ 位整数,则总时间复杂度是 $O(n\log{n})$。 ---- 整数乘法和多项式乘法具有高度的相似性。 一个整数可以分解为它的位值的和。例如,整数 $123$ 可以分解为 $1\times10^2+2\times10+3$。 设 $a=\overline{a_2a_1a_0}$,$b=\overline{b_2b_1b_0}$,则 ``` latex \begin{aligned} &a \times b \\ = \ & (a_2 \cdot 10^2 + a_1 \cdot 10 + a_0) \cdot (b_2 \cdot 10^2 + b_1 \cdot 10 + b_0) \\ = \ & a_2b_2 \cdot 10^4 + (a_2b_1 + a_1b_2) \cdot 10^3 + (a_2b_0 + a_1b_1 + a_0b_2) \cdot 10^2 + (a_1b_0 + a_0b_1) \cdot 10 + a_0b_0 \end{aligned} ``` 设 $A(x)=a_2x^2+a_1x+a_0$,$B(x)=b_2x^2+b_1x+b_0$,则 ``` latex \begin{aligned} &A(x) \times B(x)\\ = \ & (a_2x^2 + a_1x + a_0) \cdot (b_2x^2 + b_1x + b_0) \\ = \ & a_2b_2\cdot x^4 + (a_2b_1 + a_1b_2)\cdot x^3 + (a_2b_0 + a_1b_1 + a_0b_2)\cdot x^2 + (a_1b_0 + a_0b_1)\cdot x + a_0b_0 \end{aligned} ``` 可以看出整数乘法和多项式乘法基本是同构的。整数乘法在最后一步需要额外处理进位。 模板 ``` cpp // put FFT algorithm template here vector<Comp> string_to_vector(const string& s) { vector<Comp> result; for (auto rit = s.rbegin(); rit != s.rend(); ++rit) { result.push_back(*rit - '0'); } return result; } string vector_to_string(const vector<Comp>& v) { string result; for (const Comp& c : v) { result += to_string(int(c.real())) + " "; } return result; } string upgrade(vector<Comp>& a) { string result; int carry = 0; for (auto i : a) { int num = int(real(i) + 0.5) + carry; carry = num / 10; result += (num % 10) + '0'; } while (!result.empty() && result.back() == '0') { result.pop_back(); } reverse(result.begin(), result.end()); return result.empty() ? "0" : result; } string multiply_big_integers(const string& s1, const string& s2) { vector<Comp> a = string_to_vector(s1); vector<Comp> b = string_to_vector(s2); vector<Comp> c = multiply(a, b); return vector_to_string(c); } int main() { string num1, num2; cin >> num1 >> num2; string product = multiply_big_integers(num1, num2); cout << product << endl; return 0; } ``` 字符串匹配 朴素字符串匹配 设有两个字符串 $\text{text}$,$\text{pattern}$ 的长度分别为 $n,m(n\ge m)$,并只包含小写字母 $a\sim z$。问 $\text{pattern}$ 在 $\text{text}$ 中出现了几次,以及所有的出现位置。 该问题可使用 $\text{FFT}$ 在 $O(n\log{n})$ 的复杂度下解决。 !!! tip 用 $\text{FFT}$ 做字符串匹配是一个差想法。$\text{KMP}$ 可以在 $O(n+m)$ 的线性复杂度下解决字符串匹配问题,并且支持通配符。 ---- 约定 $\text{str}\Big|_a^b$ 表示从 $\text{str}[a]$ 到 $\text{str}[b]$ 的子串。 首先将 $\text{text}$ 和 $\text{pattern}$ 的所有字母都映射成数字($a\rightarrow 1,b\rightarrow 2,\cdots$),得到两个整型数组 $A$ 和 $B$。 ``` latex \begin{aligned} \text{text}&\rightarrow A:\{A_0,A_1,\cdots,A_{n-1}\}\\ \text{pattern}&\rightarrow B:\{B_0,B_1,\cdots,B_{m-1}\} \end{aligned} ``` 容易发现 ``` latex \left(A_i-B_j\right)^2=\begin{cases} 0,&A_i=B_j\\ \\ >0,&A_i\not=B_j \end{cases} ``` 进而构造 ``` latex \begin{aligned} P_k&=(A_k-B_0)^2+(A_{k+1}-B_1)^2+\cdots+(A_{k+m-1}-B_{m-1})^2\\ &=\sum_{i=0}^{m-1}\left(A_{k+i}-B_i\right)^2\\ &=\begin{cases} 0,&\text{text}\Big|_{k}^{k+m-1}=\text{pattern}\\ \\ >0,&\text{text}\Big|_{k}^{k+m-1}\not=\text{pattern} \end{cases} \end{aligned} ``` 因此可以利用 $P_k$ 判断 $\text{pattern}$ 是否出现在 $\text{text}$ 的第 $k$ 位。 展开 $P_k$: ``` latex \begin{aligned} P_k&=\sum_{i=0}^{m-1}\left(A_{k+i}-B_i\right)^2\\ &=\sum_{i=0}^{m-1}\left(A_{k+i}^2+B_{i}^2-2A_{k+i}B_{i}\right)\\ &=\sum_{i=0}^{m-1}A_{k+i}^2+\sum_{i=0}^{m-1}B_{i}^2-2\sum_{i=0}^{m-1}A_{k+i}B_{i} \end{aligned} ``` 将数组 $B$ 反转得到 $\widetilde{B}$,有 $B_i=\widetilde{B}_{m-i-1}$。代入得 ``` latex P_k=\sum_{i=0}^{m-1}A_{k+i}^2+\sum_{i=0}^{m-1}B_{i}^2-2\sum_{i=0}^{m-1}A_{k+i}\widetilde{B}_{m-i-1} ``` 分析该式的三项: 1. $\sum_{i=0}^{m-1}A_{k+i}^2$ 可以通过前缀和技巧实现 $O(n)$ 预处理,$O(1)$ 查询; 2. $\sum_{i=0}^{m-1}B_{i}^2$ 是常数,可以直接 $O(n)$ 预处理; 3. $\sum_{i=0}^{m-1}A_{k+i}\widetilde{B}_{m-i-1}$ 是卷积式,可以通过 $\text{FFT}$ 实现 $O(n\log{n})$ 预处理,$O(1)$ 查询。 对于第 3 项,更具体地说: 构造多项式 ``` latex A(x)=A_0+A_1x+A_2x^2+\cdots+A_{n-1}x^{n-1}\\ \widetilde{B}(x)=\widetilde{B}_0+\widetilde{B}_1x+\widetilde{B}_2x^2+\cdots+\widetilde{B}_{m-1}x^{m-1} ``` 利用 $\text{FFT}$ 可以 $O(n\log{n})$ 得出多项式 $C(x)=A(x)\cdot \widetilde{B}(x)$ 的系数向量。易知 $C(x)$ 中 $x^{k+m-1}$ 前的系数为 $\sum_{i=0}^{m-1}A_{k+i}\widetilde{B}_{m-i-1}$,这正是我们想要的第 3 项。 依次计算 $P_k(k=0,1,2,\cdots,n-m)$,当得到 $0$ 时说明 $\text{pattern}$ 出现在 $\text{text}$ 的第 $k$ 位。 模板 ``` cpp // put FFT algorithm template here vector<int> stringMatchingFFT(const string& text, const string& pattern) { int n = text.size(), m = pattern.size(); vector<Comp> A(n), B(m); long long sum_AA[n], sum_BB = 0; for (int i = 0; i < n; i ++) { int value = text[i] - 'a' + 1; A[i] = value; sum_AA[i] = value * value; if (i) { sum_AA[i] += sum_AA[i - 1]; } } for (int i = 0; i < m; i ++) { int value = pattern[i] - 'a' + 1; B[m - i - 1] = value; sum_BB += value * value; } vector<Comp> C = multiply(A, B); vector<int> matches; for (int i = 0; i <= n - m; i ++) { long long part1 = sum_AA[i + m - 1] - (i ? sum_AA[i - 1] : 0), part2 = sum_BB, part3 = C[i + m - 1].real(); if (part1 + part2 - 2 * part3 == 0) matches.push_back(i); } return matches; } int main() { string text, pattern; cin >> text >> pattern; vector<int> matches = stringMatchingFFT(text, pattern); cout << matches.size() << "\n"; for (int i : matches) cout << i << " "; cout << "\n"; return 0; } ``` 带通配符的字符串匹配 设有两个字符串 $\text{text}$,$\text{pattern}$ 的长度分别为 $n,m(n\ge m)$,并只包含小写字母 $a\sim z$ 和通配符 $*$(可与任意字符匹配)。问 $\text{pattern}$ 在 $\text{text}$ 中的匹配次数和所有匹配位置。 令通配符 $*$ 映射到 $0$,则 ``` latex \left(A_i-B_j\right)^2\cdot A_i\cdot B_j=\begin{cases} 0,&A_i=B_j\\ \\ >0,&A_i\not=B_j \end{cases} ``` ``` latex \begin{aligned} P_k&=\sum_{i=0}^{m-1}\left(A_{k+i}-B_i\right)^2\cdot A_{k+i}\cdot B_i\\ &=\sum_{i=0}^{m-1}\left(A_{k+i}^2+B_{i}^2-2A_{k+i}B_{i}\right)\cdot A_{k+i}\cdot B_i\\ &=\sum_{i=0}^{m-1}A_{k+i}^3B_i+\sum_{i=0}^{m-1}A_{k+i}B_{i}^3-2\sum_{i=0}^{m-1}A_{k+i}^2B_{i}^2\\ &=\sum_{i=0}^{m-1}A_{k+i}^3\widetilde{B}_{m-i-1}+\sum_{i=0}^{m-1}A_{k+i}\widetilde{B}_{m-i-1}^3-2\sum_{i=0}^{m-1}A_{k+i}^2\widetilde{B}_{m-i-1}^2 \end{aligned} ``` 构造多项式 ``` latex A_k(x)=A_0^k+A_1^kx+A_2^kx^2+\cdots+A_{n-1}^kx^{n-1}\\ \widetilde{B}_k(x)=\widetilde{B}_0^k+\widetilde{B}_1^kx+\widetilde{B}_2^kx^2+\cdots+\widetilde{B}_{m-1}^kx^{m-1} ``` 1. $\sum_{i=0}^{m-1}A_{k+i}^3\widetilde{B}_{m-i-1}$ 是 $A_3(x)\cdot \widetilde{B}_1(x)$ 中 $x^{k+m-1}$ 前的系数; 2. $\sum_{i=0}^{m-1}A_{k+i}\widetilde{B}_{m-i-1}^3$ 是 $A_1(x)\cdot \widetilde{B}_3(x)$ 中 $x^{k+m-1}$ 前的系数; 3. $\sum_{i=0}^{m-1}A_{k+i}^2\widetilde{B}_{m-i-1}^2$ 是 $A_2(x)\cdot \widetilde{B}_2(x)$ 中 $x^{k+m-1}$ 前的系数。 进行三次 $\text{FFT}$ 即可。 模板 ``` cpp // put FFT algorithm template here vector<int> stringMatchingFFT(const string& text, string pattern) { int n = text.size(), m = pattern.size(); vector<Comp> A1, A2, A3, B1, B2, B3; for (auto i : text) { int value = i == '*' ? 0 : i - 'a' + 1; A1.push_back(value); A2.push_back(value * value); A3.push_back(value * value * value); } reverse(pattern.begin(), pattern.end()); for (auto i : pattern) { int value = i == '*' ? 0 : i - 'a' + 1; B1.push_back(value); B2.push_back(value * value); B3.push_back(value * value * value); } vector<Comp> part1 = multiply(A3, B1); vector<Comp> part2 = multiply(A1, B3); vector<Comp> part3 = multiply(A2, B2); vector<int> matches; for (int i = 0; i <= n - m; i ++) { if (part1[i + m - 1].real() + part2[i + m - 1].real() - part3[i + m - 1].real() * 2 == 0) matches.push_back(i); } return matches; } int main() { string text, pattern; cin >> text >> pattern; vector<int> matches = stringMatchingFFT(text, pattern); cout << matches.size() << "\n"; for (int i : matches) cout << i << " "; cout << "\n"; return 0; } ```
-
快速傅里叶变换:蝶形优化
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E5%A4%9A%E9%A1%B9%E5%BC%8F/%E5%BF%AB%E9%80%9F%E5%82%85%E9%87%8C%E5%8F%B6%E5%8F%98%E6%8D%A2%E8%9D%B6%E5%BD%A2%E4%BC%98%E5%8C%96/
能看出蝶形的受上赏。 在快速傅里叶变换 (`FFT`)中,$\text{FFT}$ 算法使用分治思想,每次都将系数数组 $\\{a_0,\cdots,a_7\\}$ 按元素位置的奇偶性拆分成两半,并使用递归的方法,自上而下地进行运算。:-: 每次拆分时,都要把偶数项拷贝到新数组 $A_\text{even}$,把奇数项拷贝到新数组 $A_\text{odd}$,再执行运算 $\text{DFT}\left(A_\text{even}\\right)$ 和 $\text{DFT}\left(A_\text{odd}\\right)$。这么做既费时又费力。 如果我们一开始就将 $a_0\sim a_7$ 的顺序处理成 ``` latex \{a_0,a_4,a_2,a_6,a_1,a_5,a_3,a_7\} ``` 并自下而上地进行运算:-: 这样就能避免愚蠢的拷贝操作,进一步压榨计算机的算力。 这个优化过程被称为蝶形优化。 蝶形优化 简言之,我们需要的是这样的变换: ``` latex \begin{aligned} 0&\longrightarrow 0 \\ 0,1&\longrightarrow 0,1 \\ 0,1,2,3&\longrightarrow 0,2,1,3 \\ 0,1,2,3,4,5,6,7&\longrightarrow 0,4,2,6,1,5,3,7\\ & \ \cdots \end{aligned} ``` 这个变换的策略出人意料地简单:将每个数转为二进制(在最高位补零补到一样长),然后反转它们,就能得到蝶形优化的结果。 举个例子: 是不是很神奇。 ---- 如果你对该策略仍持有怀疑态度,仍然不能心安理得地直接使用蝶形优化算法,那么我们浅浅地证明一下好了。 证明 我们仅保留数组 $a$ 的下标,略去其它部分,重新画出 $\text{FFT}$ 的递归图。:-: 这张图所展示的递归策略可以被简要地概括为: !!! info 在每一层,如果数 $k$(在其序列中)在偶位,就把它划分到左侧;否则数 $k$ 在奇位,就把它划分到右侧。 !!! warning 需要特别强调「在其序列中」这个点。例如在第二层 $$0,2,4,6,\quad 1,3,5,7$$ 其中数字 $5$ 的位置应该是 $3$ 而不是 $7$。因为 $5$ 所在的序列是 $1,3,5,7$,而不是一整排。 问题来了:在某一层,如何判断数 $k$ 是处于偶位,还是处于奇位? 首先我们需要归纳 $k$ 在每一层的位置。 !!! note 容易看出: - 在第一层:数 $k$ 排在第 $k$ 位 - 在第二层:数 $k$ 排在第 $\lfloor k\div2\rfloor$ 位 - 在第三层:数 $k$ 排在第 $\lfloor k\div2^2\rfloor$ 位 - ... - 在第 $n$ 层,数 $k$ 排在第 $\lfloor k\div2^{n-1}\rfloor$ 位。 我们直接将 $\lfloor k\div2^{n-1}\rfloor$ 对 $2$ 取模,若得到 $0$,说明这个位置是个偶数,也就是 $k$ 在偶位;相反地,若得到 $1$,则说明 $k$ 在奇位。 可能有人已经发现了 $$\lfloor k\div2^{n-1}\rfloor\bmod 2$$ 这个式子就是 $k$ 在二进制下的第 $n$ 位的数字。 也就是说,我们只需要判断 $k$ 的二进制第 $n$ 位,就能知道 $k$ 在第 $n$ 层是处于偶位,还是处于奇位。 ---- ::: 考察数字 $1=(001)_2$ 从上到下的位置变化: - 在第一层,$(001)_2$ 的第一位是 $1$,说明它在奇位,因此被划分到右侧 - 在第二层,$(001)_2$ 的第二位是 $0$,说明它在偶位,因此被划分到左侧 - 在第三层,$(001)_2$ 的第三位是 $0$,说明它在偶位,因此被划分到左侧 ::::-: 现在,思考一个问题:数 $k$ 在第一层被划分到右侧,意味着什么?——意味着它最终肯定处于后 $50\\%$ 的地方。可以认为,变换后数 $k$ 的位序的二进制最高位是 $1$。 ``` latex 000\quad001\quad010\quad011\quad\overset{\large 后 \ 50\%}{\overbrace{100\quad101\quad110\quad111}} ``` !!! note 进一步地,我们可以归纳出: - 在第一层 - 被划分到左侧 $\Rightarrow$ 最高位为 $0$ - 被划分到右侧 $\Rightarrow$ 最高位为 $1$ - 在第二层 - 被划分到左侧 $\Rightarrow$ 次高位为 $0$ - 被划分到右侧 $\Rightarrow$ 次高位为 $1$ - ... 现在重新审视数字 $1=(001)_2$ 的位置变化: - 在第一层,$(001)_2$ 的第一位是 $1$,被划分到右侧,变换后它的位序的最高位是 $1$ - 在第二层,$(001)_2$ 的第二位是 $0$,被划分到左侧,变换后它的位序的次高位是 $0$ - 在第三层,$(001)_2$ 的第三位是 $0$,被划分到左侧,变换后它的位序的第三高位是 $0$ 即 $1=(001)_2$ 变换后的位序是 $(100)_2=4$,而这个 $100$ 就是 $001$ 反转的结果。 同理,$4=(100)_2$ 变换后的位序是 $(001)_2=1$。因此可以认为,变换后 $4$ 和 $1$ 发生了交换。也就是,原先是 $1$ 的地方,变换后成了 $4$;原先是 $4$ 的地方,变换后成了 $1$。 那么,原先是 $k$ 的地方,变换后成了 $k$ 的二进制反转。 证毕。 模板 ``` cpp include <bits/stdc++.h> using namespace std; const double PI = acos(-1); typedef complex<double> Comp; int reverseBits(int n, int log2n) { int reversed = 0; for (int i = 0; i < log2n; i++) { if (n & (1 << i)) { reversed |= 1 << (log2n - 1 - i); } } return reversed; } void bit_reverse_swap(vector<Comp>& a) { int n = a.size(); int log2n = 0; while ((1 << log2n) < n) log2n++; for (int i = 0; i < n; i++) { int reversed = reverseBits(i, log2n); if (i < reversed) { swap(a[i], a[reversed]); } } } void FFT(vector<Comp>& a, bool invert) { int n = a.size(); bit_reverse_swap(a); for (int len = 2; len <= n; len <<= 1) { double theta = 2 * PI / len * (invert ? -1 : 1); Comp wn(cos(theta), sin(theta)); for (int i = 0; i < n; i += len) { Comp w(1); for (int j = 0; j < len / 2; ++j) { Comp u = a[i + j]; Comp v = a[i + j + len / 2] * w; a[i + j] = u + v; a[i + j + len / 2] = u - v; w *= wn; } } } if (invert) { for (Comp& x : a) x /= n; } } vector<Comp> multiply(vector<Comp> A, vector<Comp> B) { int n = 1; while (n < A.size() + B.size()) n *= 2; A.resize(n); B.resize(n); FFT(A, false); FFT(B, false); vector<Comp> C(n); for (int i = 0; i < n; i++) C[i] = A[i] * B[i]; FFT(C, true); for (int i = 0; i < n; i ++) C[i] = round(C[i].real()); while (C.size() && ! C.back().real()) C.pop_back(); return C; } int main() { vector<Comp> A = {1, 2, 3}; // Represents the polynomial 1 + 2x + 3x^2 vector<Comp> B = {4, 5}; // Represents the polynomial 4 + 5x vector<Comp> C = multiply(A, B); for (auto i : C) cout << i.real() << ' '; cout << endl; return 0; } ```
-
快速傅里叶变换(FFT)
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E5%A4%9A%E9%A1%B9%E5%BC%8F/%E5%BF%AB%E9%80%9F%E5%82%85%E9%87%8C%E5%8F%B6%E5%8F%98%E6%8D%A2/
一个改变美苏冷战格局的算法。 快速傅里叶变换能在 $O(n\log{n})$ 的时间复杂度下计算两个 $n$ 次多项式的乘法。 多项式的表示法 系数表示法 对于 $n-1$ 次多项式 $$A(x)=a_0+a_1x+a_2x^2+\cdots+a_{n-1}x^{n-1}$$ 系数表示法用 $n$ 个系数表示它。 $$\\{a_0,a_1,\cdots,a_{n-1}\\}$$ 点值表示法 两点确定 $kx+b$,三点确定 $ax^2+bx+c$。以此类推,至少 $n$ 点才能确定 $n-1$ 次多项式。 点值表示法用至少 $n$ 个不同的点表示 $n-1$ 次多项式。 $$\\{(x_0,y_0),(x_1,y_1),\cdots,(x_{n-1},y_{n-1})\\}$$ 多项式乘法 离散傅里叶变换(Discrete Fourier Transform,`DFT`)是将多项式从系数表示法转换为点值表示法的算法,`IDFT` 是 `DFT` 的逆过程。 运用 `DFT` 和 `IDFT` 计算多项式乘法 $A(x)\cdot B(x)=C(x)$ 的主要步骤: 1. 对 $A(x)$ 和 $B(x)$ 使用 `DFT`,得到两个点集: ``` latex \left\{\big(x_i,A(x_i)\big)\mid 0\le i< N\right\} \\ \left\{\big(x_i,B(x_i)\big)\mid 0\le i< N\right\} ``` 2. 计算出 $C(x)$ 的点值表示法: ``` latex \left\{\big(x_i,A(x_i)\cdot B(x_i)\big)\mid 0\le i< N\right\} ``` 3. 使用 `IDFT` 将其转化为系数表示法。 其中 $N\ge C(x) \ 的次数 \ +1=A(x) \ 的次数 \ + B(x) \ 的次数 \ +1$,否则第二步得到的点太少,不足以确定最终的答案。 一般的 `DFT` 和 `IDFT` 时间复杂度高达 $O(n^2)$,而快速傅里叶变换(Fast Fourier Transform,`FFT`)可以把这个过程优化到 $O(n\log{n})$。 让我们从一个例子入手,先熟悉一下 $O(n^2)$ 的普通算法是什么样的。 普通的算法 现在我们来计算 $A(x)=x^2+x+1$ 和 $B(x)=x^2-3$ 的乘积 $C(x)$。 这个乘法的结果肯定是一个 $4$ 次的多项式,这意味着我们至少要取 $5$ 个点,那么就随便取 $x=1,2,3,4,5$ 好了。 第一步:计算 $A(x)$ 和 $B(x)$ 的点值表示法: ``` latex A(x)\longrightarrow\{(1,3),(2,7),(3,13),(4,21),(5,31)\}\\ B(x)\longrightarrow\{(1,-2),(2,1),(3,6),(4,13),(5,22)\} ``` 第二步:计算 $C(x)=A(x)\cdot B(x)$ 的点值表示法: ``` latex C(x)\longrightarrow\{(1,-6),(2,7),(3,78),(4,273),(5,682)\} ``` 第三步:转化为系数表示法。这一步的方法有很多,可以使用拉格朗日插值法等。总之最后算出来的结果是 $$C(x)=x^4 + x^3 - 2x^2 - 3x - 3$$ 当 $C(x)$ 是 $n-1$ 次多项式时,需要取 $n$ 个不同的 $x$。其中每次计算 $A(x)\cdot B(x)$ 的时间复杂度为 $O(n)$,并且这个过程要重复 $n$ 次,那么总的时间复杂度为 $O(n^2)$。 ---- 上述流程存在一个比较现实的问题:在实际的应用场景中,多项式的次数往往很大。如果我们随意地取 $x$ 的值,计算出的 $A(x)$ 可能会超出基本变量类型的范围,那么这个算法很有可能在第一步就会搁浅。 所以 $x$ 的取值其实很有门道。它既不能让 $A(x)$ 太大,让计算机存不下;也不能让 $\|A(x)\|$ 太小,导致计算过程中发生精度的损失。这么看来,似乎只有 $0$,$1$ 和 $-1$ 这三个数比较合适。 但是只有这三个数是远远不够的。去哪里找其它的数呢? 这样的数,数学家们在**复数域**中找到了无穷多个。 单位复根 !!! info 复习 形如 $a+bi$($a$、$b$ 均为实数)的数为复数。其中 - $a$ 被称为实部 - $b$ 被称为虚部 - $i$ 是虚数单位,$i^2=-1$ - $\sqrt{a^2+b^2}$ 是这个复数的模 在复平面上,$a+bi$ 对应的坐标为 $(a,b)$。其中 - $a$ 表示的是复平面内的横坐标 - $b$ 表示的是复平面内的纵坐标 - 表示实数 $a$ 的点都在 $x$ 轴上,所以 $x$ 轴又称为「实轴」 - 表示纯虚数 $bi$ 的点都在 $y$ 轴上,所以 $y$ 轴又称为「虚轴」 如图 1,在复平面上画一个半径为 $1$ 的单位圆。圆上的每一点 $(\cos\theta,\sin\theta)$ 都可以表示复数 $\cos{\theta}+i\sin{\theta}$,其中 $\theta$ 是幅角,即它和原点的连线与实轴正半轴的夹角。 如果把圆周角 $N$ 等分,也就是令 $\theta=\frac{2\pi}{N}$,那么这个复数就被称作 **单位复根**,记作 $\omega_N$。 $$\omega_N=\cos\frac{2\pi}{N}+i\sin\frac{2\pi}{N}$$ 中学课本告诉了我们复数乘法的规律:幅角相加模相乘。我们知道 $\omega_N$ 的模为 $1$,那么 $\omega_N^k$ 的模也就还是 $1$,且其幅角从原来的 $\frac{2\pi}{N}$ 变成了 $\frac{2k\pi}{N}$。 根据上述规律,不难发现,$\omega_N^0,\omega_N^1,\omega_N^2,\cdots,\omega_N^{N-1}$ 在单位圆上的分布是均匀的。图 2 展示了 $N=8$ 时的情况。 ::: || |:-:| |图 1| ::: || |:-:| |图 2| 单位复根还具有如下优异的性质: 1. 周期性:$\omega_N^N=1$ 2. 消去性:$\omega_{2N}^{2k}=\omega_N^k$ 3. 对称性:$\omega_N^{k+\frac{N}{2}}=-\omega_N^k$ 证明并不困难。一方面可以用上文中 $\omega_N^k$ 的图像性质去推理;另一方面,直接套用欧拉公式 ``` latex e^{\large i\theta}=\cos{\theta}+i\sin{\theta}\intro\omega_N=e^{\large\frac{2\pi}{N}i} ``` 也能能轻易得证。 ----单位复根恰好可以完美地解决 `DFT` 中取点的问题。代入 $x=\omega_N^k$ 既不会使 $y=A(x)$ 大到溢出,也不会使其小到失真,唯一别扭的地方就是 $x$ 和 $y$ 都是复数值。不过大部分编程语言都有支持复数运算的库,所以这不是大问题。因此当我们需要在 `DFT` 中取 $N$ 个点时,不妨就取 $$x=\omega_N^0,\omega_N^1,\omega_N^2,\cdots,\omega_N^{N-1}$$ 但是单位复根仅仅是解决了计算过程中的问题。目前为止,这个算法的时间复杂度仍然是 $O(n^2)$。真正使其成为「快速」傅里叶变换的,是接下来要讲的「多项式分治」。 多项式分治 对一个 $n$ 项的多项式 $$A(x)=a_0+a_1x+a_2x^2+a_3x^3+\cdots+a_{n-2}x^{n-2}+a_{n-1}x^{n-1}$$ 进行如下变换(假定 $n=2^k,k\in\mathbb{Z}$): 1. 将偶数项留在前面,将奇数项移到后面(项数从 $0$ 开始计) $$A(x)=a_0+a_2x^2+\cdots+a_{n-2}x^{n-2}+a_1x+a_3x^3+\cdots+a_{n-1}x^{n-1}$$ 2. 对后一半提取公因式 $x$ $$A(x)=a_0+a_2x^2+\cdots+a_{n-2}x^{n-2}+x\cdot\left(a_1+a_3x^2+\cdots+a_{n-1}x^{n-2}\right)$$ 设 $$A_\text{even}(x)=a_0+a_2x+\cdots+a_{n-2}x^{\frac{n}{2}-1}$$ $$A_\text{odd}(x)=a_1+a_3x+\cdots+a_{n-1}x^{\frac{n}{2}-1}$$ 则 $$A(x)=A_\text{even}\left(x^2\right)+x\cdot A_\text{odd}\left(x^2\right)$$ 注意这里我们将 $x^2$ 作为 $A_\text{even}$ 和 $A_\text{odd}$ 的变量。 将 $x=\omega_n^k$ 代入得: ``` latex \begin{aligned} A\left(\omega_n^k\right)&=A_\text{even}\left(\omega_n^{2k}\right)+\omega_n^k\cdot A_\text{odd}\left(\omega_n^{2k}\right)\newline &=\textcolor{red}{A_\text{even}\left(\omega_{n/2}^{k}\right)}+\omega_n^k\cdot \textcolor{blue}{A_\text{odd}\left(\omega_{n/2}^{k}\right)} \end{aligned} ``` 将 $x=\omega_n^{k+n/2}=-\omega_n^k$ 代入得: ``` latex \begin{aligned} A\left(\omega_n^{k+n/2}\right)&=A\left(-\omega_n^k\right)\newline &=\textcolor{red}{A_\text{even}\left(\omega_{n/2}^{k}\right)}-\omega_n^k\cdot \textcolor{blue}{A_\text{odd}\left(\omega_{n/2}^{k}\right)} \end{aligned} ``` 我们发现,$\textcolor{red}{A_\text{even}\left(\omega_{n/2}^{k}\right)}$ 和 $\textcolor{blue}{A_\text{odd}\left(\omega_{n/2}^{k}\right)}$ 各自又都是 $n/2$ 项多项式,它们也可以用同样的方法再往下拆分成更短的多项式之和。所以我们采用递归的方式去实现这个计算过程。 对每个 $A\left(\omega_n^k\right)$ 单独递归的效率太低。这里采用的递归策略是:先计算出 ``` latex \left\{\textcolor{red}{A_\text{even}\left(\omega_{n/2}^0\right)},\textcolor{red}{A_\text{even}\left(\omega_{n/2}^1\right)},\cdots,\textcolor{red}{A_\text{even}\left(\omega_{n/2}^{n/2-1}\right)}\right\} ``` ``` latex \left\{\textcolor{blue}{A_\text{odd}\left(\omega_{n/2}^0\right)},\textcolor{blue}{A_\text{odd}\left(\omega_{n/2}^1\right)},\cdots,\textcolor{blue}{A_\text{odd}\left(\omega_{n/2}^{n/2-1}\right)}\right\} ``` 再根据之前推出的公式 ``` latex \begin{cases} A\left(\omega_n^k\right)&=&\textcolor{red}{A_\text{even}\left(\omega_{n/2}^{k}\right)}&+&\omega_n^k\cdot \textcolor{blue}{A_\text{odd}\left(\omega_{n/2}^{k}\right)}\\ \\ A\left(\omega_n^{k+n/2}\right)&=&\textcolor{red}{A_\text{even}\left(\omega_{n/2}^{k}\right)}&-&\omega_n^k\cdot \textcolor{blue}{A_\text{odd}\left(\omega_{n/2}^{k}\right)} \end{cases} \quad k=0,1,\cdots,\frac{n}{2}-1 ``` 得出 ``` latex \left\{A\left(\omega_{n}^0\right),A\left(\omega_{n}^1\right),\cdots,A\left(\omega_{n}^{n-1}\right)\right\} ``` 进而得出我们所需要的 $n$ 个点值。 ---- 设 $a=\\{a_0,a_1,\cdots,a_{n-1}\\}$ 是存放 $A(x)$ 系数的数组,函数 $\text{DFT}(a)$ 的功能是算出并返回 ``` latex \left\{A\left(\omega_{n}^0\right),A\left(\omega_{n}^1\right),\cdots,A\left(\omega_{n}^{n-1}\right)\right\} ``` 以下是 $\text{DFT}(a)$ 的伪代码: ``` latex \DeclareMathOperator{\let}{let \ } \DeclareMathOperator{\if}{If \ } \DeclareMathOperator{\for}{For \ } \DeclareMathOperator{\return}{return \ } \begin{aligned} &\text{DFT}(a):\newline &\qquad n:=a.\text{size}()\newline &\qquad \if \ n=1:\newline &\qquad \qquad \text{return} \ a\newline &\qquad y^\text{even}:=\text{DFT}(\{a_0,a_2,\cdots,a_{n-2}\})\newline &\qquad y^\text{odd} \ :=\text{DFT}(\{a_1,a_3,\cdots,a_{n-1}\})\newline &\qquad y:=\{\}\newline &\qquad \omega:=1 \newline &\qquad \textstyle \omega_n:=\cos\frac{2\pi}{n}+i\sin\frac{2\pi}{n}\newline &\qquad \for \ \textstyle k = 0 \cdots \frac{n}{2}-1:\newline &\qquad \qquad y_k=y^\text{even}_k+\omega * y^\text{odd}_k\newline &\qquad \qquad y_{k+\frac{n}{2}}=y^\text{even}_k-\omega * y^\text{odd}_k\newline &\qquad \qquad \omega=\omega*\omega_n\newline &\qquad \return y \end{aligned} ``` 可以画出 $\text{DFT}$ 算法的递归图::-: 容易看出,以上分治算法每次都能递归地将规模为 $n$ 的问题拆分成两个规模为 $n/2$ 的问题,总时间复杂度为 $O(n\log{n})$。 ---- 之所以在一开始假定 $n=2^k,k\in\mathbb{Z}$,就是为了确保每次对多项式进行拆分时都能恰好平均分为两半。如果多项式的项数 $n$ 不符合这个要求,那么我们可以往后面补 $0$ 项,直到达成这个要求。 例如对于 $A(x)=1+x+4x^2+5x^3+x^4+4x^5$ 一共只有 $6$ 项,还差 $2$ 项就能符合要求,所以往后面补两个 $0$ 项: $$A(x)=1+x+4x^2+5x^3+x^4+4x^5+0x^6+0x^7$$ ---- 以上就是 `FFT` 加速算法的全部内容。不过我们只是用它加速了 `DFT` 的过程。多项式乘法的最后一步,也就是 `IDFT`,我们仍然没有提及。实际上 `IDFT` 也可以用同样的算法进行加速。 IDFT 前文所论述的 `DFT` 算法实际上是在解以下方程组: ``` latex \begin{cases} a_0 & + & a_1 & + & a_2 & + & \cdots & + & a_{n-1} & = & y_0\newline a_0 & + & a_1\left(\omega_n\right) & + & a_2\left(\omega_n\right)^2 & + & \cdots & + & a_{n-1}\left(\omega_n\right)^{n-1} & = & y_1\newline a_0 & + & a_1\left(\omega_n^2\right) & + & a_2\left(\omega_n^2\right)^2 & + & \cdots & + & a_{n-1}\left(\omega_n^2\right)^{n-1} & = & y_2\newline & & & & & &\cdots\newline a_0 & + & a_1\left(\omega_n^{n-1}\right) & + & a_2\left(\omega_n^{n-1}\right)^2 & + & \cdots & + & a_{n-1}\left(\omega_n^{n-1}\right)^{n-1} & = & y_{n-1} \end{cases} ``` 令 $\mathbf{A}=\begin{pmatrix}a_0 \newline a_1 \newline a_2 \newline\vdots \newline a_{n-1}\end{pmatrix}$,$\mathbf{Y}=\begin{pmatrix}y_0 \newline y_1 \newline y_2 \newline \vdots \newline y_{n-1}\end{pmatrix}$,则上述方程组可以写成矩阵乘法的形式: ``` latex \begin{pmatrix} 1 & 1 & 1 & \cdots & 1 \newline 1 & \omega_n & \left(\omega_n\right)^2 & \cdots & \left(\omega_n\right)^{n-1} \newline 1 & \omega_n^2 & \left(\omega_n^2\right)^2 & \cdots & \left(\omega_n^2\right)^{n-1} \newline \vdots & \vdots & \vdots & \ddots & \vdots \newline 1 & \omega_n^{n-1} & \left(\omega_n^{n-1}\right)^2 & \cdots & \left(\omega_n^{n-1}\right)^{n-1} \newline \end{pmatrix} \cdot \mathbf{A} = \mathbf{Y} ``` `DFT` 的数学本质就是已知 $\mathbf{A}$,求解 $\mathbf{Y}$。而 `IDFT` 作为其逆过程,实际上就是已知 $\mathbf{Y}$,求解 $\mathbf{A}$。这正好对应了从点值表示法向系数表示法的转换。 为了实现 `IDFT`,我们可以很自然地对上式做出以下变换: ``` latex \mathbf{A} = \begin{pmatrix} 1 & 1 & 1 & \cdots & 1 \newline 1 & \omega_n & \left(\omega_n\right)^2 & \cdots & \left(\omega_n\right)^{n-1} \newline 1 & \omega_n^2 & \left(\omega_n^2\right)^2 & \cdots & \left(\omega_n^2\right)^{n-1} \newline \vdots & \vdots & \vdots & \ddots & \vdots \newline 1 & \omega_n^{n-1} & \left(\omega_n^{n-1}\right)^2 & \cdots & \left(\omega_n^{n-1}\right)^{n-1} \newline \end{pmatrix}^{-1} \cdot \mathbf{Y} ``` 那怎么求中间的这个逆矩阵呢?我们一眼就可以盯真出这是个特殊范德蒙矩阵的逆矩阵。 !!! info 范德蒙矩阵求逆的特殊情况 在范德蒙矩阵中,当 $x_1=x_2=\cdots=x_n=x$ 时,有 ``` latex \begin{pmatrix} 1 & 1 & 1 & \cdots & 1 \\ 1 & x & x^2 & \cdots & x^{n-1} \\ 1 & x^2 & x^4 & \cdots & x^{2(n-1)} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & x^{n-1} & x^{2(n-1)} & \cdots & x^{(n-1)(n-1)} \end{pmatrix}^{-1} = \frac{1}{n} \begin{pmatrix} 1 & 1 & 1 & \cdots & 1 \\ 1 & x^{n-1} & x^{2(n-1)} & \cdots & x^{(n-1)(n-1)} \\ 1 & x^{(n-2)} & x^{2(n-2)} & \cdots & x^{(n-2)(n-1)} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & x & x^2 & \cdots & x^{n-1} \end{pmatrix} ``` 再结合单位复根的性质就可以得出 ``` latex \begin{pmatrix} 1 & 1 & 1 & \cdots & 1 \newline 1 & \omega_n & \left(\omega_n\right)^2 & \cdots & \left(\omega_n\right)^{n-1} \newline 1 & \omega_n^2 & \left(\omega_n^2\right)^2 & \cdots & \left(\omega_n^2\right)^{n-1} \newline \vdots & \vdots & \vdots & \ddots & \vdots \newline 1 & \omega_n^{n-1} & \left(\omega_n^{n-1}\right)^2 & \cdots & \left(\omega_n^{n-1}\right)^{n-1} \newline \end{pmatrix}^{-1} = \frac{1}{n} \begin{pmatrix} 1 & 1 & 1 & \cdots & 1 \newline 1 & \omega_n^{-1} & \left(\omega_n^{-1}\right)^2 & \cdots & \left(\omega_n^{-1}\right)^{n-1} \newline 1 & \omega_n^{-2} & \left(\omega_n^{-2}\right)^2 & \cdots & \left(\omega_n^{-2}\right)^{n-1} \newline \vdots & \vdots & \vdots & \ddots & \vdots \newline 1 & \omega_n^{-(n-1)} & \left(\omega_n^{-(n-1)}\right)^2 & \cdots & \left(\omega_n^{-(n-1)}\right)^{n-1} \newline \end{pmatrix} ``` 再将这个结果代入进去,并还原成方程组: ``` latex \begin{cases} y_0 & + & y_1 & + & y_2 & + & \cdots & + & y_{n-1} & = & n\cdot a_0\newline y_0 & + & y_1\left(\omega_n^{-1}\right) & + & y_2\left(\omega_n^{-1}\right)^2 & + & \cdots & + & y_{n-1}\left(\omega_n^{-1}\right)^{n-1} & = & n\cdot a_1\newline y_0 & + & y_1\left(\omega_n^{-2}\right) & + & y_2\left(\omega_n^{-2}\right)^2 & + & \cdots & + & y_{n-1}\left(\omega_n^{-2}\right)^{n-1} & = & n\cdot a_2\newline & & & & & &\cdots\newline y_0 & + & y_1\left(\omega_n^{-(n-1)}\right) & + & y_2\left(\omega_n^{-(n-1)}\right)^2 & + & \cdots & + & y_{-(n-1)}\left(\omega_n^{-(n-1)}\right)^{n-1} & = & n\cdot a_{n-1} \end{cases} ``` 这个方程组和原先的方程组极为相似。这意味着我们只需调整 `DFT` 代码中某些参数的正负号,并且将最终的结果除以 $n$,就能得到 `IDFT` 的代码。 模板 ```cpp include <bits/stdc++.h> using namespace std; typedef complex<double> Comp; const double PI = acos(-1); vector<Comp> DFT(vector<Comp> a, bool invert) { int n = a.size(); if (n == 1) return a; vector<Comp> a0(n / 2), a1(n / 2); for (int i = 0; 2 * i < n; i ++) { a0[i] = a[2*i]; a1[i] = a[2*i + 1]; } vector<Comp> y0 = DFT(a0, invert); vector<Comp> y1 = DFT(a1, invert); vector<Comp> y(n); double angle = 2 * PI / n * (invert ? -1 : 1); Comp w(1), wn(cos(angle), sin(angle)); for (int i = 0; i < n / 2; i++) { y[i] = y0[i] + w * y1[i]; y[i + n/2] = y0[i] - w * y1[i]; if (invert) { y[i] /= 2; y[i + n/2] /= 2; } w *= wn; } return y; } vector<Comp> multiply(vector<Comp> A, vector<Comp> B) { int n = 1; while (n < A.size() + B.size()) n *= 2; A.resize(n); B.resize(n); vector<Comp> yA = DFT(A, false); vector<Comp> yB = DFT(B, false); vector<Comp> yC(n); for (int i = 0; i < n; i ++) yC[i] = yA[i] * yB[i]; vector<Comp> C = DFT(yC, true); for (int i = 0; i < n; i ++) C[i] = round(C[i].real()); while (C.size() && ! C.back().real()) C.pop_back(); return C; } int main() { vector<Comp> A = {1, 2, 3}; // Represents the polynomial 1 + 2x + 3x^2 vector<Comp> B = {4, 5}; // Represents the polynomial 4 + 5x vector<Comp> C = multiply(A, B); for (auto i : C) cout << i.real() << ' '; cout << endl; return 0; } ```
-
为什么补码长成这个样
/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%AF%BC%E8%AE%BA/%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A1%A5%E7%A0%81%E9%95%BF%E6%88%90%E8%BF%99%E4%B8%AA%E6%A0%B7/
课本上没有的知识。 为什么使用补码 即答:想用加法的逻辑处理减法。 加法器的逻辑门电路易于设计、效率高。如果能把减法转换为加法,那么加减法就都能通过加法器进行,不需要额外设计减法器,能进一步简化芯片的设计。 具体怎么转换呢? !!! info 减去一个数,等于加上这个数的相反数。 $$a-b=a+(-b)$$ 这句废话文学是补码的根本原理。 补码是如何形成的 > 补码充分体现了什么叫作「bug 用得好就是特性」。 溢出 计算机中,储存数据的容器有一个重要的特性:溢出。 这是一个四位二进制数的容器,每格只能存 $0$ 或 $1$,最多只能存到 $(15)_{10}=(1111)_2$。 ``` latex \boxed{1} \ \boxed{1} \ \boxed{1} \ \boxed{1} ``` 当把 $(10010)_2$ 存入该容器时,最高位的 $1$ 会被扔掉,储存的结果变成了 $(10)_2$,<span style="color: red">相当于减少了</span> $\color{red}(10000)_2$。 ``` latex \cancel 1 \ \boxed{0} \ \boxed{0} \ \boxed{1} \ \boxed{0} \ \color{transparent}{\cancel 1} ``` 这导致,在长度为 $4$ 的容器中,$(10001)_2\Longrightarrow(1)_2$,$(10010)_2\Longrightarrow(10)_2$,$\cdots$ 越加越小 在固定长度的容器中作加法,常常会因为溢出而出现「越加越小」的情况。 还是拿长度 $4$ 的容器举例: $$(1001)_2+{\color{green}(1110)_2}=(10111)_2\Longrightarrow (111)_2$$ 而我们又知道 $$(1001)_2+{\color{blue}(-110)_2}=(111)_2\color{transparent}\Longrightarrow (10111)_2$$ 容易看出,在该容器中,加负数 ${\color{blue}(-110)_2}$ 跟加正数 ${\color{green}(1110)_2}$ 是一回事。可以认为 ${\color{blue}(-110)_2}$ 和 ${\color{green}(1110)_2}$ 等价。 有没有一种系统的方法,给出负数,就能算出等价的正数? !!! note 设与负数 $-x$ 等价的正数为 $y$,参与加法的另一个常数为 $C$,则 $$C+(-x)=C+y\color{red}-(10000)_2$$ 解得 $y={\color{red}(10000)_2}-x$ !!! info 等式的右边的 $\color{red}-(10000)_2$,是在用数学方法模拟溢出的情况。 检验:把 $x={\color{blue}(110)_2}$ 代入,解得 $y={\color{green}(1110)_2}$。 进一步说,对于长度为 $m$ 的容器,有 $$y=(1\underset{m \times 0}{\underbrace{00\cdots0}})_2-x$$ 还是减法? 尴尬的地方来了:**计算 $x$ 所用的还是减法**。 不过仔细一看,被减数都是 $(100\cdots0)_2$ 这样的。这类减法其实也就是找找规律的事。 容易发现,对一个四位二进制数 $x$,其各位取反的结果记为 $\sim x$,则 $$x+\sim x=(1111)_2$$ 例如 $$(0110)_2+(1001)_2=(1111)_2$$ 这 $(1111)_2$,再加上 $1$,恰好就等于 $(10000)_2$。也就是说 $$x+\sim x+1=(10000)_2$$ 因此 $$y=(10000)_2-x=\sim x+1$$ 进一步说,对任意定长的容器而言,都有 $y=\sim x+1$ 符号问题 实际上,我们往往会用容器的第一个格子表示符号,$0$ 表示正数,$1$ 表示负数,例如 $${\color{red}\boxed{1}} \ \boxed{1} \ \boxed{0} \ \boxed{1} \ \boxed{0}$$ 表示 $(-1010)_2$。 因此上述的取反操作显然不能把符号位涵盖进去。 结论 到此为止,我们终于可以流畅爽滑地定义所谓「补码」: !!! info 1. 正数的补码就是其本身。 2. 负数的补码为对该数除符号位外各位取反,然后在最后一位加 $1$。 计算 $a-b$ 时,直接把 $a$ 和 $-b$ 的补码相加即可。
-
伴侣点
/posts/%E6%95%B0%E5%AD%A6/%E5%87%A0%E4%BD%95/%E5%9C%86%E9%94%A5%E6%9B%B2%E7%BA%BF/%E4%BC%B4%E4%BE%A3%E7%82%B9/
平面点都有伴侣了。 定义 对椭圆 $\frac{x^2}{a^2}+\frac{y^2}{b^2}=1$,若 $x$ 轴的点 $M,N$ 满足 $x_M\cdot x_N=a^2$,则 $M,N$ 互为伴侣点。 !!! tip 若 $M,N$ 在 $y$ 轴,则应满足 $y_M\cdot y_N=b^2$。 性质 等角性 过其中任意一个伴侣点作弦 $CD$,均可产生等角。|:-:|:-: $M,N$ 为伴侣点 $\intro k_{CN}+k_{DN}=0$|$M,N$ 为伴侣点 $\intro k_{CM}+k_{DM}=0$ 证明:设 $CD$ 斜率为 $k$,暴力计算求证。 互极性 由极点极线方程知识易得 $M,N$互极。 $$\frac{x_Mx_N}{a^2}+\frac{y_My_N}{b^2}=\frac{a^2}{a^2}=1$$ 其它性质:-: 椭圆 $\frac{x^2}{a^2}+\frac{y^2}{b^2}=1$,$E(-m,0),F(m,0),Q(\frac{a^2}{m},0)$。 - $y_M\cdot y_N=b^2\left(1-\frac{a^2}{m^2}\right)$ - $k_{BM}\cdot k_{BN}=\frac{m-a}{m+a}(e^2-1)$ - $k_{EM}\cdot k_{FN}=\frac{-b^2}{m^2+a^2}$ - $\vec{EM}\cdot\vec{FN}=\frac{(a^2-m^2)(a^2+m^2-b^2)}{m^2}$ - $\vec{FM}\cdot\vec{FN}=\frac{(a^2-m^2)(a^2-m^2+b^2)}{m^2}$ - $\vec{AN}\cdot\vec{BM}=\frac{(a^2-m^2)(a^2-b^2)}{m^2}$ - $\vec{FN}\cdot\vec{BM}=\frac{(a^2-m^2)(a^2+am-b^2)}{m^2}$ 统一的证明方法:设 $P$ 坐标,暴力计算求证。 !!! tip 明白是定值就够了。 推广 对于双曲线 $\frac{x^2}{a^2}-\frac{y^2}{b^2}=1$,$M,N$ 为伴侣点 $\eq x_M\cdot x_N=a^2$。 对于抛物线 $y^2=2px$,$M,N$ 为伴侣点 $\eq x_M+x_N=0$。
-
圆锥曲线简化模型
/posts/%E6%95%B0%E5%AD%A6/%E5%87%A0%E4%BD%95/%E5%9C%86%E9%94%A5%E6%9B%B2%E7%BA%BF/%E5%9C%86%E9%94%A5%E6%9B%B2%E7%BA%BF%E7%AE%80%E5%8C%96%E6%A8%A1%E5%9E%8B/
硬算也不是不可以。 等效判别式 原理 ``` latex \begin{cases} \disp\frac{x^2}{m}+\frac{y^2}{n}=1\newline Ax+By+C=0 \end{cases} ``` $$\Delta'=(A^2m+B^2n-C^2)$$ $$\Delta=4mnB^2\Delta'$$ !!! tip 对于双曲线 $\frac{x^2}{a^2}-\frac{y^2}{b^2}=1$,将 $n$ 视作 $-b^2$。 使用例 $\frac{x^2}{4}+\frac{y^2}{3}=1$ 和 $y=kx+2$ 有两个交点,求 $k$ 的范围。 解: ``` latex \begin{cases} \disp\frac{x^2}{4}+\frac{y^2}{3}=1\newline kx-y+2=0 \end{cases} ``` ``` latex \begin{matrix} m=4 & n=3 & \newline A^2=k^2 & B^2=1 & C^2=4 \end{matrix} ``` $$\Delta'=(4k^2+3-4)>0$$ $$k^2>\frac{1}{4}$$ $$k<-\frac{1}{2} \ \text{或} \ k>\frac{1}{2}$$ 简化二次恒等式 原理 ``` latex \begin{cases} b^2x^2+a^2y^2-a^2b^2=0\newline Ax+By+C=0 \end{cases} ``` ``` latex (a^2A^2+b^2B^2)x^2+(2a^2AC)x+(a^2C^2-a^2b^2B^2)=0 ``` !!! tip 二倍爬山放中间, 直线平方交叉乘。 ``` latex \left[ \begin{matrix} b^2 & & a^2 & & -a^2b^2 \newline & \large\times_1 & & \large\times_2 & \newline A & & B & & C \end{matrix} \right] ``` ``` latex ({\large\times_{1^2}^+})x^2+(2\diagup\!\diagdown)x+({\large\times_{2^2}^+})=0 ``` 使用例 ``` latex \begin{cases} x^2+4y^2-4=0\newline kx-y+\sqrt{3}k=0 \end{cases} ``` ``` latex \left[ \begin{matrix} 1 & 4 & -4 \newline k & -1 & \sqrt{3}k \end{matrix} \right] ``` ``` latex (4k^2+1)x^2+(8\sqrt{3}k^2)x+(12k^2-4)=0 ``` 简化韦达定理 原理 ``` latex \begin{cases} \alpha x^2+\beta y^2=\gamma\newline Ax+By+C=0 \end{cases} ``` $$\epsilon=\alpha B^2+\beta A^2$$ $$x_1+x_2=\frac{-2C\cdot\beta A}{\epsilon},y_1+y_2=\frac{-2C\cdot\alpha B}{\epsilon}$$ $$x_1x_2=\frac{\beta C^2-\gamma B^2}{\epsilon},y_1y_2=\frac{\alpha C^2-\gamma A^2}{\epsilon}$$ $$x_1y_2+x_2y_1=\frac{2\gamma AB}{\epsilon}$$ !!! tip ``` latex \left[ \begin{matrix} \alpha & & \beta & & \gamma \\ & \large\times_1 & & \large\times_2 & \\ A & & B & & C \end{matrix} \right] ``` $$x_1+x_2=\frac{-2C\cdot \diagup_1}{\large\times_{1^2}^+},y_1+y_2=\frac{-2C\cdot \diagdown_1}{\large\times_{1^2}^+}$$ $$x_1x_2=\frac{\large\times_{2^2}^-}{\large\times_{1^2}^+},y_1y_2=\frac{\large\times_{12^2}^-}{\large\times_{1^2}^+}$$ $$x_1y_2+x_2y_1=\frac{2\cdot\underline{\textcolor{transparent}{66}}\diagup}{\large\times_{1^2}^+}$$ 使用例 $\frac{x^2}{4}+\frac{y^2}{3}=1$ 和 $y=kx+2$ 有两个交点 $A(x_1,y_1),B(x_2,y_2)$。 ``` latex \begin{cases} 3x^2+4y^2=12\newline kx-y+2=0 \end{cases} ``` ``` latex \left[ \begin{matrix} 3 & 4 & 12 \newline k & -1 & 2 \end{matrix} \right] ``` $$\epsilon=3\times(-1)^2+4\times k^2=3+4k^2$$ $$x_1+x_2=\frac{-2\times 2 \times 4\times k}{3+4k^2}=\frac{-16k}{3+4k^2}$$ $$x_1x_2=\frac{4\times 2^2-12\times(-1)^2}{3+4k^2}=\frac{4}{3+4k^2}$$ $$x_1y_2+x_2y_1=\frac{2\times 12\times k\times (-1)}{3+4k^2}=\frac{-24k}{3+4k^2}$$ 弦长公式 ``` latex \begin{cases} \disp\frac{x^2}{m}+\frac{y^2}{n}=1\newline Ax+By+C=0 \end{cases} ``` 弦长长度为 ``` latex \frac{2\sqrt{mn\cdot(A^2+B^2)\cdot(mA^2+nB^2-C^2)}}{mA^2+nB^2} ``` !!! tip 马牛($mn$)逼,装大方($A^2+B^2$) 组团上街耍流氓($mA^2+nB^2-C^2$) 露完上面露下面($mA^2+nB^2$) 见人就递两根烟($2\sqrt{*}$)
-
自极三角形
/posts/%E6%95%B0%E5%AD%A6/%E5%87%A0%E4%BD%95/%E5%9C%86%E9%94%A5%E6%9B%B2%E7%BA%BF/%E8%87%AA%E6%9E%81%E4%B8%89%E8%A7%92%E5%BD%A2/
看上去十分简单。 !!! warning 需要极点极线方程的相关知识。 定义 ::: 圆锥曲线任意内接四边形 $ABCD$,其边交点 $M,N$ 和对角线交点 $P$ 构成自极三角形。 - $MN$ 是 $P$ 的极线 - $MP$ 是 $N$ 的极线 - $NP$ 是 $M$ 的极线 ::::-: 证明 引理 Ceva 定理 $\Delta ABC$ 中,$AD,BE,CF$ 共点 $O$。:-: $\frac{BD}{DC}\cdot\frac{CE}{EA}\cdot\frac{AF}{FB}=1$ 证明: ``` latex \frac{BD}{DC}=\frac{S_{\Delta AOB}}{S_{\Delta AOC}}, \frac{CE}{CA}=\frac{S_{\Delta BOC}}{S_{\Delta AOB}}, \frac{AF}{FB}=\frac{S_{\Delta AOC}}{S_{\Delta BOC}} ``` 代入即得 $$\frac{BD}{DC}\cdot\frac{CE}{EA}\cdot\frac{AF}{FB}=1$$ Ceva 定理还有角元形式 $$\frac{\sin\angle BAD}{\sin\angle CAD}\cdot\frac{\sin\angle CBE}{\sin\angle ABE}\cdot\frac{\sin\angle ACF}{\sin\angle BCF}=1$$ 证明过程与边元形式大致相同。 证明过程都是充要的,故 Ceva 定理可逆用。 Pascal 定理 二次曲线任意内接六边形的所有对边交点共线。:-: $P,Q,R$ 共线 证明: 由射影几何知识得,只需要讨论圆,其余二次曲线可由圆推广。 ::::-: ::: $P,Q,R$ 共线 $\eq PQ,BC,EF$ 共点。 在 $\Delta ADQ$ 中,由Ceva 定理得 $$\frac{\sin\angle 1}{\sin\angle 2}\cdot\frac{\sin\angle 3}{\sin\angle 4}\cdot\frac{\sin\angle 5}{\sin\angle 6}=1$$ 由圆周角定理得 $$\angle n=\angle n'\quad(n=1,2,3,4,5,6)$$ 故在 $\Delta CFQ$ 中有 $$\frac{\sin\angle 1'}{\sin\angle 2'}\cdot\frac{\sin\angle 3'}{\sin\angle 4'}\cdot\frac{\sin\angle 5'}{\sin\angle 6'}=1$$ 由Ceva 定理逆定理得 $PQ,BC,EF$ 共点。即证。 证明 ::: 移动Pascal 定理中六个顶点的位置。 ::::-: 当 $B,C$ 和 $E,F$ 重合时,直线 $BC,EF$ 为椭圆的切线。同理,再将 $B,A$ 和 $E,D$ 移动至重合。|:-:|:-: ::: 结合前两图得:椭圆任意内接四边形(如图)有 $$M,I,P,J \ \text{共线}$$ 其中 $I$ 为 $AB$ 极点,$J$ 为 $CD$ 极点。由极点极线方程知识得 $$AB:\frac{x_Ix}{a^2}+\frac{y_Iy}{b^2}=1$$ $$CD:\frac{x_Jx}{a^2}+\frac{y_Jy}{b^2}=1$$ 由 $AB,CD$ 交于 $N$,将 $N$ 点坐标代入得 ``` latex \begin{cases} \disp\frac{x_Ix_N}{a^2}+\frac{y_Iy_N}{b^2}=1\\ \disp\frac{x_Jx_N}{a^2}+\frac{y_Jy_N}{b^2}=1 \end{cases} ``` 故 $I,J$ 为方程 $\frac{x_Nx}{a^2}+\frac{y_Ny}{b^2}=1$ 的两解,即 $N$ 的极线过 $I,J$。故 $MP$ 为 $N$ 的极线。 同理得 $MN$ 为 $P$ 的极线,$NP$ 为 $M$ 的极线。 证毕。 ::::-::-: 使用例 例 1 ::: $P(p,0)$ 为定点,$E$ 的运动轨迹为 $P$ 的极线。 $$\frac{px}{a^2}+\frac{0}{b^2}=1\intro x=\frac{a^2}{p}$$ ::::-: 例 2 ::: $P(m,n)$ 为定点,$E$ 的运动轨迹为 $P$ 的极线。 $$\frac{mx}{a^2}+\frac{ny}{b^2}=1$$ ::::-: 例 3 ::: $Q$ 的运动轨迹为 $P$ 的极线。 设 $P(0,p)$,则 $Q$ 的轨迹方程为 $$\frac{0}{a^2}+\frac{py}{b^2}=1\intro y=\frac{b^2}{p}$$ 故 $$\vec{OP}\cdot\vec{OQ}=(0,p)\cdot(x_Q,\frac{b^2}{p})=b^2$$ ::::-:
-
极点极线方程
/posts/%E6%95%B0%E5%AD%A6/%E5%87%A0%E4%BD%95/%E5%9C%86%E9%94%A5%E6%9B%B2%E7%BA%BF/%E6%9E%81%E7%82%B9%E6%9E%81%E7%BA%BF%E6%96%B9%E7%A8%8B/
需要出神入化的同构技巧。 定义 极点和极线是双映射关系。 对椭圆 $\frac{x^2}{a^2}+\frac{y^2}{b^2}=1$,任意一点 $P(x_0,y_0)$ 对应的极线为 $l:\frac{x_0x}{a^2}+\frac{y_0y}{b^2}=1$。同理,任意直线 $l$ 对应的极点为 $P$。 当极点 $P$ 位于不同的空间位置时,极线 $l$ 的几何意义有所区别。 P 在椭圆上:-: $l$ 为切线 证明: $\because\frac{x_0^2}{a^2}+\frac{y_0^2}{b^2}=1,\therefore l$ 过点 $P$。 对于椭圆上任意非 $P$ 点 $(x_0',y_0')$ 有 $$\frac{x_0'^2}{a_2}+\frac{y_0'^2}{b^2}=1\not=\frac{x_0x_0'}{a^2}+\frac{y_0y_0'}{b^2}$$ 即 $l$ 上有且仅有一点 $P$ 在椭圆上。$l$ 为切线。 P 在椭圆外:-: $l$ 为切点弦 证明: 设 $AB$ 为切点弦。下证 $A,B$ 在 $l$ 上。 由$P$ 在椭圆上得 $$PA:\frac{x_1x}{a^2}+\frac{y_1y}{b^2}=1,PB:\frac{x_2x}{a^2}+\frac{y_2y}{b^2}=1$$ 将 $P(x_0,y_0)$ 分别代入 ``` latex \begin{cases} \disp\frac{x_1x_0}{a^2}+\frac{y_1y_0}{b^2}=1\\ \disp\frac{x_2x_0}{a^2}+\frac{y_2y_0}{b^2}=1 \end{cases} ``` 即 $(x_1,y_1),(x_2,y_2)$ 是方程 $\frac{x_0x}{a^2}+\frac{y_0y}{b^2}=1$ 的两解,即证 $A,B$ 在 $l$ 上。 P 在椭圆内:-: 过 $l$ 上任意点 $Q$ 作的切点弦必过点 $P$ 证明: 由$P$ 在椭圆外得 $$AB:\frac{x_1x}{a^2}+\frac{y_1y}{b^2}=1$$ 将 $P(x_0,y_0)$ 代入 $$\frac{x_1x_0}{a^2}+\frac{y_1y_0}{b^2}=1$$ 即 $(x_1,y_1)$ 是方程 $\frac{x_0x}{a^2}+\frac{y_0y}{b^2}=1$ 的一个解,即证 $Q$ 在 $l$ 上。 互极性 若 $P$ 的极线过 $Q$,则 $Q$ 的极也过 $P$。此时 $P,Q$ 互极。 本质是表达式 $$\frac{x_Px_Q}{a^2}+\frac{y_Py_Q}{b^2}=1$$ 可同时认为是由「将 $Q$ 代入 $P$ 的极线」或「将 $P$ 代入 $Q$ 的极线」产生。 推广 对于双曲线 $\frac{x^2}{a^2}-\frac{y^2}{b^2}=1$ 和极点 $P(x_0,y_0)$,极线方程为 $\frac{x_0x}{a^2}-\frac{y_0y}{b^2}=1$。 对于抛物线 $y^2=2px$ 和极点 $P(x_0,y_0)$,极线方程为 $y_0y=p(x_0+x)$。 容易看出,对于一般二次曲线 $Ax^2+By^2+Cxy+Dx+Ey+F=0$ 和极点 $P(x_0,y_0)$,极线方程为 $$Ax_0x+By_0y+C\frac{x_0y+xy_0}{2}+D\frac{x_0+x}{2}+E\frac{y_0+y}{2}+F=0$$ !!! info 我有一个绝妙的证明,可惜这里已经写不下了。 !ignore 类似结论 用到了极点极线构造过程中的思想。 中点弦 以定点 $M$ 为中点的弦 $l$ 的方程为:-: $l:\frac{x_0x}{a^2}+\frac{y_0y}{b^2}=\frac{x_0^2}{a^2}+\frac{y_0^2}{b^2}$ 证明: $M\left(\frac{x_1+x_2}{2},\frac{y_1+y_2}{2}\right)$. 联立 ``` latex \begin{cases} \disp\frac{x_1^2}{a^2}+\frac{y_1^2}{b^2}=1\\ \disp\frac{x_2^2}{a^2}+\frac{y_2^2}{b^2}=1 \end{cases} ``` 得 $$\frac{x_1^2-x_2^2}{a^2}+\frac{y_1^2-y_2^2}{b^2}=0$$ $$\frac{(x_1+x_2)(x_1-x_2)}{a^2}+\frac{(y_1+y_2)(y_1-y_2)}{b^2}=0$$ $$\frac{2x_0\Delta x}{a^2}+\frac{2y_0\Delta y}{b^2}=0$$ $$k=\frac{\Delta y}{\Delta x}=-\frac{x_0b^2}{y_0a^2}$$ $$AB:y-y_0=k(x-x_0)\eq \frac{x_0x}{a^2}+\frac{y_0y}{b^2}=\frac{x_0^2}{a^2}+\frac{y_0^2}{b^2}$$ 弦中点 $l$ 过定点 $P$,弦 $l$ 的中点 $M$ 的轨迹方程为:-: $M:\frac{x^2}{a^2}+\frac{y^2}{b^2}=\frac{x_0x}{a^2}+\frac{y_0y}{b^2}$ 证明: 由中点弦得 $$l:\frac{x_1x}{a^2}+\frac{y_1y}{b^2}=\frac{x_1^2}{a^2}+\frac{y_1^2}{b^2}$$ 由 $l$ 过点 $P(x_0,y_0)$,代入 $P$ 得 $$l:\frac{x_1x_0}{a^2}+\frac{y_1y_0}{b^2}=\frac{x_1^2}{a^2}+\frac{y_1^2}{b^2}$$ 即认为对于 $M(x,y)$,始终有 $$\frac{x\cdot x_0}{a^2}+\frac{y\cdot y_0}{b^2}=\frac{x^2}{a^2}+\frac{y^2}{b^2}$$ 成立。即证。
-
e² - 1 的应用
/posts/%E6%95%B0%E5%AD%A6/%E5%87%A0%E4%BD%95/%E5%9C%86%E9%94%A5%E6%9B%B2%E7%BA%BF/e2-1%E7%9A%84%E5%BA%94%E7%94%A8/
同时适用于椭圆和双曲线。 为什么使用 e² - 1 一个圆锥曲线第三定义的例子: 在椭圆中,$-\frac{b^2}{a^2}=e^2-1$,而在双曲线中 $\frac{b^2}{a^2}=e^2-1$。使用 $e^2-1$ 可以无视正负性的影响,将椭圆和双曲线的通用理论更好地串联起来。 应用 1:-: $$k_1\cdot k_2=e^2-1$$ 证明: ``` latex \begin{aligned} k_1\cdot k_2&=\frac{y_0}{x_0+a}\cdot\frac{y_0}{x_0-a}=\frac{y_0^2}{x_0^2-a^2} \\ &=-\frac{b^2}{a^2}=e^2-1 \end{aligned} ``` 应用 2:-: $$k_1\cdot k_2=e^2-1$$ 证明: ``` latex \left.\begin{aligned} \disp\frac{x_0^2}{a^2}+\frac{y_0^2}{b^2}=1\\ \disp\frac{x_1^2}{a^2}+\frac{y_1^2}{b^2}=1 \end{aligned}\right\} \intro \frac{x_0^2-x_1^2}{a^2}+\frac{y_0^2-y_1^2}{b^2}=0 ``` ``` latex \begin{aligned} k_1\cdot k_2&=\frac{y_0-y_1}{x_0-x_1}\cdot\frac{y_0+y_1}{x_0+x_1}=\frac{y_0^2-y_1^2}{x_0^2-x_1^2}\\ &=-\frac{b^2}{a^2}=e^2-1 \end{aligned} ``` 应用 3:-: $$k_1\cdot k_2=e^2-1$$ 证明: $M\left(\frac{x_1+x_2}{2},\frac{y_1+y_2}{2}\right)$. ``` latex \left.\begin{aligned} \disp\frac{x_1^2}{a^2}+\frac{y_1^2}{b^2}=1\\ \disp\frac{x_2^2}{a^2}+\frac{y_2^2}{b^2}=1 \end{aligned}\right\} \intro \frac{x_1^2-x_2^2}{a^2}+\frac{y_1^2-y_2^2}{b^2}=0 ``` ``` latex \begin{aligned} k_1\cdot k_2&=\frac{y_1-y_2}{x_1-x_2}\cdot\frac{(y_1+y_2)/2}{(x_1+x_2)/2}=\frac{y_1^2-y_2^2}{x_1^2-x_2^2} \\ &=-\frac{b^2}{a^2}=e^2-1 \end{aligned} ``` 应用 4:-: $$k_1\cdot k_2=e^2-1$$ 证明: 切线:$\frac{x_0x}{a^2}+\frac{y_0y}{b^2}=1,k_1=-\frac{b^2x_0}{a^2y_0}$ $$k_1\cdot k_2=-\frac{b^2x_0}{a^2y_0}\cdot\frac{y_0}{x_0}=-\frac{b^2}{a^2}=e^2-1$$ 应用 5:-: $$k_1\cdot k_2=e^2-1\intro M \ \text{轨迹为相似椭圆}$$ 证明: ``` latex \begin{aligned} &k_1\cdot k_2=e^2-1\\ \intro&\frac{y_1}{x_1}\cdot\frac{y_2}{x_2}=-\frac{b^2}{a^2}\\ \intro&\frac{x_1x_2}{a^2}+\frac{y_1y_2}{b^2}=0 \end{aligned} ``` 联立 ``` latex \begin{cases} \disp\frac{x_1^2}{a^2}+\frac{y_1^2}{b^2}=1, \disp\frac{x_2^2}{a^2}+\frac{y_2^2}{b^2}=1\\ \disp\frac{x_1x_2}{a^2}+\frac{y_1y_2}{b^2}=0 \end{cases} ``` 得 $$\frac{x_1^2+x_2^2+2x_1x_2}{a^2}+\frac{y_1^2+y_2^2+2y_1y_2}{b^2}=2$$ $$\frac{(x_1+x_2)^2}{a^2}+\frac{(y_1+y_2)^2}{b^2}=2$$ $$\frac{x_M^2}{\disp\left(\frac{a}{\sqrt{2}}\right)^2}+\frac{y_M^2}{\disp\left(\frac{b}{\sqrt{2}}\right)^2}=1$$
-
平面几何
/posts/%E6%95%B0%E5%AD%A6/%E5%87%A0%E4%BD%95/%E5%B9%B3%E9%9D%A2%E5%87%A0%E4%BD%95/
常用的平面几何结论。 距离公式 $(x_1,y_1)$ 到 $(x_2,y_2)$ 距离公式 $$d=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}$$ $(x_0,y_0)$ 到 $Ax+By+C=0$ 距离公式 $$d=\frac{|Ax_0+By_0+C|}{\sqrt{A^2+B^2}}$$ $Ax+By+C_1=0$ 到 $Ax+By+C_2=0$ 距离公式 $$d=\frac{|C_1-C_2|}{\sqrt{A^2+B^2}}$$ 切线公式 切线长公式 过 $A(x_0,y_0)$ 作圆 $(x-a)^2+(y-b)^2=r^2$ 的一条切线,切点为 $B$,切线长为 $$AB=\sqrt{(x_0-a)^2+(y_0-b)^2-r^2}$$ 切线 / 切点弦方程 过圆 $(x-a)^2+(y-b)^2=r^2$ 上一点 $P(x_0,y_0)$ 的切线方程为 $$l:(x_0-a)(x-a)+(y_0-b)(y-b)=r^2$$ 过椭圆(双曲线)$\frac{x^2}{a^2}\pm\frac{y^2}{b^2}=1$ 上一点 $P(x_0,y_0)$ 的切线方程为 $$l:\frac{x_0\cdot x}{a^2}\pm\frac{y_0\cdot y}{b^2}=1$$ 过抛物线 $y^2=2px$ 上一点 $P(x_0,y_0)$ 的切线方程为 $$l:y_0\cdot y=p(x_0+x)$$ 当 $P$ 不在对应曲线上时,$l$ 表示切点弦方程。 公共弦方程圆 $x^2+y^2+A_1x+B_1y+C_1=0$ 和 $x^2+y^2+A_2x+B_2y+C_2=0$ 的公共弦方程为 $$l:(A_1-A_2)x+(B_1-B_2)y+(C_1-C_2)=0$$ 焦半径公式 坐标式 :::$$PF_1=a+ex_0,PF_2=a-ex_0$$ :::$$PF_1=|ex_0+a|,PF_2=|ex_0-a|$$ ???+ note 证明由勾股定理得 $$x_1^2-(x_0+c)^2=x_2^2-(x_0-c)^2$$ $$x_1^2-x_2^2=(x_0+c)^2-(x_0-c)^2$$ $$2a(x_1-x_2)=4cx_0$$ $$x_1-x_2=2ex_0$$ 由 $x_1+x_2=2a$ 得 $$x_1=a+ex_0,x_2=a-ex_0$$ 双曲线同理。 夹角式$$PF=\frac{b^2}{a-c\cos\theta}=\frac{\disp\frac{b^2}{a}}{1-e\cos\theta}$$ ???+ note 证明由余弦定理得 $$r^2+(2c)^2-2\cdot r\cdot 2c\cos\theta=(2a-r)^2$$ $$c^2-cr\cos\theta=a^2-ar$$ $$r=\frac{b^2}{a-c\cos\theta}$$ 双曲线同理。 焦点三角形面积椭圆 $\frac{x^2}{a^2}+\frac{y^2}{b^2}=1$ 焦点为 $F_1,F_2$,其上有一点 $P$。焦点三角形面积为 $$S_{\Delta PF_1F_2}=b^2\tan\frac{P}{2}$$ 对于双曲线 $\frac{x^2}{a^2}-\frac{y^2}{b^2}=1$,则为 $$S_{\Delta PF_1F_2}=\frac{b^2}{\tan\frac{P}{2}}$$ ???+ note 证明记 $PF_1=m,PF_2=n$,则 $m+n=2a$。 在 $\Delta PF_1F_2$ 中,根据余弦定理 ``` latex \begin{aligned} & m^2 + n^2 - 2mn\cos P = |F_1F_2|^2 \\ \intro \ & (m+n)^2 - 2mn(1 + \cos P) = 4c ^2 \\ \intro \ & mn = \frac{2b^2}{1 + \cos P} \end{aligned} ``` ``` latex S_{\Delta PF_1F_2}=\frac{1}{2}mn\sin P=b^2\frac{\sin P}{1 + \cos P}=b^2\tan\frac{P}{2} ``` 双曲线同理。 离心率 渐近线倾斜角公式$$e=\frac{1}{|\cos\theta|}=\sqrt{1+\tan^2\theta}$$ 正弦比值公式 :::$$e=\frac{2c}{2a}=\frac{F_1F_2}{PF_1+PF_2}=\frac{\sin\theta}{\sin\alpha+\sin\beta}$$ :::$$e=\frac{2c}{2a}=\frac{F_1F_2}{|PF_1-PF_2|}=\frac{\sin\theta}{|\sin\alpha-\sin\beta|}$$ 焦比弦公式$$|e\cos\theta|=\left|\frac{\lambda-1}{\lambda+1}\right|$$ 其中 $AF=\lambda BF$。 ???+ note 证明由焦半径公式 - 夹角式知 $$AF=\frac{\disp\frac{b^2}{a}}{1-e\cos\theta},BF=\frac{\disp\frac{b^2}{a}}{1+e\cos\theta}$$ $$\lambda=\frac{AF}{BF}=\frac{1+e\cos\theta}{1-e\cos\theta}$$ $$e\cos\theta=\frac{\lambda-1}{\lambda+1}$$ 最大张角公式椭圆 $C:\frac{x^2}{a^2}+\frac{y^2}{b^2}=1$ 上存在一点 $P$ 满足 $\angle F_1PF_2=\theta$,则离心率 $e$ 满足 $$\cos\theta\ge 1-2e^2$$ ???+ note 证明当 $P$ 为上顶点时为临界情况。 $$\sin\frac{\theta}{2}=\frac{c}{a}=e$$ $$e^2=\sin^2\frac{\theta}{2}=\frac{1-\cos\theta}{2}$$ $$\cos\theta=1-2e^2$$
-
立体几何
/posts/%E6%95%B0%E5%AD%A6/%E5%87%A0%E4%BD%95/%E7%AB%8B%E4%BD%93%E5%87%A0%E4%BD%95/
常用的立体几何结论。 基本公式 法向量 交点倒数法平面过 $(a,0,0),(0,b,0),(0,0,c)$,则法向量为 $\left(\frac{1}{a},\frac{1}{b},\frac{1}{c}\right)$。 平面平行于 $x$ 轴,且过 $(0,b,0),(0,0,c)$,则法向量为 $\left(0,\frac{1}{b},\frac{1}{c}\right)$。 !!! tip 坐标系可应用平移变换。 捺撇法 $\vec{PA}=(x_1,y_1,z_1),\vec{PB}=(x_2,y_2,z_2)$,平面 $PAB$ 法向量表示为: ``` latex \xymatrix@C=2em@R=2em { x_1 & y_1\ar@{-}dr] & z_1\ar@{-}[dl]\ar@{-}[dr] & x_1\ar@{-}[dl]\ar@{-}[dr] & y_1\ar@{-}[dl] & z_1 \\ x_2 & y_2 & z_2 & x_2 & y_2 & z_2 } ``` ``` latex (y_1z_2 - z_1y_2, z_1x_2 - x_1z_2, x_1y_2 - y_1x_2) ``` !!! tip 1. 向量并排写两遍 2. 掐头去尾取中间 3. 交叉相乘再相减 三垂线定理 设 $m\in\alpha$,$n$ 在 $\alpha$ 上的投影为 $n'$ $$m\bot n'\eq m\bot n$$ !!! tip 垂影必垂斜,垂斜必垂影。 三余弦定理$OH$ 是 $OA$ 在平面 $BOC$ 上的投影。 $$\cos\angle AOC = \cos\angle AOB \cdot \cos\angle BOC$$ 三正弦定理$α$ 是二面角。 $$\sin\beta=\frac{\sin\gamma}{\sin\alpha}$$ 四面体体积公式$$V=\frac{2}{3}\cdot\frac{S_{\Delta ABD}\cdot S_{\Delta ABC}\cdot\sin\theta}{AB}$$ ???+ note 证明 ``` latex V=\frac{S_{\Delta ABC}\cdot DO}{3} =\frac{S_{\Delta ABC}\cdot DE\sin\theta}{3} =\frac{S_{\Delta ABC}\cdot DE\cdot AB\sin\theta}{3AB} =\frac{2S_{\Delta ABC}S_{\Delta ABD}\sin\theta}{3AB} ``` 空间正弦定理 :::::: ``` latex \begin{aligned} \\ \\ \\ \frac{aS_A}{\sin A}=\frac{bS_B}{\sin B}=\frac{cS_C}{\sin C} \end{aligned} ``` $S_A=S_{\Delta DBC},A$ 为二面角 $C-DA-B$,以此类推。 ???+ note 证明 由 [四面体体积公式得 $$V=\frac{2S_AS_B\sin C}{3c}=\frac{2S_BS_C\sin A}{3a}=\frac{2S_AS_C\sin B}{3b}$$ 上下分别同乘 $S_C,S_A,S_B$ $$\frac{2S_AS_BS_C\sin C}{3cS_C}=\frac{2S_AS_BS_C\sin A}{3aS_A}=\frac{2S_AS_BS_C\sin B}{3bS_B}$$ 约去 $\frac{2S_AS_BS_C}{3}$ $$\frac{\sin C}{cS_C}=\frac{\sin A}{aS_A}=\frac{\sin B}{bS_B}$$ 取倒数 $$\frac{aS_A}{\sin A}=\frac{bS_B}{\sin B}=\frac{cS_C}{\sin C}$$ 祖暅原理 !!! quote 幂势既同,则积不容异。两几何体在任意高处的截面都相等,则其体积也相等。 $$\forall h,S_1(h)=S_2(h)\intro V_1=V_2$$ 可以用微积分直观理解。 $$V=\int S(h)\d h$$
-
哥德尔不完备性定理的证明
/posts/%E6%9D%82%E9%A1%B9/%E5%93%A5%E5%BE%B7%E5%B0%94%E4%B8%8D%E5%AE%8C%E5%A4%87%E6%80%A7%E5%AE%9A%E7%90%86%E7%9A%84%E8%AF%81%E6%98%8E/
公理系统中竟然存在「既不能证明,又不能证伪」的命题 ... !!! info 所有变量默认为自然数。 罗素悖论 !!! question 一个理发师要给所有「不给自己理发的人」理发,不给「给自己理发的人」理发,他是否应该给自己理发? 如果理发师给自己理发,他就属于「给自己理发的人」,但他不能给这类人理发,矛盾;如果他不给自己理发,他就属于「不给自己理发的人」,但他必须给这类人理发,也矛盾。 英国哲学家罗素(Russell)最早发现了这类悖论。罗素悖论的具体形式还有很多,比如: - 我正在说的这句话是假话。 - 一张明信片的正面写「背面的话是真的」,背面写「正面的话是假的」。 - 一本书要列出所有「不列出自己书名的书」,这本书是否应该列出它自己? 这些悖论都能以「朴素集合论」的形式概括: 设集合 $A=\\{x\mid x\not\in A\\}$ - 若 $x\in A$,则 $x\not\in A$ - 若 $x\not\in A$,则 $x\in A$ 罗素最终构建了一个新的集合论系统 $\text{ZF(C)}$,取代了朴素集合论。$\text{ZF(C)}$ 通过对集合概念的限定,将 $A=\\{x\mid x\not\in A\\}$ 这种异端从集合的名单里剔除出去,从而规避了集合论中的罗素悖论。 但是,当我们离开集合论,来到自然数公理系统时,事情似乎变得更加复杂。 哥德尔不完备性定理 1931年,德国数学家库尔特·哥德尔(Kurt Gödel)在他的论文中提出了一个惊为天人的定理: !!! quote 在蕴含皮亚诺公理(自然数公理)的公理系统中,必然存在一个「既不能证明,又不能证伪」的命题。 此定理揭示了公理系统的巨大的缺陷。此缺陷至今未被修复。 哥德尔的证明 哥德尔在皮亚诺公理的环境下找到了一个「罗素悖论」式的数学命题: $$A:\neg A$$ - 若 $A$ 是真的,则 $A$ 是假的。 - 若 $A$ 是假的,则 $A$ 是真的。 命题 $A$ 就是「既不能证明,又不能证伪」的数学命题。 那么命题 $A$ 到底是什么?哥德尔是如何找到命题 $A$ 的? 哥德尔编码 哥德尔创造了一种编码算法,将数学语句编码为自然数。 首先,哥德尔给公理系统的 $12$ 种符号分配了 $12$ 个哥德尔数。变量 $x,y,z,\cdots$ 分配到了 $12$ 以后的质数。^1]: 符号 $s$ 表示「后继」,$s0=1,ss0=2$,以此类推。 以数学语句 $0=0$ 为例。首先将各个符号转换为哥德尔数: $$6,5,6$$ 再以前 $3$ 个质数为底数,以这些哥德尔数为指数,全部相乘: $$2^6\times 3^5\times 5^6$$ 这样就得到了 $0=0$ 的哥德尔数,记作 $G(“0=0”)=2^6\times 3^5\times 5^6$。 类似地,还有: $$G(“\neg(0=s0)”)=2^1\times 3^8\times 5^6\times 7^5\times 11^7\times 13^6\times 17^9$$ 由算术基本定理[^2] 可知,每个自然数都能解码出唯一的数学语句。例如: [^2]: 算术基本定理:每个数都有唯一的质因数分解形式,如 $300=2^2\times 3^1\times 5^2$。 $$G^{-1}(15932)=G^{-1}(2^6\times 3^5\times 5^6)=“0=0”$$ 此外,数学证明过程也可编码为哥德尔数。 $$G(“A\intro B\intro C\intro D”)=2^{G(A)}\times 3^{G(B)}\times 5^{G(C)}\times 7^{G(D)}$$ 元数学命题 考虑一类命题: - 命题 $“\neg(0=s0)”$ 的第一个字符是 $“\neg”$。 - 命题 $“\neg(0=s0)”$ 有 $7$ 个字符。 - 哥德尔数为 $a$ 的命题不能被证明。 这类陈述数学命题性质的命题称作「**元数学(meta-math)命题**」,简称「**元命题**」。 我们可以利用 [哥德尔编码明智地讨论这些元命题。例如,上述前两个命题等价于: - $G(“\neg(0=s0)”)$ 有且仅有一个 $2$ 因子。 - $G(“\neg(0=s0)”)$ 能被 $17$ 整除,但不能被 $19$ 整除。 对于第三个命题,哥德尔另外定义了一个命题形式 $\text{Dem}(a,b)$,表示「哥德尔数为 $a$ 的命题」可以证明「哥德尔数为 $b$ 的命题」。 ``` latex \text{Dem}(a,b): G^{-1}(a)\Rightarrow G^{-1}(b) ``` 于是「哥德尔数为 $a$ 的命题不能被证明」这样的元命题便可以转化为普通数学命题: $$(\forall x) \neg\text{Dem}(x,a)$$ 根据黑格尔(Hegel)的三段论,上述元命题甚至也能编码为哥德尔数。至于具体是怎么做到的,以后我会出一篇文章细说。 sub 函数 哥德尔证明的核心在于「替换」。 哥德尔定义了一个替换函数 $\text{sub}(a,b,c)$,其算法如下: 1. 将自然数 $a,b$ 解码成数学语句:$A=G^{-1}(a),B=G^{-1}(b)$; 2. 将 $A$ 中的所有 $B$ 替换为自然数 $c$,得到 $A'$; 3. 以 $G(A')$ 为函数值。 !!! example 以 $\text{sub}(15932,6,1)$ 为例: 1. $A=G^{-1}(15932)=“0=0”,B=G^{-1}(6)=“0”$; 2. 将 $A$ 中的 $“0”$ 替换为自然数 $“1”$(即 $“s0”$),得到 $A'=“s0=s0”$; 3. 以 $G(A')=2^{7}\times 3^6\times 5^{5}\times 7^{7}\times 11^{6}$ 为函数值。 即 $\text{sub}(15932,6,13)=2^{7}\times 3^6\times 5^{5}\times 7^{7}\times 11^{6}$。 考虑一个关于变量 $y$ 的元命题$M$: $$M:\text{哥德尔数为} \ \text{sub}(y,17,y) \ \text{的命题不能被证明}$$ 设 $m=G(M)$。**将命题 $M$ 中的符号 $“y”$ 替换为数字 $m$,得到新命题 $N$**: $$N:\text{哥德尔数为} \ \text{sub}(m,17,m) \ \text{的命题不能被证明}$$ 那么「哥德尔数为 $\text{sub}(m,17,m)$ 的命题」是什么?哇哦,它居然也是 $N$! !!! example 我们走一遍 $\text{sub}(m,17,m)$ 函数的算法: 1. $G^{-1}(m)=M$,$G^{-1}(17)=“y”$; 2. **将 $M$ 中的符号 $“y”$ 替换为自然数 $m$,得到的恰好是 $N$**; 3. 以 $G(N)$ 为 $\text{sub}(m,17,m)$ 的函数值。 容易发现,「哥德尔数为 $\text{sub}(m,17,m)$ 的命题」就是 $N$。 也就是说,命题 $N$ 等价于: $$N:\text{命题} \ N \ \text{不能被证明}$$ 命题 $N$ 就是我们要找的「既不能证明,又不能证伪」的命题。 布洛斯的证明 !!! info 下文以 $D(f),R(f)$ 指称函数 $f$ 的定义域和值域。 1994 年,逻辑学家乔治·布洛斯(George Boolos)给出了一个更简单的证明。该证明沿用了哥德尔编码方法。 全函数 $f$ 是全函数,当且仅当 $f$ 定义在全体自然数上,即 $D(f)=\mathbb{N}$。 !!! info 此处引入的是狭义上的全函数。广义上的全函数 $f$ 满足 $D(f)\supseteq\mathbb{N}$。 可枚举集 对于集合 $A$,若存在算法满足: - 输出的均为 $A$ 中的元素。 - $A$ 中的任意元素,在有限时间内一定会被输出。 则 $A$ 是可枚举集。 !!! note 引理 $A$ 是可枚举集$\eq f$ 是全函数,$R(f)=A$ **充分性证明** 若 $A$ 是可枚举集,由定义知,存在一个与之对应的算法。令 $f(n)$ 为此算法的第 $n$ 个输出元素($n$ 从 $0$ 记起),则 $f$ 是全函数且 $R(f)=A$。 **必要性证明** 考虑如下算法:逐个计算并输出 $f(0),f(1),f(2),\cdots$ 此算法满足: - 输出的均为 $R(f)$ 中的元素。 - $R(f)$ 中的任意元素,在有限时间内一定会被输出。 即得 $R(f)=A$ 是可枚集。 证明主体 令 $S$ 为**所有**全函数的哥德尔编码集合,即: $$S=\\{G(f)\mid D(f)=\mathbb{N}\\}$$ 考虑如下命题: $$M:S \ \text{是可枚举集}$$ 假设 $M$ 成立,由引理得,存在全函数 $F,R(F)=S$。 令函数 $g(n)=G^{-1}F(n)] (n)$。注意到 $R(F)=S$,所以 $F(n)$ 是某个全函数的哥德尔数,则 $G^{-1}[F(n)]$ 是一个全函数,即 $g$ 是全函数。也就是说: ``` latex \begin{aligned} &\forall n,g(n)=G^{-1}[F(n)\\5pt] \eq & \forall n,\exists x=n,g(x)=G^{-1}[F(n)\\[5pt] \eq & \forall n,g \ \text{与} \ G^{-1}[F(n)] \ \text{有交点} \end{aligned} ``` 由于 $\forall n,G^{-1}[F(n)]$ 涵盖了所有的全函数,上述命题实际上是说 $g$ 与所有全函数都有交点。但这显然不可能。$g$ 是全函数,$g+1$ 也是全函数,但 $g$ 与 $g+1$ 没有交点。故 **命题 $M$ 不成立**。 现在从另一个角度出发。我们可以按照句子长度一个个枚举字符串,一旦发现字符串对应的函数是全函数,就输出它的哥德尔数。如此一来,我们构建了这样的算法: - 输出的均为全函数的哥德尔数。 - 任意全函数的哥德尔数,在有限时间内一定会被输出。 此算法符合可枚举集的定义,故 $S$ 是可枚举集,**命题 $M$ 成立**。 命题 $M$ 就是「既不能证明,又不能证伪」的命题。
-
皮亚诺公理
/posts/%E6%9D%82%E9%A1%B9/%E7%9A%AE%E4%BA%9A%E8%AF%BA%E5%85%AC%E7%90%86/
有人用 379 页纸证明了 1 + 1 = 2。 概述 皮亚诺公理(自然数公理)定义了一种形如「单向链表」的数数方法。 ``` latex \xymatrix { 0\ar[r] & 1\ar[r] & 2\ar[r] & 3\ar[r] & 4\ar[r] & \cdots } ``` 为了在公理系统中严谨地使用「自然数」,数学家朱塞佩 · 皮亚诺(Giuseppe Peano)提出了五条公理。 公理一 !!! quote $0$ 是自然数。 自然数的起点诞生了。 公理二 !!! quote 任何自然数都有一个后继($n$ 的后继记作 $sn$)。 自然数的雏形有了,大概长这样: ``` latex \xymatrix { 0\ar[r] & 1\ar[r] & 2\ar[r] & 3\ar[r] & 4\ar[r] & \cdots } ``` 但也有可能长这样: ``` latex \xymatrix { 0\ar[r] & 1\ar[r] & 1\ar[r] & 4\ar[r] & 5\ar[r] & 1\ar[r] & 4\ar[r] & \cdots } ``` 还可能长这样: ``` latex \xymatrix { 0\ar@/^/[r] & 1\ar@/^/[d] \\ 3\ar@/^/[u] & 2\ar@/^/[l] } ``` 公理三 !!! quote $0$ 不是任何自然数的后继。 公理三直接排除了如下情况: ``` latex \xymatrix { 0\ar@/^/[r] & 1\ar@/^/[d] \\ 3\ar@/^/[u] & 2\ar@/^/[l] } ``` <---> ``` latex \xymatrix { 0\ar[r] & 1\ar[r] & 0\ar[r] & 1\ar[r] & \cdots } ``` 同时也说明了 $0$ 必须是第一个自然数。但是还可能长成这种造型: ``` latex \xymatrix { 0\ar[r] & 1\ar[r] & 2\ar@/^1.5pc/[rr] & 3\ar[r] & 4\ar[r] & \cdots } ``` 公理四 !!! quote 任取两个自然数 $a,b$,若 $a$ 和 $b$ 后继相同,则 $a=b$,否则 $a\not=b$。 公理四排除了如下情况: ``` latex \xymatrix { 0\ar[r] & 1\ar[r] & 1\ar[r] & 4\ar[r] & 5\ar[r] & 1\ar[r] & 4\ar[r] & \cdots } ``` ``` latex \xymatrix { 0\ar[r] & 1\ar[r] & 2\ar@/^1.5pc/[rr] & 3\ar[r] & 4\ar[r] & \cdots } ``` 自然数完美了吗?——并没有。还有一种情况: ``` latex \xymatrix { 0\ar[r] &0.5\ar[r] &0.99\ar[r] &1\ar[r] &2\ar[r] &e\ar[r] & 3\ar[r] & \cdots } ``` 这种情况同时满足公理一到四。 公理五 !!! quote 对于命题形式 $A(n)$,若 $A(0)$ 成立,且 $A(n)$ 成立可推出 $A(sn)$ 成立,则 $A$ 对于任意自然数都成立。 公理五其实就是「数学归纳法」。 举个例子,对于命题形式 $A(n): n^2 \geq n$,容易证明 $A(0)$ 成立,且 $A(n)\Rightarrow A(sn)$. 由于 $0.5,0.8$ 等数不符合这个命题形式,因而如下情况被排除: ``` latex \xymatrix { 0\ar[r] &0.1\ar[r] &0.2\ar[r] &0.3\ar[r] &1\ar[r] & 2\ar[r] & \cdots } ``` 同理,我们可以把 $1.5,e,\pi$ 这类数统统从自然数中剔除。这样就得到了一个完美的自然数系统。 ``` latex \xymatrix { 0\ar[r] & 1\ar[r] & 2\ar[r] & 3\ar[r] & 4\ar[r] & \cdots } ``` ???+ question 挖坑 看似完美的皮亚诺公理,其实潜藏着一个数学危机。德国数学家哥德尔首先发现了这个危机,并进一步提出了哥德尔不完备性定理。
-
速度增量法
/posts/%E6%9D%82%E9%A1%B9/%E9%80%9F%E5%BA%A6%E5%A2%9E%E9%87%8F%E6%B3%95/
碰撞问题大杀器。 简介 质量 $m_1$,速度 $v_1$ 的小球与质量 $m_2$,速度 $v_2$ 的小球弹性碰撞,无动能损失,则碰后速度 $v_1',v_2'$ 满足 ``` latex \begin{gather} v_1+v_1'=2v_{共}\\ v_2+v_2'=2v_{共}\\ v_{共}=\frac{m_1v_1+m_2v_2}{m_1+m_2} \end{gather} ``` 简记为 ``` latex \xymatrix@C=2em@R=.5em{ v_1\ar@{-}[dr] & & v_2'\\ & v_{共}\ar@{-}[dr]\ar@{-}[ur] & \\ v_2\ar@{-}[ur] & & v_1' } ``` > - 出现负值表示反向. > - 若可能存在动能损失,碰后实际速度 $v_{1实}\in[v_{共},v_1'],v_{2实}\in[v_{共},v_2']$. 原理 由动量定理和动能定理得 ``` latex \begin{gather*} &m_1v_1+m_2v_2=m_1v_1'+m_2v_2'\tag{1}\\ &\disp\frac{1}{2}m_1v_1^2+\frac{1}{2}m_2v_2^2=\frac{1}{2}m_1v_1'^2+\frac{1}{2}m_2v_2'^2\tag{2} \end{gather*} ``` 由 $(1)$ 得 ``` latex m_1(v_1-v_1')=-m_2(v_2-v_2')\tag{3} ``` 由 $(2)$ 得 ``` latex \begin{gather} m_1v_1^2+m_2v_2^2=m_1v_1'^2+m_2v_2'^2\\ m_1(v_1^2-v_1'^2)+m_2(v_2^2-v_2'^2)=0\\ m_1(v_1-v_1')(v_1+v_1')+m_2(v_2-v_2')(v_2+v_2')=0 \end{gather} ``` 将 $(3)$ 代入 ``` latex \begin{align*} v_1+v_1'=v_2+v_2'\\ v_2'=v_1+v_1'-v_2\tag{4} \end{align*} ``` 将 $(4)$ 代入 $(1)$ ``` latex \begin{gather} m_1v_1+m_2v_2=m_1v_1'+m_2(v_1+v_1'-v_2)\\ (m_1+m_2)v_1'=v_1(m_1-m_2)+2m_2v_2\\ v_1'=\frac{v_1(m_1-m_2)+2m_2v_2}{m_1+m_2} \end{gather} ``` 同理 ``` latex v_2'=\frac{v_1(m_2-m_1)+2m_1v_1}{m_1+m_2} ``` 从初态到共速: ``` latex \Delta v_1=v_{共}-v_1=\frac{m_1v_1+m_2v_2}{m_1+m_2}-v_1=\frac{m_2(v_2-v_1)}{m_1+m_2}\\ \Delta v_2=v_{共}-v_2=\frac{m_1v_1+m_2v_2}{m_1+m_2}-v_2=\frac{m_1(v_1-v_2)}{m_1+m_2} ``` 从共速到末态: ``` latex \Delta v_1'=v_1'-v_{共}=\frac{v_1(m_1-m_2)+2m_2v_2}{m_1+m_2}-\frac{m_1v_1+m_2v_2}{m_1+m_2}=\frac{m_2(v_2-v_1)}{m_1+m_2}\\ \Delta v_2'=v_2'-v_{共}=\frac{v_1(m_2-m_1)+2m_1v_1}{m_1+m_2}-\frac{m_1v_1+m_2v_2}{m_1+m_2}=\frac{m_1(v_1-v_2)}{m_1+m_2} ``` 即 ``` latex \Delta v_1=\Delta v_1'\\ \Delta v_2=\Delta v_2' ``` 两阶段速度增量相等. $v_{共}$ 是两小球的平均速度. ``` latex v_1+v_1'=2v_{共}\\ v_2+v_2'=2v_{共} ``` 即证. 例 1 质量为 $m$,速度为 $v$ 的 $A$ 球和质量为 $3m$ 的静止 $B$ 球发生正碰,可能存在动能损失,碰后 $B$ 球的速度大小可能为 $$A.\ 0.6v\qquad B.\ 0.4v\qquad C.\ 0.3v\qquad D.\ 0.2v$$ ???+ note 解 $v_{共}=\frac{mv+0}{m+3m}=0.25v$. ``` latex \xymatrix@C=1em@R=1em{ \textcolor{transparent}{0.}v\textcolor{transparent}{5}\ar@{-}[dr] & & 0.5v\\ & 0.25v\ar@{-}[dr]\ar@{-}[ur] & \\ \textcolor{transparent}{0.}0\textcolor{transparent}{5}\ar@{-}[ur] & & -0.5v } ``` $v_{B实}\in[0.25v,0.5v]$. 选 $BC$.
-
大同小异
/posts/%E6%95%B0%E5%AD%A6/%E5%87%BD%E6%95%B0/%E5%A4%A7%E5%90%8C%E5%B0%8F%E5%BC%82/
!!! warning 未经严格证明的理论!慎用! 简介 若构造函数题满足 - $f'(x)$ 的不等号与 $f(x)$ 的相同. - $f(x_0)$ 是 $f(x)$ 的唯一定值. 则答案必为 $(x_0,+\infty)$. 若不等号相反,则必为 $(-\infty,x_0)$. !!! info - $f'(x)$ 和 $f(x)$ 必须移项至不等号左侧,且系数为正. - 同一不等式中同时出现 $f'(x)$ 与 $f(x)$,则此不等号应归为 $f'(x)$. - 若 $f(x)$ 定义域不为 $\mathbb R$,则答案需调整至定义域内. 原理 `Under Construction ...` 例 1 $f(x)$ 定义域为 $\mathbb R,f(-1)=3,f'(x)<3$,则 $f(x)>3x+6$ 的解集为 $$A.\ (-1,1)\qquad B.\ (-1,+\infty)\qquad C.\ (-\infty,-1)\qquad D.\ \mathbb R$$ ???+ note 常规解法 构造 $g(x)=f(x)-3x-6$,原问题等价于 $g(x)>0$ 的解集. $g'(x)=f'(x)-3<0,\therefore g(x)\downarrow$. $\because g(-1)=f(-1)+3-6=0,\therefore g(x)>0$ 时 $x<-1$,选 $C$. ???+ note 速通解法 - $f'(x)$ 的不等号为 $<$. - $f(x)$ 的不等号为 $>$. - $f(-1)=3$ 是唯一定值. 故答案为 $(-\infty,-1)$,选 $C$. 例 2 $f(x)$ 定义域为 $\mathbb R,f(1)=3,f'(x)>2$,则 $f(x)>2x+1$ 的解集为 $$A.\ (-\infty,0)\qquad B.\ (0,+\infty)\qquad C.\ (1,+\infty)\qquad D.\ (-\infty,1)$$ ???+ note 解 - $f'(x)$ 的不等号为 $>$. - $f(x)$ 的不等号为 $>$. - $f(1)=3$ 是唯一定值. 故答案为 $(1,+\infty)$,选 $C$. 例 3 $f(x)$ 定义域为 $\mathbb R,f(x)<2f'(x),f(\ln 4)=2$,则 $f(x)<e^{\frac{x}{2}}$ 的解集为 $\underline{\textcolor{transparent}{whatthefuck}}$. ???+ note 解 - $f'(x)$ 的不等号为 $>$. - $f(x)$ 的不等号为 $<$. - $f(\ln 4)=2$ 是唯一定值. 故答案为 $(-\infty,\ln 4)$. 例 4 $f(x)$ 定义域为 $\mathbb R,f(x)>f'(x),y=f(x)+2019$ 为奇函数,则 $f(x)+2019e^x<0$ 的解集为 $\underline{\textcolor{transparent}{whatthefuck}}$. ???+ note 解 由奇函数性质得,$f(0)+2019=0,\therefore f(0)=-2019$. - $f'(x)$ 的不等号为 $<$. - $f(x)$ 的不等号为 $<$. - $f(0)=-2019$ 是唯一定值. 故答案为 $(0,+\infty)$. 例 5 $f(x)$ 定义域为 $\mathbb R,f'(x)-f(x)>0,f(2021)=e^{2021}$,则 $f\left(\frac{1}{3}\ln x\right)<\sqrt[3]{x}$ 的解集为 $$A.\ (e^{6063},+\infty)\qquad B.\ (0,e^{2021})\qquad C.\ (e^{2021},+\infty)\qquad D.\ (0,e^{6063})$$ ???+ note 解 令 $t=\frac{1}{3}\ln x$,则 $x=e^{3t}$,即求 $f(t)<\sqrt[3]{e^{3t}}=e^t$ 的解集. - $f'(t)$ 的不等号为 $>$. - $f(t)$ 的不等号为 $<$. - $f(2021)=e^{2021}$ 是唯一定值. 故 $f(t)<e^t$ 的解集为 $(-\infty,2021)$. $\therefore\frac{1}{3}\ln x\in(-\infty,2021)$,解得 $x\in(0,e^{6063})$. 选 $D$.
-
N1 互换
/posts/%E6%95%B0%E5%AD%A6/%E6%95%B0%E5%88%97/n1%E4%BA%92%E6%8D%A2/
简介 表述 1 $a_n=\frac{b}{(kn+p)(kn+q)}$. 若 $a_n$ 满足 - $(kn+p)(kn+q)=0$ 的两根 $n_1-n_2=1$. - 大根 $n_1$ 所在的位置是 $(kn+p)$,即 $kn_1+p=0$. 则 $\\{a_n\\}$ 的前 $n$ 项和为 $$S_n=\frac{bn}{(k+p)(kn+q)}$$ > 若 $n_1-n_2\not=1$ 则不可使用 N1 互换. 表述 2 $a_n$ 为等差数列,则 $\frac{1}{a_na_{n+1}}$ 的前 $n$ 项和为 $$S_n=\frac{n}{a_1a_{n+1}}$$ 容易证明同表述 1等价. 原理 ``` latex \frac{1}{a_na_{n+1}}=\frac{1}{d}\cdot\frac{a_{n+1}-a_n}{a_na_{n+1}}=\frac{1}{d}\left(\frac{1}{a_n}-\frac{1}{a_{n+1}}\right)\\ \begin{aligned} S_n&=\frac{1}{d}\left(\frac{1}{a_1}-\frac{1}{a_2}+\frac{1}{a_2}-\frac{1}{a_3}+\cdots+\frac{1}{a_n}-\frac{1}{a_{n+1}}\right)\\ &=\frac{1}{d}\left(\frac{1}{a_1}-\frac{1}{a_{n+1}}\right)\\ &=\frac{1}{d}\cdot\frac{a_{n+1}-a_1}{a_1a_{n+1}}\\ &=\frac{n}{a_1a_{n+1}} \end{aligned} ``` 例 1 已知 $a_n=\frac{1}{(2n+1)(2n+3)}$,则 $\\{a_n\\}$ 的前 $n$ 项和 $S_n=\underline{\textcolor{transparent}{whatthefuck}}$. 1.裂项 $$a_n=\frac{1}{(2n+1)(2n+3)}=\frac{1}{2}\left(\frac{1}{2n+1}-\frac{1}{2n+3}\right)$$ 2.求和 ``` latex \begin{aligned} S_n&=a_1+a_2+a_3+\cdots+a_n\\ &=\frac{1}{2}\left(\frac{1}{3}-\frac{1}{5}+\frac{1}{5}-\frac{1}{7}+\cdots+\frac{1}{2n+1}-\frac{1}{2n+3}\right)\\ &=\frac{1}{2}\left(\frac{1}{3}-\frac{1}{2n+3}\right)\\ &=\frac{n}{3(2n+3)} \end{aligned} ``` 令 $(2n+1)(2n+3)=0,n_1-n_2=-\frac{1}{2}-\left(-\frac{3}{2}\right)=1$,可用 N1 互换. $$S_n=\frac{n}{(2+1)(2n+3)}=\frac{n}{3(2n+3)}$$ 例 2 已知 $a_n=\frac{1}{4n^2-1}$,则 $\\{a_n\\}$ 的前 $n$ 项和 $S_n=\underline{\textcolor{transparent}{whatthefuck}}$. $a_n=\frac{1}{(2n-1)(2n+1)}$. 令 $(2n-1)(2n+1)=0,n_1-n_2=\frac{1}{2}-\left(-\frac{1}{2}\right)=1$,可用 N1 互换. $$S_n=\frac{n}{(2-1)(2n+1)}=\frac{n}{2n+1}$$ 例 3 已知 $a_n=\frac{2}{n(n+1)}$,则 $\\{a_n\\}$ 的前 $n$ 项和 $S_n=\underline{\textcolor{transparent}{whatthefuck}}$. 令 $n(n+1)=0,n_1-n_2=0-(-1)=1$,可用 N1 互换. $$S_n=\frac{2n}{n+1}$$
-
端点效应
/posts/%E6%95%B0%E5%AD%A6/%E5%87%BD%E6%95%B0/%E7%AB%AF%E7%82%B9%E6%95%88%E5%BA%94/
简介 若 $f(x)$ 满足 - $f(x_0)=0$. - $f(x)\geq 0$ 在 $[x_0,+\infty)$ 恒成立. 则 $f'(x_0)\geq 0$. 需要检验充分性. <---> 例 1 $f(x)=kx-\sin x,\forall x\in[0,+\infty),f(x)\geq 0$,求 $k$ 的范围. $f'(x)=k-\cos x$. $\because f(0)=0,f(x)\geq 0$ 在 $[0,+\infty)$ 恒成立, $\therefore f'(0)=k-1\geq 0\eq k\geq 1$. 充分性检验: $k\geq 1$ 时,$f'(x)=k-\cos x\geq 0,f(x)\uparrow,f(x)\geq f(0)=0$. 符合题意. 综上 $k\in[1,+\infty)$. 例 2 $f(x)=\ln\frac{1+x}{1-x}-k(x+\frac{x^3}{3}),\forall x\in(0,1),f(x)>0$,求 $k$ 的范围. $f'(x)=\frac{2}{1-x^2}-k(1+x^2)=\frac{kx^4-k+2}{1-x^2}$. $\because f(0)=\ln\frac{1+0}{1-0}-0=0,f(x)>0$ 在 $(0,1)$ 上恒成立, $\therefore f'(0)=2-k\geq 0\eq k\leq 2$. 充分性检验: $k\leq 2$ 时,$x\in(0,1),f'(x)=\frac{k(x^4-1)+2}{1-x^2}\geq 0,f(x)\uparrow,f(x)>f(0)=0$. 符合题意. 综上 $k\in(-\infty,2]$.
-
泰勒展开
/posts/%E6%95%B0%E5%AD%A6/%E5%87%BD%E6%95%B0/%E6%B3%B0%E5%8B%92%E5%B1%95%E5%BC%80/
简介 泰勒展开是用形如 $a_0+a_1x+a_2x^2+\cdots+a_nx^n$ 的函数逼近 $e^x,\ln x$ 等超越函数的技巧。 推导 在 $x=0$ 处展开 设 $f(x)$ 有 $n$ 阶导,$g(x)=a_0+a_1x+a_2x^2+\cdots+a_nx^n$. 令 $f(x)$ 和 $g(x)$ 在 $x=0$ 处的函数值和各阶导相等,这样 $f(x)$ 在 $x=0$ 附近的图像才能逼近 $g(x)$. 因此 ``` latex \begin{aligned} &f(0)=g(0)=a_0\\ &f'(0)=g'(0)=a_1\\ &f''(0)=g''(0)=2a_2\\ &f'''(0)=g'''(0)=3\times 2a_3\\ &\cdots\\ &f^{(n)}(0)=g^{(n)}(0)=n!\times a_n \end{aligned} ``` $\therefore a_n$ 的通项为 $a_i=\frac{f^{(i)}(0)}{i!}$. 即 ``` latex f(x)\approx f(0)+\frac{f'(0)}{1!}x+\frac{f''(0)}{2!}x^2+\frac{f'''(0)}{3!}x^3+\cdots+\frac{f^{(n)}(0)}{n!}x^n ``` 此式称为 $f(x)$ 在 $x=0$ 处的 $n$ 阶泰勒展开式. 在 $x=m$ 处展开 只需设 $g(x)=a_0+a_1(x-m)+a_2(x-m)^2+\cdots+a_n(x-m)^n$. 推导过程与在 $x=0$ 处展开并无二致. ``` latex f(x)\approx f(0)+\frac{f'(0)}{1!}(x-m)+\frac{f''(0)}{2!}(x-m)^2+\cdots+\frac{f^{(n)}(0)}{n!}(x-m)^n ``` 应用 近似 以下皆为在 $x=0$ 处的展开. - $e^x\approx 1+x$ - $e^x\approx 1+x+\frac{1}{2}x^2$ - $\ln(1+x)\approx x$ - $\ln(1+x)\approx x-\frac{1}{2}x^2$ - $\sin x\approx x-\frac{x^3}{3!}+\frac{x^5}{5!}-\frac{x^7}{7!}+\cdots$ - $\cos x\approx 1-\frac{x^2}{2!}+\frac{x^4}{4!}-\frac{x^6}{6!}+\cdots$ - $\tan x\approx x+\frac{x^3}{3}+\frac{2x^5}{15}+\frac{17x^7}{315}+\frac{62x^9}{2835}+\cdots$ 放缩 低阶泰勒展开具有天然的单调性,因此高考出题人常用泰勒构造不等式. - $e^x\geq 1+x(x\in\mathbb{R})$ - $e^x\geq 1+x+\frac{1}{2}x^2(x\geq 0)$ - $\ln(1+x)\leq x(x\in\mathbb{R})$ - $\ln(1+x)\geq x-\frac{1}{2}x^2(x\geq 0)$<---><--->凡是 $n$ 阶泰勒展开构造的不等式,都可以 $n$ 次求导证明. 因为以此法构造的式子,导得越多,导函数结构必然越简单. 因此泰勒展开可作为解题的探路器. 例 1 求证 $x\geq 0$ 时 $e^x\geq 1+x+\frac{1}{2}x^2$. 观察得 $e^x$ 的 $2$ 阶泰勒展开为 $1+x+\frac{1}{2}x^2$. ------ 令 $f(x)=e^x-(1+x+\frac{1}{2}x^2)$. $f'(x)=e^x-(1+x)$. $f''(x)=e^x-1$. $\because x\geq 0,\therefore f''(x)\geq 0,f'(x)\uparrow,f'(x)>f'(0)=0$. $\therefore f'(x)\geq 0,f(x)\uparrow,f(x)>f(0)=0$. 即 $e^x-(1+x+\frac{1}{2}x^2)\geq 0$. 即证. 例 2 $f(x)=e^x-1-x-ax^2$,当 $x\geq 0$ 时 $f(x)\geq 0$ 恒成立,求 $a$ 的范围. 由泰勒展开可知 $e^x\geq 1+x+\frac{1}{2}x^2(x\geq 0)$,因此 $a\in(-\infty,\frac{1}{2}]$. 大题对 $a$ 进行分类讨论即可. ------ $f(x)=e^x-1-x-ax^2$. $f'(x)=e^x-1-2ax$. $f''(x)=e^x-2a$. $1)\quad a\leq\frac{1}{2}$ 时 $f''(x)\geq 0,f'(x)\uparrow,f'(x)>f'(0)=0$. $\therefore f'(x)\geq 0,f(x)\uparrow,f(x)\geq f(0)=0$. 符合题意. $2)\quad a>\frac{1}{2}$ 时 解 $f''(x)<0$ 得 $x\in(0,\ln 2a)$. 此时 $f''(x)<0,f'(x)\downarrow,f'(x)<f'(0)=0$. $\therefore f'(x)\leq 0,f(x)\downarrow,f(x)<f(0)=0$. 即存在 $x\in(0,\ln 2a),f(x)<0$. 不合题意,舍去. 综上 $a\in(-\infty,\frac{1}{2}]$.
-
极值点偏移
/posts/%E6%95%B0%E5%AD%A6/%E5%87%BD%E6%95%B0/%E6%9E%81%E5%80%BC%E7%82%B9%E5%81%8F%E7%A7%BB/
简介 已知 $f(x_1)=f(x_2)$,$x_0$ 是极值点,求证 $x_1+x_2>2x_0$ 或 $x_1x_2>x_0^2$. 这就是极值点偏移问题. $$极值点不偏移$$$$\frac{x_1+x_2}{2}=x_0$$ <---> $$极值点左偏$$$$\frac{x_1+x_2}{2}>x_0$$ <---> $$极值点右偏$$$$\frac{x_1+x_2}{2}<x_0$$ 原理 以极值点左偏为例,求证 $x_1+x_2>2x_0$,即 $x_1>2x_0-x_2$. 若 $x_1$ 和 $m-x_2$ 在函数的 $\uparrow$ 区间,则 $f(x_2)>f(2x_0-x_2)$. 原命题转化为关于 $x_2$ 的单变量问题. $x_1x_2>x_0^2$ 同理. 例题 $f(x)=\frac{x}{e^x}$,若 $f(x_1)=f(x_2)$ 且 $x_1\not=x_2$,求证 $x_1+x_2>2$. $f'(x)=\frac{1-x}{e^x}$. - $x\in(-\infty,1),f'(x)>0,f(x)\uparrow$. - $x\in(1,+\infty),f'(x)<0,f(x)\downarrow$. 不妨设 $x_1<1<x_2$. ``` latex x_1+x_2>2 \eq x_1>2-x_2 \eq f(x_1)>f(2-x_2) \eq f(x_2)>f(2-x_2) ``` 于是转而证明 $f(x_2)-f(2-x_2)>0$,其中 $x_2>1$. 设 $g(x)=f(x)-f(2-x)=\frac{x}{e^x}-\frac{2-x}{e^{2-x}}$,即证 $g(x)$ 在 $(1,+\infty)$ 上恒 $>0$. $g'(x)=\frac{1-x}{e^x}-\frac{1-x}{e^{2-x}}=(x-1)(e^{x-2}-e^{-x})$. $\therefore x\in(1,+\infty),g'(x)>0,g(x)\uparrow$. $\therefore g(x)>g(1)=f(1)-f(1)=0$. 命题得证.
-
若松先生传
/posts/%E6%96%87%E8%A8%80%E6%96%87/%E8%8B%A5%E6%9D%BE%E5%85%88%E7%94%9F%E4%BC%A0/
南陵[^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]之。若松先生平日清心寡欲,近竟不堪众扰,顶香居士竟与顶香居生决裂。虽避人耳目,却失了真性情。故余劝之不必予视一故。既出此文,记念若松先生,以警示其乌蝇之随,非诚勿扰。 [^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]: [岂]:同 “觊”,觊觎,渴望得到不属于自己的东西。
-
坐标系平移法
/posts/%E6%95%B0%E5%AD%A6/%E5%87%A0%E4%BD%95/%E5%9C%86%E9%94%A5%E6%9B%B2%E7%BA%BF/%E5%9D%90%E6%A0%87%E7%B3%BB%E5%B9%B3%E7%A7%BB%E6%B3%95/
简介 若圆锥曲线题围绕某特殊点展开,可将坐标系原点平移至此点,减小计算量. 例 1 已知椭圆 $E:\frac{x^2}{4}+\frac{y^2}{3}=1$,过 $P(2, 1)$ 的直线 $l$ 与椭圆 $E$ 交于 $A,B$ 且 $\vec{OP}^2=4\vec{PA}\cdot\vec{PB}$,求 $l$ 的方程. 标答 联立 $\begin{cases}y=k(x-2)+1\\\\ \frac{x^2}{4}+\frac{y^2}{3}=1\end{cases}$ 得 ``` latex (3+4k^2)x^2-8k(2k-1)x+16k^2-16k-8=0 ``` $\Delta=[-8k(2k-1)]^2-4(3+4k^2)(16k^2-16k-8)>0$ $\therefore 32(6k+3)>0,k>-\frac{1}{2}$ $x_1+x_2=\frac{8k(2k-1)}{3+4k^2},x_1x_2=\frac{16k^2-16k-8}{3+k^2}$ $\because\vec{OP}^2=4\vec{PA}\cdot\vec{PB}$ $\therefore(x_1-2,y_1-1)\cdot(x_2-2,y_2-1)=\frac{5}{4}$ $\therefore [x_1x_2-2(x_1+x_2)+4] (1+k^2)=\frac{5}{4}$ $\therefore \left[\frac{16k^2-16k-8}{3+4k^2}-2\frac{8k(2k-1)}{3+4k^2}+4\right] (1+k^2)=\frac{4+4k^2}{3+4k^2}=\frac{5}{4}$ 解得 $k=\pm\frac{1}{2},k=-\frac{1}{2}$ 不合题意,舍去. $\therefore l:y=\frac{1}{2}x$. 快速解法 将坐标系原点平移至 $P(2,1)$. 为了区分坐标系,令 $\lambda=x-2,\mu=y-1$. 新坐标系中 $A(\lambda_1,\mu_1),B(\lambda_2,\mu_2)$. 设 $l:\mu=k\lambda$. 于是 $\vec{PA}\cdot\vec{PB}=(k^2+1)\lambda_1\lambda_2=\frac{5}{4}$. 联立 $\begin{cases}\mu=k\lambda\\\\ \frac{(\lambda+2)^2}{4}+\frac{(\mu+1)^2}{3}=1\end{cases}$ 得 ``` latex (4k^2+3)\lambda^2+(12+8k)\lambda+4=0 ``` $\Delta=192k+96>0,\therefore k>-\frac{1}{2}$. $\lambda_1\lambda_2=\frac{4}{4k^2+3}$ 代入得 $(k^2+1)\frac{4}{4k^2+3}=\frac{5}{4}$,解得 $k=\frac{1}{2}$. $\therefore y=\frac{1}{2}x$. 例 2 已知双曲线 $C:x^2-\frac{y^2}{16}=1$. $T$ 在直线 $x=\frac{1}{2}$ 上. 过 $T$ 的两条直线分别交 $C$ 的右支于 $A,B$ 和 $P,Q$. 若 $|TA|\cdot|TB|=|TP|\cdot|TQ|$,求 $k_{AB}+k_{PQ}$. 快速解法 标答我就不贴了. 设 $T\left(\frac{1}{2},t\right)$. 将坐标系原点平移至 $T$. 令 $\lambda=x-\frac{1}{2},\mu=y-t$. 设 $AB:\mu=k_1\lambda,PQ:\mu=k_2\lambda$. $\because|TA|\cdot|TB|=|TP|\cdot|TQ|$ $\therefore(\lambda_A^2+\mu_A^2)(\lambda_B^2+\mu_B^2)=(\lambda_P^2+\mu_P^2)(\lambda_Q^2+\mu_Q^2)$ $\therefore(k_1^2+1)^2(\lambda_A\lambda_B)^2=(k_2^2+1)^2(\lambda_P\lambda_Q)^2$ 联立 $\begin{cases}\mu=k\lambda\\\\ \left(\lambda+\frac{1}{2}\right)^2-\frac{(\mu+t)^2}{16}=1\end{cases}$ 得 $$(16-k^2)\lambda^2+(16-2kt)\lambda-t^2-12=0$$ $\Delta>0$ 解得 $k<4$. $\lambda_1\lambda_2=\frac{-t^2-12}{16-k^2}$,即 $\lambda_A\lambda_B=\frac{-t^2-12}{16-k_1^2},\lambda_P\lambda_Q=\frac{-t^2-12}{16-k_2^2}$ 代入得 $\frac{k_1^2+1}{16-k_1^2}=\frac{k_2^2+1}{16-k_2^2}$. 设左边表达式 $=s,\therefore k_1,k_2$ 是 $f(x)=\frac{k^2+1}{16-k^2}=s$ 的两根. $\because f(-x)=f(x)$,且 $f(x)=s$ 仅有两根,$\therefore (k_1,s),(k_2,s)$ 关于 $y$ 轴对称. $\therefore k_{AB}+k_{PQ}=k_1+k_2=0$.
-
猫变
/posts/%E6%96%87%E8%A8%80%E6%96%87/%E7%8C%AB%E5%8F%98/
蜀中有李氏,寡居,饲一猫,猫色橘,然三年不肥,人皆异之,或曰:“可有妙法?” 李氏笑而不答。 后有新邻张氏,夜闻李氏屋有嘤咛[^1],色心起,窥之。见有橘发女子,身形窈窕[^2],有猫耳猫尾,与李氏交媾[^3],一夜缠绵。乃知李氏橘猫不肥,盖因交媾耗力甚巨也。 张氏羡之,亦饲一猫,日夜呼斥之,曰:“且变!” 猫不理,张氏怒,殴之,猫亦怒,化虬须[^4]赤发汉,张氏大骇,方知所饲乃雄猫也,欲走不脱,遭缚。翌日,张氏肛裂,而猫不知所踪矣。 [^1]: [嘤咛]:声音清脆娇细。 [^2]: [窈窕]:身材好。 [^3]: [交媾(gòu)]:阴阳交合。 [^4]: [虬(qiú)须]:胡须蜷曲。
-
构造函数
/posts/%E6%95%B0%E5%AD%A6/%E5%87%BD%E6%95%B0/%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0/
简介 若已知 $f(x)>0$,则构造函数 $F(x)$,使 $F'(x)=\text{某正数}\times f(x)$,利用 $F(x)\uparrow$ 解题. $f(x)<0$ 同理. 例 1 已知 $f(x)+f'(x)>0$,$f(0)=1$,求证当 $x\in[0,+\infty)$ 时 $f(x)\geq\frac{1}{e^x}$. ???+ note 证明 构造 $g(x)=e^xf(x)$,则 $$g'(x)=e^xf(x)+e^xf'(x)=e^x[f(x)+f'(x)]>0$$ $\therefore x\in[0,+\infty)$ 时,$g(x)\uparrow$,$g(x)\geq g(0)$,即 $e^xf(x)\geq e^0f(0)=1$. $\therefore f(x)\geq \frac{1}{e^x}$. 即证. 基本构造 和差构造 - $f'+g'>0\intro F=f+g$ - $f'-g'>0\intro F=f-g$ 积商构造 - $f'g+fg'>0\intro F=fg$ - $f'g-fg'>0\intro F=\frac{f}{g}\big(g\not=0\big)$ 变形 含 $x$ 形构造 - $xf'(x)+nf(x)>0\intro F(x)=x^nf(x)$ - $xf'(x)-nf(x)>0\intro F(x)=\frac{f(x)}{x^n}\big(x\not=0\big)$ 含 $e$ 形构造 - $f'(x)+nf(x)>0\intro F(x)=e^{nx}f(x)$ - $f'(x)-nf(x)>0\intro F(x)=\frac{f(x)}{e^{nx}}$ 三角构造 - $f(x)+f'(x)\tan x>0\intro F(x)=\sin x f(x)$ - $f(x)-f'(x)\tan x>0\intro F(x)=\frac{f(x)}{\sin x}\big(\sin x\not=0\big)$ - $f(x)\tan x+f'(x)>0\intro F(x)=\frac{f(x)}{\cos x}\big(\cos x\not=0\big)$ - $f(x)\tan x-f'(x)>0\intro F(x)=\cos x f(x)$ 对数形构造 - $f'(x)+\ln af(x)>0\intro F(x)=a^xf(x)$ - $f'(x)-\ln af(x)>0\intro F(x)=\frac{f(x)}{a^x}$
-
不动点法
/posts/%E6%95%B0%E5%AD%A6/%E6%95%B0%E5%88%97/%E4%B8%8D%E5%8A%A8%E7%82%B9%E6%B3%95/
> 本篇证明可略过. 不动点 函数不动点 $y=f(x)$ 与 $y=x$ 的交点是 $f(x)$ 的不动点,即不动点 $x_0$ 满足 $$f(x_0)=x_0$$ 数列不动点 若 $a_n=f(a_{n-1})$,则 $f(x)$ 的不动点也是数列 $\\{a_n\\}$ 的不动点. 一次不动点 已知 $a_n=A\cdot a_{n-1}+B(A\not=0)$. 若 $\\{a_n\\}$ 有不动点 $x_0$,则 $\\{a_n-x_0\\}$ 是公比为 $A$ 的等比数列. 由不动点的定义得 $$Ax_0+B=x_0$$ $$\therefore B=x_0-Ax_0$$ $$\therefore a_n-x_0=A\cdot a_{n-1}+B-x_0=A(a_{n-1}-x_0)$$ $\therefore$ 数列 $\\{a_n-x_0\\}$ 是公比为 $A$ 的等比数列 齐次不动点 已知 $a_n=\dfrac{Aa_{n-1}+B}{Ca_{n-1}+D}(C\not=0,AD-BC\not=0)$. 单根 若 $\\{a_n\\}$ 有唯一不动点 $x_0$,则 $\left\\{\dfrac{1}{a_n-x_0}\right\\}$ 是公差为 $\dfrac{2C}{A+D}$ 的等差数列. 由不动点的定义得 $$x_0=\frac{Ax_0+B}{Cx_0+D}$$ $$B-Dx_0=Cx_0^2-Ax_0\tag{1}$$ $$Cx_0^2+(D-A)x_0-B=0\tag{2}$$ $\because$ 方程 $(2)$ 仅有一根,故 $$x_0=\dfrac{A-D}{2C}\tag{3}$$ ``` latex \begin{aligned} \therefore a_n-x_0&=\frac{Aa_{n-1}+B}{Ca_{n-1}+D}-\frac{Ca_{n-1}x_0+Dx_0}{Ca_{n-1}+D}\\ &=\frac{(A-Cx_0)a_{n-1}+B-Dx_0}{Ca_{n-1}+D}\\ \end{aligned} ``` 代入 $(1)$: ``` latex \begin{aligned} a_n-x_0&=\frac{(A-Cx_0)a_{n-1}+Cx_0^2-Ax_0}{Ca_{n-1}+D}\\ &=\frac{(a_{n-1}-x_0)(A-Cx_0)}{Ca_{n-1}+D}\\ &=\frac{(a_{n-1}-x_0)(A-Cx_0)}{C(a_{n-1}-x_0)+Cx_0+D}\\ \\ \therefore \frac{1}{a_n-x_0}&=\frac{C(a_{n-1}-x_0)+Cx_0+D}{(a_{n-1}-x_0)(A-Cx_0)}\\ &=\frac{C}{(A-Cx_0)}+\frac{Cx_0+D}{(a_{n-1}-x_0)(A-Cx_0)}\\ \end{aligned} ``` 代入 $(2)$: ``` latex \begin{aligned} \frac{1}{a_n-x_0}&=\frac{C}{\left(\dfrac{A+D}{2}\right)}+\frac{\dfrac{A-D}{2}+D}{(a_{n-1}-x_0)\left(\dfrac{A+D}{2}\right)}\\ &=\frac{2C}{A+D}+\frac{1}{a_{n-1}-x_0} \end{aligned} ``` $\therefore$ 数列 $\left\\{\dfrac{1}{a_n-x_0}\right\\}$ 是公差为 $\dfrac{2C}{A+D}$ 的等差数列. 已知 $a_1=2,a_{n+1}=\dfrac{2a_n-1}{a_n}$,求 $a_n$ 与 $a_1$ 的关系. **解:** 解 $x=\dfrac{2x-1}{x}$ 得唯一不动点 $x_0=1$. $$\therefore\frac{1}{a_n-1}=\frac{1}{\dfrac{2a_{n-1}-1}{a_{n-1}}-1}=\frac{1}{a_{n-1}-1}+1$$ $\therefore$ $\left\\{\dfrac{1}{a_n-1}\right\\}$ 是首项为 $1$,公差为 $1$ 的等差数列. $$\therefore \frac{1}{a_n-1}=n$$ $$\therefore a_n=1+\frac{1}{n}$$ 双根 若 $\\{a_n\\}$ 有不动点 $x_1,x_2$,则 $\left\\{\dfrac{a_n-x_1}{a_n-x_2}\right\\}$ 是公比为 $\dfrac{A-Cx_1}{A-Cx_2}$ 的等比数列. 由不动点的定义得 ``` latex \begin{cases} x_1=\frac{Ax_1+B}{Cx_1+D}\\ \\ x_2=\frac{Ax_2+B}{Cx_2+D} \end{cases} \intro \begin{cases} x_1(Cx_1-A)=B-Dx_1\\ x_2(Cx_2-A)=B-Dx_2 \end{cases} ``` ``` latex \begin{aligned} \therefore a_n-x_1&=\frac{Aa_{n-1}+B}{Ca_{n-1}+D}-x_1\\ \therefore (a_n-x_1)(Ca_{n-1}+D)&=Aa_{n-1}+B-x_1(Ca_{n-1}+D)\\ &=(A-Cx_1)a_{n-1}+B-Dx_1\\ &=(A-Cx_1)a_{n-1}-x_1(A-Cx_1)\\ &=(A-Cx_1)(a_{n-1}-x_1) \end{aligned} ``` 即 $$(a_n-x_1)(Ca_{n-1}+D)=(A-Cx_1)(a_{n-1}-x_1)$$ 同理可得 $$(a_n-x_2)(Ca_{n-1}+D)=(A-Cx_2)(a_{n-1}-x_2)$$ ``` latex \begin{aligned} \therefore \frac{a_n-x_1}{a_n-x_2}=\frac{A-Cx_1}{A-Cx_2}\cdot\frac{a_{n-1}-x_1}{a_{n-2}-x_2} \end{aligned} ``` $\therefore$ 数列 $\left\\{\dfrac{a_n-x_1}{a_n-x_2}\right\\}$ 是公比为 $\dfrac{A-Cx_1}{A-Cx_2}$ 的等比数列. 已知 $a_{n+1}=\dfrac{2a_n}{a_n+1}$,求 $a_n$ 与 $a_1$ 的关系. **解:** 解 $x=\dfrac{2x}{x+1}$ 得两个相异的不动点 $x_1=0,x_2=1$. $$\therefore\frac{a_n-0}{a_n-1}=\frac{\cfrac{2a_{n-1}}{a_{n-1}+1}-0}{\cfrac{2a_{n-1}}{a_{n-1}+1}-1}=2\frac{a_{n-1}-0}{a_{n-1}-1}$$ $\therefore$ $\left\\{\dfrac{a_n}{a_n-1}\right\\}$ 是首项为 $\dfrac{a_1}{a_1-1}$,公比为 $2$ 的等比数列. $$\therefore \frac{a_n}{a_n-1}=\frac{a_1}{a_1-1}\cdot 2^{n-1}$$ $$\therefore a_n=\frac{2^{n-1}a_1}{(2^{n-1}-1)a_1+1}$$
-
特征根法
/posts/%E6%95%B0%E5%AD%A6/%E6%95%B0%E5%88%97/%E7%89%B9%E5%BE%81%E6%A0%B9%E6%B3%95/
二次特征根 已知 $af_{n+2}+bf_{n+1}+cf_n=0$. 两边同除 $a$: $$f_{n+2}+\frac{b}{a}f_{n+1}+\frac{c}{a}f_n=0$$ 根据韦达定理,方程 $ax^2+bx+c=0$ 的解满足 ``` latex \begin{cases} x_1+x_2=-\dfrac{b}{a}\\ x_1\cdot x_2=\dfrac{c}{a} \end{cases} ``` 代入化简得 $$f_{n+2}-(x_1+x_2)f_{n+1}+(x_1\cdot x_2)f_n=0$$ $$f_{n+2}-x_1f_{n+1}=x_2(f_{n+1}-x_1f_n)$$ $\therefore$ 数列 $\\{f_{n+1}-x_1f_n\\}$ 是公比为 $x_2$ 的等比数列. 由韦达定理的对称性知,$x_1,x_2$ 可互换. $$f_{n+1}-x_1f_n=(f_2-x_1f_1)\cdot x_2^{n-1}\tag{1}$$ $$f_{n+1}-x_2f_n=(f_2-x_2f_1)\cdot x_1^{n-1}\tag{2}$$ 联立 $(1)(2)$ 可解得 $f_n$ 通项. 已知 $f_1=f_2=1,f_{n+2}=f_{n+1}+f_n$,求 $f_n$ 的通项. **解**:设 $f_{n+2}-rf_{n+1}=q(f_{n+1}-rf_n)$, 则 $f_{n+2}=(q+r)f_{n+1}-q\cdot rf_n$. 对比 $f_{n+2}=f_{n+1}+f_n$ 得 ``` latex \begin{cases} q+r=1\\ q\cdot r=-1 \end{cases} ``` 解得 ``` latex \begin{cases} q=\cfrac{1-\sqrt{5}}{2}\\ r=\cfrac{1+\sqrt{5}}{2} \end{cases} \quad\text{或}\quad \begin{cases} q=\cfrac{1+\sqrt{5}}{2}\\ r=\cfrac{1-\sqrt{5}}{2} \end{cases} ``` ∴ 数列 $\\{f_{n+1}-rf_n\\}$ 是以 $f_2-rf_1$ 为首项,$q$ 为公比的等比数列. ``` latex ∴ \begin{cases} f_{n+1}-\cfrac{1+\sqrt{5}}{2}f_n=\left(\cfrac{1-\sqrt{5}}{2}\right)^n\\ f_{n+1}-\cfrac{1-\sqrt{5}}{2}f_n=\left(\cfrac{1+\sqrt{5}}{2}\right)^n \end{cases} ``` 两式相减得: $$\sqrt{5}f_n=\left(\cfrac{1+\sqrt{5}}{2}\right)^n-\left(\cfrac{1-\sqrt{5}}{2}\right)^n$$ $$f_n=\cfrac{\sqrt{5}}{5}\left[\left(\cfrac{1+\sqrt{5}}{2}\right)^n-\left(\cfrac{1-\sqrt{5}}{2}\right)^n\right]$$ 三次特征根 已知 $af_{n+3}+bf_{n+2}+cf_{n+1}+df_n=0$. 两边同除 $a$: $$f_{n+3}+\frac{b}{a}f_{n+2}+\frac{c}{a}f_{n+1}+\frac{d}{a}f_n=0$$ 根据韦达定理,方程 $ax^3+bx^2+cx+d=0$ 的解满足 ``` latex \begin{cases} x_1+x_2+x_3=-\dfrac{b}{a}\\ x_1x_2+x_1x_3+x_2x_3=\dfrac{c}{a}\\ x_1x_2x_3=-\dfrac{d}{a} \end{cases} ``` 代入化简得 $$f_{n+3}-(x_1+x_2+x_3)f_{n+2}+(x_1x_2+x_1x_3+x_2x_3)f_{n+1}-(x_1x_2x_3)f_n=0$$ $$f_{n+3}-(x_1+x_2)f_{n+2}+x_1x_2f_{n+1}=x_3\big[f_{n+2}-(x_1+x_2)f_{n+1}+x_1x_2f_n\big]$$ $\therefore$ 数列 $\\{f_{n+2}-(x_1+x_2)f_{n+1}+x_1x_2f_n\\}$ 是公比为 $x_3$ 的等比数列. $n$ 次特征根 已知 $a_nf_{k+n}+a_{n-1}f_{k+n-1}+\cdots+a_0f_k=0$. 两边同除 $a_n$ $$f_{k+n}+\frac{a_{n-1}}{a_n}f_{k+n-1}+\frac{a_{n-2}}{a_n}f_{k+n-2}+\cdots+\frac{a_0}{a_n}f_k=0$$ 根据韦达定理,方程 $a_nx^n+a_{n-1}x^{n-1}+\cdots+a_1x_1+a_0=0$ 的根满足 ``` latex \begin{cases} \sum_{i=1}^nx_i=-\frac{a_{n-1}}{a_n}\\ \sum_{1\leq i < j \leq n}x_ix_j=\frac{a_{n-2}}{a_n}\\ \sum_{1\leq i < j < k \leq n}x_ix_jx_k=-\frac{a_{n-3}}{a_n}\\ \cdots\\ \prod_{i=1}^nx_i=(-1)^n\frac{a_0}{a_n} \end{cases} ``` 即从 $x_1\wave x_n$ 中任取 $m$ 项相乘,所得的结果之和为 $(-1)^m\dfrac{a_{n-m}}{a_n}$. $$W_n^m=(-1)^m\frac{a_{n-m}}{a_n}$$ 代入得 $$f_{k+n}-W_{n}^{1}f_{k+n}+W_{n}^{2}f_{k+n-2}+\cdots+W_{n}^{n}f_k=0$$ 易知 $W$ 函数具有性质 $$W_{n}^{m}=x_n\cdot W_{n-1}^{m-1}+\prod_{i=1}^{n-1}x_i$$ 故 $(1)$ 式可化为 $$\sum_{i=1}^n(-1)^{i-1}W_{n-1}^{i-1}f_{k+n-i+1}=x_n\sum_{i=1}^n(-1)^{i-1}W_{n-1}^{i-1}f_{k+n-i}$$ $∴$ 数列 $\left\\{\sum_{i=1}^n(-1)^{i-1}W_{n-1}^{i-1}f_{k+n-i}\right\\}$ 是公比为 $x_n$ 的等比数列.
-
三角函数公式
/posts/%E6%95%B0%E5%AD%A6/%E5%87%BD%E6%95%B0/%E4%B8%89%E8%A7%92%E5%87%BD%E6%95%B0%E5%85%AC%E5%BC%8F/
诱导公式 奇变偶不变,符号看象限. - $ \sin{(2k\pi+α)}=\sin{α}$ - $ \cos{(2k\pi+α)}=\cos{α}$ - $ \tan{(2k\pi+α)}=\tan{α}$ <---> - $ \sin{(2k\pi-α)}=-\sin{α}$ - $ \cos{(2k\pi-α)}=\cos{α}$ - $ \tan{(2k\pi-α)}=-\tan{α}$ <br> - $ \sin{(\pi+α)}=-\sin{α}$ - $ \cos{(\pi+α)}=-\cos{α}$ - $ \tan{(\pi+α)}=\tan{α}$ <---> - $ \sin{(\pi-α)}=\sin{α}$ - $ \cos{(\pi-α)}=-\cos{α}$ - $ \tan{(\pi-α)}=-\tan{α}$ <br> - $ \sin{(\frac{\pi}{2}+α)}=\cos{α}$ - $ \cos{(\frac{\pi}{2}+α)}=-\sin{α}$ - $ \tan{(\frac{\pi}{2}+α)}=-\frac{1}{\tan{α}}$ <---> - $ \sin{(\frac{\pi}{2}-α)}=\cos{α}$ - $ \cos{(\frac{\pi}{2}-α)}=\sin{α}$ - $ \tan{(\frac{\pi}{2}-α)}=\frac{1}{\tan{α}}$ <br> - $ \sin{(\frac{3\pi}{2}+α)}=-\cos{α}$ - $ \cos{(\frac{3\pi}{2}+α)}=\sin{α}$ - $ \tan{(\frac{3\pi}{2}+α)}=-\frac{1}{\tan{α}}$ <---> - $ \sin{(\frac{3\pi}{2}-α)}=-\cos{α}$ - $ \cos{(\frac{3\pi}{2}-α)}=-\sin{α}$ - $ \tan{(\frac{3\pi}{2}-α)}=\frac{1}{\tan{α}}$ <br> - $ \sin{(-α)}=-\sin{α}$ - $ \cos{(-α)}=\cos{α}$ - $ \tan{(-α)}=-\tan{α}$ 和差角公式 - $ \sin{(α+β)}=\sin{α}\cos{β}+\cos{α}\sin{β}$ - $ \sin{(α-β)}=\sin{α}\cos{β}-\cos{α}\sin{β}$ - $ \cos{(α+β)}=\cos{α}\cos{β}-\sin{α}\sin{β}$ - $ \cos{(α-β)}=\cos{α}\cos{β}+\sin{α}\sin{β}$ - $ \tan{(α+β)}=\frac{\tan{α}+\tan{β}}{1-\tan{α}\tan{β}}$ - $ \tan{(α-β)}=\frac{\tan{α}-\tan{β}}{1+\tan{α}\tan{β}}$ 二倍角公式 降幂扩角 升幂缩角. - $ \sin{2α}=2\sin{α}\cos{α}$ - $ \cos{2α}=\cos^2{α}-\sin^2{α}=1-2\sin^2{α}=2\cos^2{α}-1$ - $ \tan{2α}=\frac{2\tan{α}}{1-\tan^2{α}}$ - $ 1+\sin{2α}=(\sin{α}+\cos{α})^2$ - $ 1-\sin{2α}=(\sin{α}-\cos{α})^2$ - $ 1+\cos{2α}=2\cos^2{α}$ - $ 1-\cos{2α}=2\sin^2{α}$ 三倍角公式 - $ \sin{3α}=3\sin{α}-4\sin^3{α}$ - $ \cos{3α}=-3\cos{α}+4\cos^3{α}$ - $ \tan{3α}=\frac{3\tan{α}-\tan^3{α}}{1-3\tan^2{α}}=\tan{α}\tan{(\frac{\pi}{3}+α)}\tan{(\frac{\pi}{3}-α)}$ 半角公式 - $ \sin{\frac{α}{2}}=±\sqrt{\frac{1-\cos{α}}{2}}$ - $ \cos{\frac{α}{2}}=±\sqrt{\frac{1+\cos{α}}{2}}$ - $ \tan{\frac{α}{2}}=\frac{\sin{α}}{1+\cos{α}}=\frac{1-\cos{α}}{\sin{α}}=±\sqrt{\frac{1-\cos{α}}{1+\cos{α}}}$ 辅助角公式 ``` latex a\sin{θ}+b\cos{θ}=\sqrt{a^2+b^2}\;\cdot \sin{(θ+φ)} ``` 其中 $ \sin{φ}=\frac{b}{\sqrt{a^2+b^2}},\quad\cos{φ}=\frac{a}{\sqrt{a^2+b^2}},\quad\tan{φ}=\frac{b}{a}$. 万能公式 - $\sin{α}=\frac{2\tan{\large\frac{α}{2}}}{1+\tan^2{\large\frac{α}{2}}}$ - $\cos{α}=\frac{1-\tan^2{\large\frac{α}{2}}}{1+\tan^2{\large\frac{α}{2}}}$ - $\tan{α}=\frac{2\tan{\large\frac{α}{2}}}{1-\tan^2{\large\frac{α}{2}}}$ 和差化积 - $ \sin{α}+\sin{β}=2\sin{\frac{α+β}{2}}\cos{\frac{α-β}{2}}$ - $ \sin{α}-\sin{β}=2\sin{\frac{α-β}{2}}\cos{\frac{α+β}{2}}$ - $ \cos{α}+\cos{β}=2\cos{\frac{α+β}{2}}\cos{\frac{α-β}{2}}$ - $ \cos{α}-\cos{β}=-2\sin{\frac{α+β}{2}}\sin{\frac{α-β}{2}}$ - $ \tan{α}+\tan{β}=\frac{\sin(α+β)}{\cos{α}\cos{β}}$ - $ \tan{α}-\tan{β}=\frac{\sin(α-β)}{\cos{α}\cos{β}}$ 积化和差 - $ \sin{α}\cos{β}=\frac{1}{2}[\sin{(α+β)}+\sin{(α-β)}]$ - $ \cos{α}\sin{β}=\frac{1}{2}[\sin{(α+β)}-\sin{(α-β)}]$ - $ \cos{α}\cos{β}=\frac{1}{2}[\cos{(α+β)}+\cos{(α-β)}]$ - $ \sin{α}\sin{β}=-\frac{1}{2}[\cos{(α+β)}-\cos{(α-β)}]$ 正弦定理 ``` latex \frac{a}{\sin{A}}=\frac{b}{\sin{B}}=\frac{c}{\sin{C}}=2R ``` 其中 $R$ 为 $ΔABC$ 外接圆半径. 余弦定理 - $ a^2=b^2+c^2-2bc\cos{A}$ - $ b^2=a^2+c^2-2ac\cos{B}$ - $ c^2=a^2+b^2-2ab\cos{B}$ 三角形面积公式 - $ S_{ΔABC}=\frac{1}{2}\cdot a\cdot h$ - $ S_{ΔABC}=\frac{1}{2}ab\sin{C}=\frac{1}{2}bc\sin{A}=\frac{1}{2}ac\sin{B}$ - $ S_{ΔABC}=\frac{abc}{4R}$($R$ 为 $ΔABC$ 外接圆半径) - $ S_{ΔABC}=\frac{a+b+c}{2}\cdot r$($r$ 为 $ΔABC$ 内接圆半径) - $ S_{ΔABC}=\sqrt{p(p-a)(p-b)(p-c)},p=\frac{a+b+c}{2}$ 其它公式 - $ \sin^2{θ}+\cos^2{θ}=1$ - $ 1+\tan^2{θ}=\frac{1}{\cos^2{θ}}$ - $ 1+\frac{1}{\tan^2{θ}}=\frac{1}{\sin^2{θ}}$ - $ \tan{A}+\tan{B}+\tan{C}=\tan{A}\tan{B}\tan{C}$($ΔABC$ 非 $Rt$ 三角形) - $ \sin^2{A}+\sin^2{B}+\sin^2{C}=2+2\cos{A}\cos{B}\cos{C}$(在 $ΔABC$ 中) - $ \cos^2{A}+\cos^2{B}+\cos^2{C}=1-2\cos{A}\cos{B}\cos{C}$(在 $ΔABC$ 中)
-
换根 DP
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/%E6%8D%A2%E6%A0%B9-dp/
简介 换根 DP 是基于树形 DP的更高效算法. 通常设 $f[u]$ 表示以 $u$ 为树根时的解,进而推出 $f[$其它节点$]$. 例 1 给定一棵 $n$ 个点的树,请求出一个节点,使得以其为根时,所有节点的深度之和最大(根节点的深度为 $1$). $d[u]$:(以 $1$ 为树根时)$u$ 的深度. $\text{size}[u]$:(以 $1$ 为树根时)以 $u$ 为根的子树的节点数. $f[u]$:以 $u$ 为树根时的节点深度和. 以 $1$ 为根时: ``` mermaid flowchart 1 --> 2 & 3 2 --> 4 & 5 3 --> 6 & 7 & 8 ``` 以 $3$ 为根时: ``` mermaid flowchart 3 --> 1 & 6 & 7 & 8 1 --> 2 --> 4 & 5 ``` 观察发现,原先 $3$ 的子树中(包括 $3$)节点的深度减少了 $1$,而其余节点的深度增加了 $1$. 因此 $$f[3]=f[1]-\text{size}[3]+(n-\text{size}[3])$$ 进一步,设 $u$ 是 $v$ 的父节点,则 $$f[v]=f[u]-\text{size}[v]+(n-\text{size}[v])$$ 先假定 $1$ 为树根,用树形 DP 推出 $\text{size}$ 数组和 $d$ 数组;再用树形 DP 推出 $f$ 数组,并取最大值. 初始条件为 $\displaystyle f[1]=\sum d[i]$,时间复杂度为 $O(n)$. ``` cpp void dfs1(int u, int fa) { size[u] = 1; d[u] = d[fa] + 1; for (int i = 0; i < g[u].size(); i ++) { int v = g[u][i]; // v 是 u 的第 i 个子节点 if (v == fa) continue; dfs1(v, u); size[u] += size[v]; } } void dfs2(int u, int fa) { for (int i = 0; i < g[u].size(); i ++) { int v = g[u][i]; if (v == fa) continue; f[v] = f[u] - size[v] + (n - size[v]); dfs2(v, u); } } int main() { dfs1(1, 0); for (int i = 1; i <= n; i ++) f[1] += d[i]; dfs2(1, 0); for (int i = 1; i <= n; i ++) maxn = max(maxn, f[i]); cout << maxn; } ```
-
矩阵快速幂
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E7%9F%A9%E9%98%B5%E5%BF%AB%E9%80%9F%E5%B9%82/
简介 矩阵快速幂能将 $O(n)$ 的线性递推优化成 $O(\log n)$. 矩阵 矩阵相当于二维数组. - 矩阵 $A$ 有 $m$ 行 $n$ 列,称为 $m×n$ 矩阵,简记为 $A_{mn}$. - 矩阵 $A$ 第 $i$ 行 $j$ 列的元素写作 $a_{ij}$. ``` latex A=\begin{bmatrix} a_{11}& a_{12}& \cdots & a_{1n}\\ a_{21}& a_{22}& \cdots & a_{2n}\\ \vdots & \vdots & \ddots & \vdots \\ a_{m1}& a_{m2}& \cdots & a_{mn} \end{bmatrix} ``` 单位矩阵 主对角线上的元素都为 $1$,其余元素为 $0$ 的 $n×n$ 矩阵称为 $n$ 阶单位矩阵,记作 $I_n$ 或 $E_n$. ``` latex I_n=\begin{bmatrix} 1 & 0 & 0 & \cdots & 0 \\ 0 & 1 & 0 & \cdots & 0 \\ 0 & 0 & 1 & \cdots & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & 0 & \cdots & 1 \end{bmatrix} ``` 矩阵加、减法 矩阵的加减法就是将两个矩阵对应位置上的元素相加减. $$C_{ij}=A_{ij}±B_{ij}$$ 矩阵乘法 定义矩阵 $A×B=C$: - 必须满足 $A$ 的列数 $=$ $B$ 的行数. - $A_{mr}×B_{rn}$ 会得到一个 $m×n$ 矩阵. $$C_{ij}=\sum_{k=1}^nA_{ik}\cdot B_{kj}$$ 任意矩阵乘 $I_n$ 都等于它本身. > - 矩阵乘法满足结合律,不满足交换律. > > - 只有行数 $=$ 列数的矩阵才能进行乘幂运算. 矩阵快速幂 基于快速幂的思想求 $n$ 阶矩阵的 $x$ 次方. 时间复杂度:$O(n^3\log x)$. > 矩阵运算可能需要高精度或取模. ``` cpp const int N = 101; // 矩阵的行数和列数 struct matrix { int atN][N]; matrix() { memset(at, 0, sizeof at); } int* operator[{ return at[key]; } }; matrix operator *(matrix a, matrix b) { // 矩阵 A × 矩阵 B matrix c; for (int i = 0; i < N; i ++) for (int j = 0; j < N; j ++) for (int k = 0; k < N; k ++) c[i][j] += a[i][k] * b[k][j]; return c; } matrix operator ^(matrix a, int x) { // 矩阵 A 的 x 次方 matrix res; for (int i = 0; i < N; i ++) // 初始化为单位矩阵 res[i][i] = 1; while(x) { if(x % 2) res = res * a; a = a * a, x /= 2; } return res; } ``` 应用 若已知 $f(1),f(2)$,且有矩阵 $A$ 满足 ``` latex A\times \begin{bmatrix} f(n)\\ f(n-1) \end{bmatrix} = \begin{bmatrix} f(n+1)\\ f(n) \end{bmatrix} ``` 就可推出 $f(n)$ 的值. ``` latex (A^{n-2})\times \begin{bmatrix} f(2)\\ f(1) \end{bmatrix} = \begin{bmatrix} f(n)\\ f(n-1) \end{bmatrix} ``` 可用矩阵快速幂求 $A^{n-2}$. 时间复杂度:$O(\log n)$. 例 1 求斐波那契数列的第 $n$ 项 $f(n)$($n\geq 10^{12}$). ``` latex f(n)= \begin{cases} 1&n=1,2\\ f(n-1)+f(n-2)&n\geq 3 \end{cases} ``` 设矩阵 $A=\begin{bmatrix}a_{11}&a_{12}\newline a_{21}&a_{22}\end{bmatrix}$ 满足 ``` latex \begin{bmatrix} a_{11}&a_{12}\\ a_{21}&a_{22} \end{bmatrix} \times \begin{bmatrix} f(n)\\ f(n-1) \end{bmatrix} = \begin{bmatrix} f(n+1)\\ f(n) \end{bmatrix} ``` 即 ``` latex \begin{cases} f(n+1)&=&a_{11}\cdot f(n)+a_{12}\cdot f(n-1)\\ f(n)&=&a_{21}\cdot f(n)+a_{22}\cdot f(n-1) \end{cases} ``` 解得 ``` latex \begin{cases} a_{11}=1&a_{12}=1\\ a_{21}=1&a_{22}=0 \end{cases} , \quad A= \begin{bmatrix} 1&1\\ 1&0 \end{bmatrix} ``` 因此 ``` latex \begin{bmatrix} 1&1\\ 1&0 \end{bmatrix} ^{n-2} \times \begin{bmatrix} f(2)\\ f(1) \end{bmatrix} = \begin{bmatrix} f(n)\\ f(n-1) \end{bmatrix} ``` 将 $f(1)=f(2)=1$ 代入即可. ``` cpp int main() { matrix A; A[1][1] = 1, A[1][2] = 1; A[2][1] = 1, A[2][2] = 0; matrix B; B[1][1] = 1; // f(1) = 1 B[2][1] = 1; // f(2) = 1 cin >> n; B = (A ^ (n - 2)) * B; cout << B[1][1]; } ``` 例 2 求数列的第 $n$ 项 $f(n)$($n\geq 10^{12}$). ``` latex f(n)= \begin{cases} 1&n=1,2\\ f(n-1)+f(n-2)+n-1&n\geq 3 \end{cases} ``` 设 $4$ 阶矩阵 $A$ 满足 ``` latex \begin{bmatrix} a_{11}&a_{12}&a_{13}&a_{14}\\ a_{21}&a_{22}&a_{23}&a_{24}\\ a_{31}&a_{32}&a_{33}&a_{34}\\ a_{41}&a_{42}&a_{43}&a_{44} \end{bmatrix} \times \begin{bmatrix} f(n)\\ f(n-1)\\ n\\ 1 \end{bmatrix} = \begin{bmatrix} f(n+1)\\ f(n)\\ n+1\\ 1 \end{bmatrix} ``` 解得 ``` latex A= \begin{bmatrix} 1&1&1&0\\ 1&0&0&0\\ 0&0&1&1\\ 0&0&0&1 \end{bmatrix} ```
-
莫比乌斯反演
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E8%8E%AB%E6%AF%94%E4%B9%8C%E6%96%AF%E5%8F%8D%E6%BC%94/
!!! info 若无特殊说明,本章涉及的变量皆为正整数. 简介 由函数 $f$ 定义一个函数 $g$: $$g(n)=\sum_{d\mid n}f(d)\quad\big(\text\{或\}\quad g(n)=\sum_{n\mid d}f(d)\big)$$ 已知 $g(n)$,即可用莫比乌斯反演推出 $f(n)$. 莫比乌斯函数 定义 $n$ 的莫比乌斯函数记为 $\mu(n)$: ``` latex \mu(n)=\begin{cases} 0 & n \ 有平方因数 \\ 1 & n \ 无平方因数,且有偶数个质因数 \\ -1 & n \ 无平方因数,且有奇数个质因数 \end{cases} ``` 特别地,$μ(1)=1$. 性质 性质 1 ``` latex \sum_{d\mid n}μ(d) = \left\lfloor\frac{1}{n}\right\rfloor = \begin{cases} 1 & n=1\\ 0 & n>1 \end{cases} ``` 设 $n$ 有 $k$ 个质因子 $p_1,p_2,\cdots,p_k$,则: ``` latex \begin{aligned} \sum_{d\mid n}μ(d) = \ &μ(1)+μ(p_1)+μ(p_2)+\cdots+μ(p_k)+μ(p_1p_2)+\cdots+μ(p_1p_2\cdots p_k)\\ = \ &{k\choose 0}+{k\choose 1}(-1)+{k\choose 2}(-1)^2+\cdots+{k\choose k}(-1)^k\\ = \ &\sum_{i=0}^k{k\choose i}(-1)^i \end{aligned} ``` 由二项式定理得: $$\sum_{i=0}^k{k\choose i}(-1)^i=(1-1)^k=\begin{cases}1&k=1\\\\0&k>1\end{cases}$$ 当且仅当 $n=1$ 时,$k=1$. 因此: $$\sum_{d\mid n}μ(d)=\begin{cases}1&n=1\\\\0&n>1\end{cases}$$ 性质 2 ``` latex \sum_{d\mid gcd(n,m)}μ(d)= \begin{cases} 1 & n,m \ 互质\\ 0 & n,m \ 不互质 \end{cases} ``` 根据性质 1自证. <!-- 性质 3 $$\sum_{d\mid n}\dfrac{μ(d)}{d}=\dfrac{\varphi(n)}{n}$$ 动一动你聪明的小脑瓜. --> 积性函数 $$\displaystyleμ(nm)=\begin{cases}μ(n)\cdotμ(m)&n,m \ 互质\\\\0&n,m \ 不互质\end{cases}$$ 读者自证. 模板 基于质数的线性筛法求 $μ(1)\simμ(n)$. ``` cpp const int N = 1e6; vector<int> prime; int miuN]; bool isPrime[N]; void getMiu(int n) { miu[1] = 1; memset(isPrime, true, sizeof isPrime); for (int i = 2; i <= n; i ++) { if (isPrime[i]) prime.push_back(i), miu[i] = -1; for (int j = 0; j < prime.size(); j ++) { if (i * prime[j] > n) break; isPrime[i * prime[j]] = false; if (i % prime[j] == 0) { miu[i * prime[j]] = 0; break; } else { miu[i * prime[j]] = miu[i] * miu[prime[j]]; } } } } ``` 狄利克雷卷积 定义 函数 $f(x)$ 和 $g(x)$ 的狄利克雷卷积 $h(x)$ 定义为: $$h(x)=\sum_{d|x}f(d)g\\!\left(\frac{x}{d}\right)=\sum_{ab=x}f(a)g(b)$$ 简记为: $$h=f*g$$ 性质 - 交换律:$f* g=g*f$. - 结合律:$(f* g)*h=f *(g *h)$. - 分配律:$(f+g)*h=f *h+g *h$. - 等式的性质:$f=g\eq f* h=g*h,h(1)\not=0$. - 单位元:即单位函数 $ε$,满足 $f*ε=f$. $$ε(n)=\begin{cases}1&n=1\\\\0&n>1\end{cases}$$ <!-- - 逆元:若 $f*g=ε$,则 $g(x)$ 是 $f(x)$ 的逆元. $$g(x)=\frac{ε(x)-\sum_{ d|x,d\not=1}f(d)g\Big(\frac{x}{d}\Big)}{f(1)}$$ --> > 值为常数的函数,如 $f(n)=C$,在狄利克雷卷积中简记为 $C$. > > $$g(x)=\sum_{d|x}C\cdot f(d)\eq g=C*f$$ 重要结论 $$ε(n)=\sum_{d\mid n}μ(d)\quad ( 或 \quad\normalsize 1*μ=ε)$$ 由 $μ$ 函数的 [性质 1得: $$\sum_{d\mid n}μ(d)=\begin{cases}1&n=1\\\\0&n>1\end{cases}=ε(n)$$ $$∴1*μ=ε$$ 莫比乌斯反演 形式 1 $$f(n)=\sum_{d\mid n}g(d)\Longrightarrow g(n)=\sum_{d\mid n}μ(d)f\\!\left(\frac{n}{d}\right)$$ 原式可简记为: $$f=g*1\Longrightarrow g=μ *f$$ 由重要结论得: $$f=g* 1\Longrightarrow f* μ=g* (1 *μ)=g *ε=g$$ 即证. 形式 2 $$f(n)=\sum_{n\mid d}g(d)\Longrightarrow g(n)=\sum_{n\mid d}μ\\!\left(\frac{d}{n}\right)f(d)$$ ``` latex \begin{aligned} & \sum_{n\mid d}μ\!\left(\frac{d}{n}\right)f(d)\\ = \ &\sum_{k=1}^{+∞}μ(k)f(kn)&&\quad 设 \ k=\frac{d}{n},代入\\ = \ &\sum_{k=1}^{+∞}μ(k)\sum_{kn\mid d}g(d)&&\quad 将 \ f(n)=\sum_{n\mid d}g(d) \ 代入\\ = \ &\sum_{n\mid d}g(d)\sum_{k\mid\frac{d}{n}}μ(k)\\ = \ &\sum_{n\mid d}g(d)ε\!\left(\frac{d}{n}\right)&&\quad 套用 \ 重要结论\\ = \ &g(n) \end{aligned} ```
-
博弈论
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E5%8D%9A%E5%BC%88%E8%AE%BA/
简介 博弈论研究在一局博弈中如何最优化玩家的策略. 公平组合游戏 ICG - 两名玩家轮流行动,且行动规则相同. - 最终不能行动的玩家判负. NIM 游戏 简介 有 $n$ 堆石子,第 $i$ 堆石子数为 $A_i$. 两名玩家轮流取走任意一堆的任意个石子,但不能不取. 取走最后一个石子的玩家胜. NIM 游戏属于公平组合游戏,且不存在平局,只有「先手必胜」和「后手必胜」两种情况. 策略 当且仅当 $A_1\oplus A_2\oplus\cdots\oplus A_n\not=0$ 时,先手必胜($\oplus$ 表示二进制异或). $A\oplus B$:将 $A$ 和 $B$ 的二进制位对齐,相等取 $0$,不相等取 $1$. ``` latex \begin{aligned} 1 \ 0 \ 0 \ 1 \ 0 \ 1 \ 0\\ \underline{\oplus \ 1 \ 1 \ 0 \ 1 \ 1 \ 0 \ 1}\\ 0 \ 1 \ 0 \ 0 \ 1 \ 1 \ 1 \end{aligned} ``` ???+ note 证明 ??? note 引理 1 设 $A_1\oplus A_2\oplus\cdots\oplus A_n=k\not=0$. 设 $k$ 在二进制下有 $\text{len}(k)$ 位. 由异或的定义得,至少存在一个 $i$,使 $A_i$ 的第 $\text{len}(k)$ 位为 $1$,这样才能保证 $k$ 的第 $\text{len}(k)$ 位也为 $1$. 在此条件下,显然有 $A_i>A_i\oplus k$. 于是可以取第 $i$ 堆石子,使其剩下 $A_i\oplus k$ 个. 取完石子后有 $$(A_1\oplus\cdots\oplus A_{i-1})\oplus(A_i\oplus k)\oplus(A_{i+1}\oplus\cdots\oplus A_n)=k\oplus k=0$$ ??? note 引理 2 设 $A_1\oplus A_2\oplus\cdots\oplus A_n=0$. 此时更改任意一个 $A_i$ 为 $A_i'$,都有: $$(A_1\oplus\cdots\oplus A_{i-1})\oplus(A_i')\oplus(A_{i+1}\oplus\cdots\oplus A_n)=0\oplus A_i\oplus A_i'\not=0$$ 即任意一步操作都会导致异或和 $\not=0$. 结合引理 1,引理 2 可得: - 存在一步操作,使异或和 $\not=0\Longrightarrow$ 异或和 $=0$. - 任意一步操作,使异或和 $=0\Longrightarrow$ 异或和 $\not=0$. - 石子被取光时失败(对手取走最后一个石子,获胜),此时异或和为 $0$. 当 $A_1\oplus A_2\oplus\cdots\oplus A_n\not=0$ 时,先手可以使博弈进入一种循环:``` latex \xymatrix { 异或和\not=0 } ``` 轮到后手时,异或和必为 $0$,即失败的局面只可能轮到后手. 先手必胜. 当 $A_1\oplus A_2\oplus\cdots\oplus A_n=0$ 时,后手可以同样的方法制胜. 此时先手必败. 有向图游戏 简介 在一个有向无环图中,只有一个起点,上面有一枚棋子. 两名玩家轮轮流沿有向边推动棋子,无法移动者判负. 任何公平组合游戏都可以转换为有向图游戏:每个节点表示一个状态,并且向后继状态连有向边. 策略 `Mex` 运算 设 $S$ 为非负整数集合. $\mex(S)$ 为不属于 $S$ 的最小非负整数. $$\mex(S)=\min\\{x\mid x\in N, x\notin S\\}$$ `SG` 函数 状态 $x$ 有 $k$ 个后继状态 $y_1,y_2,\cdots,y_k$,定义 $SG$ 函数: $$SG(x)=\mex(\\{SG(y_1),SG(y_2),\cdots,SG(y_k)\\})$$ 单图游戏 设起点为 $s$,当且仅当 $SG(s)\not=0$ 时,先手必胜. ??? note 证明 - 当棋子无法移动时失败,此时没有后继状态,$SG(s)=\mex(\varnothing)=0$. - 存在一步操作,使 $SG(s)\not=0\Longrightarrow SG(s)=0$. - 任意一步操作,使 $SG(s)=0\Longrightarrow SG(s)\not=0$. 当 $SG(s)\not=0$ 时,先手可以通过类似于`NIM` 游戏的方法制胜.当 $SG(s)=0$ 时,后手必胜. 组合图游戏 对于 $n$ 个有向图游戏组合成的游戏,起点分别为 $s_1,s_2,\cdots,s_n$. 当且仅当 $SG(s_1)\oplus SG(s_2)\oplus\cdots\oplus SG(s_n)\not=0$ 时,先手必胜($\oplus$ 表示异或). ??? note 证明 由单图游戏可知: - $SG(s_i)=0$(无法移动每个棋子)时失败,此时异或和 $=0$. - 存在一步操作,使异或和 $\not=0\Longrightarrow$ 异或和 $=0$. - 任意一步操作,使异或和 $=0\Longrightarrow$ 异或和 $\not=0$. 当 $SG(s_1)\oplus SG(s_2)\oplus\cdots\oplus SG(s_n)\not=0$ 时,先手可以通过类似于`NIM` 游戏的方法制胜.
-
容斥原理
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E7%BB%84%E5%90%88%E6%95%B0%E5%AD%A6/%E5%AE%B9%E6%96%A5%E5%8E%9F%E7%90%86/
!!! info 若无特殊说明,本章涉及的变量皆为正整数. 简介 容斥原理是一种不重不漏的计数原理. 例,$A,B,C$ 三人竞选扫黄队长: ::: - $15$ 人投给 $A$ - $16$ 人投给 $B$ - $17$ 人投给 $C$ - $2$ 人同时投给 $A,B,C$ ::: - $4$ 人同时投给 $A,B$ - $5$ 人同时投给 $A,C$ - $6$ 人同时投给 $B,C$ 问共有多少人参与投票. $A,B,C$ 三人的得票情况以用韦恩图描述:::: - $|A|=15$ - $|B|=16$ - $|C|=17$ - $|A∩B∩C|=2$ ::: - $|A∩B|=4$ - $|A∩C|=5$ - $|B∩C|=6$ 求的是投票人数,即 $|A∪B∪C|$. $$|A∪B∪C|=|A|+|B|+|C|-|A∩B|-|A∩C|-|B∩C|+|A∩B∩C|$$ 将上述问题推广到普遍情况,就是容斥原理. 公式 并集 对于 $n$ 个集合 $S_1,S_2\cdots S_n$,$|S|$ 表示集合 $S$ 的元素数,则: $$\left|\bigcup_{i=1}^nS_i\right|=\sum_i|S_i|-\sum_{i < j}|S_i∩S_j|+\sum_{i < j < k}|S_i∩S_j∩S_k|-\cdots+(-1)^{n+1}\left|\bigcap_{i=1}^nS_i\right|$$ ??? note 证明 设 $x\in S_{a_1},S_{a_2},\cdots,S_{a_m}$,根据公式,$\left|\bigcup_{i=1}^nS_i\right|$ 中包含元素 $x$ 的个数为: $$cnt(x)=m-{m\choose 2}+{m\choose 3}-\cdots+(-1)^{n+1}{m\choose m}={m\choose 0}-\sum_{i=0}^m(-1)^i{m\choose i}$$ 根据二项式定理有: $$(1-1)^m=\sum_{i=0}^m{m\choose i}1^{m-i}\cdot(-1^i)=\sum_{i=0}^m(-1)^i{m\choose i}$$ $$∴cnt(x)={m\choose 0}-(1-1)^m=1$$ 每个元素只出现一次,等价于并集,证毕. 交集 对于 $n$ 个集合 $S_1,S_2\cdots S_n$,已知全集为 $U$: $$\left|\bigcap_{i=1}^nS_i\right|=|U|-\left|\bigcup_{i=1}^n\overline{S_i}\right|$$ 其中 $\left|\bigcup_{i=1}^n\overline{S_i}\right|$ 套用并集的公式计算即可. 多重集的排列数 !!! info 普通集合:不允许有相同的元素. 多重集:相同的元素可以出现多次. 设 $S=\\{n_1\cdot a_1,n_2\cdot a_2,\cdots,n_k\cdot a_k\\}$ 是由 $n_1$ 个 $a_1$,$n_2$ 个 $a_2$,$\cdots$,$n_k$ 个 $a_k$ 组成的多重集. 设 $ n=\sum_{i=1}^kn_i$. $S$ 的全排列个数为: $$\frac{n!}{n_1!n_2!\cdots n_k!}$$ ??? note 证明 $n$ 个不同元素的全排列有 $n!$ 种. 若有 $j$ 个元素相同,则 $n!$ 种排列中有 $j!$ 种排列应算作同一种. 排列数为: $$\dfrac{n}{j!}$$ 根据乘法原理,多重集的全排列个数为: $$\frac{n!}{n_1!n_2!\cdots n_k!}$$ 多重集的组合数 1 设 $S=\\{∞\cdot a_1,∞\cdot a_2,\cdots,∞\cdot a_k\\}$,从 $S$ 中任取 $λ$ 个元素的方案数为: $${k+λ-1\choose k-1}$$ ??? note 证明 原问题等价于求以下方程非负整数解的个数: $$x_1+x_2+\cdots+x_k=λ$$ 解的个数用插板法计算: 在 $λ$ 个 $1$ 之间插 $k-1$ 块板,使其分为 $k$ 个区间. 第 $i$ 个区间的和代表 $x_i$. 这样就构造出了一组解.需要注意,板插在两端或同一个空隙中是允许的. 最初有 $λ+1$ 个空隙. 插第一块板有 $λ+1$ 种方法,第二块有 $λ+2$ 种,第三块有 $λ+3$ 种 $\cdots$ 第 $k-1$ 块有 $λ+k-1$ 种. 根据乘法原理,总的方案数为: $$(λ+1)(λ+2)(λ+3)\cdots(λ+k-1)=A_{λ+k-1}^{k-1}$$ 但是插板的顺序与答案无关. 这么算显然会包含一些重复情况:每一个方案都有 $(k-1)!$ 种不同的插板顺序,故答案为: $$\dfrac{A_{λ+k-1}^{k-1}}{(k-1)!}={k+λ-1\choose k-1}$$ 多重集的组合数 2 设 $S=\\{n_1\cdot a_1,n_2\cdot a_2,\cdots,n_k\cdot a_k\\}$,从 $S$ 中任取 $λ(λ≤\sum_in_i)$ 个元素的方案数为: ``` latex {k+λ-1\choose k-1}-\sum_i{k+λ-n_i-2\choose k-1}+\sum_{i < j}{k+λ-n_i-n_j-3\choose k-1}-\cdots+(-1)^k{k+λ-\sum_in_i-k-1\choose k-1} ``` ??? note 证明 考虑容斥原理: 合法方案数 $=$ 总方案数 $-$ 不合法方案数 总方案数 $={k+λ-1\choose k-1}$,详见多重集的组合数 1. 于是问题转化为求不合法方案数. - 设 $T_i$ 为 $a_i$ 超标的多重集. 先取 $n_i+1$ 个 $a_i$,再任取 $λ-n_i-1$ 个元素,加入 $T_i$. 不同 $T_i$ 的数量为 ${k+λ-n_i-2\choose k-1}$. - 设 $T_{ij}$ 为 $a_i,a_j$ 超标的多重集. 先取 $n_i+1$ 个 $a_i$ 和 $n_j+1$ 个 $a_j$,再任取 $λ-n_i-n_j-2$ 个元素,加入 $T_{ij}$. 不同 $T_{ij}$ 的数量为 ${k+λ-n_i-n_j-3\choose k-1}$. 依次类推,由容斥原理可得,不合法的方案数为: $$\left|\bigcup\\{T\\}\right|=\sum_i{k+λ-n_i-2\choose k-1}-\sum_{i < j}{k+λ-n_i-n_j-3\choose k-1}+\cdots+(-1)^{k+1}{k+λ-\sum_in_i-k-1\choose k-1}$$ 于是合法的方案数为: $${k+λ-1\choose k-1}-\left|\bigcup\\{T\\}\right|$$
-
中国剩余定理
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E5%90%8C%E4%BD%99/%E4%B8%AD%E5%9B%BD%E5%89%A9%E4%BD%99%E5%AE%9A%E7%90%86/
> 若无特殊说明,本章涉及的变量皆为正整数. 简介 中国剩余定理最早发现于《孙子算经》中. > 有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二. 问物几何? 即求满足下列条件的 $x$: ``` latex \left\{\begin{aligned} x \bmod 3 = 2 \\ x \bmod 5 = 3 \\ x \bmod 7 = 2 \\ \end{aligned}\right. ``` 它的通解公式为 $x=233+105k$. 《孙子算经》中只给出了最小正整数解,也就是 $k=-2$ 时的解:$x=23$. 不过,今天我们只关心中国剩余定理更普遍的应用. 问题 中国剩余定理指关于 $x$ 的同余方程组的解法: ``` latex \left\{\begin{aligned} x&≡a_1 \ (\bmod \ m_1)\\ x&≡a_2 \ (\bmod \ m_2)\\ &\cdots\\ x&≡a_k \ (\bmod \ m_k) \end{aligned}\right. ``` 其中 $a_1, a_2, \cdots, a_k$ 两两互质. 解法 设 $M=\prod^k_{i=1}m_i$. 设 $ M_i=\frac{M}{m_i}$,即除 $m_i$ 外,其余所有 $m$ 的乘积. 设 $M_it_i≡1\pmod{m_i}$,$t_i$ 为 $M_i$ 关于模 $m_i$ 的乘法逆元. 利用以上数据可以构造一个解: ``` latex \begin{aligned} x= \ &a_1M_1t_1 + a_2M_2t_2 + a_3M_3t_3 + \cdots + a_kM_kt_k\\ = \ &\sum^k_{i=1}a_iM_it_i \end{aligned} ``` 其正确性显然. 模板 ``` cpp include <bits/stdc++.h> using namespace std; typedef long long LL; const int N = 16; LL n, a[N], m[N], M[N], prodM = 1, ansX; void exGcd(int a, int b, int &x, int &y) { if (b == 0) { x = 1, y = 0; return; } exGcd(b, a % b, x, y); int t = x; x = y, y = t - a / b * y; } int main() { cin >> n; for (int i = 1; i <= n; i ++) { cin >> a[i] >> m[i]; prodM *= m[i]; } for (int i = 1; i <= n; i ++) { M[i] = prodM / m[i]; LL x = 0, y = 0; exGcd(M[i], m[i], x, y); ansX += a[i] * M[i] * (x + m[i]) % m[i]; } cout << ansX % prodM; return 0; } ```
-
Lucas 定理
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E7%BB%84%E5%90%88%E6%95%B0%E5%AD%A6/lucas-%E5%AE%9A%E7%90%86/
> 若无特殊说明,本章涉及的变量皆为正整数. 简介 `Lucas` 定理用于求解大组合数对质数 $p$($p≤10^6$)取模: ``` latex {n\choose m}≡{n \bmod p\choose m \bmod p}\cdot{\lfloor n/p\rfloor\choose\lfloor m/p\rfloor} \ (\bmod \ p) ``` 证明 > 我不会,长大后再学习. 解法 对于 $n,m≤10^6$ 的组合数 ${n\choose m}$,可以直接代入公式,并使用乘法逆元将除法转为乘法. ``` latex \binom{n}{m}=\frac{n!}{m!(n-m)!}≡n!\cdot(m!)^{-1}\cdot(n-m)!]^{-1} \ (\bmod \ p) ``` ``` cpp /* fac[x]: x! * inv[x]: x! 关于模 p 的乘法逆元(注意有阶乘) * 以上需要预处理 */ LL C(LL n, LL m) { if (n < m) return 0; return fac[n] * inv[m] % P * inv[n - m] % P; } ``` 对于更大的组合数,套用 `Lucas` 定理. $${n\choose m}≡{n \bmod p\choose m \bmod p}\cdot{\lfloor n/p\rfloor\choose\lfloor m/p\rfloor} \ (\bmod \ p)$$ 其中 ${n \bmod p\choose m \bmod p}$ 可以直接算,${\lfloor n/p\rfloor\choose\lfloor m/p\rfloor}$ 需要递归求解. ``` cpp LL lucas(LL n, LL m) { if (m == 0) return 1; return C(n % P, m % P) * lucas(n / P, m / P) % P; } ``` 预处理 - $fac[i]=i!\bmod p$,线性递推: $$fac[i]=fac[i-1]\cdot i\bmod p$$ - $inv[i]=(i!)^{-1}$,参考 [线性求逆元: $$i^{-1}=(-\Big\lfloor\frac{p}{i}\Big\rfloor\cdot(p\bmod i)^{-1}) \bmod p$$ $$inv[i]=inv[i - 1]\cdot i^{-1}\bmod p$$ ``` cpp void init() { fac[0] = fac[1] = 1; for (LL i = 2; i <= P; i ++) fac[i] = fac[i - 1] * i % P; inv[0] = inv[1] = 1; for (LL i = 2; i <= P; i ++) inv[i] = (P - P / i) * inv[P % i] % P; for (LL i = 2; i <= P; i ++) inv[i] = inv[i - 1] * inv[i] % P; } ``` 模板 总时间复杂度:$O(p+\log_p{n})$. ``` cpp typedef long long LL; const int N = 1e6 + 1, P = 10007; LL fac[N], inv[N]; void init() { fac[0] = fac[1] = 1; for (LL i = 2; i <= P; i ++) fac[i] = fac[i - 1] * i % P; inv[0] = inv[1] = 1; for (LL i = 2; i <= P; i ++) inv[i] = (P - P / i) * inv[P % i] % P; for (LL i = 2; i <= P; i ++) inv[i] = inv[i - 1] * inv[i] % P; } LL C(LL n, LL m) { if (n < m) return 0; return fac[n] * inv[m] % P * inv[n - m] % P; } LL lucas(LL n, LL m) { if (m == 0) return 1; return C(n % P, m % P) * lucas(n / P, m / P) % P; } ```
-
卡特兰数列
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E7%BB%84%E5%90%88%E6%95%B0%E5%AD%A6/%E5%8D%A1%E7%89%B9%E5%85%B0%E6%95%B0%E5%88%97/
> 若无特殊说明,本章涉及的变量皆为正整数. 简介 卡特兰数列是许多看似毫不相关的问题的解. - $n$ 个节点能构成 $Cat_n$ 种不同的二叉树. - $n$ 个左括号和 $n$ 个右括号组成的合法序列有 $Cat_n$ 种. - $n$ 个元素的进栈顺序为 $1,2,\cdots,n$,合法的出栈顺序有 $Cat_n$ 种. - 在圆上选择 $2n$ 个点成对连接,使得 $n$ 条线段不相交的方法数为 $Cat_n$. - 通过若干条互不相交的对角线,把凸 $n$ 边形拆分成若干个三角形的方案数为 $Cat_{n-2}$. - 在平面直角坐标系上,每一步只能向上或向右走 $1$ 个单位,从 $(0,0)$ 走到 $(n,n)$ 且不接触直线 $y=x$ 的路径数量为 $2Cat_n-1$. 通项公式 $$Cat_n={2n\choose n}÷(n+1)={2n\choose n}-{2n\choose n-1}$$ 递推公式 $$Cat_n=\sum_{i=0}^{n-1}Cat_i\cdot Cat_{n-i+1}$$ 附表 |$Cat_0$|$Cat_1$|$Cat_2$|$Cat_3$|$Cat_4$|$Cat_5$|$Cat_6$|$Cat_7$|$Cat_8$|$\cdots$| |:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:------:| |$1 $|$1 $|$2 $|$5 $|$14 $|$42 $|$132 $|$429 $|$1430 $|$\cdots$|
-
二项式定理
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E7%BB%84%E5%90%88%E6%95%B0%E5%AD%A6/%E4%BA%8C%E9%A1%B9%E5%BC%8F%E5%AE%9A%E7%90%86/
**注意**:若无特殊说明,本章涉及的变量皆为正整数. 简介 $$(a+b)^n=\sum_{i=0}^n\binom{n}{i}a^{n-i}b^i$$ 证明 使用数学归纳法. 设 $n=k$ 时二项式定理成立,考察 $n=k+1$ 时是否也成立: ``` latex \begin{aligned} &\textcolor{transparent}{=}(a+b)^{k+1}\\ &=(a+b)\cdot(a+b)^k\\ &=a(a+b)^k+b(a+b)^k\\ &=a\sum_{i=0}^k\binom{k}{i}a^{k-i}b^i+b\sum_{j=0}^k\binom{k}{j}a^{k-j}b^j\\ &=\sum_{i=0}^k\binom{k}{i}a^{k-i+1}b^i+\sum_{j=0}^k\binom{k}{j}a^{k-j}b^{j+1}&&将 \ a,b \ 乘进去\\ &=a^{k+1}+\sum_{i=1}^k\binom{k}{i}a^{k-i+1}b^i+\sum_{j=0}^k\binom{k}{j}a^{k-j}b^{j+1}&&提出 \ i=0 \ 的项\\ &=a^{k+1}+\sum_{i=1}^k\binom{k}{i}a^{k-i+1}b^i+\sum_{λ=1}^{k+1}\binom{k}{λ-1}a^{k-λ+1}b^λ&&设 \ λ=j+1,代入\\ &=a^{k+1}+\sum_{i=1}^k\binom{k}{i}a^{k-i+1}b^i+b^{k+1}+\sum_{λ=1}^{k}\binom{k}{λ-1}a^{k-λ+1}b^λ&&提出 \ λ=k+1 \ 的项\\ &=a^{k+1}+b^{k+1}+\sum_{i=1}^k\binom{k+1}{i}a^{k+1-i}b^i&&套用 \ 帕斯卡法则\\ &=\sum_{i=0}^{k+1}\binom{k+1}{i}a^{k+1-i}b^i\\ \end{aligned} ``` ∴ 二项式定理满足递推成立关系: $n=k$ 时成立 $\Longrightarrow$ $n=k+1$ 时成立 ∵ $n=1$ 时 $(a+b)^1=\sum_{i=0}^1\binom{n}{i}a^{n-i}b^i=a+b$ 成立, ∴ 二项式定理在 $n=1$ 之后的任何整数都成立.
-
组合数学
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E7%BB%84%E5%90%88%E6%95%B0%E5%AD%A6/%E7%BB%84%E5%90%88%E6%95%B0%E5%AD%A6/
**注意**:若无特殊说明,本章涉及的变量皆为正整数. 简介 - 排列:从 $n$ 个元素中取出 $m$ 个,按一定顺序排列. - 组合:从 $n$ 个元素中取出 $m$ 个,不计排列顺序. 加法原理 一算法有 $n$ 种方式,第 $i$ 种方式有 $a_i$ 种方法,该算法共有 $\sum_{i=1}^na_i$ 种实现方法. 从 $A$ 地到 $B$ 地有爬行、骑车、飞行三种方式,可以任选一个. 而爬行、骑车、飞行分别有 $a_1,a_2,a_3$ 种方法,那么 $A→B$ 共有 $a_1+a_2+a_3$ 种方法. 乘法原理 一算法有 $n$ 个步骤,第 $i$ 个步骤有 $a_i$ 种方法,该算法共有 $\prod_{i=1}^na_i$ 种实现方法. 从 $A$ 地到 $B$ 地必须先爬行到车站,再骑车到机场,最后飞行到北京,而爬行、骑车、飞行分别有 $a_1,a_2,a_3$ 种方法,那么 $A→B$ 共有 $a_1\cdot a_2\cdot a_3$ 种方法. 排列数 从 $n$ 个元素中取出 $m$ 个,按一定顺序排列的方案数,用符号 $A_n^m$ 表示. $$A_n^m=n(n-1)(n-2)\cdots(n-m+1)=\frac{n!}{(n-m)!}$$ $n=m$ 时的排列称为全排列,$A_n^n=n!$. 组合数 从 $n$ 个元素中取出 $m$ 个,不计排列顺序的方案数,用符号 $C_n^m$ 或 ${n\choose m}$ 表示. $$\binom{n}{m}=\frac{A_n^m}{m!}=\frac{n!}{m!(n-m)!}$$ > 特别地,当 $m>n$ 时,$A_n^m=C_n^m=0$ 互补性 $$\binom{n}{m}=\binom{n}{n-m}$$ $$\binom{n}{n-m}=\frac{n!}{(n-m)![n-(n-m)]!}=\frac{n!}{(n-m)!m!}=\binom{n}{m}$$ 帕斯卡法则 $$\binom{n}{m}=\binom{n-1}{m}+\binom{n-1}{m-1}$$ ``` latex \begin{aligned} &\binom{n-1}{m}=\frac{(n-1)!}{m!(n-m-1)!}=\frac{1}{m}\cdot\frac{(n-1)!}{(m-1)!(n-m-1)!}\\ &\binom{n-1}{m-1}=\frac{(n-1)!}{(m-1)!(n-m)!}=\frac{1}{n-m}\cdot\frac{(n-1)!}{(m-1)!(n-m-1)!}\\ \end{aligned} \\ \begin{aligned} \\ \binom{n-1}{m}+\binom{n-1}{m-1}&=(\frac{1}{m}+\frac{1}{n-m})\cdot\frac{(n-1)!}{(m-1)!(n-m-1)!}\\ &=\frac{n}{m(n-m)}\cdot\frac{(n-1)!}{(m-1)!(n-m-1)!}\\ &=\frac{n!}{m!(n-m)!}\\ &=\binom{n}{m} \end{aligned} ```
-
欧拉函数
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E5%90%8C%E4%BD%99/%E6%AC%A7%E6%8B%89%E5%87%BD%E6%95%B0/
**注意**:若无特殊说明,本章涉及的变量皆为正整数. 定义 $n$ 的欧拉函数为 $1,n]$ 中与 $n$ 互质的数的个数,记为 $\varphi(n)$. 特别地,$\varphi(1)=1$. 性质 质数的欧拉函数 $p$ 为质数,则 $\varphi(p)=p-1$. $p$ 与 $[1,p-1]$ 中的每个数互质. $p^k$ 的欧拉函数 $p$ 为质数,$n=p^k$,则 $\varphi(n)=p^k-p^{k-1}$. 在 $[1,n]$ 中,只有 $p$ 的倍数不与 $n=p^k$ 互质. $∵\\;[1,n]$ 中 $p$ 的倍数有 $\dfrac{n}{p}=\dfrac{p^k}{p}=p^{k-1}$ 个, $∴\varphi(n)=n-p^{k-1}=p^k-p^{k-1}$. 通项公式 $n$ 有 $m$ 个质因子 $p_1∼p_m$,则 $\varphi(n)=n\prod_{i=1}^{m}(1-\frac{1}{p_i})$. 若 $n$ 有质因子 $p$,则 $p$ 的倍数不与 $n$ 互质. $[1,n]$ 中 $p$ 的倍数有 $\cfrac{n}{p}$ 个,则剩下的 $n-\cfrac{n}{p}=n\cdot(1-\cfrac{1}{p})$ 个数不是 $p$ 的倍数. 也就是说,这 $n$ 个数中,非 $p$ 的倍数占比为 $1-\cfrac{1}{p}$. 根据定义,$[1,n]$ 中有 $\varphi(n)$ 个数不是 $p_1∼p_m$ 的倍数(即与 $n$ 互质). 由乘法原理得: $$\varphi(n)=n(1-\frac{1}{p_1})(1-\frac{1}{p_2})\cdots(1-\frac{1}{p_m})=n\prod_{i=1}^{m}(1-\frac{1}{p_i})$$ 积性函数 $n,m$ 互质,则 $\varphi(nm)=\varphi(n)\cdot \varphi(m)$. 设 $n$ 有 $x$ 个质因子 $p_1\sim p_x$, 设 $m$ 有 $y$ 个质因子 $q_1\sim q_y$, $∵n,m$ 互质,$∴n,m$ 的质因子互不相同. $∴nm$ 有 $x+y$ 个质因子 $p_1\sim p_x,q_1\sim q_y$. 由 $\varphi$ 函数的通项公式可得: ``` latex \begin{aligned} \varphi(n)\cdot \varphi(m)&=(n\prod_{i=1}^{x}(1-\frac{1}{p_i}))\cdot (m\prod_{i=1}^{y}(1-\frac{1}{q_i}))\\ &=nm\prod_{i=1}^{x}(1-\frac{1}{p_i})\prod_{i=1}^{y}(1-\frac{1}{q_i})\\ &=\varphi(nm) \end{aligned} ``` 即证. 定理 欧拉定理 若 $a,n$ 互质,则 $a^{ \varphi(n)}≡1\pmod{n}$. 设 $C=\\{C_1,C_2,\cdots,C_{\varphi(n)}\\}$ 为模 $n$ 的 [简化剩余系. 设 $aC=\\{aC_1,aC_2,\cdots,aC_{\varphi(n)}\\}$,即 $C$ 中的每个元素乘 $a$. 由剩余系的定义得: $$C_i\not≡C_j \ (\bmod \ n)(i\not=j)$$ $∵a,n$ 互质,由同余的同乘性可知: $$aC_i\not≡aC_j \ (\bmod \ n)(i\not=j)$$ $∴aC$ 为模 $n$ 的剩余系. $∵a,n$ 互质,根据欧几里得算法有: $$gcd(a,n)=gcd(n,a \bmod n)=1$$ $∴a \bmod n$ 也与 $n$ 互质. 根据简化剩余系的定义可知: $$a \bmod n\in C$$ $∵\\;$简化剩余系满足乘法封闭性质,所以: ``` latex \begin{cases} a \bmod n\in C \\ C_i\in C \end{cases} \eq(a \bmod n)\cdot C_i \bmod n\in C \eq aC_i \bmod n\in C ``` $∴aC_i \bmod n$ 与 $n$ 互质,即 $aC$ 中的任意元素模 $n$ 后与 $n$ 互质. 结合引理 $1$、引理 $2$ 可知 $aC$ 也为 $n$ 的简化剩余系. 显然,$C$ 的所有元素之积和 $aC$ 的所有元素之积模 $n$ 同余,即: $$\prod_{i=1}^{\varphi(n)}C_i≡\prod_{i=1}^{\varphi(n)}aC_i \ (\bmod \ n)$$ $$\prod_{i=1}^{\varphi(n)}C_i≡a^{\varphi(n)}\cdot \prod_{i=1}^{\varphi(n)}C_i \ (\bmod \ n)$$ $$(a^{\varphi(n)}-1)\prod_{i=1}^{\varphi(n)}C_i≡0 \ (\bmod \ n)$$ $∵C$ 中的所有元素模 $n$ 后与 $n$ 互质,所以: $$\prod_{i=1}^{\varphi(n)}C_i\not≡0 \ (\bmod \ n)$$ $∴a^{\varphi(n)}-1≡0\pmod{n}$,即 $a^{\varphi(n)}≡1\pmod{n}$. 费马小定理 若 $p$ 为质数,则 $a^{p-1}≡a\\pmod{p}$. 当 $p\mid a$ 时,费马小定理显然成立,故只需讨论 $p\nmid a$ 的情况. 当 $p\nmid a$ 时,$∵p$ 是质数 $∴a,p$ 没有公因数(即 $a,p$ 互质). 根据欧拉定理有: $$a^{ \varphi(p)}≡1 \ (\bmod \ p)$$ $∵p$ 为质数,由性质可知 $\varphi(p)=p-1$. 代入上式: $$a^{ p-1}≡1 \ (\bmod \ p)$$ 即证. <!-- 当 $p\mid a$ 时,费马小定理显然成立,故只需讨论 $p\nmid a$ 的情况. 设 $C=\\{0,1,2,\cdots,p-1\\}$ 为 $p$ 的完全剩余系. 设 $aC=\\{0,a,2a,\cdots,(p-1)\cdot a\\}$,即 $C$ 中的每个元素乘 $a$. 由剩余系的定义得: $$C_i\not≡C_j \ (\bmod \ p)(i\not=j)$$ $∵p\nmid a$,由同余的同乘性可知: $$aC_i\not≡aC_j \ (\bmod \ p)(i\not=j)$$ $∴aC$ 为模 $p$ 的剩余系. $∵aC$ 有 $p$ 个元素 $∴aC$ 也为 $p$ 的完全剩余系. 显然,$C$ 的非零元素之积和 $aC$ 的非零元素之积模 $p$ 同余,即: $$\prod_{i=2}^{p}C_i≡\prod_{i=2}^{p}aC_i \ (\bmod \ p)$$ $$(p-1)!≡a^{p-1}\cdot (p-1)! \ (\bmod \ p)$$ $$(a^{p-1}-1)(p-1)!≡0 \ (\bmod \ p)$$ $∵p$ 是质数,$(p-1)!\not≡0\pmod{p}$, $∴a^{p-1}-1≡0\pmod{p}$,即 $a^p≡a\pmod{p}$. --> 扩展欧拉定理 若 $a,n$ 互质,则对于任意 $b$,有 $a^b≡a^{b \bmod \varphi(n)}\\pmod{n}$. 设 $b \bmod \varphi(n)=q$,则 $b$ 可以表示为 $k\cdot \varphi(n)+q$. 代入 $a^b$ 得: $$a^b=a^{k\cdot \varphi(n)+q}=\big(a^{\varphi(n)}\big)^k\cdot a^q$$ 根据欧拉定理,当 $a,n$ 互质时,有: $$a^{\varphi(n)}≡1 \ (\bmod \ n)$$ 由同余的同幂性、同乘性可知: $$\big(a^{\varphi(n)}\big)^k≡1^k=1 \ (\bmod \ n)$$ $$\big(a^{\varphi(n)}\big)^k\cdot a^q≡a^q \ (\bmod \ n)$$ $$a^b≡a^q \ (\bmod \ n)$$ 即 $a^b≡a^{b \bmod \varphi(n)}\\pmod{n}$. 模板 欧拉函数 ```cpp int varphi(int n) { int ans = n; for(int i = 2; i <= sqrt(n); i ++) { if(n % i == 0) { ans = ans / i * (i - 1); while(n % i == 0) n /= i; } } if(n > 1) ans = ans / n * (n - 1); return ans; } ``` 线性欧拉算法 > 求 $1\sim n$ 的欧拉函数. 对质数的线性筛法加以改造. - 若 $i$ 为质数,则 $\varphi(i)=i-1$. - 利用 $i$ 和 $prime_j$ 生成合数时,利用积性函数这一性质: $$\varphi(i\cdot prime_j)=\varphi(i)\cdot\varphi(prime_j)$$ 时间复杂度为 $O(n)$. ```cpp const int N = 1e6; int phi[N]; vector<int> prime; void varphi(int n) { phi[1] = 1; for(int i = 2; i <= n; i ++) { if(!phi[i]) phi[i] = i - 1, prime.push_back(i); for(int j = 0; j < prime.size(); j ++) { if(i * prime[j] > n) break; phi[i * prime[j]] = phi[i] * phi[prime[j]]; // 积性函数 if(i % prime[j] == 0) break; } } } ```
-
乘法逆元
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E5%90%8C%E4%BD%99/%E4%B9%98%E6%B3%95%E9%80%86%E5%85%83/
**注意**:若无特殊说明,本章涉及的变量皆为正整数. 定义 若 $a\cdot b≡1\pmod{p}$,则 $b$ 为 $a$ 在模 $p$ 意义下的逆元,记作 $a^{-1}$ 或 $inv(a)$. $$a·a^{-1}≡1 \ (\bmod \ p)$$ 乘法逆元能够很好地将模运算中的除法转为乘法: $$\dfrac{a}{b}≡a\cdot b^{-1} \ (\bmod \ p)$$ > 在模运算中,$a^{-1}$ 是整数,并不是 $a$ 的 $-1$ 次方. 解法 对于整数 $x$,可以求解关于 $x^{-1}$ 的线性同余方程$x\cdot x^{-1}≡1 \ (\bmod \ p)$,其中 $x$ 为已知常数. ``` cpp int exGcd(int a, int b, int& x, int& y) { if(b == 0) { x = 1, y = 0; return a; } int d = exGcd(b, a % b, x, y); int t = x; x = y, y = t - a / b * y; return d; } int liEu(int a, int b, int c) { // a·x ≡ c (mod b) int x, y, d = exGcd(a, b, x, y); x *= (c / d); int t = b / d; return (x % t + t) % t; } int inv(int x, int m) { return liEu(x, m, 1); } ``` 质数的逆元 当 $p$ 为质数时,由费马小定理得: ``` latex \begin{aligned} a^{p-1}&≡1 \ (\bmod \ p)\\ a^{p-2}&≡a^{-1} \ (\bmod \ p) \end{aligned} ``` $∴p^{-1}=a^{p-2} \bmod p$. 其中 $a^{p-2}$ 可用快速幂求解. 线性求逆元 > 求 $1\sim n$ 在模 $p$ 意义下的逆元. 假设已经求出了 $1\sim i-1$ 的乘法逆元. 设 $ m=p \bmod i=p-\Big\lfloor\frac{p}{i}\Big\rfloor\cdot i$,则有: $$m+\Big\lfloor\frac{p}{i}\Big\rfloor\cdot i≡0 \ (\bmod \ p)$$ 两边同乘 $m^{-1}i^{-1}$: $$i^{-1}+\Big\lfloor\frac{p}{i}\Big\rfloor\cdot m^{-1}≡0 \ (\bmod \ p)$$ 由于 $m=p \bmod i≤i-1$,所以 $m^{-1}$ 已知. 于是得到 $i^{-1}$ 的表达式: $$i^{-1}=(-\Big\lfloor\frac{p}{i}\Big\rfloor\cdot(p\bmod i)^{-1}) \bmod p$$ ``` cpp inv[1] = 1; // 1 的乘法逆元 = 1 for(int i = 2; i <= n; i ++) { inv[i] = (- p / i * inv[p % i]) % p; inv[i] = (inv[i] + p) % p; // 化为正整数 } ```
-
线性同余方程
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E5%90%8C%E4%BD%99/%E7%BA%BF%E6%80%A7%E5%90%8C%E4%BD%99%E6%96%B9%E7%A8%8B/
**注意**:若无特殊说明,本章涉及的变量皆为正整数. 简介 形如 $ax≡c\pmod{b}$ 的方程称为线性同余方程. 特殊解 $ax≡c\pmod{b}\eq ax+by=c$. 由裴蜀定理可知,当且仅当 $gcd(a,b)\mid c$ 时有整数解. 先用扩展欧几里得算法求出一组 $x_0,y_0$,使得: $$ax_0+by_0=gcd(a,b)$$ 两边同时乘 $\frac{c}{gcd(a,b)}$: $$a\frac{c}{gcd(a,b)}x_0+b\frac{c}{gcd(a,b)}y_0=c$$ 于是找到方程 $ax+by=c$ 的一组特殊解: ``` latex \left\{\begin{aligned} x=\frac{c}{gcd(a,b)}x_0\\ y=\frac{c}{gcd(a,b)}y_0 \end{aligned}\right. ``` 通解 若 $x_0,y_0$ 为方程 $ax+by=c$ 的一组解,则该方程的任意解可表示为: ``` latex \left\{\begin{aligned} x=x_0+bt\\ y=y_0-at \end{aligned}\right. ``` 在实际问题中,往往只需要最小正整数解: ``` latex x=(x_0 \bmod t+t) \bmod t, \ t=\frac{b}{gcd(a,b)} ``` 模板 ``` cpp int exGcd(int a, int b, int& x, int& y) { if (b == 0) { x = 1, y = 0; return a; } int d = exGcd(b, a % b, x, y); int t = x; x = y, y = t - a / b * y; return d; } int liEu(int a, int b, int c) { // ax ≡ c (mod b) int x, y, d = exGcd(a, b, x, y); if (c % d != 0) return -1; // 没有整数解 return x * (c / d); // 特殊解 // int t = b / d; // return x = (x % t + t) % t; // 最小正整数解 } ```
-
欧几里得算法
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E5%90%8C%E4%BD%99/%E6%AC%A7%E5%87%A0%E9%87%8C%E5%BE%97%E7%AE%97%E6%B3%95/
最大公因数 $a$ 和 $b$ 的最大公因数记作 $gcd(a,b)$,简记为 $(a,b)$. 若 $a,b$ 有公因数 $d$,则 $a \bmod b = a - b \cdot\left\lfloor\frac{a}{b}\right\rfloor$ 也有因数 $d$. 也就是说,$a$ 和 $b$ 的所有公因数,同时也是 $b$ 和 $a \bmod b$ 的公因数,因此它们的最大公因数也相等. $$gcd(a,b)=gcd(b,a \bmod b)$$ 递归到 $b=0$ 时,$a$ 即为 $gcd(a,b)$. 时间复杂度为 $O(log{(a+b)})$. ``` cpp int gcd(int a, int b) { if (b == 0) return a; return gcd(b, a % b); } ``` 最小公倍数 $a$ 和 $b$ 的最小公倍数记作 $lcm(a,b)$,简记为 $[a,b]$. 设 $gcd(a,b)=d$,则 $a=k_1d,b=k_2d$,所以: $$lcm(a,b)=k_1k_2d=\frac{k_1d\cdot k_2d}{d}=\frac{ab}{gcd(a,b)}$$ 时间复杂度为 $O(log{(a+b)})$. ```cpp int lcm(int a, int b) { return a * b / gcd(a, b); } ``` 扩展欧几里得算法 扩展欧几里得算法用于求方程 $ax+by=gcd(a,b)$ 关于 $x,y$ 的解,其中 $a,b$ 是常数. 设 $ax_1+by_1=gcd(a,b)$, 设 $bx_2+(a \bmod b)y_2=gcd(b,a \bmod b)$, 由 $gcd(a,b)=gcd(b,a \bmod b)$ 可知: ``` latex \begin{aligned} ax_1+by_1&=bx_2+(a \bmod b) y_2\\ &=bx_2+(a-\left\lfloor\frac{a}{b}\right\rfloor\cdot b) y_2\\ &=bx_2+ay_2-\left\lfloor\frac{a}{b}\right\rfloor\cdot by_2\\ &=ay_2+b(x_2-\left\lfloor\frac{a}{b}\right\rfloor\cdot y_2) \end{aligned} ``` $$∴\left\\{\begin{aligned}x_1&=y_2\\\\y_1&=x_2-\left\lfloor\frac{a}{b}\right\rfloor\cdot y_2\end{aligned}\right.$$ 因此,考虑先递归求解 $bx+(a \bmod b)\ y=gcd(a,b)$,再由其解推出 $ax+by=gcd(b,a \bmod b)$ 的解. 边界条件:当递归到 $b=0$ 时,一定有一组解 $\left\\{\begin{aligned}x&=1\\\\y&=0\end{aligned}\right.$. ``` cpp struct Set { int x, y; Set(int x, int y) : x(x), y(y) {} }; Set exGcd(int a, int b) { // 求解 ax + by = gcd(a, b) if (b == 0) return Set(1, 0); Set ans = exGcd(b, a % b); // 先递归求解 bx + (a % b)y = gcd(b, a % b) return Set(ans.y, ans.x - (a / b) * ans.y); } ```
-
贝祖定理
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E5%90%8C%E4%BD%99/%E8%B4%9D%E7%A5%96%E5%AE%9A%E7%90%86/
**注意**:若无特殊说明,本章涉及的变量皆为正整数. 简介 对于任意 $a,b$,$ax+by=c\eq gcd(a,b)\mid c$. 证明 设 $gcd(a,b)=d$,则: ``` latex \left\{\begin{aligned} &d\mid a\\ &d\mid b \end{aligned}\right. \eq \left\{\begin{aligned} &d\mid ax\\ &d\mid by \end{aligned}\right. \eq d\mid (ax+by) \eq d\mid c ``` 即 $gcd(a,b)\mid c$.
-
快速幂
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E5%BF%AB%E9%80%9F%E5%B9%82/
快速幂 如何快速求 $a^b$($a,b\in\mathbb{Z}$)? 递归写法 根据乘方公式 $a^{m+n}=a^m\cdot a^n$,有: ``` latex a^b=\left\{\begin{aligned} &a^{\left\lfloor b/2\right\rfloor}\cdot a^{\left\lfloor b/2\right\rfloor} && b=2k\\ &a^{\left\lfloor b/2\right\rfloor}\cdot a^{\left\lfloor b/2\right\rfloor}\cdot a && b=2k+1 \end{aligned}\right. ,k\in\mathbb{Z} ``` ``` cpp typedef long long LL; LL Pow(LL a, LL b) { if(!b) return 1; // 边界条件 LL res = Pow(a, b / 2); if(b % 2 == 0) // b 是偶数 return res * res; else // b 是奇数 return res * res * a; } ``` 递推写法 若 $b$ 在二进制下的第 $k_1,k_2,k_3,\cdots$ 位为 $1$,则 $b=2^{k_1}+2^{k_2}+2^{k_3}+\cdots(k_i≤\log{b})$. 举个例子,$(14)_{10}=(1011)_2$,二进制下 $14$ 的第 $0,1,3$ 位都为 $1$,那么 $14=2^0+2^1+2^3$. 将 $b=2^{k_1}+2^{k_2}+2^{k_3}+\cdots$ 代入 $a^b$: ``` latex \begin{aligned} a^b&=a^{\Large(2^{ k_1}+2^{ k_2}+2^{ k_3}+\cdots)}\\ &=a^{2^{k_1}}\cdot a^{2^{k_2}}\cdot a^{2^{k_3}}\cdot \ \cdots \end{aligned} ``` 扫描 $b$ 的每个二进制位,如果第 $k$ 位是 $1$,则将答案乘上 $a^{2^k}$. 时间复杂度为 $O(\log{b})$. ``` cpp typedef long long LL; LL Pow(LL a, LL b) { LL res = 1; while(b) { if(b % 2 == 1) // 二进制下该位为 1 res *= a; a *= a, b /= 2; } return res; } ```
-
同余
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E5%90%8C%E4%BD%99/%E5%90%8C%E4%BD%99/
**注意**:若无特殊说明,本章涉及的变量皆为正整数. 定义 若 $a \bmod m=b \bmod m$,则 $a$ 和 $b$ 模 $m$ 同余,记作 $a≡b\\pmod{m}$. $$a≡b \ (\bmod \ m)\eq a=b+km\eq m\mid(a-b)$$ 性质 - 自反性:$a≡a\pmod{m}$. - 对称性:$a≡b\pmod{m}\eq b≡a\pmod{m}$. - 传递性:$\left\\{\begin{aligned}a&≡b \ (\bmod \ m)\\\\b&≡c \ (\bmod \ m)\end{aligned}\right.\eq a≡c\pmod{m}$. - 同加性:$a≡b\pmod{m}\eq a+c≡b+c\pmod{m}$. - 同乘性:$a≡b\pmod{m}\eq ac≡bc\pmod{m}$. - 同幂性:$a≡b\pmod{m}\eq a^n≡b^n\pmod{m}$. > 同余不满足同除性. 当 $a≡b \ (\bmod \ m)$ 时不一定有 $\frac{a}{n}≡\frac{b}{n} \ (\bmod \ m)$. 同余类 集合 $A$ 是模 $m$ 的同余类,当且仅当: - $A$ 中的所有元素模 $m$ 都等于同一个值 $a$. $$A=\\{x\mid x \bmod m=a\\}$$ $a$ 称为该同余类的代表元. 模 $m$ 的同余类有 $m$ 个,其代表元分别为 $0,1,2,\cdots,m-1$. 剩余系 集合 $A$ 是模 $m$ 的剩余系,当且仅当: - $A$ 中的元素模 $m$ 互不相同. $$A_i\not≡A_j \ (\bmod \ m)(i\not=j)$$ 完全剩余系 模 $m$ 的完全剩余系有 $m$ 个元素,是元素最多的剩余系. 简化剩余系 模 $m$ 的简化剩余系有 $\varphi(m)$ 个元素,每个元素模 $m$ 后都与 $m$ 互质. 乘法封闭 从模 $m$ 的简化剩余系中任取两个数 $a, b$,则 $ab \bmod m$ 也在模 $m$ 的简化剩余系中. 设 $a,b$ 属于模 $m$ 的简化剩余系,则 $a \bmod m,b \bmod m$ 与 $m$ 互质,$(a \bmod m)(b \bmod m)$ 也与 $m$ 互质. 根据欧几里得算法有: ``` latex \begin{aligned} &gcd((a \bmod m)(b \bmod m),m)\newline = \ &gcd(m,(a \bmod m)(b \bmod m) \bmod m)\newline = \ &gcd(m,ab \bmod m)=1 \end{aligned} ``` $∴ab \bmod m$ 与 $m$ 互质,也属于模 $m$ 的简化剩余系.
-
因数
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E5%9B%A0%E6%95%B0/
> 若无特殊说明,本章涉及的变量皆为正整数. 定义 若 $n \div d$ 为整数,则 $d$ 是 $n$ 的因数,记作 $d\mid n$. 算数基本定理 任意正整数 $n$ 都能唯一地分解为有限个质数的乘积. $$n=p_1^{\normalsize c_1}\cdot p_2^{\normalsize c_2}\cdots p_m^{\normalsize c_m}$$ 分解质因数 试除法 > 将 $n$ 分解成算数基本定理的形式. 如 > $$360=2^3\times 3^2\times 5$$ 枚举 $i=2,3,\cdots,\sqrt{n}$,除尽 $n$ 中的 $i$,并记录除的次数. ```cpp const int N = 1e6; vector<int> P, C; void factor(int n) { for (int i = 2; i <= sqrt(n); i ++) { if (n % i == 0) { P.push_back(i), C.push_back(1); while (n % i == 0) n /= i, C.back() ++; } } if (n != 1) { P.push_back(n); C.push_back(1); } } ``` 倍数法 > 求 $1,n]$ 中各整数的因数集 $fact$. 如 > > $$fact[12]=\\{1,2,3,4,6,12\\}$$ 对各整数依次进行 [试除法效率差,此时考虑倍数法: > $i$ 的倍数必有因数 $i$. 枚举 $i=1,2,\cdots,n$,并在 $i$ 的倍数的因数集中加入 $i$. 时间复杂度为 $O(n\log{n})$. ```cpp const int N = 1e6; vector<int> fact[N]; void factor(int n) { for (int i = 1; i <= n; i ++) { for (int j = 1; i * j <= n; j ++) { fact[i * j].push_back(i); } } } ```
-
质数
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E8%AE%BA/%E8%B4%A8%E6%95%B0/
> 若无特殊说明,本章涉及的变量皆为正整数. 定义 - 若 $n$ 只能被 $1$ 和 $n$ 整除,则 $n$ 是质数,否则是合数. - $\pi(n)$:$n$ 以内的质数个数,$\pi(n)≈\frac{n}{\ln{n}}$. - $p(n)$:第 $n$ 个质数,$p(n)≈n\ln{n}$. 质数判定 若 $n$ 为合数,则必定存在 $i≤\sqrt{n}$,使 $n$ 能整除 $i$. $n=0,1$ 需要特判. 时间复杂度:$O(\sqrt n)$. ```cpp bool isPrime(int n) { if (n < 2) return false; for (int i = 2; i <= sqrt(n); i ++) if (n % i == 0) return false; return true; } ``` 质数筛法 求 $n$ 以内的所有质数. 暴力算法 对 $2,n]$ 中的所有整数进行一次 [质数判定. 时间复杂度:$O(n\sqrt{n})$. ```cpp vector<int> prime; bool isPrime(int n) { if(n < 2) return false; for(int i = 2; i <= sqrt(n); i ++) if(n % i == 0) return false; return true; } void getPrime(int n) { for(int i = 2; i <= n; i ++) if(isPrime(i)) prime.push_back(i); } ``` 埃氏筛法 > 任意数的倍数必定是合数. 划掉 $2,n]$ 中每个数的倍数,剩下的就是质数. - 划掉 $2$ 的倍数:$4,6,8,\cdots$ - 划掉 $3$ 的倍数:$6,9,12,\cdots$ - $4$ 已划,则 $4$ 的倍数也已划,跳过. - 划掉 $5$ 的倍数:$10,15,20,\cdots$ - $6$ 已划,跳过. - $\cdots$ 时间复杂度:$O(n\log{n})$.埃氏筛法优化:$i$ 的倍数中,$2i$ 已被 $2$ 划掉,$3i$ 已被 $3$ 划掉,$\cdots$,$(i-1)i$ 已被 $i-1$ 划掉,故只需从 $i^2$ 开始划. 时间复杂度:$O(n\log\log n)$. ```cpp const int N = 1e6; bool mark[N]; // mark[i]: i 是否被划掉 vector<int> p; void getPrime(int n) { for (int i = 2; i <= n; i ++) { if (mark[i]) continue; p.push_back(i); for (int v = i * i; v <= n; v += i) mark[v] = true; } } ``` 线性筛法 [埃氏筛法中,有些数会被重复划,如 $i=2,3$ 时都划了 $12$. 针对此问题,线性筛法改变了划数的策略: > 使每个合数只被其最小质因子划去. 在第 $i$ 次循环中,埃氏筛法简单地划掉了 $i$ 的倍数 $i^2,i(i+1),i(i+2),\cdots$. ``` cpp for (int v = i * i; v <= n; v += i) { mark[v] = true; } ``` 而线性筛法划掉的是 $i\times p[j=0,1,\cdots]$,且 $i\times p[j]$ 的最小质因子必须是 $p[j]$. ``` cpp for (int j = 0; j < p.size() && i * p[j] <= n; j ++) { if (/* i × p[j] 的最小质因子是 p[j] */) { mark[i * p[j]] = true; } } ``` 对于任意 $n$ 以内的合数 $v$,若 $v$ 的最小质因子是 $p$,则 $v$ 会且仅会在 $i=\dfrac{v}{p}$ 时被划掉,真正做到了不重不漏. 现在只需讨论 `if()` 的条件. 举个例子:$i=15$ 时 $p=\\{2,3,5,7,11,13\\}$: - $15\times 2=30$,最小质因子是 $2$,可以划. - $15\times 3=45$,最小质因子是 $3$,可以划. - $15\times 5=75$,最小质因子是 $3\not=5$,跳过. - $15\times 7=105$,最小质因子是 $3\not=7$,跳过. - $15\times 11=165$,最小质因子是 $3\not=11$,跳过. - $\cdots$ 不难发现,由于 $15$ 本身有因数 $3$,这导致 $15\times 3$ 之后的情况都可以跳过. 因此当 $i\mod p[j]==0$ 时直接跳出循环. ``` cpp for (int j = 0; j < p.size() && i * p[j] <= n; j ++) { mark[i * p[j]] = true; if (i % p[j] == 0) break; } ``` 时间复杂度:$O(n)$. ```cpp const int N = 1e6; bool mark[N]; vector<int> p; void getPrime(int n) { for (int i = 2; i <= n; i ++) { if (!mark[i]) p.push_back(i); for (int j = 0; j < p.size() && i * p[j] <= n; j ++) { mark[i * p[j]] = true; if (i % p[j] == 0) break; } } } ```
-
差分约束系统
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E5%9B%BE%E8%AE%BA/%E5%B7%AE%E5%88%86%E7%BA%A6%E6%9D%9F%E7%B3%BB%E7%BB%9F/
简介 给定差分不等式组 ``` latex \begin{cases} X_1 - X_2 ≤ 1\\ X_3 - X_2 ≤ 3\\ X_4 - X_1 ≤ -2\\ \cdots \end{cases} ``` 求一组满足所有条件的 $X_1\cdots X_n$ 的解. 以上形式的不等式组称作「差分约束系统」. 原理 根据松弛操作原理,当`SPFA` 程序结束时,图中任意两个节点 $i,j$ 满足 $\dis[j] ≤ \dis[i] + g[i,j]$.事实上,差分约束系统的不等式可以变形为 $$X_j ≤ X_i + C$$ 于是令 $\dis[j]=X_j,\dis[i]=X_i,g[i,j]=C$.在图上跑一遍 `SPFA` 后,$X_1\cdots X_n$ 便满足差分不等式组. 模板 ``` cpp bool in[], X[]; // 将 dis[] 换成 X[],便于理解 struct node { int val, len; }; vector<int> g[]; void add(int X_j, X_i, C) { // X_j - X_i ≤ C g[X_i].push_back(node{X_j, C}); } void spfa(int s) { memset(in, false, sizeof in); memset(X, 0x7f, sizeof X); X[s] = 0; queue<int> Q; Q.push(s), in[s] = true; while(!Q.empty()) { int u = Q.front(); Q.pop(), in[u] = false; for(int i = 0; i < (int) g[u].size(); i ++) { int v = g[u][i].val; int d = g[u][i].len; if(X[v] > X[u] + d) { X[v] = X[u] + d; if(!in[v]) { Q.push(v), in[v] = true; } } } } } ```
-
强连通分量
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E5%9B%BE%E8%AE%BA/%E5%BC%BA%E8%BF%9E%E9%80%9A%E5%88%86%E9%87%8F/
简介 强连通 在有向图 $G$ 中,如果同时存在 $u→v$ 和 $v→u$ 的路径,那么称 $u$ 和 $v$ 强连通. 强连通图 如果有向图 $G$ 的任意两个节点都强连通,那么称 $G$ 是强连通图. 强连通分量 如果图 $G$ 的子图是强连通图,那么该子图称作 $G$ 的强连通分量.本章介绍求强连通分量的三种算法. Tarjan 算法 图的结构 `Tarjan` 算法基于对图的深度优先遍历(`DFS`),并且将图近似地看成一棵树.因此,这棵树中会不可避免地出现一些奇怪的边.- <font color=green>前向边</font>:与普通边方向一致,但跨越多个节点. - <font color=red>返祖边</font>:与普通边方向相反,从子孙指向祖先. - <font color=4169e1>横向边</font>:边的两个端点居于树的同一深度. 时间戳 `DFS` 遍历一张图时,按访问顺序给节点打标记.$u$ 是第 $i$ 个被访问的节点,记作 $dfnu]=i$.这个标记叫做「时间戳」.在图中,时间戳标记在节点的右上方. 追溯值 `Tarjan` 算法还引入了「追溯值」:$low[u]$,定义为以下节点时间戳的最小值: - 以 $u$ 为根的子树中的所有节点. - 从这棵子树的任意节点出发,经过 $1$ 条前向边、返祖边或横向边,能到达的节点. 以 $low[2]$ 为例,其子树中有 $2,4,5$ 号节点,还可以通过一条返祖边到达 $1$ 号节点.$low[2]$ 为这些节点时间戳的最小值,也就是 $1$. 基本原理 `Tarjan` 算法是如何利用 $dfn$ 和 $low$ 来求强连通分量的呢? - 首先,强连通分量中必然存在环,也就必然存在返祖边. - 如果节点 $u$ 满足 $dfn[u]=low[u]$,存在两种情况: - 如果 $u$ 没有出边,则 $u$ 独自为一个强连通分量. - 否则 $u$ 的子树中必有一条返祖边指向 $u$ 本身,此处必存在多节点的强连通分量. 要想具体地确定哪些节点是强连通分量,还得借助 [栈.具体看以下样例. === 1 $dfn[1]=low[1]=1$.|$u$|$1$|$2$|$3$|$4$|$5$|$6$| |:--:|:--:|:--:|:--:|:--:|:--:|:--:| |$dfn[u]$|$1$|||||| |$low[u]$|$1$|||||| === 2 $dfn[2]=low[2]=1$.|$u$|$1$|$2$|$3$|$4$|$5$|$6$| |:--:|:--:|:--:|:--:|:--:|:--:|:--:| |$dfn[u]$|$1$|$2$||||| |$low[u]$|$1$|$2$||||| === 3 $dfn[4]=low[4]=3$.节点 $4$ 有一条返祖边指向 $1$,故更新 $low[4]=low[1]=1$.|$u$|$1$|$2$|$3$|$4$|$5$|$6$| |:--:|:--:|:--:|:--:|:--:|:--:|:--:| |$dfn[u]$|$1$|$2$||$3$||| |$low[u]$|$1$|$2$||$\textcolor{red}{1}$||| === 4 回溯至节点 $2$,更新 $low[2]=\min(low[2],low[4])=1$.|$u$|$1$|$2$|$3$|$4$|$5$|$6$| |:--:|:--:|:--:|:--:|:--:|:--:|:--:| |$dfn[u]$|$1$|$2$||$3$||| |$low[u]$|$1$|$\textcolor{red}{1}$||$1$||| === 5 $dfn[5]=low[5]=4$.节点 $5$ 没有出边和子树,单独一个节点构成强连通分量.移除栈顶的 $5$.|$u$|$1$|$2$|$3$|$4$|$5$|$6$| |:--:|:--:|:--:|:--:|:--:|:--:|:--:| |$dfn[u]$|$1$|$2$||$3$|$\textcolor{red}{4}$|| |$low[u]$|$1$|$1$||$1$|$\textcolor{red}{4}$|| === 6 回溯至节点 $2$,$low[2]=\min(low[2],low[5])=1$.|$u$|$1$|$2$|$3$|$4$|$5$|$6$| |:--:|:--:|:--:|:--:|:--:|:--:|:--:| |$dfn[u]$|$1$|$2$||$3$|$4$|| |$low[u]$|$1$|$1$||$1$|$4$|| === 7 回溯至节点 $1$,$low[1]=\min(low[1],low[2])=1$.此时 $low[1]=dfn[1]$,栈顶的红色部分同属一个强连通分量.将其从栈中删除.|$u$|$1$|$2$|$3$|$4$|$5$|$6$| |:--:|:--:|:--:|:--:|:--:|:--:|:--:| |$dfn[u]$|$\textcolor{red}{1}$|$2$||$3$|$4$|| |$low[u]$|$\textcolor{red}{1}$|$1$||$1$|$4$|| === ... 时间复杂度为 $O(n+m)$($n$ 为点数,$m$ 为边数). ``` cpp // ans: 强连通分量个数 const int N = 1e6; int dfn[N], low[N], tot, ans; int in[N]; // u 号节点在第 in[u] 个强连通分量 vector<int> g[N]; // g[u]: u 指向的节点集合 stack<int> s; void tarjan(int u) { dfn[u] = low[u] = ++ tot; s.push(u); for(int i = 0; i < g[u].size(); i ++) { int v = g[u][i]; if(!dfn[v]) { tarjan(v); low[u] = min(low[u], low[v]); } else if(!in[u]) low[u] = min(low[u], dfn[v]); } if(dfn[u] == low[u]) { in[u] = ++ ans; while(s.top() != u) // 栈顶到 u 这部分的节点在同一个强连通分量 in[s.top()] = ans, s.pop(); s.pop(); } } ``` Kosaraju 算法 `Kosaraju` 的核心思想可以总结为两点: - 强连通分量反过来还是强连通分量. - 把强连通分量反一半过来会得到两条共起点、终点的路径. 第一次正向遍历,第二次反向遍历,若出现共起点、终点的两条路径,则此处必有强连通分量. ``` cpp const int N = 1e6; int sccCnt, color[N], vis[N]; vector<int> g[N]; // g[u]: u 指向的节点集合 vector<int> g2[N]; // g2[u]: 指向 u 的节点集合(反图) vector<int> s; void dfs1(int u) { vis[u] = true; for(int i = 0; i < g[u].size(); i ++) { int v = g[u][i]; if(!vis[v]) dfs1(v); } s.push_back(u); } void dfs2(int u) { color[u] = sccCnt; for(int i = 0; i < g2[u].size(); i ++) { int v = g2[u][i]; if(!color[v]) dfs2(v); } } void kosaraju() { sccCnt = 0; for(int i = 1; i <= n; i ++) if(!vis[i]) dfs1(i); for(int i = n; i >= 1; i --) { if(!color[s[i]]) { ++ sccCnt; dfs2(s[i]); } } } ``` Garbow 算法 > 未完待续 ... ``` cpp int garbow(int u) { stack1[++p1] = u; stack2[++p2] = u; low[u] = ++dfs_clock; for (int i = head[u]; i; i = e[i].next) { int v = e[i].to; if (!low[v]) garbow(v); else if (!sccno[v]) while (low[stack2[p2]] > low[v]) p2--; } if (stack2[p2] == u) { p2--; scc_cnt++; do { sccno[stack1[p1]] = scc_cnt; } while (stack1[p1--] != u); } return 0; } void find_scc(int n) { dfs_clock = scc_cnt = 0; p1 = p2 = 0; memset(sccno, 0, sizeof(sccno)); memset(low, 0, sizeof(low)); for (int i = 1; i <= n; i++) if (!low[i]) garbow(i); } ```
-
二叉堆
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%A0%86/%E4%BA%8C%E5%8F%89%E5%A0%86/
简介 二叉堆(`Binary Heap`) 是一种基于完全二叉树的数据结构. - 小根堆:任意节点 $≥$ 其父节点,根节点最小. - 大根堆:任意节点 $≤$ 其父节点,根节点最大.本篇以小根堆为例,介绍二叉堆的实现方式. 构造 按照从上到下,从左到右的顺序给节点编号.该二叉堆具有以下性质: - $1$ 号节点是根节点. - $u$ 号的父节点为 $\frac{u}{2}$(向下取整). - $u$ 号的左子节点为 $2u$,右子节点为 $2u+1$. - 二叉堆的任意一条支路都按照升序排序.使用数组保存二叉堆. ``` cpp int t[], n; // t[u] : u 号节点的值 // n : 节点总数 ``` 插入 如何往小根堆中插入元素 $2$? 1. 在堆底新建节点,值为 $2$; 2. 对新节点所在支路进行排序.重复执行以下步骤: - 若新节点 $<$ 其父节点,则交换它们的位置,否则跳出循环. === 1=== 2=== 3时间复杂度为 $O(n\log{n})$. ``` cpp void push(int val) { // 插入元素 val t[++ n] = val; // 新建节点 for(int u = n; u != 1; u /= 2) { if(t[u] < t[u / 2]) swap(t[u], t[u / 2]); else break; } } ``` 移除 将小根堆的根节点移除,如何调整使其仍为小根堆?1. 把堆底最后一个元素移到根节点; 2. 从根节点 $u=1$ 开始,重复执行以下步骤: - 比较 $u$ 的两个子节点,取最小的一个,记为 $v$; - 若 $t[u]>t[v]$,交换节点 $u$ 和 $v$ 的位置,并使 $u=v$,否则跳出循环. === 1=== 2=== 3``` cpp void pop() { // 移除根节点 t[1] = t[n --]; for(int u = 1; 2 * u <= n; ) { int v = 2 * u; if(v + 1 <= n && t[v + 1] < t[v]) v ++; // 取最小的子节点 if(t[u] > t[v]) swap(t[u], t[v]), u = v; else break; } } ``` 模板 ``` cpp int t[], n; void push(int val) { t[++ n] = val; for(int u = n; u != 1; u /= 2) { if(t[u] < t[u / 2]) swap(t[u], t[u / 2]); else break; } } void pop() { t[1] = t[n --]; for(int u = 1; 2 * u <= n; ) { int v = 2 * u; if(v + 1 <= n && t[v + 1] < t[v]) v ++; if(t[u] > t[v]) swap(t[u], t[v]), u = v; else break; } } ```
-
最短路径
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E5%9B%BE%E8%AE%BA/%E6%9C%80%E7%9F%AD%E8%B7%AF%E5%BE%84/
简介 现在给你 $n$ 个节点(编号为 $1\sim n$)和它们之间的边长,求任意两个节点之间的最短路径. 松弛操作 使用邻接矩阵$g$ 存图.如果 $gi,k] + g[k,j] < g[i,j]$,则路径 $i→k→j$ 比原先 $i→j$ 的路径更短,那么就令 $g[i,j] = g[i,k] + g[k,j]$.这就是松弛操作. Floyed 算法 一开始,我们将所有节点全部拨到图外面,然后按 $1$ 号到 $n$ 号顺序依次往图中加入节点. $g[k,i,j]$:当图中已经加入了 $1 \sim k$ 号节点时,从节点 $i$ 到 $j$ 的最短路径. 当节点 $k$ 被加入图中时,枚举节点 $i$ 和 $j$,利用新加入的 $k$ 对路径 $i-j$ 进行 [松弛操作: - 若路径 $i-j$ 不经过节点 $k$,则 $gk,i,j] = g[k - 1,i,j]$; - 若路径 $i-j$ 经过节点 $k$,则 $g[k,i,j] = g[k - 1,i,k] + g[k - 1,k,j]$. $$g[k,i,j] = \min(g[k - 1,i,j], g[k - 1,i,k] + g[k - 1,k,j])$$ ``` cpp for(int k = 1; k <= n; k ++) for(int i = 1; i <= n; i ++) for(int j = 1; j <= n; j ++) g[k][i][j] = min(g[k - 1][i][j], g[k - 1][i][k] + g[k - 1][k][j]); ``` 实际上,$g$ 数组的第一维不影响结果,可以摘掉. ``` cpp for(int k = 1; k <= n; k ++) for(int i = 1; i <= n; i ++) for(int j = 1; j <= n; j ++) g[i][j] = min(g[i][j], g[i][k] + g[k][j]); ``` `Floyed` 算法适用于任何图,但图中必须存在最短路.时间复杂度为 $O(n^3)$. Bellman-Ford 算法 `Bellman-Ford` 算法是基于 `Floyed` 算法的优化版本,但是只能处理单源最短路径:每跑一次 `Bellman-Ford` 算法,都需要给定源节点 $s$,并且只能求得 $s$ 到其它节点的最短路. `Floyed` 枚举节点的效率太低,于是 `Bellman-Ford` 改为枚举边. $n$ 个节点 $m$ 条边的图中,如果存在最短路径,则最短路径所包含的边数 $≤ n-1$.故每条路最多被松弛 $n-1$ 次.$dis[i]$ 表示 $s→i$ 的最短路长度,初始时要设为无穷大. 时间复杂度为 $O(nm)$. ``` cpp void bellman_ford(int s) { memset(dis, 0x7f, sizeof dis); for(int i = 1; i < n; i ++) // 松弛 n - 1 次 for(int j = 1; j <= m; j ++) dis[to[j]] = min(dis[to[j]], dis[from[j]] + len[j]); } ``` 判断负权回路 若图中存在长度为负数的 [回路,则此回路称为 **负权回路(负环)**.有负权回路的图不存在最短路. 使用 `Bellman-Ford` 算法时,如果一条路径被松弛了 $n$ 次及以上,则一定存在负权回路. ``` cpp bool check(int s) { memset(dis, 0x7f, sizeof dis); for(int i = 1; i < n; i ++) for(int j = 1; j <= m; j ++) dis[to[j]] = min(dis[to[j]], dis[from[j]] + len[j]); for(int j = 1; j <= m; j ++) if(dis[to[j]] > dis[from[j]] + len[j]) return false; // 松弛了 n - 1 次后,还能进行松弛操作,则存在负权回路 return true; } ``` SPFA 算法 `SPFA` 算法是 `Bellman-Ford` 算法的队列优化版本.只有上一次被松弛的节点的出边,才有可能引起下一次的松弛操作.每次取队首节点,对其出边进行松弛,将松弛到的节点加入队列.时间复杂度为 $O(kn)$.平均情况下,$k$ 为 $(1,2)$ 中的常数. ``` cpp const int N = 1e6; bool in[N], dis[N]; // in[i]: u 是否在栈中 struct node { int val, len; }; vector<int> g[N]; // g[u]: u 节点的邻接节点集合 void spfa(int s) { memset(in, false, sizeof in); memset(dis, 0x7f, sizeof dis); dis[s] = 0; queue<int> Q; Q.push(s), in[s] = true; while(!Q.empty()) { int u = Q.front(); Q.pop(), in[u] = false; for(int i = 0; i < (int) g[u].size(); i ++) { int v = g[u][i].val; int d = g[u][i].len; if(dis[v] > dis[u] + d) { dis[v] = dis[u] + d; if(!in[v]) { // 已经在栈里的节点没必要再进栈一次 Q.push(v), in[v] = true; } } } } } ``` 判断负权回路 ``` cpp const int N = 1e6; bool in[N], dis[N], relax[N]; // relax[u]: dis[u] 被松弛的次数 struct node { int val, len; }; vector<int> g[N]; bool spfa(int s) { memset(in, false, sizeof in); memset(dis, 0x7f, sizeof dis); dis[s] = 0; queue<int> Q; Q.push(s), in[s] = true; while(!Q.empty()) { int u = Q.front(); Q.pop(), in[u] = false; for(int i = 0; i < (int) g[u].size(); i ++) { int v = g[u][i].val; int d = g[u][i].len; if(dis[v] > dis[u] + d) { dis[v] = dis[u] + d; if(++ relax[v] >= n) // 松弛了 n 次及以上,存在负权回路 return false; if(!in[v]) { Q.push(v), in[v] = true; } } } } return true; } ``` Dijkstra 算法 从节点 $s$ 出发.首先把所有节点分成两个集合:已确定最短路长度的,和未确定的.一开始只有 $s$ 在第一个集合,且 $dis[s] = 0,dis[$ 除 $s$ 以外的其他节点 $]=∞$. 重复以下操作直到第二个集合中没有节点: 1. 松弛刚加入第一个集合的节点的所有出边. 2. 从第二个集合中,选取 $dis$ 值最小的节点,加入第一个集合. 只能处理单源最短路径,不能处理负权路径.时间复杂度为 $O(n^2)$. ``` cpp const int N = 1e6; int avl[N], dis[N]; void dijkstra(int s) { memset(avl, true, sizeof avl); memset(dis, 0x7f, sizeof dis); dis[s] = 0; for(int i = 1; i <= n; i ++) { int minn = INF, minp = 0; for(int j = 1; j <= n; j ++) if(avl[j] && dis[j] < minn) minn = dis[j], minp = j; if(!minp) continue; avl[minp] = false; for(int j = 1; j <= n; j ++) if(avl[j] && dis[j] > minn + g[minp][j]) dis[j] = minn + g[minp][j]; } } ```
-
并查集
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%B9%B6%E6%9F%A5%E9%9B%86/
简介 并查集支持以下操作: - 往一个集合中加入元素; - 查询两个元素是否在同一集合; - 合并两个集合. 问题 $\\{a,b\\}$,$\\{e,c\\}$,$\\{e,d\\}$ 分别在同一个集合,则共有几个集合?$b$ 和 $d$ 是否同集? 构造 把同集的两节点相连,则集合数 $=$ 连通图数. 若两节点连通,则它们同集. 并查集在每个集合中选取一个代表元素作为根节点,构造树型结构. 如图,$a,e$ 分别为两集合的代表元素. ``` mermaid flowchart a --> b e --> c e --> d ``` $fa[i]$ 为节点 $i$ 的父节点编号.上图中 $fa[b]=a$,$fa[c]=e$,$fa[d]=e$. 根节点的 $fa$ 值都设为 $0$,$fa[a]=0$,$fa[e]=0$. 查询代表元素 集合的代表元素就是并查集中的根节点.若节点 $x$ 没有父节点,则它自己是根节点,否则递归查询它的父节点.时间复杂度为 $O(\log{n})$. ``` cpp int find(int x) { // 返回节点 x 的根节点 if (!fa[x]) return x; return find(fa[x]); } ``` 查询是否同集 若两个节点所在树的根节点相同,则它们同集. ``` cpp bool judge(int x, int y) { return find(x) == find(y); } ``` 合并集合 若 $\\{b,c\\}$ 也同集,则合并其所在的集合. 将两集合根节点相连,即新建一条从 $a$ 连向 $e$ 的边:$fa[e]=a$. ``` mermaid flowchart a --> b a --> e e --> c & d ``` ``` cpp void merge(int x, int y) { if(find(x) != find(y)) // 如果 x 和 y 已经同集,则没必要再合并 fa[find(x)] = find(y); } ``` 路径压缩 - 每次查询出节点 $i$ 所在集合的根节点 $e$ 后,使 $fa[i]=e$. - 再次查询 $i$ 的根节点时,`find(i)` 函数就会直接返回 $e$,免去递归过程. 时间复杂度优化为 $O(1)$. 该做法类似于记忆化搜索. ``` cpp int find(int x) { if(!fa[x]) return x; return fa[x] = find(fa[x]); // 最后一行等价于 { fa[x] = find(fa[x]); return fa[x]; } } ```
-
AC 自动机
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%AE%97%E6%B3%95/ac-%E8%87%AA%E5%8A%A8%E6%9C%BA/
简介 `AC` 自动机不是能自动 `AC` 的机器,而是一种著名的多模式串匹配算法. 问题 给定 $n$ 个模式串 $A_1\sim A_n$ 和一个长为 $m$ 的主串 $S$,问有多少个模式串在 $S$ 中出现过. 假设 $n$ 个模式串互不相同,且字符串中仅含小写字母.可以考虑枚举 $S$ 的所有子串并进行判断. ``` cpp int n, ans; string A[], S; int m = S.size(); for(int l = 0; l < m; l ++) { for(int r = l + 1; r < m; r ++) { string substr = S.substr(l, r - l + 1); // substr = S[l...r] for(int i = 1; i <= n; i ++) { if(substr == A[i]) { ans ++; } } } } cout << ans; ``` 原理 > `Under Construction ...` 模板 ``` cpp void build() { for(int i = 0; i < 26; i ++) if(tr[0][i]) q.push(tr[0][i]); while(q.size()) { int u = q.front(); q.pop(); for(int i = 0; i < 26; i ++) if(tr[u][i]) fail[tr[u][i]] = tr[fail[u]][i], q.push(tr[u][i]); else tr[u][i] = tr[fail[u]][i]; } } void insert(char *s) { int u = 0; for(int i = 1; s[i]; i ++) { if(!tr[u][s[i] - 'a']) tr[u][s[i] - 'a'] = ++ tot; u = tr[u][s[i] - 'a']; } e[u]++; } int query(char *t) { int u = 0, res = 0; for(int i = 1; t[i]; i ++) { u = tr[u][t[i] - 'a']; for(int j = u; j && e[j] != -1; j = fail[j]) res += e[j], e[j] = -1; } return res; } ```
-
KMP 算法
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%AE%97%E6%B3%95/kmp-%E7%AE%97%E6%B3%95/
简介 `KMP` 算法不是看毛片算法,而是一种字符串匹配算法. `KMP` 是此算法的发明者 `Kruth`,`Morris` 和 `Pratt` 的名字缩写. 问题 给定字符串 $A$(长度为 $m$)和 $B$(长度为 $n$),问 $A$ 中是否包含 $B$ ? ``` latex \begin{aligned} A&=a \ b \ a \ b \ a \ b \ a \ b \ c\\ B&=a \ b \ a \ b \ c \end{aligned} ``` 考虑暴力算法: - 先将 $A$ 和 $B$ 左端对齐. - 如果匹配失败,就将 $B$ 右移一位,直到匹配成功. ``` latex \begin{aligned} A&=\textcolor{red}{a \ b \ a \ b \ a} \ b \ a \ b \ c\\ B&=\textcolor{red}{a \ b \ a \ b \ c} \end{aligned} ``` ``` latex \begin{aligned} A=a \ &\textcolor{red}{b \ a \ b \ a \ b} \ a \ b \ c\\ B=\;&\textcolor{red}{a \ b \ a \ b \ c} \end{aligned} ``` ``` latex \begin{aligned} A=a \ b \ &\textcolor{red}{a \ b \ a \ b \ a} \ b \ c\\ B=\;&\textcolor{red}{a \ b \ a \ b \ c} \end{aligned} ``` ``` latex \begin{aligned} A=a \ b \ a \ &\textcolor{red}{b \ a \ b \ a \ b} \ c\\ B=\;&\textcolor{red}{a \ b \ a \ b \ c} \end{aligned} ``` ``` latex \begin{aligned} A=a \ b \ a \ b \ &\textcolor{green}{a \ b \ a \ b \ c}\\ B=\;&\textcolor{green}{a \ b \ a \ b \ c} \end{aligned} ``` 时间复杂度:$O(mn)$. > 本章统一使用<font color=red>红色</font>表示不匹配,<font color=green>绿色</font>表示匹配. ``` cpp bool match(string a, string b) { int m = a.size(), n = b.size(); int i = 0, j = 0; while(i < m) { while(j < n && ai] == b[j]) j ++; if(j == n) return true; i ++, j = 0; } return false; } ``` 原理 当匹配失败时,我们忽视了一些重要的信息.当第一次匹配失败时,两个绿串相等. ``` latex \begin{aligned} A&=\textcolor{green}{a \ b \ a \ b} \ \textcolor{red}{a} \ b \ a \ b \ c\\ B&=\textcolor{green}{a \ b \ a \ b} \ \textcolor{red}{c} \end{aligned} ``` 该绿串还有着相同的前缀[^1]和后缀.我们管它叫做「公共前后缀」. $abab$ 的公共前后缀为 $ab$. [^1]: 字符串 $s$ 左部的任意子串为 $s$ 的前缀,且 $s$ 的前缀不能是 $s$ 本身.例如 `freeze` 的前缀有 `f`,`fr`,`fre`,`free`,`freez`. ``` latex \underset{\large前缀}{\underline{\textcolor{green}{a \ b}}} \ \underset{\large后缀}{\underline{\textcolor{green}{a \ b}}} ``` 从下面这个角度看,$A$ 和 $B$ 划线的两段,都是公共前后缀,它们相等. ``` latex \begin{aligned} A&=a \ b \ \underline{\textcolor{green}{a \ b}} \ a \ b \ a \ b \ c\\ B&=\underline{\textcolor{green}{a \ b}} \ a \ b \ c \end{aligned} ``` 若 $B$ 右移后能与 $A$ 匹配,那么 $B$ 至少要右移 $2$ 位,也就是使相等的两段对齐.这样就避免了一位一位地移,从而提高效率. ``` latex \begin{aligned} A=a \ b \ &\underline{\textcolor{green}{a \ b}} \ a \ b \ a \ b \ c\\ B=\;&\underline{\textcolor{green}{a \ b}} \ a \ b \ c \end{aligned} ``` 到此为止,`KMP` 的思路已经很明朗了. === 1 将 $A$ 串和 $B$ 串左端对齐($σ$ 表示不定字符). ``` latex \begin{aligned} A &= σσσσσσσσσσσσ\\ B &= σσσσσσσ \end{aligned} ``` === 2 从左往右比较字符,直到不匹配.此时 $A$ 和 $B$ 的绿串相等. ``` latex \begin{aligned} A &= \color{green}{σσσσσ}\color{red}σ\color{black}σσσσσσ\\ B &= \color{green}{σσσσσ}\color{red}σ\color{black}σ \end{aligned} ``` === 3 在绿串中找出相同的前缀和后缀(公共前后缀). ``` latex \begin{aligned} A &= \color{lightgreen}{σσσ}\color{green}{\underline{σσ}}\color{black}σσσσσσσ\\ B &= \color{green}{\underline{σσ}}\color{lightgreen}{σσσ}\color{black}σσ \end{aligned} ``` === 4 右移 $B$ 串,直到前缀和后缀竖直对齐. ``` latex \begin{aligned} A = \color{lightgreen}{σσσ}&\color{green}{\underline{σσ}}\color{black}σσσσσσσ\\ B = \;&\color{green}{\underline{σσ}}\color{lightgreen}{σσσ}\color{black}σσ \end{aligned} ``` === 5 从对齐部分的后一个字符开始,继续向右匹配,重复前面的过程. ``` latex \begin{aligned} A = σσσ&\color{green}{\underline{σσ}}\color{goldenrod}σ\color{black}σσσσσσ\\ B = \;&\color{green}{\underline{σσ}}\color{goldenrod}σ\color{black}σσσσ \end{aligned} ``` 现在还有一个问题:在程序中,如何实现字符串的右移?—— 使用指针. 指针 $i,j$ 分别指向 $A$ 串和 $B$ 串,表示 $A[i]$ 和 $B[j]$ 前的两个绿串匹配.如果将 $B$ 串右移 $x$ 位,那么在程序中,只需要将 $j$ 指针左移 $x$ 位. ``` latex \begin{aligned} A&=\textcolor{green}{σσσσσ}\overset{\overset{i}{↓}}{\textcolor{red}{σ}}σσσσσσ\\ B&=\textcolor{green}{σσσσσ}\underset{\underset{j}{↑}}{\textcolor{red}{σ}}σ \end{aligned} ``` ``` latex \begin{aligned} A=σσσ\textcolor{green}{σσ}\overset{\overset{i}{↓}}{σ}&σσσσσσ\\ B=\textcolor{green}{σσ}\underset{\underset{j}{↑}}{σ}&σσσσ \end{aligned} ``` $pre[i]$ 表示 $B[0\cdots i]$ 的公共前后缀长度,$pre[ \ ]$ 具体怎么预处理后续会说.现在假设 $pre[ \ ]$ 数组已经处理好了.依照刚才的思路,`KMP` 的主程序可以分解为以下步骤(结合下面的样例理解): 1. 令 $i,j$ 分别指向 $A$ 和 $B$ 的开头,即 $i=0,j=0$. 2. 如果 $A[i]=B[j]$,$i$ 和 $j$ 同时右移一位. 3. 如果 $A[i]\not=B[j]$,又分两种情况讨论: - $j\not=0$ 时,绿串的公共前后缀长度为 $pre[j-1]$.那么就令 $j=pre[j-1]$. - $j=0$ 时,$j$ 不能再往左移.此时只能将 $i$ 右移一位. 4. 重复 2、3 两个步骤,直到匹配完成. === 1 令指针 $i$ 和 $j$ 分别指向 $A$ 串和 $B$ 串的开头. ``` latex \begin{aligned} A&=\overset{\overset{ i}{↓}}{a}bababc\\ B&=\underset{\underset{ j}{↑}}{a}babc \end{aligned} ``` === 2 $A[i]=B[i]$,$i$ 和 $j$ 同时右移一位. ``` latex \begin{aligned} A&=\overset{\overset{ i}{↓}}{\textcolor{green}{a}}bababc\\ B&=\underset{\underset{ j}{↑}}{\textcolor{green}{a}}babc \end{aligned} ``` === 3 $A[i]=B[j]$,$i$ 和 $j$ 同时右移一位. ``` latex \begin{aligned} A&=\overset{\overset{ i}{↓}}{\textcolor{green}{ab}a}babc\\ B&=\underset{\underset{ j}{↑}}{\textcolor{green}{ab}a}bc \end{aligned} ``` === 4 $A[i]=B[j]$,$i$ 和 $j$ 同时右移一位. ``` latex \begin{aligned} A&=\textcolor{green}{ab}\overset{\overset{ i}{↓}}{\textcolor{green}{a}}babc\\ B&=\textcolor{green}{ab}\underset{\underset{ j}{↑}}{\textcolor{green}{a}}bc \end{aligned} ``` === 5 $A[i]=B[j]$,$i$ 和 $j$ 同时右移一位. ``` latex \begin{aligned} A&=\textcolor{green}{ab}\overset{\overset{ i}{↓}}{\textcolor{green}{ab}a}bc\\ B&=\textcolor{green}{ab}\underset{\underset{ j}{↑}}{\textcolor{green}{ab}c} \end{aligned} ``` === 6 $A[i]\not=B[j]$,令 $j=pre[j-1]=2$.这样,$A[i]$ 和 $B[j]$ 前的绿串就又匹配了. ``` latex \begin{aligned} A&=\textcolor{lightgreen}{ab}\underline{\textcolor{green}{ab}}\overset{\overset{ i}{↓}}{\textcolor{red}{a}}bc\\ B&=\underline{\textcolor{green}{ab}}\textcolor{lightgreen}{ab}\underset{\underset{ j}{↑}}{\textcolor{red}{c}} \end{aligned} ``` ``` latex \begin{aligned} A&=ab\underline{\textcolor{green}{ab}}\overset{\overset{ i}{↓}}{a}bc\\ B&=\underline{\textcolor{green}{ab}}\underset{\underset{ j}{↑}}{a}bc \end{aligned} ``` > 字符串的下标是从 $0$ 开始计算的.$j=2$ 实际上就是将 $j$ 指向 $B$ 串的第 $3$ 个字符. === 7 $A[i]=B[j]$,$i$ 和 $j$ 同时右移一位. ``` latex \begin{aligned} A&=ab\textcolor{green}{ab}\overset{\overset{ i}{↓}}{\textcolor{green}{a}}bc\\ B&=\textcolor{green}{ab}\underset{\underset{ j}{↑}}{\textcolor{green}{a}}bc \end{aligned} ``` === 8 $A[i]=B[j]$,$i$ 和 $j$ 同时右移一位. ``` latex \begin{aligned} A&=ab\textcolor{green}{ab}\overset{\overset{ i}{↓}}{\textcolor{green}{ab}c}\\ B&=\textcolor{green}{ab}\underset{\underset{ j}{↑}}{\textcolor{green}{ab}c} \end{aligned} ``` === 9 $A[i]=B[j]$,$i$ 和 $j$ 同时右移一位. ``` latex \begin{aligned} A&=ab\textcolor{green}{abab}\overset{\overset{ i}{↓}}{\textcolor{green}{c}}\\ B&=\textcolor{green}{abab}\underset{\underset{ j}{↑}}{\textcolor{green}{c}} \end{aligned} ``` 此时 $j=B\\;$串长度 $ =5$,指到了 $B$ 串外面,代表匹配成功. ``` latex \begin{aligned} A&=ab\textcolor{green}{ababc}\overset{\overset{ i}{↓}}{\color{white}{b}}\\ B&=\textcolor{green}{ababc}\underset{\underset{ j}{↑}}{\color{white}{b}} \end{aligned} ``` ``` cpp int pre[]; bool kmp(string A, string B) { int i = 0, j = 0; while(i < A.size()) { if(A[i] == B[j]) i ++, j ++; else if(j) j = pre[j - 1]; else i ++; if(j == B.size()) //匹配成功 return true; } return false; } ``` 预处理 $pre[ \ ]$ 数组的求法和 `KMP` 的主程序很相似. 指针 $i,j$ 都指向 $B$ 串,表示当前求的是 $B[0\cdots i]$ 的公共前后缀长度 $pre[i]$.并且 $B[0\cdots i]$ 的公共前后缀为 $B[0\cdots j]$,即 $pre[i]=j+1$. 因为 $pre[0]=0$ 不用求,所以直接从 $pre[1]$ 开始求就好,初始时 $i=1,j=0$. ``` latex \begin{aligned} B&=\overset{\overset{ i}{↓}}{aba}bc\\ B&=\underset{\underset{ j}{↑}}{a}babc \end{aligned} ``` 然后,我们直接采用 [`KMP` 主程序的策略: 1. 如果 $B[i]=B[j]$,那么 $pre[i]=j+1$,并将 $i$ 和 $j$ 同时右移一位. 2. 如果 $B[i]\not=B[j]$,又分两种情况讨论: - $j\not=0$ 时,绿串的公共前后缀长度为 $pre[j-1]$.那么就令 $j=pre[j-1]$. - $j=0$ 时,绿串长度为 $0$,$j$ 不能再往左移了.此时只能将 $i$ 右移一位. 3. 重复前两步,直到 $pre[ \ ]$ 全部求完. 为什么可以这么做呢?因为仅当 $B[i]$ 和 $B[j]$ 前的若干个字符相匹配时,$B[0\cdots j]$ 才可能是公共前后缀.而 `KMP` 的主程序能够维护这一特征. ``` latex \begin{aligned} B&=ab\underline{\textcolor{green}{ab}}\overset{\overset{ i}{↓}}{c}\\ B&=\underline{\textcolor{green}{ab}}\underset{\underset{ j}{↑}}{a}bc \end{aligned} ``` 也就是说,预处理 $pre[ \ ]$ 数组,和 `KMP` 的主程序,其实是在干同一件事情. ``` cpp int pre[]; void getPre(string B) { int i = 1, j = 0; // 注意 i = 1 while(i < B.size()) { if(B[i] == B[j]) pre[i] = j + 1, i ++, j ++; else if(j) j = pre[j - 1]; else i ++; } } ``` 模板 ``` cpp const int N = 1e6; int pre[N]; void getPre(string str) { int i = 1, j = 0; while(i < str.size()) { if(str[i] == str[j]) pre[i] = j + 1, i ++, j ++; else if(j) j = pre[j - 1]; else i ++; } } bool kmp(string str, string substr) { getPre(substr); int i = 0, j = 0; while(i < str.size()) { if(str[i] == substr[j]) i ++, j ++; else if(j) j = pre[j - 1]; else i ++; if(j == substr.size()) return true; } return false; } int main() { string str, substr; cin >> str >> substr; cout << kmp(str, substr); return 0; } ```
-
Trie 树
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/trie-%E6%A0%91/
简介 `Trie` 树又叫「字典树」,能够像字典一样录入和查询多个字符串. 构造 一般我们会用数组保存字符串,可是这么做既浪费空间,查询速度又慢. ``` cpp const int N = 4; const string str[N] = { "his", "her", "hello", "this" }; bool query(string s) { // 查询 str[] 中是否有字符串 s for(int i = 0; i < N; i ++) if(str[i] == s) return true; return false; } ``` 如果将字符串放在链表里,就会有相当一部分节点可以合并.例如 `his`,`her`,`hello` 的开头都是 `h`,那么它们可以共享同一个 `h` 节点.同理,`her` 和 `hello` 可以共享 `he`.最后,在上方建一个空节点,指向各个字符串的开头,一棵标准的 `Trie` 树就建好了.至于这个空节点,纯粹是为了让程序写起来更方便. 节点 `Trie` 树的节点存储于结构体中: ``` cpp const int N = 1e6; struct Node { bool isEnd = false; // 该节点是否为某单词的末尾,默认为 false int next[26]; // 该节点的子节点 } trie[N]; ``` 按照节点的创建顺序为其编号,令 `trie[u]` 表示第 $u$ 个建立的节点.`trie[u].next['s']` 表示 $u$ 号节点的子节点中,代表字符 $s$ 的节点的编号.``` cpp trie[1].next['d'] = 2 trie[1].next['h'] = 3 ``` 在程序中,我们一般用 `0 - 25` 替换 `a - z`,数组就只要开到 `next[26]`. 当然,也有人习惯于用二维数组存储 `Trie` 树节点. 插入 如何往 `Trie` 树中插入一个字符串? 本着勤俭节约的精神,能共享的节点就尽量共享.例如要在刚才那棵 `Trie` 树中插入 `thus`,并且发现 `t`,`h` 可以共享,那么就只要新建 `u`,`s` 两个节点.``` cpp const int N = 1e6; int tot; struct Node { bool isEnd = false; int next[26]; } trie[N]; void insert(string str) { // 往 trie 树中插入字符串 str int p = 0; // 最上方的空节点编号为 0 for(int i = 0; i < str.size(); i ++) { int ch = str[i] - 'a'; if(!trie[p].next[ch]) // 如果不能共享,就新建节点 trie[p].next[ch] = ++ tot; p = trie[p].next[ch]; } trie[p].isEnd = true; // 标记字符串结尾 } ``` 查询 查询 `Trie` 树中是否有某个字符串,只需要从空节点向下搜索即可. ``` cpp bool search(string str) { // 查询 trie 树中是否有字符串 str int p = 0; for(int i = 0; i < str.size(); i ++) { int ch = str[i] - 'a'; if(!trie[p].next[ch]) return false; p = trie[p].next[ch]; } return trie[p].isEnd; // 如果想查询 str 是否为前缀,直接返回 true } ``` 模板 === 结构体 ``` cpp const int N = 1e6; int tot; struct Node { bool isEnd = false; int next[26]; } trie[N]; void insert(string str) { int p = 0; for(int i = 0; i < str.size(); i ++) { int ch = str[i] - 'a'; if(!trie[p].next[ch]) trie[p].next[ch] = ++ tot; p = trie[p].next[ch]; } trie[p].isEnd = true; } bool search(string str) { int p = 0; for(int i = 0; i < str.size(); i ++) { int ch = str[i] - 'a'; if(!trie[p].next[ch]) return false; p = trie[p].next[ch]; } return trie[p].isEnd; } ``` === 二维数组 ``` cpp const int N = 1e6; int trie[N][26], tot; bool isEnd[N]; void insert(string str) { int p = 0; for(int i = 0; i < str.size(); i ++) { int ch = str[i] - 'a'; if(!trie[p][ch]) trie[p][ch] = ++ tot; p = trie[p][ch]; } isEnd[p] = true; } bool search(string str) { int p = 0; for(int i = 0; i < str.size(); i ++) { int ch = str[i] - 'a'; if(!trie[p][ch]) return false; p = trie[p][ch]; } return isEnd[p]; } ```
-
最近公共祖先(LCA)
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E5%9B%BE%E8%AE%BA/%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88/
简介 最近公共祖先(`Least Common Ancestors`,简称 `LCA`). 节点 $p,q$ 的最近公共祖先 $s$ 是这棵树中到 $p,q$ 的距离之和最小的节点.如何求两个节点的 `LCA`? === step 1 $p,q$ 两个指针分别指向这两个节点,并且 $p$ 的深度比 $q$ 深.=== step 2 将 $p$ 不断往父节点方向移,直到 $p,q$ 处于同一深度. </center> === step 3 $p$ 和 $q$ 同时往父节点移,直到它们相遇于 $s$ 节点.$s$ 节点为 $p$ 和 $q$ 的 `LCA`. <center> ``` cpp int d]; // d[u]: 节点 u 的深度 int f[]; // f[u]: 节点 u 的父节点 int LCA(int p, int q) { if(d[p] < d[q]) swap(p, q); // 使 p 的深度 ≥ q while(d[p] > d[q]) p = f[p]; // 步骤 1 while(p != q) p = f[p], q = f[q]; // 步骤 2 return p; } ``` 原理 暴力算法慢在哪里? $- -$ $p$ 和 $q$ 每次只能向父节点跳一步!这是因为我们只保存了每个节点的父节点. 进一步思考,开一个二维数组 $f$:$f[p,i]$ 表示 $p$ 的第 $2^i$ 级父节点.一张图中最多有 $2^{24}$ 个节点,否则光是读入这张图都会超时.因此数组只要开到 $f[n,25]$. 第一步 如果已经预处理出了 $f$ 数组,如何让 $p$ 向上跳到和 $q$ 同样深的位置? 采用「试跳法」.枚举 $i=24→0$: - 若 $f[p,i]$ 的深度小于 $q$ 的深度,会跳过头,所以选择不跳; - 否则就令 $p = f[p,i]$. 为什么要从 $24$ 开始枚举 $i$ 呢?因为 $p$ 不可能有超过 $2^{24}$ 级的父节点. 时间复杂度为 $O(24)$. ``` cpp for(int i = 24; i >= 0; i --) if(d[f[p][i]] < d[q]) continue; else p = f[p][i]; ``` 第二步 令 $p$ 和 $q$ 同时往父节点跳,直到它们相遇. 仍然使用「试跳法」,枚举 $i=24→0$: - 若 $f[p,i] = f[q,i]$,此时 $p,q$ 同时向上跳 $2^i$ 步会相遇,但是我们选择不跳. - 若 $f[p,i] \not= f[q,i]$,令 $p = f[p,i], q = f[q,i]$,此时 $p,q$ 离相遇又近了一步. 枚举结束时,$p$ 和 $q$ 处于相遇和不相遇的临界状态,如下图.$p$ 或 $q$ 的父节点就是 `LCA`.``` cpp for(int i = 24; i >= 0; i --) { if(f[p][i] != f[q][i]) { p = f[p][i]; q = f[q][i]; } } ``` 合并两个步骤,得到求 `LCA` 的代码: ``` cpp const int LogN = 24; int LCA(int x, int y) { if(d[x] < d[y]) swap(x, y); for(int i = LogN; i >= 0; i --) { if(d[f[x][i]] >= d[y]) x = f[x][i]; if(x == y) return x; // 如果已经相遇,x 和 y 就是 LCA } for(int i = LogN; i >= 0; i --) { if(f[x][i] != f[y][i]) { x = f[x][i]; y = f[y][i]; } } return f[x][0]; // 返回的是父节点 } ``` 预处理 出于 `LCA()` 函数的需要,我们还需要预处理: - 每个节点的深度 $d[u]$; - $f$ 数组. 首先 $d[u]=d[u$ 的父节点$]+1$,我们可以用一次深搜搞定 $d$ 数组. $u$ 的 $2^{i+1}$ 级父节点,同时也是「$u$ 的 $2^i$ 级父节点」的 $2^i$ 级父节点. $$ f[u,i + 1] = f[f[u,i],i] $$ 计算顺序:$f[u,1→24]$. 如果你还不理解为什么要算到 $f[u,24]$,请再仔细看一遍 [原理. 采用树形 DP方法进行预处理.时间复杂度为 $O(24n)$. ``` cpp const int N = 1e6, LogN = 24; int n, d[N + 1], f[N + 1][LogN + 1]; vector<int> g[N + 1]; // g[u]: 与节点 u 相连的点集合 void pre(int u, int fa) { // u 的父节点是 fa f[u][0] = fa; d[u] = d[fa] + 1; for(int i = 0; i < LogN; i ++) f[u][i + 1] = f[f[u][i]][i]; for(int i = 0; i < g[u].size(); i ++) { int v = g[u][i]; if(v == fa) continue; pre(v, u); } } int main() { /* ...省略数据的输入... */ pre(root, 0); // root 为根节点,如果题目没指定,可以为任意节点 } ``` 模板 ``` cpp const int N = 1e6, LogN = 24; int n, d[N + 1], f[N + 1][LogN + 1]; vector<int> g[N + 1]; void pre(int u, int fa) { f[u][0] = fa; d[u] = d[fa] + 1; for(int i = 0; i < LogN; i ++) f[u][i + 1] = f[f[u][i]][i]; for(int i = 0; i < g[u].size(); i ++) { int v = g[u][i]; if(v == fa) continue; pre(v, u); } } int LCA(int x, int y) { if(d[x] < d[y]) swap(x, y); for(int i = LogN; i >= 0; i --) { if(d[f[x][i]] >= d[y]) x = f[x][i]; if(x == y) return x; } for(int i = LogN; i >= 0; i --) { if(f[x][i] != f[y][i]) { x = f[x][i]; y = f[y][i]; } } return f[x][0]; } ```
-
RMQ 算法
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/rmq-%E7%AE%97%E6%B3%95/
简介 RMQ 是 Range Maximum/Minimum Query 的缩写,意为区间的最大(或最小)值. 问题 已知数组 $A$ 中一共有 $n$ 个元素,给出 $m$ 次询问: - 给出 $l,r$,求 $A[l\cdots r]$ 中的最大值. ``` cpp int n, a[]; void query(int l, int r) { // 暴力算法 int ans = a[l]; for(int i = l; i <= r; i ++) ans = max(ans, a[i]); return ans; } ``` 预处理 $f[i,j]$ 表示从 $A[i]$ 开始往后数 $2^j$ 个数的最大值,也就是 $\max\\{A[i]\sim A[i+2^j-1]\\}$.将 $2^j$ 个数从中间平均分成两部分,每一部分的元素为 $2^{j-1}$ 个. ``` latex \overbrace{\underbrace{A[i],A[i+1],\cdots,A[i+2^{j-1}-1]}_个元素},\underbrace{A[i+2^{j-1}],\cdots,A[i+2^j-1]}_个元素}}^{{ 2^j}个元素} ``` 整个区间的最大值一定为左右两部分的最大值: $$ f[i,j]=\max(f[i,j-1],f[i+2^{j-1},j-1]) $$ 由于 $f[\*,j]$ 是由 $f[\*,j-1]$ 推出的,故第一层循环枚举 $j=0→\log{n}$. ``` cpp const int N = 1e6 + 1, logN = 32; int n, a[N], f[N][logN]; void pre() { for(int i = 1; i <= n; i ++) f[i][0] = a[i]; for(int j = 1; j < logN; j ++) // f[][] 的第 2 维一定要先枚举 for(int i = 1; i + (1 << (j - 1)) <= n; i ++) f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1)))][j - 1]); } ``` 原理 现在我们要查询 $A[l]\sim A[r]$ 的最大值.$A[l]\sim A[r]$ 一共有 $r-l+1$ 个元素,我们设 $s=\log(r-l+1)$,然后在整个区间内划出两个长度为 $2^s$ 的子区间:根据前面的定义,左子区间的最大值为 $f[l,s]$,右子区间的最大值为 $f[r-2^s+1,s]$. 虽然两个子区间有重叠部分,但它们包含了整个 $[l,r]$ 区间.因此 $$ \max\\{A[l]\sim A[r]\\}=\max(f[l,s],f[r-2^s+1,s]) $$ 由于 $C++$ 提供的 $log2()$ 函数太慢,于是预处理 $Log[ \ ]$ 数组替代 $log2()$ 函数. ``` cpp int Log[N]; // Log[i] = log2(i) int pre() { ... Log[0] = -1; // 为了使 Log[1] = Log[1/2] + 1 = 0, Log[0] 得先赋值 -1 for(int i = 1; i <= n; i ++) Log[i] = Log[i / 2] + 1; } int query(int l, int r) { // 返回 a[l] ~ a[r] 的最大值 int s = Log[r - l + 1]; return max(f[l][s], f[r - (1 << s) + 1][s]); } ``` 模板 ``` cpp const int N = 1e6, logN = 32; int n, l, r, a[N], f[N][logN], Log[N]; void pre() { Log[0] = -1; // Log[] 的预处理在这里 for(int i = 1; i <= n; i ++) f[i][0] = a[i], Log[i] = Log[i / 2] + 1; for(int j = 1; j < logN; j ++) for(int i = 1; i + (1 << (j - 1)) <= n; i ++) f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]); } int query(int l, int r) { int s = Log[r - l + 1]; return max(f[l][s], f[r - (1 << s) + 1][s]); } int main() { cin >> n; for(int i = 1; i <= n; i ++) cin >> a[i]; pre(); while(cin >> l >> r) cout << query(l, r) << endl; } ```
-
哈希表
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%93%88%E5%B8%8C%E8%A1%A8/
简介 **哈希表** 由哈希函数和链表组成,相当于「超级数组」. - 数组的下标^1]可以是整数,浮点数,字符串等. - 不用定义数组的长度. [^1]: 中括号里的数称之为「下标」,例如 $a[14]$ 的下标为 $14$. 问题 医院的排队系统需要记录每个病人对应的问诊顺序.此处假设每个病人的名称都是数字. $p[i]$ 表示「名称为 $i$ 的病人的问诊顺序」.查询问诊顺序的时间复杂度为 $O(1)$. ``` cpp int p[]; void update(int i, int x) { // 记录病人 i 的问诊顺序为 x p[i] = x; } int query(int i) { // 查询病人 i 的问诊顺序 return p[i]; } ``` 然而总有某些病人不按套路取名. 要存储 $p[2147483647]=3$,得先开一个长度为 $2147483647$ 的数组.这明显不现实. 构造 开一个如此丑陋的数组,会浪费超过 $99\\%$ 的空间.毕竟只用到了其中的四个元素.有没有办法使数组只占很少空间? 设计**哈希函数** $getHash()$,用 $p[getHash(i)]$ 表示病人 $i$ 的问诊顺序.对于本例,有: $$ getHash(i)=i\\%5 $$ 这样数组就只要开到 $p[5]$.但这么做会导致其它问题:病人 $114514$ 和病人 $404$ 的问诊顺序会同时存入 $p[4]$,因为 $getHash(114514)=getHash(404)=4$.于是我们可以将 $getHash()$ 相同的病人放在同一个链表里.查询病人 $i$ 的问诊顺序时,只需在 $getHash(i)$ 开头的链表中找到该病人. 哈希函数 $getHash()$ 有多种构造方式: 1. 取模法 $$ getHash(x)=x\\%p $$ 其中 $p$ 为质数(如 $999997$). 2. 乘积取整法 $$ getHash(x)=\lfloor A\cdot x\rfloor\\%p $$ 其中 $p$ 为质数,$A$ 为区间 $(0,1)$ 中的无理数(如 $\frac{\sqrt{5}-1}{2}$). 3. 数位分析法 $$ getHash(s)=(s[0]+s[1]\cdot b+s[2]\cdot b^2+s[3]\cdot b^3+\cdots)\\%p $$ 其中 $s$ 为字符串,$b,p$ 为质数.详见 [哈希函数. 模板 ``` cpp static struct hash_map { define MOD (999997) struct node { int val, next, key; } edgeMOD]; int head[MOD], cnt; int getHash(int x) { return x % MOD; } int& operator[{ int h = getHash(key); for(int p = head[h]; p; p = edge[p].next) if(edge[p].key == key) return edge[p].val; edge[++ cnt] = (node){0, head[h], key}, head[h] = cnt; return edge[cnt].val; } hash_map() { cnt = 0; memset(head, 0, sizeof head); } } ```
-
哈希函数
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%AE%97%E6%B3%95/%E5%93%88%E5%B8%8C%E5%87%BD%E6%95%B0/
简介 哈希函数 $getHash()$ 能够将字符串转化成整数,并保证字符串不同,对应的整数也不同.该整数称为哈希值.这样,判断两个字符串是否相等,就只要判断它们的哈希值是否相等. 原理 假设所有字符串中只包含小写字符 $a\sim z$.以字符串 `fantasy` 为例: 1. 将 $a\sim z$ 替换为数字 $1\sim 26$,得到一个数列. $$fantasy→\\{6,1,14,20,1,19,25\\}$$ 2. 将该数列看作一个 $27$ 进制数(逢 $27$ 进一). $$getHash(fantasy)=6\cdot 27^6+1\cdot 27^5+14\cdot 27^4+\cdots+25\cdot 27^0$$ 按此方法设计的哈希函数,可保证不同字符串的哈希值必不同.但字符串长度过长时,哈希值会超出 `long long` 的范围. 滚动哈希 为解决一般哈希函数适用范围有限的问题,采用滚动哈希. 选取两个合适的质数 $b$ 和 $p$,将字符串看作 $b$ 进制数($b >$ 字符种数). $getHash(fantasy)=(6\cdot b^6+1\cdot b^5+14\cdot b^4+\cdots+25\cdot b^0)\%p$ 按此方法设计的哈希函数,不同字符串的哈希值相同的概率较小,且哈希值不会超出 `long long` 的范围.时间复杂度为 $O(n)$. ``` cpp typedef long long LL; const LL b = 29, p = 10000019; LL getHash(string str) { // 返回 str 的哈希值 LL h = 0; for(int i = 0; i < str.size(); i ++) h = (h * b + stri] - 'a') % p; return h; } ``` 哈希冲突 使用 [滚动哈希时,有概率会使不同字符串的哈希值相同.该现象称为哈希冲突. 一种降低哈希冲突概率的可靠方法是双哈希:使用两组不同的质数 $b_1,p_1$ 和 $b_2,p_2$,分别计算哈希值.只有两个哈希值分别相同,才能判定字符串的匹配. ``` cpp typedef long long LL; const LL b_1 = 29, p_1 = 10000019; const LL b_2 = 31, p_2 = 10000079; LL getHash_1(string str) { LL h = 0; for(int i = 0; i < str.size(); i ++) h = (h * b_1 + str[i] - 'a') % p_1; return h; } LL getHash_2(string str) { LL h = 0; for(int i = 0; i < str.size(); i ++) h = (h * b_2 + str[i] - 'a') % p_2; return h; } bool cmp(string s1, string s2) { // 比较 s1 和 s2 的哈希值是否相同 return getHash_1(s1) == getHash_1(s2) && getHash_2(s1) == getHash_2(s2); } ```
-
线段树
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E7%BA%BF%E6%AE%B5%E6%A0%91/
问题 数组 $A$ 中共 $n$ 个元素,对其反复进行以下操作共 $m$ 次: - **单点修改**:将 $Aid]$ 修改为 $v$. - **区间查询**:查询 $A[l\cdots r]$ 的最小值. - **区间修改**:将 $A[l\cdots r]$ 每个数加上 $v$. ``` cpp int a[]; void set(int id, int v) { // 单点修改 a[id] = v; } int ask(int l, int r) { // 区间查询 int ans = 0; for(int i = l; i <= r; i ++) ans = min(ans, a[i]); return ans; } void add(int l, int r, int v) { // 区间修改 for(int i = l; i <= r; i ++) a[i] += v; } ``` 构造 查询数组 $A=\{6,2,3,7,1,5,4,2\}$ 中的最小值时,通常使用「两两比较法」:每次比较相邻两项,只保留更小的一项.比较的过程可以画成一棵二叉树,树根是答案.这么做又有什么好处呢?假如数列中的 $1$ 被改为 $7$,你可以通过修改极少的数值,重新得出正确答案.线段树就是这样的一颗二叉树,它的每个节点都代表一段区间中的最小值. 线段树具有以下性质: - 根节点的值为 $t[1]$,代表整个数组的最小值. - $t[u]$ 的左子节点为 $t[2u]$,右子节点为 $t[2u+1]$. - $t[u]=\min(t[2u],t[2u+1])$.因此,每个节点需要保存以下信息: - 节点的值:$val$. - 节点代表的区间:$[l,r]$. 从根节点开始,自顶向下递归构建线段树.时间复杂度为 $O(n\log{n})$. ``` cpp struct Node { int val, l, r; define t(u) t[u].val define l(u) t[u].l define r(u) t[u].r } t[]; void build(int u, int l, int r) { l(u) = l, r(u) = r; if(l == r) { // 当前节点为叶节点 t(u) = a[l]; return; } int m = (l + r) / 2; build(2 * u, l, m); // 递归构建左子树 build(2 * u + 1, m + 1, r); // 递归构建右子树 t(u) = min(t(2 * u), t(2 * u + 1)); } ``` 单点修改 假设你要将 $A[5]$ 修改为 $3$,则 $A[5]$ 的所有祖先都有可能变动.$set(u,id,v)$:在以节点 $u$ 为根的子树中,找到 $A[id]$,并将其更新为 $v$. 1. 令 $ m=\frac{l(u)+r(u)}{2}$; 2. 若 $id≤m$,则 $A[id]$ 在左子树中,搜索左子树; 3. 若 $id>m$,则 $A[id]$ 在右子树中,搜索右子树; 4. 更新当前节点值:$t[u]=\min(t[2u],t[2u+1])$. 时间复杂度为 $O(\log{n})$. 根节点是搜索的入口.执行 $set(1, id, v)$ 以进行单点修改. ``` cpp void set(int u, int id, int v) { // 将 a[id] 改为 v if(l(u) == r(u)) { // 叶节点 a[id] = t(u) = v; return; } int m = (l(u) + r(u)) / 2; if(id <= m) set(2 * u, id, v); // 搜索左子树 else set(2 * u + 1, id, v); // 搜索右子树 t(u) = min(t(2 * u), t(2 * u + 1)); } ``` 区间查询 线段树中,每个节点代表一个区间.那么反过来想,每个区间都可以用若干节点表示.例如 $A[1\cdots 6]$ 可以用 $t[2]$ 和 $t[6]$ 表示,那么 $A[1\cdots 6]$ 的最小值 $=\min(t[2],t[6])=2$.从根节点开始,自顶向下搜索出范围在 $[l,r]$ 之内的节点,这些节点的最小值即为答案. $get(u,l,r)$:从节点 $u$ 开始,向下搜索 $A[l\cdots r]$ 的最小值. 1. 若 $u$ 的范围在 $[l,r]$ 之内,直接返回 $t[u]$; 2. 若 $u$ 的范围与 $[l,r]$ 不重叠,返回 $∞$[^1]; 3. 否则递归搜索 $u$ 的两个子节点. [^1]: $\min(∞,a)=a$,因此返回 $∞$ 相当于不参与最小值的比较. 时间复杂度为 $O(\log{n})$.执行 $get(1,l,r)$ 以进行区间查询. ``` cpp int get(int u, int l, int r) { if(l <= l(u) && r(u) <= r) return t(u); // 1. 被包含 if(l(u) > r || r(u) < l) return 0x3f3f3f; // 2. 不重叠 return min(get(2 * u, l, r), get(2 * u + 1, l, r)); // 3. 递归搜索 } ``` 区间修改 + 延迟标记 如果一次性将 $A[3\cdots 8]$ 每个数加上 $v$,需要更新大量节点,时间复杂度接近 $O(n\log{n})$.这不是我们希望看到的.事实上,大部分节点用不着马上更新——直到它们再次被访问.于是我们可以先给部分节点打标记. 在本例中,$t[3]$ 和 $t[5]$ 被打上了标记,这代表它们的所有子节点都还没加上 $v$.当访问 $A[5\cdots 6]$ 时,再更新 $t[3]$ 的左子树 .$t[3]$ 的标记被下传到了它的右节点 $t[7]$.代码与 [区间查询类似.时间复杂度为 $O(\log{n})$. ``` cpp int mark]; void spread(int u) { // 更新 u 的子节点,并下传标记 if(mark[u]) { t(2 * u) += mark[u]; t(2 * u + 1) += mark[u]; mark[2 * u] += mark[u]; mark[2 * u + 1] += mark[u]; mark[u] = 0; } } void add(int u, int l, int r, int v) { // 将 A[l...r] 每个数加上 v if(l <= l(u) && r(u) <= r) { // 完全覆盖 t(u) += v, mark[u] += v; return; // 标记 } else if(l(u) > r || r(u) < l) return; spread(u); // 下传标记 int m = (l + r) / 2; add(2 * u, l, r, v); add(2 * u + 1, l, r, v); t(u) = min(t(2 * u), t(2 * u + 1)); } ``` 同时,[单点修改和区间查询需要添加下传标记的操作. ``` cpp void set(int u, int id, int v) { if(l(u) == r(u)) { a[id] = t(u) = v; return; } spread(u); // 下传标记 int m = (l(u) + r(u)) / 2; if(id <= m) set(2 * u, id, v); else set(2 * u + 1, id, v); t(u) = min(t(2 * u), t(2 * u + 1)); } int get(int u, int l, int r) { if(l <= l(u) && r(u) <= r) return t(u); else if(l(u) > r || r(u) < l) return 0x3f3f3f; spread(u); // 下传标记 int m = (l + r) / 2; return min(get(2 * u, l, m), get(2 * u + 1, m + 1, r)); } ``` 模板 ``` cpp struct Node { int val, l, r; define t(u) t[u].val define l(u) t[u].l define r(u) t[u].r } t[]; int mark[]; void build(int u, int l, int r) { l(u) = l, r(u) = r; if(l == r) { t(u) = a[l]; return; } int m = (l + r) / 2; build(2 * u, l, m); build(2 * u + 1, m + 1, r); t(u) = min(t(2 * u), t(2 * u + 1)); } void spread(int u) { if(mark[u]) { t(2 * u) += mark[u]; t(2 * u + 1) += mark[u]; mark[2 * u] += mark[u]; mark[2 * u + 1] += mark[u]; mark[u] = 0; } } void set(int u, int id, int v) { if(l(u) == r(u)) { a[id] = t(u) = v; return; } spread(u); int m = (l(u) + r(u)) / 2; if(id <= m) set(2 * u, id, v); else set(2 * u + 1, id, v); t(u) = min(t(2 * u), t(2 * u + 1)); } int get(int u, int l, int r) { if(l <= l(u) && r(u) <= r) return t(u); else if(l(u) > r || r(u) < l) return 0x3f3f3f; spread(u); int m = (l + r) / 2; return min(get(2 * u, l, m), get(2 * u + 1, m + 1, r)); } void add(int u, int l, int r, int v) { if(l <= l(u) && r(u) <= r) { t(u) += v, mark[u] += v; return; } else if(l(u) > r || r(u) < l) return; spread(u); int m = (l + r) / 2; add(2 * u, l, r, v); add(2 * u + 1, l, r, v); t(u) = min(t(2 * u), t(2 * u + 1)); } ``` 区间和线段树 线段树还可以查询区间和. 令每个节点代表一段区间的元素和.递推方程应为 $t[u]=t[2u]+t[2u+1]$.若 $t[u]$ 表示区间 $[l,r]$,而 $A[l\cdots r]$ 每个数都要加上 $v$,则 $t[u]$ 需要加上 $(r-l+1)×v$.因此标记下传函数也需要调整. ``` cpp void spread(int u) { if(mark[u]) { t(2 * u) += mark[u] * (l(2 * u) - r(2 * u) + 1); t(2 * u + 1) += mark[u] * (l(2 * u + 1) - r(2 * u + 1) + 1); mark[2 * u] += mark[u]; mark[2 * u + 1] += mark[u]; mark[u] = 0; } } ``` 权值线段树 令每个节点代表一段区间内的元素个数.下图是基于数组 $A=\\{2,3,6\\}$ 构建的权值线段树.$t[2]$ 代表的区间为 $[1,4]$,$t[2]=2$ 说明数组 $A$ 中有 $2$ 个元素在 $[1,4]$ 区间中,它们分别是 $2$ 和 $3$.权值线段树可以用来做什么呢?— — 它可以求任一区间内第 $k$ 小的数.> 未完待续 ...
-
树状数组
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/
问题 数组 $A$ 中共 $n$ 个元素,对其反复进行以下操作共 $m$ 次: - **单点修改**:将 $A x]$ 加上 $k$. - **区间查询**:查询 $A[l\cdots r]$ 的和. ``` cpp int a[]; void set(int id, int v) { // 单点修改 a[id] = v; } int ask(int l, int r) { // 区间查询 int ans = 0; for(int i = l; i <= r; i ++) ans += a[i]; return ans; } ``` 构造 在原数组的上方构建树型结构,每个节点表示一段区间和:$C_1=A_1$; $C_2=A_1+A_2$; $C_3=A_3$; $C_4=A_1+A_2+A_3+A_4$; $\cdots \ \cdots$ 父节点 如何求 $C_i$ 的父节点? 将 $C$ 数组的下标转换成二进制数,观察该图.- $C[$000<font color="red">1</font>$]$ 的父节点为 $C[$00<font color="red">10</font>$]$. - $C[$0<font color="red">100</font>$]$ 的父节点为 $C[$<font color="red">1000</font>$]$. - $C[$010<font color="red">1</font>$]$ 的父节点为 $C[$01<font color="red">10</font>$]$. $\cdots \ \cdots$ 不难总结出规律: - $C_i$ 的父节点为 $C[i+$ [$lowbit(i)$$]$. 左邻节点 $C_i$ 的「左邻节点」与 $C_i$ 的左端相邻.例如 $C_5$ 的左邻节点为 $C_4$.观察同一张图:- $C$001<font color="red">1</font>$]$ 的左邻节点为 $C[$001<font color="red">0</font>$]$. - $C[$010<font color="red">1</font>$]$ 的左邻节点为 $C[$010<font color="red">0</font>$]$. - $C[$01<font color="red">10</font>$]$ 的左邻节点为 $C[$01<font color="red">00</font>$]$. 总结规律: - $C_i$ 的左邻节点为 $C[i-lowbit(i)]$. 单点修改 将 $A[x]$ 增加 $k$,$A[x]$ 的所有祖先都会跟着变动.以 $A_3$ 为例:$C_3=\textcolor{red}{A_3}$; $C_4=A_1+A_2+\textcolor{red}{A_3}+A_4$; $C_8=A_1+A_2+\textcolor{red}{A_3}+A_4+A_5+A_6+A_7+A_8$. 因此,修改 $A[3]$ 的同时,$C_3,C_4,C_8$ 也需要加上 $k$. 对于给定的 $x$,从 $C_x$ 开始逐层访问 [父节点,并给其值加上 $k$.时间复杂度为 $O(\log{n})$. ``` cpp int lowbit(int x) { return x & -x; } void add(int p, int k) { // 将 ap] 增加 k for(; p <= n; p += lowbit(p)) c[p] += k; } ``` 区间查询 采用 [前缀和思想.$sumx]$ 表示 $A[1]+A[2]+\cdots+A[x]$,则: $A[l\cdots r]$ 的和 $=sum[r]-sum[l-1]$ 自此,问题转换为求 $sum[x]$.以 $sum[7]$ 为例:$C_4=A_1+A_2+A_3+A_4$; $C_6=A_5+A_6$; $C_7=A_7$; 因而 $sum[7]=C_4+C_6+C_7$. 查询 $sum[x]$ 时,从 $C_x$ 开始依次遍历 [左邻节点.时间复杂度为 $O(\log{n})$. ``` cpp int ask(int p) { // 查询 A1...p] 的和 int sum = 0; for(; p; p -= lowbit(p)) sum += c[p]; return sum; } int get(int l, int r) { // 查询 A[l...r] 的和 return ask(r) - ask(l - 1); } ``` 模板 ``` cpp int lowbit(int x) { return x & -x; } void add(int p, int k) { for(; p <= n; p += lowbit(p)) c[p] += k; } int ask(int p) { int sum = 0; for(; p; p -= lowbit(p)) sum += c[p]; return sum; } int get(int l, int r) { return ask(r) - ask(l - 1); } ``` 拓展 区间修改 如果你已经学过 [差分,区间修改就容易的多. 1. 在数组 $A$ 的「差分数组」上建立树状数组. 2. 将 $A[l\cdots r]$ 所有元素都加上 $v$ 时,$f[l]$ 增加了 $v$,$f[r+1]$ 减少了 $v$. ``` cpp void seg_add(int l, int r, int v) { // 将 A[l...r] 加上 v add(l, v); add(r + 1, -v); } int main() { /* 输入部分省略 */ for(int i = 1; i <= n; i ++) { add(i, a[i] - a[i - 1]); } } ```
-
数位 DP
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/%E6%95%B0%E4%BD%8D-dp/
简介 如何统计区间 $l,r]$ 中有多少整数符合某条件? 1. 暴力算法,枚举 $[l,r]$ 中的每一个整数,逐个判断是否满足条件,此方法遇大数据必 $gg$. 2. 优雅地使用**数位 DP**. 问题 统计区间 $[l,r]$($0≤l<r≤100$)中有多少整数符合「相邻两个数字之差 $≥2$」. 预处理 采用「试填法」:从个位填到最高位,如果第 $d$ 位填了 $i$,那么第 $d+1$ 位只能填 $[0,i-2]$ 或 $[i+2,9]$ 中的整数. $f[i,d]$ 表示「所有最高位为 $i$ 的 $d$ 位数中,符合条件的个数」. 通过给定条件可推出: $$ f[i,d]=\sum_{|k-i|≥2} f[k,d-1] $$ ``` cpp int f[][]; for(int i = 0; i <= 9; i ++) f[i][1] = 1; // 初始条件 for(int d = 2; d <= N; d ++) // N : 位数的上限,N ≈ log(r) for(int i = 0; i <= 9; i ++) for(int k = 0; k <= 9; k ++) if(abs(k - i) >= 2) f[i][d] += f[k][d - 1]; ``` 数位统计 考虑 [前缀和思想: $dp(n)$ 表示 $[0,n]$ 中有多少个数满足条件. $[l,r]$ 中符合条件的个数 $=dp(r)-dp(l-1)$. $dp(n)$ 的实现步骤: step 1 提取 $n$ 每一位上的数字,存入数组 $at[ \ ]$: ``` cpp int cap = 0, at[]; // cap : n 的位数;at[i] : n 的第 i 位数字 while(n) at[++ cap] = n % 10, n /= 10; ``` step 2 所有 $1\cdots cap-1$ 位数都被包含于 $[0,n]$ 区间中. 统计它们中符合条件的个数: ``` cpp int ans = 0; // ans : 符合条件的个数 for(int d = 1; d < cap; d ++) // d : 位数 for(int i = 1; i <= 9; i ++) // i : 最高位填的数 ans += f[i][d]; ``` step 3 统计所有 $cap$ 位数中符合条件的个数. 使用「试填法」,枚举 $d=cap→1$,从最高位填到最低位,并使填的数 $<n$: - 若 $d=cap$,该位不能填 $0$,只能填 $1\cdots at[d]-1$. 统计符合条件的情况; - 若 $d\not=cap$,该位只能填 $0\cdots at[d]-1$. 统计符合条件的情况; - 若此时 $|at[d+1]-at[d]|<2$,下一位无论怎么填都不符合条件,跳出循环; - 若上一步未跳出循环且 $d=1$,说明 $n$ 本身也符合条件. 但「试填法」最多只填到 $n-1$,故还要多算一个. ``` cpp for(int d = cap; d >= 1; d --) { // d : 当前填到第 d 位 for(int i = (d == cap); i < at[d]; i ++) if(abs(at[d + 1] - i) >= 2) ans += f[i][d]; if(d != cap && abs(at[d + 1] - at[d]) < 2) break; if(d == 1) ans ++; } ``` 模板 ``` cpp int dp(int n) { // 求 [0, n] 中有几个数符合条件 if(n <= 0) return !n; // 特判 int cap = 0, ans = 0, at[]; while(n) at[++ cap] = n % 10, n /= 10; for(int d = 1; d < cap; d ++) for(int i = 1; i <= 9; i ++) ans += f[i][d]; for(int d = cap; d >= 1; d --) { for(int i = (d == cap); i < at[d]; i ++) if(abs(last - i) >= 2) ans += f[i][d]; // 条件按照题目的需要 if(d != cap && abs(at[d + 1] - at[d]) < 2) break; if(d == 1) ans ++; } return ++ ans; } ```
-
树形 DP
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/%E6%A0%91%E5%BD%A2-dp/
简介 **树形 DP** 以树形结构为研究对象. 通常设 $fu]$ 为树中 $u$ 号节点的值,利用树形关系推出其它节点的值. DP 过程多为 [记忆化搜索. 例 1 给定一棵 $n$ 个点,$m$ 条边的树,顶点编号为 $1\sim n$,且以 $1$ 号节点为根. 以 $i$ 号节点为根的子树有几个节点? $f[i]$:以 $i$ 号节点为根的子树的节点数. $Son[i]$:$i$ 号节点的子节点集合. ``` latex f[i]=1+\sum_{v\in Son[i]}f[v] ``` 计算顺序为 $f[$子节点$]→f[$父节点$]$. 使用记忆化搜索. ``` cpp vector<int> son[]; // son[u] : 节点 u 的子节点集合 void dfs(int u) { // 求以 u 为根的子树中节点个数 f[u] = 1; for (int i = 0; i < son[u].size(); i ++) { int v = son[u][i]; // 节点 u 的第 i 个子节点 dfs(v); f[u] += f[v]; } } ``` 例 2 公司有 $n$ 个人,编号为 $1\cdots n$,其中 $1$ 号员工是 boss. 现要举⾏⼀场晚会,如果邀请了某个⼈,那么他的上司不会来(他上司的上司,上司的上司的上司 $\cdots$ 都可以来). 每个⼈都有⼀个欢乐值,给出公司所有人的上下级关系,求⼀个邀请⽅案,使欢乐值的和最⼤. $f[i,j]$:从员工 $i$ 和所有下属中邀请部分人参会的最大欢乐值. 当 $j=0$ 时 $i$ 号员工不参会,$j=1$ 时参会. $Son[i]$:员工 $i$ 的下属集合. - 若 $i$ 号员工参会,他的直接下属都不来: ``` latex f[i,1]=H_i+\sum_{v\in Son(i)}f[v,0] ``` - 若 $i$ 号员工参会,他的直接下属爱来不来,于是取最大值: ``` latex f[i,0]=\sum_{v\in Son(i)}\max\{f[v,0],f[v,1]\} ``` 时间复杂度为 $O(n)$,最终答案为 $\max\\{f[1,0],f[1,1]\\}$. ``` cpp vector<int> son[]; // son[u] : 员工 u 的下属集合 void dfs(int u) { // 求出 u 号员工对应的 f[u][0] 和 f[u][1] f[u][1] = h[u]; for (int i = 0; i < son[u].size(); i ++) { int v = son[u][i]; // 员工 u 的第 i 个下属 dfs(v); f[u][1] += f[v][0]; f[u][0] += max(f[v][0], f[v][1]); } } ``` 树形 DP + 背包 DP 处理某些问题时,需要结合树形 DP 和背包 DP 的思想. 现有 $n$ 门课程,第 $i$ 门课程的学分为 $s_i$,每门课程有 $0$ 或 $1$ 门先修课.有先修课的课程需要先学完先修课,才能学习该课程.求学习 $m$ 门课程能获得的最多学分. 将每门课程看作树中的节点,$a→b$ 代表 $a$ 比 $b$ 先修: ``` mermaid flowchart 2-->1 2-->4 2-->7 7-->5 7-->6 3 ``` 为了方便解决问题,新增 $0$ 号节点,使其指向所有无先修课的课程: ``` mermaid flowchart 0-->2 0-->3 2-->1 2-->4 2-->7 7-->5 7-->6 3 ``` $f[u,j]$ 表示以 $u$ 为根节点,选 $j$ 个节点,获得的最大学分. $dfs(u)$ 的功能是算出以 $u$ 为根节点时,分别选 $0\sim m$ 个节点时能获得的最大学分. 执行 $dfs(u)$ 时,枚举 $u$ 的子节点 $v$,在内层循环枚举选取的节点数 $j=m→1$: - 将 $j$ 个节点分成两组,一组 $k$ 个,另一组 $j-k$ 个; - 将第一组 $k$ 个节点放在以 $v$ 为根节点的子树中,最大学分为 $f[v][k]$; - 将第二组 $j-k$ 个节点放在以 $u$ 为根节点的子树中,但不放在以 $v$ 为根节点的子树中,最大学分为 $f[u][j-k]$. $$f[u,j]=\max_{v\in Son(u)}\{f[u,j],f[u,j-k]+f[v,k]\}$$ 在主程序中执行 $dfs(0)$ 后输出 $f[0,m]$ 即可. ``` cpp void dfs(int u) { f[u][1] = s[u]; for(int i = 0; i < son[u].size(); i ++) { int v = son[u][i]; dfs(v); for(int j = m; j >= 1; j --) for(int k = j - 1; k > 0; k --) f[u][j] = max(f[u][j], f[v][k] + f[u][j - k]); } } ```
-
拓扑排序
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E5%9B%BE%E8%AE%BA/%E6%8B%93%E6%89%91%E6%8E%92%E5%BA%8F/
简介 给出 $n$ 个元素的 $m$ 组关系:$a > b,a > c,b > c,b > d,\cdots$,试将这 $n$ 个元素按大小排序. 将上述关系转化为有向图,$a → b$ 代表 $a > b$.这类反映节点大小关系的图称作 `AOV` 网.拓扑排序求的是符合条件的优先顺序,即拓扑序列. 原理 根据定义,没被箭头指着的节点(即入度为 $0$ 的节点)是当前最大的节点. === 1 节点 $a$ 的入度为 $0$.在拓扑序列中追加 $a$,并删除 $a$ 和它的所有邻边:拓扑序列:$a$ === 2 节点 $e$ 的入度为 $0$.在拓扑序列中追加 $e$,并删除 $e$ 和它的所有邻边:拓扑序列:$a,e$ === 3 节点 $b$ 的入度为 $0$.在拓扑序列中追加 $b$,并删除 $b$ 和它的所有邻边:拓扑序列:$a,e,b$ === 4 节点 $c$ 的入度为 $0$.在拓扑序列中追加 $c$,并删除 $c$ 和它的所有邻边:拓扑序列:$a,e,b,c$ === 5 节点 $d$ 的入度为 $0$.在拓扑序列中追加 $d$,并删除 $d$ 和它的所有邻边:拓扑序列:$a,e,b,c,d$ === 6 节点 $f$ 的入度为 $0$.在拓扑序列中追加 $f$,并删除 $f$ 和它的所有邻边:拓扑序列:$a,e,b,c,d,f$ 故样例的一种拓扑序列为 $a,e,b,c,d,f$. > 同一张 `AOV` 网可能有多个拓扑序列. $deg[u]$:节点 $u$ 的入度,需要预处理. 1. 定义一个队列,用于存放节点; 2. 将所有入度为 $0$ 的节点入队; 3. 取出队头的节点,并删除它的所有出边.若出现入度为零的节点,将其入队. 拓扑排序结束时,若拓扑序列未包含全部节点,则剩下的节点形成了闭环.此时不存在拓扑序列. 时间复杂度为 $O(n+m)$. ``` cpp const int N = 1e6; int n, deg[N]; vector<int> g[N]; // g[u]: u 的邻接点集合 vector<int> vec; // vec: 拓扑序列 void pre() { // 预处理所有节点的入度 memset(deg, 0, sizeof deg); for(int u = 1; u <= n; u ++) for(int i = 0; i < g[u].size(); i ++) deg[g[u][i]] ++; } void topSort() { pre(); vec.clear(); for(int i = 1; i <= n; i ++) if(!deg[i]) q.push(i); while(!q.empty()) { int u = q.front(); q.pop(); vec.push_back(u); for(int i = 0; i < g[u].size(); i ++) if(! -- deg[g[u][i]]) // 出现新的节点入度为 0 q.push(g[u][i]); } if(vec.size() != n) // 不存在拓扑序列 vec.clear(); } ```
-
最小生成树
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E5%9B%BE%E8%AE%BA/%E6%9C%80%E5%B0%8F%E7%94%9F%E6%88%90%E6%A0%91/
简介无向图$G$ 的生成树满足以下性质: - 包含 $G$ 中的所有节点. - 任意两个节点都连通. - 具有树的所有性质. 图 $b$ 和图 $c$ 皆为图 $a$ 的生成树.最小生成树,即边权和最小的生成树.对于 $n$ 个节点的无向图,最小生成树一定有 $n-1$ 条边. Kruskal 算法 `Kruskal` 是一种贪心算法. 1. 将 $m$ 条边按照边权升序排序; 2. 从小到大枚举边: - 若此边的两个顶点未连通,则在树中加入此边,并连通两个顶点. - 若此边的两个顶点已连通,直接跳到下一条边. 重复直到树中共加入 $n-1$ 条边. 使用并查集判断和维护两个顶点是否连通. 时间复杂度为 $O(m\log{m})$,适用于稀疏图. ``` cpp const int N = 1e6; int n, m, faN]; struct edge { int x, y, len; } g[N]; bool cmp(edge x, edge y) { return x.len < y.len; } int find(int x) { return !fa[x] ? x : fa[x] = find(fa[x]); } int kruskal() { sort(g + 1, g + m + 1, cmp); int tot = 0, sum = 0; for(int i = 1; i <= m; i ++) { if(tot == n - 1) break; int rx = find(g[i].x); int ry = find(g[i].y); if(rx != ry) { tot ++, fa[rx] = ry, sum += g[i].len; } } return tot == n - 1 ? sum : -1; //如果存在最小生成树,则返回边权和,否则返回 -1 } ``` Prim 算法 $dis[u]$ 表示节点 $u$ 到树的最短距离. 1. 建立一棵只有 $1$ 号节点的树,$dis[1]=0$; 2. 选择离树最近($dis$ 最小)的节点加入树,对该点的所有临边进行 [松弛操作.重复执行直到加入 $n-1$ 条边. 时间复杂度为 $O(n^2)$,适用于稠密图. ``` cpp int prim() { int sum = 0; memset(avl, true, sizeof avl); memset(dis, 0x7f, sizeof dis); dis[1] = 0; for(int i = 2; i <= n; i ++) { int minn = INF, minp = 0; for(int j = 2; j <= n; j ++) if(avl[j] && dis[j] < minn) minn = dis[j], minp = j; if(!minp) return -1; avl[minp] = false, sum += minn; for(int j = 1; j <= n; j ++) if(avl[j] && dis[j] > g[minp][j]) dis[j] = g[minp][j]; } return sum; } ```
-
图
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E5%9B%BE%E8%AE%BA/%E5%9B%BE/
定义 **图(Graph)** 由若干 **顶点** 和 **边** 组成,用大写字母 $G$ 表示,$V$ 为顶点集合,$E$ 为边集合,记作 $G=(V,E)$.图是描述实际问题的工具.如进行城市道规划时,可将每个城市视作顶点,连接城市的道路视作边. 边的方向 - 图的每条边都有起点和终点,则图为 **有向图**; - 相反,边没有方向(可以理解为双向)的图为 **无向图(双向图)**. 边权和点权 为解决实际问题,引入 **边权** 和 **点权** 的概念: - **边权** 及边的长度.解决最短路径问题时,将城市视作顶点,城市之间的道路长度视作边权; - **点权** 即点的大小.解决最小收费问题时,将收费站视作顶点,收费站之间的道路视作边,通过收费站支付的费用视作点权. 度数 若图中有 $d$ 条边与节点 $i$ 相连,则节点 $i$ 的 **度数** 为 $d$(即节点的 **连边** 个数).如下图,节点 $1$ 的度为 $6$:- 若有向图中有 $d$ 条边的 **终点** 是节点 $i$,则节点 $i$ 的 **入度** 为 $d$(即节点的 **入边** 个数); - 若有向图中有 $d$ 条边的 **起点** 是节点 $i$,则节点 $i$ 的 **出度** 为 $d$(即节点的 **出边** 个数); 子图 图 $G$ 的子图 $H$ 满足以下条件: - $G$ 中包含 $H$ 的所有节点和边; - $G$ 和 $H$ 同时为无向图或有向图. 即 $G=(V,E),H=(V',E'),V'\in V,E'\in E$.如下图,$H$ 是 $G$ 的子图: 路径和连通 从节点 $i$ 走到节点 $j$,经过的边的序列为 $i$ 到 $j$ 的 **路径**.路径的长度为经过边的边权和. 如下图,节点 $1$ 到 $6$ 的一条路径为 $1-4-5-6$.若两个节点之间存在路径,则称它们 **连通**. 回路(环) 若图中存在起点和终点相同的路径,则此路径称作 **回路(环)**. 完全图和连通图 - 若无向图 $G$ 的任意两个节点之间都有连边,则 $G$ 称为 **完全图**. $n$ 个节点的完全图有 $\frac{1}{2}n(n-1)$ 条边; - 若无向图 $G$ 的任意两个节点都连通,则 $G$ 称为 **连通图**. $n$ 个节点的连通图至少有 $n-1$ 条边. 强连通分量 - 若有向图 $G$ 的任意两个节点都连通,则 $G$ 称为 **强连通图**; - 若有向图 $G$ 的子图 $H$ 是强连通图,则 $H$ 称为 $G$ 的 **强连通分量**. 图的存储 直接存边 把每条边的起点、终点、长度存入数组中. === 结构体 ```cpp const int N = 1e6; struct node { int from, to, len; } edge[N]; ``` === 打散 ```cpp const int N = 1e6; int from[N], to[N], len[N]; ``` 邻接矩阵 用二维数组 $g$(邻接矩阵)存储边长,$g[i,j]$ 表示边 $i→j$ 的权值.缺点是不能存储重边、浪费空间. ``` cpp const int N = 1e6; int g[N][N]; ``` 邻接表 把节点 $i$ 的所有相邻节点插入以 $head[i]$ 开头的链表中.``` cpp int top, head[]; struct Node { int val, len, next; // 这里只需要用到单向链表 } edge[]; void insert(int u, int v, int len) { // 追加一条从 u 到 v,长度为 len 的边 edge[++ top] = Node{v, len, head[u]}; head[u] = top; } int path(int u, int v) { // 获取以 u, v 为端点的边的长度(没有边则返回 -1) for(int p = head[u]; p; p = edge[p].next) if(edge[p].val == v) return edge[p].len; return -1; } ```
-
树
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E5%9B%BE%E8%AE%BA/%E6%A0%91/
定义 树有多种等价的定义方式: - 连通且无环的无向图. - 有 $n$ 个节点和 $n-1$ 条边的无向图. - 任意两个顶点间只有一条路径的无向图. 图论中的树看起来更像现实中倒悬的树:树的节点存在「父子关系」: - 有连边的两个节点中,上节点为下节点的父节点.节点 $2$ 是节点 $5$ 的父节点; - 有连边的两个节点中,下节点为上节点的子节点,节点 $5$ 是节点 $2$ 的子节点; - 没有父节点的节点为根节点,节点 $1$; - 没有子节点的节点为叶节点,节点 $5,6,3,8,9$. 有根树和无根树 有根树必须明确根节点,而无根树的任意节点都可以是根节点.下面的左图和右图是同一棵无根树: 子树 将节点 $i$ 和其父节点断开,分裂出的以 $i$ 为根的新树,称作节点 $i$ 的子树.如下图,红色部分为节点 $3$ 的子树. 层和深度 定义根节点在第 $1$ 层,子节点层数 $=$ 父节点层数 $+ \ 1$:树的深度 $=$ 总层数.上图中树的深度为 $4$.树中各个节点的深度为节点所在的层数. 二叉树 任意节点的子节点数量 $≤2$ 的树是二叉树: 满二叉树 深度为 $k$ 的二叉树最多有 $2^k-1$ 个节点.节点最多的那棵树是满二叉树:满二叉树除最后一层外,其它层任意节点都有 $2$ 个子节点. 完全二叉树 将满二叉树最后一层右边连续的若干节点删除,得到完全二叉树:满二叉树是一类特殊的二叉树. 森林 多棵树组成的图为森林: 二叉树的遍历 对于二叉树,可以使用DFS 算法遍历所有节点.二叉树定义了 $3$ 种遍历方式,遍历顺序各不同.=== 前序遍历 1. 访问根节点 $u$; 2. 递归遍历 $u$ 的左子树; 3. 递归遍历 $u$ 的右子树. 上图的前序遍历顺序为 $1→2→4→5→3$. ``` cpp void dfs(int u) { // 遍历以 u 为根的树 if(!u) return; cout << u << ' '; dfs(l[u]); // l[u]: u 的左子节点 dfs(r[u]); // r[u]: u 的右子节点 } ``` === 中序遍历 1. 递归遍历 $u$ 的左子树; 2. 访问根节点 $u$; 3. 递归遍历 $u$ 的右子树. 上图的中序遍历顺序为 $4→2→5→1→3$. ``` cpp void dfs(int u) { if(!u) return; dfs(l[u]); cout << u << ' '; dfs(r[u]); } ``` === 后序遍历 1. 递归遍历 $u$ 的左子树; 2. 递归遍历 $u$ 的右子树; 3. 访问根节点 $u$. 上图的后序遍历顺序为 $4→5→2→3→1$. ``` cpp void dfs(int u) { if(!u) return; dfs(l[u]); dfs(r[u]); cout << u << ' '; } ``` 二叉树的恢复 给定一棵二叉树的前序和中序遍历序列,求后序遍历序列. 前序遍历:$DBACEGF$. 中序遍历:$ABCDEFG$. 后序遍历:$ACBFGED$. 阅读程序,得到各个遍历方式的规律: ::: - 前序遍历: 第一个元素是根节点. $$\overset{根节点}{ \ \ \ \ \ \ \overset{ ↓}{D}BA}CEGF \ \ \ \ \ \ $$ ::: - 中序遍历: 根节点左边的都在左子树,右边的都在右子树. $$\overset{根节点}{\underset{左子树}{\underbrace{ABC}}\overset{ ↓}{D}\underset{右子树}{\underbrace{EFG}}}$$ ::: - 后序遍历: 最后一个元素是根节点. $$ \ \ \ \ \ \ ACBF\overset{根节点}{GE\overset{ ↓}{D} \ \ \ \ \ \ }$$ 根据前序遍历可以确定 $D$ 是根节点.于是在中序遍历序列中找到 $D$. $$ABC\textcolor{red}{D}EFG$$ 处于 $D$ 左边的 $ABC$ 在 $D$ 的左子树上;$D$ 右边的 $EFG$ 在右子树上.比较整棵树的前序、中序遍历序列,还可以得出左右子树的前序、中序遍历序列: 前序遍历:$\textcolor{red}{D}\textcolor{green}{BAC}\textcolor{blue}{EGF}$. 中序遍历:$\textcolor{green}{ABC}\textcolor{red}{D}\textcolor{blue}{EFG}$. ::: - 左子树: - 前序遍历:$BAC$. - 中序遍历:$ABC$. ::: - 右子树: - 前序遍历:$EGF$. - 中序遍历:$EFG$. 对左右子树进行相同的操作,就能得出整棵树的结构.最后再后序遍历依次,输出序列. ``` cpp include <bits/stdc++.h> using namespace std; char l[255], r[255]; void build(string pre, string mid) { // pre: 前序序列 mid: 中序序列 if(!pre.size()) return; char root = pre[0]; // 前序遍历序列的第一个是根节点 int p = 0; while(mid[p] != root) p ++; // 在中序遍历中找到根节点位置 string l_pre = pre.substr(1, p); // 左子树 - 前序 string l_mid = mid.substr(0, p); // 左子树 - 中序 string r_pre = pre.substr(p + 1); // 右子树 - 前序 string r_mid = mid.substr(p + 1); // 右子树 - 中序 if(l_pre.size()) l[root] = l_pre[0]; if(r_pre.size()) r[root] = r_pre[0]; build(l_pre, l_mid); // 递归构建左子树 build(r_pre, r_mid); // 递归构建右子树 } void dfs(char u) { if(!u) return; dfs(l[u]); dfs(r[u]); cout << u << ' '; } int main() { build("DBACEGF", "ABCDEFG"); dfs('D'); // D 是根节点 return 0; } ```
-
链表
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E9%93%BE%E8%A1%A8/
简介 - 链表只能按顺序依次访问元素,而数组支持随机访问. - 链表支持在任意位置插入或删除元素,而数组不支持. 链表节点 用一个结构体表示链表的节点,其中可以存储任意数据.每个节点有 `prev` 和 `next` 两个指针,指向前后相邻的节点.``` cpp struct Node { int val; // 数据(可以是任意类型) Node *prev, *next; // 指针 }; ``` 初始化 初始化链表时,额外建立两个节点 `head` 和 `tail` 代表链表头尾,把实际节点存储在 `head` 与 `tail` 之间,简化链表边界的判断. ``` cpp Node *head, *tail; void init() { head = new Node(); tail = new Node(); head->next = tail; tail->prev = next; } ``` 插入/删除节点 如何在 1 和 2 之间插入 3 ?=== step 1=== step 2删除节点运用到类似的方法. ``` cpp void insert(Node *before, int val) { // 在节点 before 后面插入数据为 val 的新节点 q = new Node(); q->val = val; Node *after = before->next; after->prev = q, q->next = after; // Step 1 before->next = q, q->prev = before; // Step 2 } void remote(Node *p) { // 删除节点 p p->prev->next = p->next; p->next->prev = p->prev; delete p; } ``` 查找节点 ``` cpp Node* indexOf(int val) { // 返回链表中第一个出现的值为 val 的节点指针 for (Node *p = head->next; p != tail; p = p->next) if (p->val == val) return p; return NULL; } ``` 清空链表 ``` cpp void clear() { while(head != tail) { head = head->next; delete head->prev; } delete tail; } ``` 用数组模拟链表 使用指针动态分配空间,效率较低且不稳定.一般使用数组模拟链表. ``` cpp struct Node { int val; int prev, next; } node[]; int head, tail, tot; void init() { head = 0, tail = 1, tot = 2; node[head].next = tail; node[tail].prev = head; } void insert(int p, int val) { int q = ++ tot; node[q].val = val; node[node[p].next].prev = q; node[q].next = node[p].next; node[p].next = q, node[q].next = p; } void remove(int p) { node[node[p].prev].next = node[p].next; node[node[p].next].prev = node[p].prev; } int indexOf(int val) { for (int p = node[head].next; p != tail; p = node[p].next) if (node[p].val == val) return p; return -1; } void clear() { head = tail = tot = 0; memset(node, 0, sizeof node); } ```
-
栈
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E6%A0%88/
简介 栈是一种「先进后出」的数据结构.类似于在桶中堆积物品,取物品时只能从顶端开始取,最先进桶的被压在最底下,最后被取出来.基本操作见 `STL Stack`. 表达式计算 算术表达式分为三类($op$ 为运算符,$A,B$ 为数字或表达式): 1. **中缀表达式**:全国人民都在用的表达式,如「$5×(3+2)$」; 2. **前缀表达式**:形如「$op \ \textcolor{red}{A} \ \textcolor{blue}{B}$」,如「$× \ \textcolor{red}{5} \ \textcolor{blue}{+} \ \textcolor{blue}{3} \ \textcolor{blue}{2}$」; 3. **后缀表达式**:形如「$\textcolor{red}{A} \ \textcolor{blue}{B} \ op$」,如「$\textcolor{red}{3} \ \textcolor{red}{2} \ \textcolor{red}{+} \ \textcolor{blue}{5} \ ×$」. 计算前/后缀表达式时,先递归求出 $A,B$ 的值,二者再做 $op$ 运算.计算方案是唯一确定的,且不需要使用括号.计算后缀表达式的算法最容易设计. 后缀表达式 1. 定义一个栈,用于存放数; 2. 逐一扫描后缀表达式中的元素: - 若扫到一个数 $n$,则把 $n$ 入栈; - 若扫到运算符 $op$ ,则弹出栈顶的两个元素,二者做 $op$ 计算.将计算结果入栈. 最终的栈顶元素就是计算结果.时间复杂度为 $O(n)$. ``` cpp bool isdigit(char ch) { // 判断是否为数字 return ch >= '0' && ch <= '9'; } bool isop(char ch) { // 判断是否为运算符 return ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '^'; } double postfix_calc(string str) { stack<double> s; int i = 0; s = stack<double>(); while(i < str.size()) { if(isdigit(stri])) { // 假定输入数据只包含整数 double x = 0; while(isdigit(str[i])) x = x * 10 + str[i ++] - '0'; s.push(x); continue; } else if(isop(str[i])) { double r = s.top(); s.pop(); double l = s.top(); s.pop(); double ans; switch(str[i]) { case '+' : ans = l + r; break; case '-' : ans = l - r; break; case '*' : ans = l * r; break; case '/' : ans = l / r; break; case '^' : ans = pow(l, r); break; } s.push(ans); } i ++; } return s.top(); } ``` 中缀表达式 先将中缀表达式转换成 [后缀表达式再计算. 1. 定义一个栈,用于存放运算符; 2. 逐一扫描中缀表达式中的元素: - 若扫到一个数 $n$,直接输出 $n$; - 若扫到「左括号」,把左括号入栈; - 若扫到「右括号」,重复取栈顶并输出,直到栈顶为左括号,再出栈左括号; - 若扫到其它运算符 $op$,重复取栈顶并输出,直到栈顶的**优先级**大于 $op$,再把 $op$ 入栈. 运算符优先级越大,越晚出栈,因此可以将「左括号」的优先级视作最低,「右括号」的优先级视作最高. 时间复杂度为 $O(n)$. ``` cpp int lev(char ch) { // 返回运算符对应的优先级 int level; switch(ch) { case '(' : level = 0; break; case ')' : level = 4; break; case '+' : level = 1; break; case '-' : level = 1; break; case '*' : level = 2; break; case '/' : level = 2; break; case '^' : level = 3; break; } return level; } double infix_calc(string str) { string data; stack<char> op; int i = 0; while(i < str.size()) { if(isdigit(str[i])) { while(isdigit(str[i])) data += str[i ++]; data += ' '; // 数与数之间用空格区分 continue; } else if(isop(str[i])) { while(!op.empty() && lev(op.top()) <= lev(str[i])) { if(op.top() != '(' && op.top() != ')') data += op.top(); op.pop(); } op.push(str[i]); } i ++; } while(!op.empty()) { // 输出栈内剩余元素 if(op.top() != '(' && op.top() != ')') data += op.top(); op.pop(); } return postfix_calc(data); } ``` 单调栈 单调栈中的元素从栈底到栈顶满足单调性. 插入元素 将元素 $x$ 入栈,同时维护栈的单调性.以单调递增栈为例: - 从栈顶依次弹掉比 $x$ 大的元素,保证 $x≥$ 栈顶; - 将 $x$ 入栈. ``` cpp stack<int> s; void insert(int x) while(!s.empty() && s.top() > x) s.pop(); s.push(x); } ``` 应用 单调递增栈可以实现快速查询「左边第一个更小的元素」.例如 $A=\\{1,2,5,4,3,6\\}$,$A_5=3$ 的左边第一个比它小的元素是 $A_2=2$. 顺序扫描 $A$,将 $A_i$ 插入单调栈前,栈中比 $A_i$ 大的元素都被弹掉了,栈顶元素即为左边第一个比 $A_i$ 小的元素. ``` cpp stack<int> s; for(int i = 1; i <= n; i ++) { while(!s.empty() && s.top() > a[i]) s.pop(); if(!s.empty()) cout << s.top() << endl; // 输出 a[i] 左边第一个更小的元素 else cout << "none" << endl; // 如果没有则输出 none s.push(a[i]); } ``` !!! info - 求左边第一个更小的元素:顺序扫描 + 单调递增栈. - 求左边第一个更大的元素:顺序扫描 + 单调递减栈. - 求右边第一个更小的元素:倒序扫描 + 单调递增栈. - 求右边第一个更大的元素:倒序扫描 + 单调递减栈.
-
队列
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E9%98%9F%E5%88%97/
简介 队列是一种「先进先出」的数据结构.元素从队列的前端进入(入队),从末端离开(出队),类似于排队.基本操作见 `STL Queue`. 双向队列 队列元素只能从一端进,另一端出,有时无法满足问题的需要.双向队列应运而生,它支持从两端插入或删除元素. 双向队列的基本操作见 `STL Deque`. 单调队列 单调队列的元素从队头到队尾满足单调性,适用于查询某一动态区间的最大(或最小)元素. 插入元素 将 $A[i]$ 入队,维护队列单调性,同时保证队列元素在 $A[p\cdots i]$ 范围内.以单调递增队列为例: - 重复弹出队头,直到队头 $≥p$; - 重复弹出队尾,直到 $A[$队尾$]<A[i]$(若单调递减,则重复直到 $A[$队尾$]>A[i]$). - 将 $i$ 入队. 涉及双端操作,须使用双向队列.此时 $A[p\cdots i]$ 范围内最小元素为 $A[$队头$]$. ``` cpp deque<int> q; // 存储元素下标 void insert(int i, int p) { // 将 a[i] 入队,维护队列元素在 a[p...i] 范围内 while(!q.empty() && q.front() < p) q.pop_front(); while(!q.empty() && a[q.back()] >= a[i]) q.pop_back(); q.push_back(i); } ``` 滑动窗口 一个滑动窗口(长度为 $k$)从数组 $A$ (长度为 $n$)的左端移动到右端,每次只向右移一位.求每次滑动时窗口区中的最大值. 示例($k=3,n=8$,红色数值在窗口区内): 朴素算法 枚举 $i=k\rightarrow n$,枚举出区间 $[i-k+1,i]$ 中的最大整数.时间复杂度为 $O(nk)$. ``` cpp for(int i = k; i <= n; i ++) { int maxn = a[i]; for(int j = i - k + 1; j <= i; j ++) maxn = max(maxn, a[j]); cout << maxn << ' '; } ``` 单调队列优化 使用单调递减队列优化「查找区间 $[i-k+1,i]$ 中的最大整数」的效率.时间复杂度为 $O(n)$. ``` cpp deque<int> q; void insert(int i, int p) { while(!q.empty() && q.front() < p) q.pop_front(); while(!q.empty() && a[q.back()] <= a[i]) q.pop_back(); q.push_back(i); } for(int i = 1; i <= n; i ++) { insert(i, i - k + 1); if(i >= k) cout << a[q.front()] << ' '; } ```
-
状压 DP
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/%E7%8A%B6%E5%8E%8B-dp/
简介 在程序中,我们如何保存一面棋盘? <p>$$ \def\arraystretch{2} \begin{array}{|c|c|c|c|}\hline \ & \Large♕ & \ & \Large♕ \\ \hline \Large♕ & \ & \Large♕ & \Large♕ \\ \hline \ & \ & \Large♕ & \ \\ \hline \Large♕ & \Large♕ & \ & \Large♕ \\ \hline \end{array} $$</p> 用 $bool$ 数组 $A \ ][ \ ]$.$A[i][j]=1$ 表示第 $i$ 行第 $j$ 列有一枚棋子. ``` cpp bool A[][] = { {0, 1, 0, 1}, {1, 0, 1, 1}, {0, 0, 1, 0}, {1, 1, 0, 1} }; ``` 现在我摆出了棋盘的第 $1$ 行和第 $4$ 行,并规定任意两个棋子不能相邻,则中间两行一共有多少种可行的摆法? <p>$$ \def\arraystretch{2} \begin{array}{|c|c|c|c|}\hline \Large♕ & \ & \Large♕ & \ \\ \hline \ & \ & \ & \ \\ \hline \ & \ & \ & \ \\ \hline \ & \Large♕ & \ & \Large♕ \\ \hline \end{array} $$</p> 设 $f[\square\\!\square\\!\square\\!\square][i]$ 表示从第 $1$ 行摆到第 $i$ 行,且第 $i$ 行摆放 $\square\\!\square\\!\square\\!\square\\!$ 的可行方案总数. 由于第一行只能为 $\blacksquare\\!\square\\!\blacksquare\\!\square$,因此 $f[\blacksquare\\!\square\\!\blacksquare\\!\square][1]=1$($\square$ 表示这一格没有棋子,$\blacksquare$ 表示有棋子).现在的目标是求 $f[\square\\!\blacksquare\\!\square\\!\blacksquare][4]$. 因为黑棋和白棋不相邻,而第 $4$ 行已经给出,所以第 $3$ 行有 $4$ 种摆法(全空着也算一种摆法): <p>$$1. \def\arraystretch{2} \begin{array}{|c|c|c|c|}\hline \Large♕ & \color{white}{\Large ♕} & \Large♕ & \color{white}{\Large ♕} \\ \hline \end{array} $$</p> <---> <p>$$2. \def\arraystretch{2} \begin{array}{|c|c|c|c|}\hline \Large♕ & \color{white}{\Large ♕} & \color{white}{\Large ♕} & \color{white}{\Large ♕} \\ \hline \end{array} $$</p> <p>$$3. \def\arraystretch{2} \begin{array}{|c|c|c|c|}\hline \color{white}{\Large ♕} & \color{white}{\Large ♕} & \Large♕ & \color{white}{\Large ♕} \\ \hline \end{array} $$</p> <---> <p>$$4. \def\arraystretch{2} \begin{array}{|c|c|c|c|}\hline \color{white}{\Large ♕} & \color{white}{\Large ♕} & \color{white}{\Large ♕} & \color{white}{\Large ♕} \\ \hline \end{array} $$</p> 于是可以列出递推方程: <p>$$ f[\square\!\blacksquare\!\square\!\blacksquare][4]=f[\blacksquare\!\square\!\blacksquare\!\square][3]+f[\blacksquare\!\square\!\square\!\square][3]+f[\square\!\square\!\blacksquare\!\square][3]+f[\square\!\square\!\square\!\square][3] $$</p> 但这么做未免也太滑稽了,而且 $C++$ 并不支持特殊符号.那怎么办呢? 还记得我们如何 [在程序中保存棋盘吗?对,用 $bool$ 数组.例如 $\square\\!\blacksquare\\!\square\\!\blacksquare$ 可以用 $bool$ 数组表示为 $\\{0, 1, 0, 1\\}$,然后再用状态压缩算法将它压缩成二进制数 $(0101)_2$.一顿操作之后,我们的方程就变为: <p>$$ f[(0101)_2][4]=f[(1010)_2][3]+f[(1000)_2][3]+f[(0010)_2][3]+f[(0000)_2][3] $$</p> 看上去比之前像话多了. 将一组状态压缩成一个二进制数,塞进 $f[ \ ]$ 的中括号里,再进行递推,这就是「状压 $DP$」. 问题 在 $n×n$ 的棋盘上放 $m$ 个国王,国王可攻击相邻的 $8$ 个格子.求使他们无法互相攻击的方案总数. 预处理 按照状压 $DP$ 的套路,这道题大抵需要我们用二进制数表示某一行的状态. <p>$$ \def\arraystretch{2} \begin{array}{|c|c|c|c|}\hline \color{white}{\Large ♕} & \Large♕ & \color{white}{\Large ♕} & \Large♕ \\ \hline \end{array} →\{0,1,0,1\}→(0101)_2 $$</p> 为了后续的需要,我们需要进行预处理: 1. 筛选出所有符合题目条件的状态. 2. 保存每种合法状态对应的国王个数. 由于每行有 $n$ 个格子,因此表示状态的二进制数最大为 $2^n-1$. ``` cpp int n, tot; // tot: 合法的状态个数 int state[]; // s[i]: 第 i 种合法的状态 int nums[]; // nums[i]: 第 i 种状态的国王数 void pre() { for(int i = 0; i < (1 << n); i ++) { // 枚举 i 为每一种状态 if(i & (i << 1)) continue; // 如果不为 0,那么必定有两个国王是挨着的,不合法 int cnt = 0; // 记录状态 i 中的国王数 for(int j = i; j; j >>= 1) if(j & 1) cnt ++; state[++ tot] = i; nums[tot] = cnt; } } ``` 原理 $f[i][s][k]$:棋盘的前 $i$ 行已经摆好,第 $i$ 行摆的状态是 $s$,并且一共摆了 $k$ 个国王时有几种合法的方案. ``` cpp int f[][][], ans; bool avl(int a, int b) { // 若上一行的状态是 a,判断下一行的状态能否是 b return (!(a & b)) && (!((a << 1) & b)) && (!((a >> 1) & b)); } void dp() { f[0][1][0] = 1; for(int i = 1; i <= n; i ++) // 放前 i 行 for(int s = 1; s <= tot; s ++) // 第 i 行的状态是 s for(int k = nums[s]; k <= m; k ++) // 一共摆了 k 个国王 for(int t = 1; t <= tot; t ++) // 第 i - 1 行的状态是 t if(avl(state[s], state[t])) f[i][s][k] += f[i - 1][t][k - nums[s]]; for(int i = 1; i <= tot; i ++) ans += f[n][i][m]; cout << ans; } ```
-
状态压缩
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E7%8A%B6%E6%80%81%E5%8E%8B%E7%BC%A9/
用二进制数存储 bool 数组. 简介 由于 `bool` 变量只有 $0$ 和 $1$ 两种值,二进制位也具备此特征,故每个 `bool` 数组都可用一个二进制数表示. 将 `bool` 数组 $a$(长为 $n$)用 $n$ 位二进制数表示,它的第 $i$ 位表示 $a[i]$. 状态压缩的相关操作方法: lowbit 运算 $lowbit(n)$:$n$ 在二进制下「最低位的 $1$ 和其后所有的 $0$」构成的数. 例:$n=(101000)_2$,$lowbit(n)=(1000)_2$. $n_i$:$n$ 在二进制下的第 $i$ 位数字. 设 $n_k=1$,$n_0\cdots n_{k-1}=0$. 1. 将 $n$ 的每一位取反,此时 $n_k=0$,$n_0\cdots n_{k-1}=1$,其余位和原来相反. 2. 再令 $n=n+1$,此时 $n_k=1$,$n_0\cdots n_{k-1}=0$,其余位仍和原来相反. 3. 因此 $n\\&(\sim n+1)$ 仅有第 $k$ 位为 $1$. 由于在补码表示下 $-n=\sim n+1$,故 $$lowbit(n)=n\\&(\sim n+1)=n\\&-n$$ ``` cpp int lowbit(int x) { return x & -x; } ```
-
区间 DP
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/%E5%8C%BA%E9%97%B4-dp/
简介 **区间 DP** 以区间为研究对象. 通常设 $fl,r]$ 为区间 $[l,r]$ 的值,用 $f[小区间]$ 的值推出 $f[大区间]$ 的值. 问题 $n$ 堆石子排成一列,第 $i$ 堆石子重量为 $A_i$. 每次合并相邻两堆石子,消耗的体力值为其重量和. 求将所有石子合并为一堆,最少消耗多少体力. 原理 $f[l,r]$:合并第 $l$ 堆至第 $r$ 堆石子的最少体力值. 合并第 $l\sim r$ 堆石子可分为三步(设 $k$ 为 $[l, r)$ 中的某个数): - 合并第 $l\sim k$ 堆石子,消耗体力值 $f[l,k]$. - 合并第 $k+1\sim r$ 堆石子,消耗体力值为$f[k+1,r]$. - 合并剩下两堆石子,消耗体力值 $\displaystyle\sum_{i=l}^r A_i$. 枚举 $k$,找出最小的体力值. ``` latex f[l,r]=\min_{l≤k< r}\{f[l,k]+f[k+1,r]\}+\sum_{i=l}^r A_i ``` 其中 $\displaystyle\sum_{i=l}^r A_i$ 可以用 [前缀和优化. 计算顺序为 $f[小区间]\rightarrow f[大区间]$,故枚举区间长度的循环在最外层. 时间复杂度为 $O(n^3)$ ``` cpp memset(f, 0x7f, sizeof f); for (int i = 1; i <= n; i ++) { f[i][i] = 0; sum[i] = sum[i - 1] + a[i]; } for (int len = 2; len <= n; len ++) { // 区间长度 for (int l = 1; l + len - 1 <= n; l ++) { // 枚举区间左端点 int r = l + len - 1; // 区间右端点 for (int k = l; k < r; k ++) f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r]); f[l][r] += sum[r] - sum[l - 1]; } } ```
-
记忆化搜索
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/%E8%AE%B0%E5%BF%86%E5%8C%96%E6%90%9C%E7%B4%A2/
求 Fibonacci 第 $i$ 项的深搜程序如下: ``` cpp int f(int x) { if (x <= 2) return 1; return f(x - 1) + f(x - 2); } ``` 该程序直观,但运行效率低. 以 $f(7)$ 为例,列出函数调用情况:随着 $n$ 的增大,$f(n)$ 的时间复杂度呈指数级增长. 我们发现,有很多函数被重复调用. 使用「记忆化搜索」可避免此情况. 建立数组 $F$ 保存计算结果. - 若 $f(x)$ 未被调用过,算出 $f(x)$ 的值,并存入 $F[ x]$ - 若 $f(x)$ 已被调用过,直接返回 $F[ x]$ ``` cpp int F[]; F[1] = F[2] = 1; int f(int x) { if (F[x] != 0) // 若 F[x] 有值,则说明 f(x) 被调用过 return F[x]; return F[x] = f(x - 1) + f(x - 2); // 返回时保存 } ```记忆化搜索的时间复杂度与动态规划相当,但效率略低. 若动态规划难以设计,则可以采用该算法.
-
高精度
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E9%AB%98%E7%B2%BE%E5%BA%A6/
支持高位数的运算系统. 简介 高精度是支持高位数的运算系统. 本章仅介绍最常用的正整数运算系统. 构造 在 `vector<int>` 容器内保存每位数字,并实现自动处理进位. > `vector<int>` 的位数要从 $0$ 记起(第 $0$ 位,第 $1$ 位,$\cdots$). ``` cpp struct Wint:vector<int> { // 以 vector 为基类 Wint(int n = 0) { // 初始化为 0 push_back(n); upgrade(); } Wint& upgrade() { // 处理进位 while (!empty() && !back()) pop_back(); // 去除最高位多余的 0 if (empty()) return *this; for (int i = 1; i < size(); i ++) { // 满 10 进 1 (*this)[i] += (*this)[i - 1] / 10; (*this)[i - 1] %= 10; } while (back() >= 10) { // 最高位 >= 10 时,新增一位 push_back(back() / 10); (*this)[size() - 2] %= 10; } return *this; } }; ``` 输入 取出字符串的每一位,倒序存入数组. ``` cpp Wint init(string s) { Wint n; for (int i = s.size() - 1; i >= 0; i --) n.push_back(s[i] - '0'); return n; } ``` 输出 数据是倒序储存的,故倒序输出. ``` cpp void print(Wint n) { if (n.empty()) printf("0"); for (int i = n.size() - 1; i >= 0; i --) printf("%d", n[i]); } ``` 比较 先比较位数. 若位数相同,则从高位到低位逐位比较. ``` cpp bool operator != (const Wint &a, const Wint &b) { if (a.size() != b.size()) return true; for (int i = a.size() - 1; i >= 0; i --) if (a[i] != b[i]) return true; return false; } bool operator == (const Wint &a, const Wint &b) { return !(a != b); } bool operator < (const Wint &a, const Wint &b) { if (a.size() != b.size()) return a.size() < b.size(); for (int i = a.size() - 1; i >= 0; i --) if (a[i] != b[i]) return a[i] < b[i]; return false; } bool operator > (const Wint &a, const Wint &b) { return b < a; } bool operator <= (const Wint &a, const Wint &b) { return !(a > b); } bool operator >= (const Wint &a, const Wint &b) { return !(a < b); } ``` 加法 从低位到高位,将两数对应位置相加. 先实现 `+=` 方法,节省传参时间. ``` cpp Wint& operator += (Wint &a, const Wint &b) { if (a.size() < b.size()) a.resize(b.size()); for (int i = 0; i < b.size(); i ++) a[i] += b[i]; return a.upgrade(); } Wint operator + (Wint a, const Wint &b) { return a += b; } ``` 减法 返回差的绝对值. 从低位到高位,将两数对应位置相减. 不够减要借位. ``` cpp Wint& operator -= (Wint &a, Wint b) { if (a < b) swap(a,b); for (int i = 0; i < b.size(); i ++) { if (a[i] < b[i]) { // 需要借位 a[i + 1] --; a[i] += 10; } a[i] -= b[i]; } return a.upgrade(); } Wint operator - (Wint a, const Wint &b) { return a -= b; } ``` 乘法 将 $a$ 的第 $i$ 位乘 $b$ 的第 $j$ 位累加在答案的第 $i+j$ 位上. ``` cpp Wint operator * (const Wint &a, const Wint &b) { Wint n; n.assign(a.size( ) + b.size() - 1, 0); for (int i = 0; i < a.size(); i ++) for (int j = 0; j < b.size(); j ++) n[i + j] += a[i] * b[j]; return n.upgrade(); } Wint& operator *= (Wint &a, const Wint &b) { return a = a * b; } ``` 除法 先实现带余除法函数,再重载符号方法. 将 $b$ 和 $a$ 最高位对齐,重复相减,统计减的次数. ``` cpp Wint divmod(Wint &a, const Wint &b) { Wint ans; for (int t = a.size() - b.size(); a >= b; t --) { Wint d; d.assign(t + 1, 0); d.back() = 1; Wint c = b * d; while (a >= c) { a -= c; ans += d; } } return ans; } Wint operator / (Wint a, const Wint &b) { return divmod(a, b); } Wint& operator /= (Wint &a, const Wint &b) { return a = a / b; } Wint& operator %= (Wint &a, const Wint &b) { divmod(a, b); return a; } Wint operator % (Wint a, const Wint &b) { return a %= b; } ```
-
背包问题
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/%E8%83%8C%E5%8C%85-dp/
01 背包 > 给定 $n$ 个物品,第 $i$ 个物品价值为 $ci]$, 体积为 $w[i]$. 现有容积为 $m$ 的背包,求将物品装入背包得到的最大价值. $f[i,v]$: 从前 $i$ 个物品中,选出总体积为 $v$ 的物品,能得到的最大价值. - 不选第 $i$ 个物品:$f[i,v]=f[i-1,v]$. - 选第 $i$ 个物品:$f[i,v]=f[i-1,v-w_i]+c_i$. ``` latex f[i,v] = \max\left\{\begin{aligned} &f[i-1,v]\\ &f[i-1,v-w_i]+c_i\quad(v≥w_i)\\ \end{aligned}\right. ``` 时间复杂度:$O(nm)$. ```cpp for (int i = 1; i <= n; i ++) { for (int v = 0; v <= m; v ++) { f[i][v] = f[i - 1][v]; if (v >= w[i]) f[i][v] = max(f[i][v], f[i - 1][v - w[i]] + c[i]); } } // 问题的解是 f[n][m] ``` 空间优化 实际上,状态转移方程的第一维可以去掉,即让新状态覆盖旧状态,降低空间开销. ``` latex f[v] = \max\left\{\begin{aligned} &f[v]\\ &f[v-w_i]+c_i\quad(v≥w_i) \end{aligned}\right. ``` 但在程序中,直接移除第一维会导致错误. 令 $w=1,c=2$,执行内层循环,可见错误的根源. === $v=1$ |$v$|$0$|$1$|$2$|$3$|$4$| |:--:|:--:|:--:|:--:|:--:|:--:| |$f[v]$|$\textcolor{blue}{0}$|$\textcolor{blue}{f[v-w]}+c=2$|$0$|$0$|$0$| === $v=2$ |$v$|$0$|$1$|$2$|$3$|$4$| |:--:|:--:|:--:|:--:|:--:|:--:| |$f[v]$|$0$|$\textcolor{blue}{2}$|$\textcolor{blue}{f[v-w]}+c=4$|$0$|$0$| === $v=3$ |$v$|$0$|$1$|$2$|$3$|$4$| |:--:|:--:|:--:|:--:|:--:|:--:| |$f[v]$|$0$|$2$|$\textcolor{blue}{4}$|$\textcolor{blue}{f[v-w]}+c=6$|$0$| === $v=4$ |$v$|$0$|$1$|$2$|$3$|$4$| |:--:|:--:|:--:|:--:|:--:|:--:| |$f[v]$|$0$|$2$|$4$|$\textcolor{blue}{6}$|$\textcolor{blue}{f[v-w]}+c=8$| 在内层循环中,$f[v-w]$ 不能比 $f[v]$ 先更新,否则相当于同一物品被装多次. 倒序枚举 $v$ 以解决问题. === $v=4$ |$v$|$0$|$1$|$2$|$3$|$4$| |:--:|:--:|:--:|:--:|:--:|:--:| |$f[v]$|$0$|$0$|$0$|$\textcolor{blue}{0}$|$\textcolor{blue}{f[v-w]}+c=2$| === $v=3$ |$v$|$0$|$1$|$2$|$3$|$4$| |:--:|:--:|:--:|:--:|:--:|:--:| |$f[v]$|$0$|$0$|$\textcolor{blue}{0}$|$\textcolor{blue}{f[v-w]}+c=2$|$2$| === $v=2$ |$v$|$0$|$1$|$2$|$3$|$4$| |:--:|:--:|:--:|:--:|:--:|:--:| |$f[v]$|$0$|$\textcolor{blue}{0}$|$\textcolor{blue}{f[v-w]}+c=2$|$2$|$2$| === $v=1$ |$v$|$0$|$1$|$2$|$3$|$4$| |:--:|:--:|:--:|:--:|:--:|:--:| |$f[v]$|$\textcolor{blue}{0}$|$\textcolor{blue}{f[v-w]}+c=2$|$2$|$2$|$2$| ```cpp for (int i = 1; i <= n; i ++) for (int v = m; v >= w[i]; v --) // 倒序枚举 f[v] = max(f[v], f[v - w[i]] + c[i]); ``` 完全背包 > 基于 [01 背包,每种物品可装无限次. $fi,v]$: 从前 $i$ 种物品中,选出总体积为 $v$ 的物品,能得到的最大价值. - 不选第 $i$ 种物品:$f[i,v]=f[i-1,v]$. - 多选一个第 $i$ 种物品:$f[i,v]=f[i,v-w_i]+c_i$. ``` latex f[i,v] = \max\left\{\begin{aligned} &f[i-1,v]\\ &f[i,v-w_i]+c_i\quad(v≥w_i) \end{aligned}\right. ``` 时间复杂度:$O(nm)$. ``` cpp for (int i = 1; i <= n; i ++) { for (int v = 0; v < m; v ++) { f[i][v] = f[i - 1][v]; if (v >= w[i]) f[i][v] = max(f[i][v], f[i][v - w[i]] + c[i]); } } ``` 空间优化 参照 [01 背包 - 空间优化中的错误做法:直接移除第一维,并升序枚举 $v$,相当于将同种物品装多次. === $v=1$ |$v$|$0$|$1$|$2$|$3$|$4$| |:--:|:--:|:--:|:--:|:--:|:--:| |$fv]$|$\textcolor{blue}{0}$|$\textcolor{blue}{f[v-w]}+c=2$|$0$|$0$|$0$| === $v=2$ |$v$|$0$|$1$|$2$|$3$|$4$| |:--:|:--:|:--:|:--:|:--:|:--:| |$f[v]$|$0$|$\textcolor{blue}{2}$|$\textcolor{blue}{f[v-w]}+c=4$|$0$|$0$| === $v=3$ |$v$|$0$|$1$|$2$|$3$|$4$| |:--:|:--:|:--:|:--:|:--:|:--:| |$f[v]$|$0$|$2$|$\textcolor{blue}{4}$|$\textcolor{blue}{f[v-w]}+c=6$|$0$| === $v=4$ |$v$|$0$|$1$|$2$|$3$|$4$| |:--:|:--:|:--:|:--:|:--:|:--:| |$f[v]$|$0$|$2$|$4$|$\textcolor{blue}{6}$|$\textcolor{blue}{f[v-w]}+c=8$| ```cpp for (int i = 1; i <= n; i ++) for (int v = w[i]; v <= m; v ++) f[v] = max(f[v], f[v - w[i]] + c[i]); ``` 多重背包 > 基于 [01 背包,第 $i$ 种物品可装 $s_i$ 次. 在内层循环中,分别绑定 $1,2,\cdots s_i$ 个物品作为一个物品,参与 01 背包. ``` latex fv] = \max_{k=1,2,\cdots,s_i}\begin{cases} f[v]\\ f[v-k\cdot w_i]+k\cdot c_i\quad(v≥k\cdot w_i) \end{cases} ``` 时间复杂度:$O(m\sum s_i)$. ```cpp for (int i = 1; i <= n; i ++) for (int v = m; v >= w[i]; v --) for (int k = 0; k <= s[i] && v >= k * w[i]; k ++) f[v] = max(f[v], f[v - k * w[i]] + k * c[i]); ``` 二进制优化 - 任意正整数 $n$ 可拆分为 $1,2,4,\cdots,2^k,n-2^k$($n-2^k≥0$ 且 $k$ 尽量大). - 相反,$1,2,4,\cdots,2^k,n-2^k$ 能且仅能组合出 $[1,n]$ 中的所有整数. 将 $s_i$ 个物品拆分为 $1,2,4,\cdots,2^k,s_i-2^k$ 个物品(共 $\log s_i$ 组),并绑定每组为一个物品,参与 01 背包. 例如,$s_i=15$ 可以拆分为以下 $4$ 组: 此 $4$ 组物品可组合出所有策略(选 $1\cdots 15$ 个物品). 例如要选 $12$ 个物品,则同时选第 $3,4$ 组即可. 时间复杂度:$O(m\sum\log{s_i})$. ```cpp for (int i = 1; i <= n; i ++) { int wi, ci, s; cin >> wi >> ci >> s; for (int k = 1; k <= s; k *= 2) { w[++ cnt] = k * wi, c[cnt] = k * ci; s -= k; } if (s) w[++ cnt] = s * wi, c[cnt] = s * ci; } for (int i = 1; i <= tot; i ++) for (int v = m; v >= w[i]; v --) f[v] = max(f[v], f[v - w[i]] + c[i]); ``` 分组背包 > 背包体积为 $m$,有 $t$ 组物品,第 $k$ 组有 $s_k$ 个,其中第 $i$ 个体积为 $w_{ki}$,价值为 $c_{ki}$. 每组只能取走一个物品. 求将物品装入背包得到的最大价值. $f[k,v]$:从前 $k$ 组物品中,选出总体积为 $v$ 的物品,能得到的最大价值. - 不选第 $k$ 组:$f[k,v]=f[k-1,v]$. - 选第 $k$ 组:$ f[k,v]=\max_{1\leq i\leq s_k}\ f[k-1,v-w_{ki}]+c_{ki}$. ``` latex f[k,v] = \max\left\{\begin{aligned} &f[k-1,v]\\ &\max_{1\leq i\leq s_k}\ f[k-1,v-w_{ki}]+c_{ki}\quad(v≥w_{ki}) \end{aligned}\right. ``` 可以省去第一维. 时间复杂度:$O(nm)$. ``` cpp for (int k = 1; k <= t; k ++) for (int v = m; v >= 0; v --) for (int i = 1; i <= s[k]; i ++) if (v >= w[k][i]) f[v] = max(f[v],f[v - w[k][i]] + c[k][i]); ``` 二维背包 > 背包体积为 $V$,承重为 $M$. 有 $n$ 个物品,第 $i$ 个体积为 $v_i$,质量为 $m_i$,价值为 $c_i$. 求将物品装入背包得到的最大价值. $f[p,q]$:用体积为 $p$,承重为 $q$ 的背包放物品,能获得的最大总价值. ``` latex f[p,q] = \max\left\{\begin{aligned} &f[p,q]\\ &f[p-v_i,q-m_i]+c_i\quad(p≥v_i,q≥m_i) \end{aligned}\right. ``` 时间复杂度为 $O(nVM)$. ```cpp for (int i = 1; i <= n; i ++) for (int p = V; p >= v[i]; p --) for (int q = M; q >= m[i]; q --) f[p][q] = max(f[p][q], f[p - v[i]][q - m[i]] + c[i]); ``` 方案总数 > 基于 [01 背包,求将物品放入背包的方案数. $gi,v]$:把前 $i$ 个物品(部分或全部)放入体积为 $v$ 的背包的方案总数. - 不放第 $i$ 个物品:有 $g[i-1,v]$ 种 - 放第 $i$ 个物品:有 $g[i-1,v-w_i]$ 种 $$g[i,v]=g[i-1,v]+g[i-1,v-w_i]$$ 同样可以省去第一维: $$g[v]=g[v]+g[v-w_i]\quad(v≥w_i)$$ 初始条件:$g[0]=1$. 因为当背包体积为 $0$ 时只有一个方案:不放任何物品. ```cpp g[0] = 1; for (int i = 1; i <= n; i ++) for (int v = m; v >= w[i]; v --) g[v] += g[v - w[i]]; ``` 最优方案总数 > 基于 [01 背包,求最优方案数. 已知 01 背包 的状态转移方程: ``` latex f[i,v] = \max\left\{\begin{aligned} &f[i-1,v]\\ &f[i-1,v-w_i]+c_i\quad(v≥w_i)\\ \end{aligned}\right. ``` $g[i,v]$:把前 $i$ 个物品(部分或全部)放入体积为 $v$ 的背包的最优方案数. - 若 $f[i-1,v]>f[i-1,v-w_i]+c_i$,不放物品 $i$ 最优:$g[i,v]=g[i-1,v]$. - 若 $f[i-1,v]<f[i-1,v-w_i]+c_i$,放物品 $i$ 最优:$g[i,v]=g[i-1,v-w_i]$. - 若相等,则两种方案都最优:$g[i,v]=g[i-1,v]+g[i-1,v-w_i]$. ``` cpp for (int i = 1; i <= n; i ++) { for (int v = 0; v <= m; v ++) { f[i][v] = f[i - 1][v]; if (v >= w[i]) f[i][v] = max(f[i][v], f[i - 1][v - w[i]] + c[i]); if (f[i][v] == f[i - 1][v]) g[i][v] += g[i - 1][v]; if (f[i][v] == f[i - 1][v - w[i]] + c[i]) g[i][v] += g[v - w[i]]; } } ``` 省去第一维: ``` cpp for (int i = 1; i <= n; i ++) { for (int v = m; v >= w[i]; v --) { int maxn = max(f[v], f[v - w[i]] + c[i]); int maxg = 0; // 最优方案数 if(maxn == f[v]) maxg += g[v]; if(maxn == f[v - w[i]] + c[i]) maxg += g[v - w[i]]; f[v] = maxn, g[v] = maxg; } } ```
-
基础 DP
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/%E5%9F%BA%E7%A1%80-dp/
斐波那契数列 斐波那契数列是形如 $\\{1,1,2,3,5,8,\cdots\\}$ 的数列. 求数列的第 $n$ 项. ??? note 分析 $fn]$:数列的第 $n$ 项. ``` latex f[n]= \begin{cases} 1&n=1,2\\ f[n-1]+f[n-2]&n\geq 3 \end{cases} ``` ``` cpp int f[]; f[1] = f[2] = 1; for (int i = 3; i <= n; i ++) f[i] = f[i - 1] + f[i - 2]; ``` 汉诺塔问题 汉诺塔由 $n$ 个不同的盘子和 $3$ 根杆子组成. 初始时,$n$ 个盘子从小到大叠在 $a$ 杆上:现在,按以下规则将 $n$ 个盘子从 $a$ 杆移到 $c$ 杆. 1. 一次只能动一个盘子. 2. 盘子只能放在杆上. 3. 大盘子不能叠在小盘子上. 求移动盘子的最少次数. ??? note 分析 $f[n]$:将 $n$ 个盘子从一杆移至另一杆,所需的最少移动次数. 将 $n$ 个盘子从 $a$ 杆移到 $c$ 杆,需要以下 $3$ 步: 1. 将 $a$ 杆的 $n-1$ 个盘子移至 $b$ 杆(共 $f[n-1]$ 次). 2. 将 $a$ 杆的最后一个盘子移至 $c$ 杆(共 $1$ 次). 3. 将 $b$ 杆的 $n-1$ 个盘子移至 $c$ 杆(共 $f[n-1]$ 次). ``` latex f[n]= \begin{cases} 1&n=1\\ 2\cdot f[n-1]+1&n\geq 2 \end{cases} ``` ``` cpp int f[]; f[1] = 1; for(int i = 2; i <= n; i ++) f[i] = 2 * f[i - 1] + 1; ``` 骨牌问题 用若干 $1×2$ 的骨牌铺满 $2×n$ 的方格. 如图为 $n=3$ 时的所有铺法:求任意 $n$ 对应的方法总数. ??? note 分析 $f[n]$:$n$ 对应的方法总数. - 若第一个骨牌竖放在左边,则剩余 $2×(n-1)$ 个空方格,铺法数为 $f[n-1]$. - 若第一个骨牌横放在左上角,为了不留空,第二个骨牌必须横放在其正下方. 剩余 $2×(n-2)$ 个空方格,铺法数为 $f[n-2]$. ``` latex f[n]= \begin{cases} 1&n=1\\ 2&n=2\\ f[n-1]+f[n-2]&n\geq 3 \end{cases} ``` 状态转移方程几乎和 [斐波那契数列一致. 平面分割问题 平面上有 $n$ 条闭曲线,每 $2$ 条恰好交于 $2$ 点,且每 $3$ 条不交于同一点. 求平面被分割成的区域个数. ??? note 分析 $a[n]$:$n$ 条封闭曲线分割成的区域个数.由上图可得: ``` latex a[1]=2\\ a[2]-a[1]=2\\ a[3]-a[2]=4\\ a[4]-a[3]=6 ``` 即 ``` latex a[n]=\begin{cases} 2&n=1\\ a[n-1]+2(n-1)&n≥2 \end{cases} ``` 正确性证明: 新增一条曲线时,每与一条已有曲线相交一次,就增加一个区域. 而新增的第 $n$ 条曲线与已有的 $n-1$ 条曲线各有 $2$ 个交点 $∴$ 新增区域数 $=$ 新增交点数 $=2\cdot (n-1)$. $∴$ 现有区域个数 $a[n]=$ 原有区域个数 $+$ 新增区域个数 $=a[n-1]+2(n-1)$. ``` cpp int a[]; a[1] = 2; for(int i = 2; i <= n; i ++) a[i] = a[i - 1] + 2 * (n - 1); ``` 最长上升子序列 (LIS) 求序列 $A$(长度 $n$)的最长上升子序列 $\text{LIS}(A)$ 的长度. 例:$A=\\{2,3,6,4,5,1\\}$, $\text{LIS}(A)=\\{2,3,4,5\\}$(长度 $4$). ??? note 分析 $f[i]$:$\text{LIS}(A_{1\cdots i})$ 的长度. 枚举 $j=1\cdots i-1$: - 若 $A_j<A_i$,则 $A_i$ 可以接在 $\text{LIS}(A_{1\cdots j})$ 后面,形成的上升子序列长度为 $f[j]+1$. - 若 $A_j≥A_i$,$A_j$ 对 $f[i]$ 没有贡献,直接跳过. $$f[i]=\max_{j<i, \ A_j<A_i}(f[j]+1)$$ ``` cpp for (int i = 1; i <= n; i ++) { f[i] = 1; for (int j = 1; j < i; j ++) if (a[j] < a[i]) f[i] = max(f[i], f[j] + 1); } // 问题的解是 f[n] ``` ??? note 单调栈优化 扫描每一个数,将其加入单调栈. 假设当前单调栈内有 $\\{2,3,6\\}$,而扫描到 $4$. 根据贪心原理,将 $6$ 替换为 $4$ 必定更优. 当扫描到 $a[i]$: - 若 $a[i]$ 大于栈尾,则直接将其入栈. - 否则在栈中二分查找第一个 $\geq a[i]$ 的数,将其替换为 $a[i]$. 最终栈的长度即为 LIS 的长度. 时间复杂度为 $O(n\log{n})$. ``` cpp vector<int> s; s.push_back(a[1]); for (int i = 2; i <= n; i ++) { if(a[i] > s.back()) s.push_back(a[i]); else *lower_bound(s.begin(), s.end(), a[i]) = a[i]; } ``` 最长公共子序列 (LCS) 求序列 $A$(长度 $n$)和序列 $b$(长度 $m$)的最长公共子序列 $\text{LCS}(A,B)$ 的长度. 例:$A=$ `freeze`, $B=$ `refeze`, $\text{LCS}(A,B)=$ `reeze`(长度 $5$). ??? note 分析 $f[i,j]$:$\text{LCS}(A_{1\cdots i},B_{1\cdots j})$ 的长度. 枚举 $i=1\cdots n$: 枚举 $j=1\cdots m$: - 若 $A_i\not=B_j$,继承最优子状态:$f[i,j]=\max(f[i-1,j],f[i,j-1])$. - 若 $A_i=B_j$,则 $A_i$(或 $B_j$)可以接在 $\text{LCS}(A_{1\cdots i-1},B_{1\cdots j-1})$ 之后,形成的公共序列长度为 $f[i-1,j-1]+1$. ``` latex f[i,j]=\max\left\{\begin{aligned} &f[i-1,j]\\ &f[i,j-1]\\ &f[i-1,j-1]+1,A_i=B_j \end{aligned}\right. ``` ``` cpp for (int i = 1; i <= n; i ++) { for (int j = 1; j <= m; j ++) { f[i][j] = max(f[i - 1][j], f[i][j - 1]); if (a[i] == b[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1); } } ``` 数字金字塔 三角矩阵 $A$ 有 $n$ 行,第 $i$ 行有 $i$ 列. 从第一行第一列出发,每次可以移动到下一行相邻的两个数字. 到达底部时,经过的数字之和最大为多少?此例中,最优路径为 $13→8→26→15$,最大值为 $62$. ??? note 分析 $(i,j)$:第 $i$ 行第 $j$ 列的数字. $f[i,j]$:走到 $(i,j)$ 时,经过的数字之和的最大值. 逆推法:要走到 $(i,j)$,则上一步只能在 $(i-1,j-1)$ 或 $(i-1,j)$. ``` latex f[i,j]=(i,j)+\max\left\{\begin{aligned} &f[i-1,j-1]\\ &f[i-1,j] \end{aligned}\right. ``` **注意**:当 $j=1$ 或 $j=i$ 时,$f[i-1,j-1]$ 和 $f[i-1,j]$ 会越界. 故 $f$ 数组须初始化为 $-∞$,以使越界的元素在 $\max$ 操作中被自动淘汰. ``` cpp memset(f, 0x80, sizeof f); f[1][1] = 1; for (int i = 2; i <= n; i ++) { for (int j = 1; j <= i; j ++) { cin >> f[i][j]; f[i][j] += max(f[i - 1][j - 1], f[i - 1][j]); } } for (int i = 1; i <= n; i ++) ans = max(ans, f[n][i]); ``` 数字矩阵 有 $n$ 行 $m$ 列的数字矩阵 $A$. 从左上角出发,每次只能向下或向右走一步. 到右下角时,经过的数字之和最大为多少?此例中,最优路径为 $17→1→20→12→9→2$,最大值为 $61$. ??? note 分析 $(i,j)$:第 $i$ 行第 $j$ 列的数字. $f[i,j]$:走到 $(i,j)$ 时,经过的数字之和的最大值. 逆推法:由于只能向左走或向下走,要走到 $(i,j)$,上一步只能在 $(i-1,j)$ 或 $(i,j-1)$. ``` latex f[i,j]=(i,j)+\max\left\{\begin{aligned} &f[i-1,j]\\ &f[i,j-1] \end{aligned}\right. ``` **注意**:当 $i=1$ 或 $j=1$ 时,$f[i-1,j]$ 和 $f[i,j-1]$ 会越界. $f$ 数组须初始化为 $-∞$. ``` cpp memset(f, 0x80, sizeof f); for (int i = 1; i <= n; i ++) { for (int j = 1; j <= m; j ++) { cin >> f[i][j]; if (!(i == 1 && j == 1)) f[i][j] += max(f[i-1][j], f[i][j - 1]); } } ``` 前缀和 前缀和是一种重要的预处理技巧,能大幅降低查询区间元素和的时间复杂度. 预处理 数列 $A$ 有 $n$ 个元素. $f[i]$: $A_1$ 到 $A_i$ 的和. $$f[i]=f[i-1]+A_i$$ 时间复杂度:$O(n)$. ``` cpp for (int i = 1; i <= n; i ++) f[i] = f[i - 1] + a[i]; ``` 查询 $g[i,j]$:$A_i$ 到 $A_j$ 的和. $$g[i,j]=f[j]-f[i-1]$$ 单次查询的时间复杂度:$O(1)$. ``` cpp int g(int i, int j) { // sum of a[i ... j] return f[j] - f[i - 1]; } ``` 二维前缀和 预处理 矩阵 $A$ 有 $n$ 行 $m$ 列. $f[i,j]$:以 $(1,1)$ 为左上角,以 $(i, j)$ 为右下角的矩阵的元素和. $$f[i,j]=f[i-1,j]+f[i,j-1]-f[i-1,j-1]+(i,j)$$时间复杂度:$O(nm)$. ``` cpp f[0][0] = 0; for (int i = 1; i <= n; i ++) for (int j = 1; j <= m; j ++) f[i][j] = f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + a[i][j]; ``` 查询 $g[x_1,y_1,x_2,y_2]$:以 $(x_1,y_1)$ 为左上角,以 $(x_2, y_2)$ 为右下角的矩阵的元素和. $$g[x_1,y_1,x_2,y_2]=f[x_2,y_2]-f[x_1-1,y_2]-f[x_2,y_1-1]+f[x_1-1,y_1-1]$$单次查询的时间复杂度:$O(1)$. ``` cpp int g(int x1, int y1, int x2, int y2) { return f[x2][y2] - f[x1 - 1][y2] - f[x2][y1 - 1] + f[x1 - 1][y1 - 1]; } ``` 差分 差分是前缀和的逆运算,能大幅降低区间修改的时间复杂度. 预处理 数列 $A$ 有 $n$ 个元素. 令 $f[i]=A_i-A_{i-1}$. $f$ 为差分数列. 时间复杂度:$O(n)$. ``` cpp for (int i = 1; i <= n; i ++) f[i] = a[i] - a[i - 1]; ``` 区间修改 当给 $A_l\cdots A_r$ 统一加上 $x$ 时,$f[l]$ 增加了 $x$,$f[r+1]$ 减少了 $x$, $f$ 数列中其余元素不变. 因此可以每次只修改 $f[l]$ 和 $f[r+1]$,最后通过 $f$ 数列还原出 $A$. 单次修改的时间复杂度:$O(1)$. ``` cpp void add(int l, int r, int x) { // add x to A[l ... r] f[l] += x, f[r + 1] -= x; } void restore() { for(int i = 1; i <= n; i ++) a[i] = a[i - 1] + f[i]; } ```
-
动态规划(DP)
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/
简介 动态规划(DP)是打表的最高境界. 我们从一个案例入手 DP. 案例 斐波那契数列形如 $\\{1,1,2,3,5,8,\cdots\\}$. 计算此数列的第 $n$ 项 $fn]$. 根据数列特征,列出递推公式 ``` latex f[n]= \begin{cases} 1&n=1,2\\ f[n-1]+f[n-2]&n\geq 3 \end{cases} ``` 首先,将 $f[1]=1,f[2]=1$ 填入表. 计算 $f[3]=f[2]+f[1]$ 并填入表. 计算 $f[4]=f[3]+f[2]$ 并填入表. 重复计算填表的步骤,直到得到 $f[n]$. 时间复杂度:$O(n)$. ``` cpp f[1] = f[2] = 1; for (int i = 3; i <= n; i ++) f[i] = f[i - 1] + f[i - 2]; ``` 步骤 1. 为问题设计 [状态. 2. 列出状态转移方程. 3. 以正确的顺序解决各级问题. 术语 状态 状态是表格中所填数字的意义. 状态转移方程 状态转移方程描述状态之间的关系. 斐波那契数列的状态转移方程为 ``` latex f[n]= \begin{cases} 1&n=1,2\\ f[n-1]+f[n-2]&n\geq 3 \end{cases} ```
-
搜索
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E6%90%9C%E7%B4%A2/
运用计算机的高性能枚举问题的答案. 简介 搜索算法:枚举问题的所有可能答案,并逐一校验. 案例 设集合 $A$ 满足以下性质: - 若 $x\in A$,则 $2x$ 和 $3x-1$ 必属于 $A$. 已知 $3\in A$,问 $23$ 是否属于 $A$? $A$ 的性质可抽象为下图: ``` latex \xymatrix@C=0em{ & x \ar[dl]\ar[dr] &\\ \margin 2x \margin & & 3x-1 } ``` 以 $3$ 为树根,向下拓展树形图. 于是只需搜索 $23$ 是否在树中. ``` latex \xymatrix@C=0em{ &&&3\ar[dll]\ar[drr]\\ &6\ar[dl]\ar[dr]&&&&8\ar[dl]\ar[dr]\\ 12&&17&&16&&23 } ``` 对于复杂的问题,可以先抽象出关系图,再搜索求解. 广度优先搜索(BFS) 广度优先搜索(`Breadth First Search`,`BFS`)按层次搜索节点. 其原理如下: 1. 建立空队列. 2. 将根节点入队. 3. 取出队头,将其所有子节点入队. 4. 重复上一步直到队列为空. `BFS` 搜索该图的步骤: 1. 节点 $1$ 入队 2. 节点 $1$ 出队,$2,3$ 入队 3. 节点 $2$ 出队,$4,5$ 入队 4. 节点 $3$ 出队,$6,7$ 入队 5. 节点 $4$ 出队 6. 节点 $5$ 出队 7. 节点 $6$ 出队 8. 节点 $7$ 出队 <---> ``` latex \xymatrix@C=0.2em{ &&&1\ar[dll]\ar[drr]\\ &2\ar[dl]\ar[dr]&&&&3\ar[dl]\ar[dr]\\ 4&&5&&6&&7 } ``` > 同一个节点不能被重复访问,否则将导致死循环. ``` cpp bool vis[]; // vis[u]:节点 u 是否访问过 void bfs(int s) { queue<int> Q; Q.push(s); while(!Q.empty()) { int u = Q.top(); Q.pop(); for ( /* 枚举 u 的子节点 v */ ) { if (!vis[v]) { Q.push(v); vis[v] = true; } } } } ``` 深度优先搜索(DFS) 深度优先搜索(`Depth First Search`,`DFS`)尽可能往更深处搜索节点. 其本质是递归. `DFS` 搜索该图的步骤: 1. 访问根节点 $1$. 1. 访问子节点 $2$. 1. 访问子节点 $4$. 2. 访问子节点 $5$. 2. 访问子节点 $3$. 1. 访问子节点 $6$. 2. 访问子节点 $7$. <---> ``` latex \xymatrix@C=0.2em{ &&&1\ar[dll]\ar[drr]\\ &2\ar[dl]\ar[dr]&&&&3\ar[dl]\ar[dr]\\ 4&&5&&6&&7 } ``` > 同一个节点不能被重复访问,否则将导致死循环. ``` cpp bool vis[]; // vis[u]:节点 u 是否访问过 void dfs(int s) { vis[s] = true; for ( /* 枚举 u 的子节点 v */ ) { if (!vis[v]) { dfs(v); } } } ``` 多维搜索:前驱 搜索二维数组 $a[n,m]$ 时,$a[i,j]$ 抽象为节点 $(i,j)$. 其子节点为 $(i+1,j),(i-1,j),(i,j+1),(i,j-1)$,即与其相邻的四个节点. ``` latex \xymatrix @C=1em@R=1em { & 1 & 2 & 3 & \color{red}j & m\\ 1 & *+[o][F-]{\color{transparent}o}\ar@{-}[r]\ar@{-}[d] & *+[o][F-]{\color{transparent}o}\ar@{-}[r]\ar@{-}[d] & *+[o][F-]{\color{transparent}o}\ar@{-}[r]\ar@{-}[d] & *+[o][F-]{\color{transparent}o}\ar@{-}[r]\ar@{-}[d] & *+[o][F-]{\color{transparent}o}\ar@{-}[d]\\ 2 & *+[o][F-]{\color{transparent}o}\ar@{-}[r]\ar@{-}[d] & *+[o][F-]{\color{transparent}o}\ar@{-}[r]\ar@{-}[d] & *+[o][F-]{\color{transparent}o}\ar@{-}[r]\ar@{-}[d] & *+[o][F-]{\color{transparent}o}\ar@{-}[r]\ar@{<-}[d] & *+[o][F-]{\color{transparent}o}\ar@{-}[d]\\ \color{red}i & *+[o][F-]{\color{transparent}o}\ar@{-}[r]\ar@{-}[d] & *+[o][F-]{\color{transparent}o}\ar@{-}[r]\ar@{-}[d] & *+[o][F-]{\color{transparent}o}\ar@{<-}[r]\ar@{-}[d] & *+[o][F-]{\color{transparent}o}\ar@{->}[r]\ar@{->}[d] & *+[o][F-]{\color{transparent}o}\ar@{-}[d]\\ n & *+[o][F-]{\color{transparent}o}\ar@{-}[r] & *+[o][F-]{\color{transparent}o}\ar@{-}[r] & *+[o][F-]{\color{transparent}o}\ar@{-}[r] & *+[o][F-]{\color{transparent}o}\ar@{-}[r] & *+[o][F-]{\color{transparent}o} } ``` 现在从某个节点开始搜索,枚举子节点的工作就显得繁琐. 以 `DFS` 为例: ``` cpp bool vis[][]; // vis[x][y]: a[x][y] 是否访问过 void dfs(int x, int y) { vis[x][y] = true; if (x + 1 <= n && !vis[x + 1][y]) { dfs(x + 1, y); } if (x - 1 >= 1 && !vis[x - 1][y]) { dfs(x - 1, y); } if (y + 1 <= n && !vis[x][y + 1]) { dfs(x, y + 1); } if (y - 1 >= 1 && !vis[x][y - 1]) { dfs(x, y - 1); } } ``` 届时可以通过前驱节省工作量. ``` cpp bool vis[][]; const int dx[] = {1, 0, -1, 0}; // 第一维度前驱 const int dy[] = {0, 1, 0, -1}; // 第二维度前驱 void dfs(int x, int y) { vis[x][y] = true; for (int i = 0; i < 4; i ++) { int nx = x + dx[i], ny = y + dy[i]; if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && !vis[nx][ny]) { dfs(nx, ny); } } } ```
-
二分
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E4%BA%8C%E5%88%86/
据说只有 10% 的程序员能写对二分. 简介 玩个游戏. - 想一个 $1000$ 以内的正整数 $n$. - 每次我给出一个整数 $x$,告诉我「$n>x$」「$n<x$」或「$n=x$」. 我能保证在 $10$ 次以内猜到它. 首先我猜 $x=500$. 除了正好猜中以外,我能把 $n$ 的范围缩小一半. - $n>x\intro n\in[501,1000]$ - $n<x\intro n\in[1,499]$ 然后如法炮制,重复给出可行范围的中间数,每次都能把范围折半. 由于 $\log_2{1000}<10$,最多 $10$ 次就能确定 $n$. $n=324$: 1. $x=500$. $n<x\intro n\in[1,499]$. 2. $x=250$. $n>x\intro n\in[251,499]$. 3. $x=375$. $n<x\intro n\in[251,375]$. 4. $x=313$. $n>x\intro n\in[313,375]$. 5. $x=344$. $n<x\intro n\in[313,344]$. 6. $x=328$. $n<x\intro n\in[313,328]$. 7. $x=320$. $n>x\intro n\in[320,328]$. 8. $x=324$. $n=x$. 条件 数组须呈现广义上的「单调性」. 将数组 $a$ 对半分,前段都不满足 $P$,后段都满足 $P$,则可用二分算法确定分割点,进而确定「第一个满足 $P$ 的元素」. 原理 查找数组 $a$(长度 $n$)中第一个满足条件 $P$ 的元素: ```cpp bool check(int k) { /* 返回 a[k] 是否满足条件 P */ } int binQuery() { int l = 0, r = n + 1; while (l + 1 < r) { int m = (l + r) / 2; if (check(a[m])) r = m; else l = m; } return a[r]; } ``` Q & A 作者列出了大家在学习二分算法中的一些疑问和解答,供参考学习. > **Q**:为何循环的条件是 $l+1<r$?我在其他参考书上看的是 $l<r$. > > **A**:在我的版本中,当 $l+1=r$ 时,$a[l]$ 或 $a[r]$ 就是要找的分割点,此时应退出循环. 故循环能进行的条件是 $l+1<r$. > **Q**:针对一段数组区间,比如对 $a[L\cdots R]$ 进行二分查找怎么办? > > **A**:在程序开头令 $l=L-1,r=R+1$,此举的目的是保证 $m=\left\lfloor\dfrac{ l+r}{2}\right\rfloor$ 能取到范围端点 $L$ 或 $R$. > **Q**:$m$ 会溢出 $[L,R]$ 的范围吗? > > **A**:不会. 那时已因不满足 $l+1<r$ 而退出循环了. > **Q**:如果反过来,数组 $a$ 的前段满足 $P$,后段不满足 $P$ 怎么办? > > **A**:我们把两种情况都讨论一遍. > > 若前段($a[1\cdots l]$)不满足 $P$,后段($a[r\cdots n]$)满足 $P$: > > - $a[l]$ 总不满足 $P$,$a[r]$ 总满足 $P$. > - 因此当 $a[m]$ 满足 $P$ 时,应令 $r=m$,否则令 $l=m$. > - 第一个满足 $P$ 的是 $a[r]$. > > 若前段($a[1\cdots l]$)满足 $P$,后段($a[r\cdots n]$)不满足 $P$: > > - $a[l]$ 总满足 $P$,$a[r]$ 总不满足 $P$. > - 因此当 $a[m]$ 满足 $P$ 时,应令 $l=m$,否则令 $r=m$. > - 最后一个满足 $P$ 的是 $a[l]$. 整数域上的二分 在单调递增数组 $a$(长度 $n$)中查找第一个 $\ge x$ 的数: 前段 $<x$,后段 $\ge x$,因此 $a[m]\ge x$ 时应令 $r=m$,最终答案是 $a[r]$. ```cpp int binQuery(int x) { int l = 0, r = n + 1; while (l + 1 < r) { int m = (l + r) / 2; if (a[m] >= x) r = m; else l = m; } return a[r]; } ``` 在单调递增数组 $a$(长度 $n$)中查找最后一个 $\le x$ 的数: 前段 $\le x$,后段 $> x$,因此 $a[m]\le x$ 时应令 $l=m$,最终答案是 $a[l]$. ```cpp int binQuery(int x) { int l = 0, r = n + 1; while (l + 1 < r) { int m = (l + r) / 2; if (a[m] <= x) l = m; else r = m; } return a[l]; } ``` 实数域上的二分 实数域上的二分需要指定精度 $eps$,以 $l+eps<r$ 为循环条件. ```cpp while (l + eps < r) { double m = (l + r) / 2; if (check(m)) r = m; else l = m; } ``` 此外还可采用固定次数的二分. 此方法得到的答案精度通常更高. ``` cpp for (int i = 0; i < 1000; i ++) { double m = (l + r) / 2; if (check(m)) r = m; else l = m; } ```
-
排序
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E6%8E%92%E5%BA%8F/
你听说过猴子排序吗? 简介 排序数组 $a$(长度 $n$)中的元素. **本章只研究升序排序.** 选择排序 在第 $i$ 次遍历中,交换 $ai]$ 和第 $i$ 小的数. 时间复杂度:$O(n^2)$. ``` cpp for (int i = 1; i <= n; i ++) { int tmp = i; // tmp: 第 i 小的数 for (int j = i + 1; j <= n; j ++) if (a[j] < a[tmp]) tmp = j; swap(a[i], a[tmp]); } ``` 冒泡排序 重复扫描数组 $a$. 若 $a[i]>a[i+1]$ 就交换它们. 当没有可交换元素时结束排序. 时间复杂度:$O(n^2)$. ``` cpp while (true) { bool swapped = false; for (int i = 1; i <= n; i ++) { if (a[i] > a[i + 1]) { swap(a[i], a[i + 1]); swapped = true; } } if (!swapped) break; } ``` 插入排序 将数组 $a$ 分为「已排序」和「未排序」区. 每次将「未排序」区的一个元素插入「已排序区」的正确位置. 时间复杂度:$O(n^2)$. ``` cpp for (int i = 2; i <= n; i ++) { int tmp = a[i], j = i - 1; while (j >= 1 && a[j] > tmp) a[j + 1] = a[j --]; a[j + 1] = tmp; } ``` 计数排序 记录每个元素出现的次数,然后依次输出. $f[x]$:数字 $x$ 出现了几次. 时间复杂度:$O(n)$. **不适用于大范围元素.** ``` cpp int maxn = - 1e9; for (int i = 1; i <= n; i ++) { f[a[i]] ++; maxn = max(maxn, a[i]); } int p = 0; for (int i = 1; i <= maxn; i ++) for (int i = 1; i <= f[i]; i ++) a[++ p] = i; ``` 快速排序 1. 设定基准数为数组的中间数 $a[m]$. 2. 扫描数组,将所有比 $a[m]$ 小的元素移至其左,大于 $a[m]$ 的元素移至其右. 3. 对 $a[m]$ 左右的子数组进行相同操作. 平均时间复杂度:$O(n\log n)$. 最坏时间复杂度:$O(n^2)$. ``` cpp void qsort(int l, int r) { // 对 a[l ... r] 排序 if (l >= r) return; int i = l, j = r, m = (l + r) / 2; while (i <= j) { while (a[i] < a[m]) i ++; while (a[j] > a[m]) j --; if (i <= j) swap(a[i ++], a[j --]); } qsort(l, j), qsort(i, r); } ``` 归并排序 1. 将数组分为 $a[l\sim m]$ 和 $a[m+1\sim r]$ 两个子数组. 2. 递归排序两个子数组. 3. 合并两个已排序的子数组. - 设定指针 $i$ 和 $j$,最初位置分别为两个子数组的起始位置. - 比较 $a[i]$ 和 $a[j]$,选择更小的放入数组 $b$ ,并移动其指针向下一位置. - 重复上一步直到某指针超出数组末尾. - 将子数组剩下的所有元素放入数组 $b$,再将数组 $b$ 拷贝到数组 $a$. 时间复杂度:$O(n\log{n})$. ``` cpp void msort(int l, int r) { if (l == r) return; int m = (l + r) / 2; msort(l, m); msort(m + 1, r); int i = l, j = m + 1, k = l; while (i <= m && j <= r) { if (a[i] <= a[j]) b[k ++] = a[i ++]; else b[k ++] = a[j ++]; } while (i <= m) b[k ++] = a[i ++]; while (j <= n) b[k ++] = a[j ++]; for (i = l; i <= r; i ++) a[i] = b[i]; } ``` 猴子排序 随机交换两个元素,直到排完序. 平均时间复杂度:$O(n\cdot n!)$. 最好时间复杂度:$O(n)$. 最坏时间复杂度:$O(\infty)$. ``` cpp srand(unsigned(time(0))); while (true) { swap(a[rand() % n + 1], a[rand() % n + 1]); bool solve = true; for (int i = 2; i <= n; i ++) if (a[i] < a[i - 1]) solve = false; if (solve) break; } ``` 逆序对数 若 $i<j$ 且 $a[i]>a[j]$,则 $(a[i],a[j])$ 是一组逆序对. 在 [归并排序的第 3 步:合并两个有序数组时,若出现 $a[i]>a[j]$ 的情况,由 $i<j$ 可知 $a[i\sim m]$ 和 $a[j]$ 构成了 $m-i+1$ 个逆序对,便可以统计到答案中. ``` cpp void msort(int l,int r){ if(l == r) return; int m = (l + r) / 2; msort(l, m); msort(m + 1, r); int i = l, j = m + 1, k = l; while (i <= m && j <= r) { if(a[i] <= a[j]) b[k ++] = a[i ++]; else { b[k ++] = a[j ++]; ans += m - i + 1; // 统计逆序对 } } while (i <= m) b[k ++] = a[i ++]; while (j <= r) b[k ++] = a[j ++]; for (i = l; i <= r; i ++) a[i] = b[i]; } ``` 稳定性 若在原数组中,任意相同元素在排序前后的相对位置不变,则此排序是稳定的. 稳定排序不会破坏相同元素的原顺序. 例如对于结构体 `Student`: ``` cpp struct Student { int score; int id; } ``` 现给出已按 `id` 排序的 `Student` 数组,要求按 `score` 再排序. 稳定排序能保证所有 `score` 相同的 `Student` 仍按照 `id` 排序.
-
递归
/posts/%E4%BF%A1%E6%81%AF%E5%AD%A6/%E7%AE%97%E6%B3%95/%E9%80%92%E5%BD%92/
> 递归的定义:参见递归. 简介 你去找银行经理办事. - 经理 A:不关我的事. 找经理 B. - 经理 B:不关我的事. 找经理 A. 于是你在两个经理之间往返了一整天. 在程序中,该行为称作「递归」. 直接递归 函数 `f()` 在内部调用了自己. ``` cpp void f() { /* Do something */ f(); } ``` 这和以下死循环等价. ``` cpp while (true) { /* Do something */ } ``` 间接递归 这类似于两个银行经理的情况. ```cpp void A() { B(); } void B() { A(); } ``` 边界条件 合法的递归需要边界条件,使函数在恰当的时机停止. ```cpp void f() { if (.../* 边界条件 */) return; /* Do something */ f(); } ``` 这和以下循环等价. ``` cpp while (.../* 边界条件 */) { /* Do something */ } ``` 例 1 计算阶乘 $n!$. $f(n)=n!$ 可以定义为 ```latex f(n)= \begin{cases} 1 & n=0 \\ f(n-1)\times n & n\geq1 \end{cases} ``` ```cpp int f(int n) { if (n == 0) return 1; return f(n - 1) * n; } ``` 运行 `f(3)`: - A:来人,计算 $f(3)$. - B:来人,计算 $f(2)$. - C:来人,计算 $f(1)$. - D:来人,计算 $f(0)$. - E:报告,$f(0)=1$. - D:报告,$f(1)=f(0)\times 1=1$. - C:报告,$f(2)=f(1)\times 2=2$. - B:报告,$f(3)=f(2)\times 3=6$. - A:报告,$f(3)=6$. 例 2 计算斐波那契数列的第 $i$ 项. ```latex f(n)= \begin{cases} 1&n=1,2\\ f(n-1)+f(n-2)&n\geq 3 \end{cases} ``` ```cpp int f(int n) { if (n == 1 || n == 2) return 1; return f(n - 1) + f(n - 2); } ```
-
About
/about/
博客主题 本站使用 Hugo 搭建,移植了 Hexo Stellar 主题,并有所改动。 -Hugo Stellar-Hexo Theme Stellar(原主题) 作者信息 ::: - 姓名:🈚️🌧️❄️ - 性别:🚹 - 年龄:1️⃣8️⃣ - 职业:🧑🎓 - 学校:🧧 ::: ``` latex \xymatrix@!0{ & \lambda\omega \ar@{-}rr]\ar@{-}'[d][dd] & & \lambda C \ar@{-}[dd] \\ \lambda 2 \ar@{-}[ur]\ar@{-}[rr]\ar@{-}[dd] & & \lambda P2 \ar@{-}[ur]\ar@{-}[dd] \\ & \lambda\underline\omega \ar@{-}'[r][rr] & & \lambda P\underline\omega \\ \lambda{\to} \ar@{-}[rr]\ar@{-}[ur] & & \lambda P \ar@{-}[ur] } ``` About Hugo > Written in Go, Hugo is an open source static site generator available under the [Apache Licence 2.0.Hugo supports TOML, YAML and JSON data file types, Markdown and HTML content files and uses shortcodes to add rich content. Other notable features are taxonomies, multilingual mode, image processing, custom output formats, HTML/CSS/JS minification and support for Sass SCSS workflows. Hugo makes use of a variety of open source projects including: * https://github.com/russross/blackfriday * https://github.com/alecthomas/chroma * https://github.com/muesli/smartcrop * https://github.com/spf13/cobra * https://github.com/spf13/viper Hugo is ideal for blogs, corporate websites, creative portfolios, online magazines, single page applications or even a website with thousands of pages. Hugo is for people who want to hand code their own website without worrying about setting up complicated runtimes, dependencies and databases. Websites built with Hugo are extremelly fast, secure and can be deployed anywhere including, AWS, GitHub Pages, Heroku, Netlify and any other hosting provider. Learn more and contribute onGitHub.