1一句话问题
给一棵
n个点的树,以及指定的根root(n ≤ 5×10⁵)。
q个询问,每次给两个点u和v,输出两个数: 它们的最近公共祖先(LCA),以及u到v的距离(走几条边)。
输入
7 4 1 1 2 1 3 2 4 2 5 3 6 6 7 4 5 4 7 2 4 1 7
输出
2 2 1 5 2 1 1 3
这棵树是:1 底下挂着 2 和 3;2 底下挂着 4 和 5;3 底下挂着 6,6 底下挂着 7。
4和5的 LCA 是2,走 2 步;4和7的 LCA 是根1,走 5 步;- ★
2和4—— 一个是另一个的祖先,那时 LCA 就是上面那个(2),走 1 步。
输入
7 3 4 1 2 1 3 2 4 2 5 3 6 6 7 1 5 7 5 2 3
输出
2 2 2 5 2 2
★ 同一棵树,根换成了 4 —— 答案整个变了。
「最近公共祖先」这个词里的「祖先」是相对于根说的:换个根,谁是谁的祖先就全变了。 ⇒ 所以这道题的根写在输入里,代码不许自己假设是 1 号。
输入
6 3 1 1 2 2 3 3 4 4 5 5 6 1 6 3 6 6 6
输出
1 5 3 3 6 0
一条链上的三个询问。★ 最后一行 6 6 是同一个点问自己:LCA 是它自己,距离 0。
第 35 章那条:题面多问一句,对拍就多一条腿。
而这一句不是硬凑的 —— 「树上两点距离」才是 LCA 在真实题目里最常见的用途:
dist(u, v) = dep[u] + dep[v] - 2 * dep[lca(u, v)](从 u 走到 LCA 再走到 v;LCA 上面那一段被两个人各算了一遍,所以要减两次。)
⇒ 只问 LCA 的话,「LCA 算对了但 dep 用错了」的写法一路蒙混过关(第 10 步 ④)。
2暴力:一步一步往上爬
// 标准答案:一步一步往上爬 —— 而且用的是和正解**完全不同**的两条路//// ★ 这一份故意不建 dep 数组、也不套那个距离公式,两问各自独立算出来:// · **LCA**:把 u 到根的整条链**做个记号**,再让 v 一步步往上走 ——// 第一个撞上记号的点就是 LCA。(这一步一个「深度」都没用到。)// · **距离**:就是「u 爬到 LCA 用了几步」+「v 爬到 LCA 用了几步」,**数出来的**,// 不是 dep[u] + dep[v] − 2·dep[lca] 算出来的。// ⇒ 于是它能验正解那两件事里的任何一件(第 34 章那条:先找一个能独立算出来的量)。//// 复杂度是 O(深度) 一次询问。⚠ 而这一章第 4 步会量出一件很反直觉的事:// **随机树的深度只有 log 级**,所以在顺手写的数据上,这份暴力一点都不慢。//// ⚠ 记号用「时间戳」而不是每次清空 —— 清空是 O(n),会把「深度」这个主语盖掉。
#include <bits/stdc++.h>using namespace std;
const int MAXN = 500005;int fa[MAXN];int stamp_[MAXN];vector<int> g[MAXN];int n, q, root;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cin >> n >> q >> root; for (int i = 0; i < n - 1; i++) { int u, v; cin >> u >> v; g[u].push_back(v); g[v].push_back(u); } /* BFS 一趟只求父亲(不求深度 —— 下面用不上) */ { vector<char> vis(n + 1, 0); queue<int> que; fa[root] = root; vis[root] = 1; que.push(root); while (!que.empty()) { int u = que.front(); que.pop(); for (int v : g[u]) { if (vis[v]) continue; vis[v] = 1; fa[v] = u; que.push(v); } } }
for (int i = 1; i <= q; i++) { int u, v; cin >> u >> v; int a = u, stepU = 0; while (true) { // ① 把 u 到根的链做上记号 stamp_[a] = i; if (a == root) break; a = fa[a]; } int b = v, stepV = 0; while (stamp_[b] != i) { b = fa[b]; stepV++; } // ② v 往上走,第一个撞上记号的就是 LCA int lca = b; a = u; while (a != lca) { a = fa[a]; stepU++; } // ③ 距离:两段各数几步 cout << lca << ' ' << stepU + stepV << '\n'; } return 0;}点「运行 ▶」看结果
它是这道题最直白的翻译,也是这一章的标准答案。
★ 注意它连 dep 数组都没建:LCA 靠「做记号」找,距离靠「数了几步」数出来 ——
和正解那两件事(倍增表、距离公式)没有一处重合。
3⚠⚠ 转折点:它在随机树上一点都不慢
按惯例,这一步该实测「暴力有多慢」。可这一章又量不出来 —— 而且原因和第 48 章(KMP)一模一样:你顺手造的数据太温柔了。
// 换一把尺子:数「往上爬了多少步」//// ★ 为什么需要它:这一章的秒表在小数据上全是 0.00 秒,而**步数**是能数出来的、// 换台机器也不变的东西。两种做法各放一个计数器,数的是同一个动作:// **顺着父亲指针往上走一格。**// (倍增那边一次「跳 2^k 步」也只算**一格** —— 它本来就是一次数组访问。)//// 顺带把树本身量一下:最大深度、平均深度。⚠ 这两个数才是这一章真正的主角 ——// 第 4 步会看到:**随机树的深度只有 log 级**,所以暴力在顺手写的数据上一点都不慢。//// 用法:./count < 输入 (和主程序同一份输入)// ./count csv < 输入 只打 `键,值`,给 check:viz 用
#include <bits/stdc++.h>using namespace std;
const int MAXN = 500005;const int LOG = 20;
int up[LOG][MAXN];int dep[MAXN], fa[MAXN], stamp_[MAXN];vector<int> g[MAXN];int n, q, root;
int main(int argc, char** argv) { ios::sync_with_stdio(false); cin.tie(nullptr); bool csv = (argc > 1 && string(argv[1]) == "csv");
cin >> n >> q >> root; for (int i = 0; i < n - 1; i++) { int u, v; cin >> u >> v; g[u].push_back(v); g[v].push_back(u); } { vector<char> vis(n + 1, 0); queue<int> que; fa[root] = root; vis[root] = 1; que.push(root); while (!que.empty()) { int u = que.front(); que.pop(); for (int v : g[u]) if (!vis[v]) { vis[v] = 1; dep[v] = dep[u] + 1; fa[v] = u; que.push(v); } } } for (int v = 1; v <= n; v++) up[0][v] = fa[v]; for (int k = 1; k < LOG; k++) for (int v = 1; v <= n; v++) up[k][v] = up[k - 1][up[k - 1][v]];
int maxDep = 0; double avgDep = 0; for (int v = 1; v <= n; v++) { maxDep = max(maxDep, dep[v]); avgDep += dep[v]; } avgDep /= n;
long long stepBrute = 0, stepFast = 0, rootLca = 0, ancPair = 0; for (int i = 1; i <= q; i++) { int u, v; cin >> u >> v; /* ① 暴力:标记 u 的祖先链,再让 v 往上撞 */ { int a = u; while (true) { stamp_[a] = i; stepBrute++; if (a == root) break; a = fa[a]; } int b = v; while (stamp_[b] != i) { b = fa[b]; stepBrute++; } int lca = b; a = u; while (a != lca) { a = fa[a]; stepBrute++; } } /* ② 倍增 */ { int a = u, b = v; if (dep[a] < dep[b]) swap(a, b); int d = dep[a] - dep[b]; for (int k = 0; k < LOG; k++) if ((d >> k) & 1) { a = up[k][a]; stepFast++; } if (a == b) { ancPair++; if (a == root) rootLca++; continue; } for (int k = LOG - 1; k >= 0; k--) if (up[k][a] != up[k][b]) { a = up[k][a]; b = up[k][b]; stepFast += 2; } if (up[0][a] == root) rootLca++; } }
if (csv) { printf("n,%d\nq,%d\nmaxdep,%d\navgdep,%.2f\nbrute,%lld\nfast,%lld\nrootlca,%lld\nancpair,%lld\n", n, q, maxDep, avgDep, stepBrute, stepFast, rootLca, ancPair); return 0; } printf("%d 个点、%d 个询问,根是 %d\n\n", n, q, root); printf(" 树的最大深度 %10d\n", maxDep); printf(" 树的平均深度 %13.2f\n", avgDep); printf("\n 暴力 一共往上爬了 %12lld 步 (平均每次 %.2f 步)\n", stepBrute, (double)stepBrute / q); printf(" 倍增 一共跳了 %12lld 次 (平均每次 %.2f 次)\n", stepFast, (double)stepFast / q); printf("\n ★ 暴力 / 倍增 = %.1f 倍\n", stepFast ? (double)stepBrute / stepFast : 0.0); printf(" ⚠ LCA 恰好就是根的询问占 %.1f%%;一个点是另一个点祖先的占 %.1f%%\n", 100.0 * rootLca / q, 100.0 * ancPair / q); return 0;}点「运行 ▶」看结果
本机实测(A 机:WSL2 6.18 / 8 核 / 8 GB,2026-08-26,独占;./genBig 100000 10000 <形状> 1,
也就是 10 万个点、1 万个随机询问):
| 树的形状 | 最大深度 | 平均深度 | ★ 暴力平均爬几格 | 倍增平均跳几次 |
|---|---|---|---|---|
| ★ 随机(顺手写法) | 34 | 19.08 | ★ 40.50 | 5.42 |
二叉(fa[i] = i/2) |
31 | 28.35 | 56.45 | 6.09 |
| 毛毛虫(一条主链挂满叶子) | 32 290 | 13 564.09 | 30 289.68 | 13.06 |
| 链 | 64 578 | 27 125.33 | ★ 60 573.56 | 14.00 |
| 星(一个点挂着所有人) | 2 | 2.00 | 5.00 | 几乎 0 |
随机树的最大深度只有 34。 n = 10⁵ 个点,深度 34 ——
因为「父亲随便挑一个更早的点」造出来的树,深度是 log 级的。
⇒ 那种数据上暴力平均只爬 40.50 格,而倍增平均跳 5.42 次
外加一张 n log n 的表。秒表说得更直白(同一台机器、同样独占):
数据(n = q = 10⁵) |
暴力 | 倍增 |
|---|---|---|
| ★ 随机树 | ★ 0.04 秒 / 19.6 MiB | 0.09 秒 / 26.8 MiB |
| 链 | 28.20 秒 | 0.06 秒 |
随机树上暴力赢了,而且内存还省三分之一。 把 n 拉到 5×10⁵ 再看一次:
暴力 0.32 秒 / 35.5 MiB,倍增 1.68 秒 / 71.9 MiB —— 差距更大了。
(同样规模的链上,暴力十分钟都没跑完,倍增是 0.92 秒。)
⇒ 「所以要学倍增」这句话,在你顺手造的树上是站不住的。 ★ 这是第 33 章那条「上界证出来了不等于随机数据碰得到」在树上的现场, 也是第 48、49、50 章连着三章那条的第四次 —— 而这一章的旋钮只有一个:树的形状。
4★ 关键的一步:把「往上跳多少步」拆成二进制
暴力慢在哪儿?它一次只能往上挪一格。 那能不能一次挪很多格?
★ 能 —— 只要提前把「跳 2 的整数次幂步」的结果都记下来。
up[k][v] = 从 v 出发往上跳 2^k 步,落在谁头上
这张表怎么填?第 k 层全靠第 k−1 层:
up[0][v] = fa[v] // 跳 1 步就是它的父亲
up[k][v] = up[k-1][ up[k-1][v] ] // ★ 跳 2^k 步 = 连着跳两次 2^(k-1) 步
第 42 章讲快速幂时,那一句是:
a^b = 把 b 看成二进制,逐位处理这一章是:
往上跳 d 步 = 把 d 看成二进制,逐位处理⇒ 一模一样的形状。d = 13 = 1101₂,就是「跳 8 步、跳 4 步、跳 1 步」三下 ——
而不是老老实实挪 13 次。任何一个数都能拆成不超过 log₂n 个 2 的幂,
所以往上跳任意远,都只要 O(log n) 下。
up[0][root] = root这样「跳过头」会老老实实停在根上,不用到处判「跳出树外了吗」。
★ 表里那一整片相同的数就是它 —— 第 5 步的动画能看得很清楚。 ⚠ 但方便是有代价的:它把「跳出树外」这个错误从一眼可见的崩溃,变成了安静的错答案。 第 13 步会把这笔账明写出来。
5动画一:这张表是怎么一层层推出来的
★ 盯住第 k 行怎么用第 k−1 行算出来:先跳 2^(k-1) 步落在中间那个点(黄的)上,
再从那儿跳同样多步。
★ 换到「一条链」那一组,看那张表怎么变成一片整整齐齐的斜线; 再换到「同一棵树,换个根」,看整张表全变了 —— 这就是第 1 步那个样例说的事。
| v=1 | v=2 | v=3 | v=4 | v=5 | v=6 | v=7 | |
| k=0 | · | · | · | · | · | · | · |
| k=1 | · | · | · | · | · | · | · |
| k=2 | · | · | · | · | · | · | · |
6★ 查询:两段完全不同的循环
有了表,回答一个询问分两段。⚠ 这两段最容易糊在一起,它们的方向、条件、目的全都不一样:
① 对齐深度:把 dep[u] - dep[v] 拆成二进制
for k = 0 .. LOG-1:
if ((d >> k) & 1) u = up[k][u]; // 这一位是 1 就跳
// 目的:让两个人站在同一层
⚠ 中间必须判一次:if (u == v) return u; // 一个本来就是另一个的祖先
② 一起往上跳:
for k = LOG-1 .. 0: // ★ 从大到小
if (up[k][u] != up[k][v]) { u = up[k][u]; v = up[k][v]; }
return up[0][u]; // ★ 停在 LCA 正下方,再上一步
第 ② 段的目标不是「跳到 LCA」,而是「跳到 LCA 的正下方」。
道理是这样:up[k][u] == up[k][v] 说明跳 2^k 步已经到 LCA 或者更上面了 ——
那就不能跳,跳了会越过答案。反过来,up[k][u] != up[k][v] 说明还没到,放心跳。
⇒ k 从大到小试一遍之后,两个人恰好停在「再往上一步就是 LCA」的位置,
所以答案是 up[0][u]。
⚠ 而这也解释了中间那一句 if (u == v):如果 v 本来就是 u 的祖先,
对齐深度之后两个人已经是同一个点了,进第 ② 段的话
up[k][u] != up[k][v] 永远为假,最后 return up[0][u] 会返回 LCA 的父亲 ——
答案差一层(第 10 步 ②)。
7正解
// 正解:倍增求 LCA,预处理 O(n log n)、单次查询 O(log n)//// ★ 关键的一步只有一句话:**把「往上跳多少步」看成二进制**。// 这和第 42 章快速幂是同一件事换了个主语 —— 那里拆的是指数,这里拆的是步数。//// up[0][v] = fa[v]// up[k][v] = up[k-1][ up[k-1][v] ] // 跳 2^k 步 = 连着跳两次 2^(k-1) 步//// 有了这张表,查询就是两段**完全不同**的循环(这两段最容易糊在一起,正文第 6 步专门拆开讲):// ① 对齐深度:把 dep[u] − dep[v] 拆成二进制,该跳哪几位就跳哪几位(k 从小到大、从大到小都行);// ② 一起往上跳:k 从大到小,★ 只在 up[k][u] != up[k][v] 时才跳 ——// 跳完两个人停在 LCA 的**正下方**,答案是 up[0][u]。// ⚠ 而 ① 和 ② 之间必须先判一次 u == v(那时 u 就是 LCA,进 ② 会跳过头)。//// ⚠⚠ 建表这一趟**一定要用 BFS,不能递归 DFS**:// 这道题的最坏形状就是一条链,而第 30 章实测过 —— 本机 8 MB 栈、一帧约 30 字节,// 递归到 28 万层就段错误。50 万个点的链必炸。//// ⚠ 根的父亲设成**它自己**:这样「跳过头」会老老实实停在根上,不用到处判边界。
#include <bits/stdc++.h>using namespace std;
const int MAXN = 500005;const int LOG = 20; // 2^19 = 524288 > 5×10^5,取 20 留一格
int up[LOG][MAXN];int dep[MAXN];vector<int> g[MAXN];int n, q, root;
/** BFS 一趟求出 dep 和 up[0],再把 up 的其余层推出来 */void build() { vector<char> vis(n + 1, 0); queue<int> que; dep[root] = 0; up[0][root] = root; // ⚠ 根的父亲是它自己 vis[root] = 1; que.push(root); while (!que.empty()) { int u = que.front(); que.pop(); for (int v : g[u]) { if (vis[v]) continue; vis[v] = 1; dep[v] = dep[u] + 1; up[0][v] = u; que.push(v); } } for (int k = 1; k < LOG; k++) for (int v = 1; v <= n; v++) up[k][v] = up[k - 1][up[k - 1][v]];}
int lca(int u, int v) { if (dep[u] < dep[v]) swap(u, v); // ⚠ 先保证 u 是更深的那个 int d = dep[u] - dep[v]; for (int k = 0; k < LOG; k++) // ① 对齐深度:把 d 拆成二进制 if ((d >> k) & 1) u = up[k][u]; if (u == v) return u; // ⚠ 一个是另一个的祖先,到此为止 for (int k = LOG - 1; k >= 0; k--) // ② 一起往上跳:只在「还不一样」时跳 if (up[k][u] != up[k][v]) { u = up[k][u]; v = up[k][v]; } return up[0][u]; // ⚠ 停在 LCA 正下方,再上一步才是答案}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cin >> n >> q >> root; for (int i = 0; i < n - 1; i++) { int u, v; cin >> u >> v; g[u].push_back(v); g[v].push_back(u); } build(); for (int i = 0; i < q; i++) { int u, v; cin >> u >> v; int a = lca(u, v); cout << a << ' ' << dep[u] + dep[v] - 2 * dep[a] << '\n'; } return 0;}点「运行 ▶」看结果
8动画二:看那两段跳
★ 第一组(问 4 和 7)看完整的两段。
★ 换到「祖先对:问 2 和 4」那一组 —— 对齐深度之后就结束了,第 ② 段根本没进。
★ 再换到「一条链」,看深度差 5 是怎么拆成 101₂ 两下跳完的。
9⚠ 建表那一趟:为什么必须用 BFS
求 dep 和 up[0] 要把整棵树走一遍。用递归 DFS 写最顺手,但这一章不能用 ——
因为这道题的最坏形状是一条链,而链正是递归最怕的东西。
// ⚠ 把「建表那趟不能用递归 DFS」演示出来//// 这一章的最坏形状是**一条链**,而链正好也是递归最怕的东西:// 递归求 dep / fa 时,链有多长就要压多少层栈。// 第 30 章实测过:本机默认栈 8 MB、一帧约 30 字节,压到二十几万层就段错误。//// 用法:./deep dfs < 输入 用**递归** DFS 建表,打印它压了多少层(链上会直接段错误)// ./deep bfs < 输入 用 BFS 建表,打印同一棵树的最大深度(永远不会炸)// ./deep csv < 输入 只打 `键,值`(走 BFS 那条路,安全),给 check:viz 用//// ⚠ 两条路算出来的 dep 数组是**一模一样**的 —— 差别只在「谁来记住还没做完的事」:// 递归让操作系统的栈记,BFS 让你自己的队列记(队列在堆上,堆比栈大得多)。
#include <bits/stdc++.h>using namespace std;
const int MAXN = 500005;int dep[MAXN], fa[MAXN];vector<int> g[MAXN];int n, q, root;int curDepth = 0, maxStack = 0;
/** 递归版:⚠ 链上会压到 n 层 */void dfs(int u, int p) { curDepth++; maxStack = max(maxStack, curDepth); fa[u] = p; dep[u] = (u == root) ? 0 : dep[p] + 1; for (int v : g[u]) if (v != p) dfs(v, u); curDepth--;}
int main(int argc, char** argv) { ios::sync_with_stdio(false); cin.tie(nullptr); string mode = (argc > 1) ? argv[1] : "bfs"; cin >> n >> q >> root; for (int i = 0; i < n - 1; i++) { int u, v; cin >> u >> v; g[u].push_back(v); g[v].push_back(u); }
if (mode == "dfs") { printf("用递归 DFS 建表(n = %d)…\n", n); fflush(stdout); dfs(root, root); printf("没炸。最深压了 %d 层栈。\n", maxStack); return 0; }
vector<char> vis(n + 1, 0); queue<int> que; fa[root] = root; vis[root] = 1; que.push(root); while (!que.empty()) { int u = que.front(); que.pop(); for (int v : g[u]) if (!vis[v]) { vis[v] = 1; dep[v] = dep[u] + 1; fa[v] = u; que.push(v); } } int mx = 0; for (int v = 1; v <= n; v++) mx = max(mx, dep[v]); if (mode == "csv") { printf("n,%d\nmaxdep,%d\n", n, mx); return 0; } printf("用 BFS 建表(n = %d):最大深度 %d,一层栈都没压。\n", n, mx); printf("⚠ 同一棵树用递归 DFS 建表,要压的栈层数就是这个最大深度 + 1 = %d。\n", mx + 1); return 0;}点「运行 ▶」看结果
本机实测(./genBig <n> 1 chain 1,链,根是随机挑的):
n |
这棵树的最大深度 | 递归 DFS 建表 | BFS 建表 |
|---|---|---|---|
| 10 万 | 64 578 | 压了 64 579 层,没炸 | ✓ |
| 20 万 | 110 416 | 压了 110 417 层,没炸 | ✓ |
| 25 万 | 211 936 | ✗ 段错误 | ✓ |
| 50 万 | 431 264 | ✗ 段错误 | ✓ |
本机默认栈 8 MB,一个栈帧几十个字节 —— 二十来万层正好用完。
第 30 章讲图上 DFS 时量过一模一样的账(50 万点的图,285 380 层就段错误)。 不同的是:那一章的最坏情况要碰运气,这一章的最坏情况就是题目的标准数据 —— 一条 50 万点的链,是出题人一定会造的。
⇒ 建表一律用 BFS(或者手写栈)。
★ 两条路算出来的 dep 一模一样,差别只在「谁来记住还没做完的事」:
递归让操作系统的栈记(8 MB 封顶),BFS 让你自己的队列记(队列在堆上,堆大得多)。
10★ 对拍:五个写错的版本,外加一份试金石
// 正解:倍增求 LCA,预处理 O(n log n)、单次查询 O(log n)//// ★ 关键的一步只有一句话:**把「往上跳多少步」看成二进制**。// 这和第 42 章快速幂是同一件事换了个主语 —— 那里拆的是指数,这里拆的是步数。//// up[0][v] = fa[v]// up[k][v] = up[k-1][ up[k-1][v] ] // 跳 2^k 步 = 连着跳两次 2^(k-1) 步//// 有了这张表,查询就是两段**完全不同**的循环(这两段最容易糊在一起,正文第 6 步专门拆开讲):// ① 对齐深度:把 dep[u] − dep[v] 拆成二进制,该跳哪几位就跳哪几位(k 从小到大、从大到小都行);// ② 一起往上跳:k 从大到小,★ 只在 up[k][u] != up[k][v] 时才跳 ——// 跳完两个人停在 LCA 的**正下方**,答案是 up[0][u]。// ⚠ 而 ① 和 ② 之间必须先判一次 u == v(那时 u 就是 LCA,进 ② 会跳过头)。//// ⚠⚠ 建表这一趟**一定要用 BFS,不能递归 DFS**:// 这道题的最坏形状就是一条链,而第 30 章实测过 —— 本机 8 MB 栈、一帧约 30 字节,// 递归到 28 万层就段错误。50 万个点的链必炸。//// ⚠ 根的父亲设成**它自己**:这样「跳过头」会老老实实停在根上,不用到处判边界。
#include <bits/stdc++.h>using namespace std;
const int MAXN = 500005;const int LOG = 20; // 2^19 = 524288 > 5×10^5,取 20 留一格
int up[LOG][MAXN];int dep[MAXN];vector<int> g[MAXN];int n, q, root;
/** BFS 一趟求出 dep 和 up[0],再把 up 的其余层推出来 */void build() { vector<char> vis(n + 1, 0); queue<int> que; dep[root] = 0; up[0][root] = root; // ⚠ 根的父亲是它自己 vis[root] = 1; que.push(root); while (!que.empty()) { int u = que.front(); que.pop(); for (int v : g[u]) { if (vis[v]) continue; vis[v] = 1; dep[v] = dep[u] + 1; up[0][v] = u; que.push(v); } } for (int k = 1; k < LOG; k++) for (int v = 1; v <= n; v++) up[k][v] = up[k - 1][up[k - 1][v]];}
int lca(int u, int v) { if (dep[u] < dep[v]) swap(u, v); // ⚠ 先保证 u 是更深的那个 int d = dep[u] - dep[v]; for (int k = 0; k < LOG; k++) // ① 对齐深度:把 d 拆成二进制 if ((d >> k) & 1) u = up[k][u]; if (u == v) return u; // ⚠ 一个是另一个的祖先,到此为止 for (int k = LOG - 1; k >= 0; k--) // ② 一起往上跳:只在「还不一样」时跳 if (up[k][u] != up[k][v]) { u = up[k][u]; v = up[k][v]; } return up[0][u]; // ⚠ 停在 LCA 正下方,再上一步才是答案}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cin >> n >> q >> root; for (int i = 0; i < n - 1; i++) { int u, v; cin >> u >> v; g[u].push_back(v); g[v].push_back(u); } build(); for (int i = 0; i < q; i++) { int u, v; cin >> u >> v; int a = lca(u, v); cout << a << ' ' << dep[u] + dep[v] - 2 * dep[a] << '\n'; } return 0;}300 轮实测(种子 1..300,最终档 3):
| 故意写错的地方 | 被抓 | 靠什么现形 |
|---|---|---|
④ wrongDepth:距离少乘了个 2 |
254 / 300 | 随机就抓 —— 但只在第二问上 |
① wrongRootOne:不读 root,从 1 号建表 |
220 / 300 | ★ 要 root ≠ 1,顺手档上是精确的 0 |
② wrongNoSame:对齐深度后忘了判 u == v |
231 / 300 | 要「一个是另一个的祖先」的询问 |
③ wrongLast:跳完直接 return u |
150 / 300 | 随机就抓(差一层) |
⑤ wrongLog:LOG 写死成 5 |
★★★ 0 / 300 | 对拍原理上抓不到 —— 见下一步 |
★ 每一份的文件头都写着「它靠什么现形」—— 那不是注释,那是下一步调生成器的依据。
11★★★ LOG 那一课:对拍看不见,随机的大数据也看不见
LOG 要满足 2^LOG > n。写死成 5 会怎样?
在深度不到 32 的树上,它一个字节都不错。
于是它成了本书第三个「对拍看不见」的错法 —— 而原因和前两个都不一样:
| 为什么对拍抓不到 | |
|---|---|
第 48 章 wrongSlow |
只影响复杂度,答案永远对 |
第 50 章 wrongMemset |
只影响复杂度,答案永远对 |
★ 本章 wrongLog |
⚠ 在你对拍的那个规模上,它是真的对 |
| 拿什么数据去打它 | 200 个询问里错了几个 |
|---|---|
对拍(./gen,n ≤ 14) |
★ 0 / 300 轮 |
★ ./genBig 100000 200 rand(随机树,n = 10⁵) |
★★ 0 / 200 |
./genBig 100 200 chain(链,才 100 个点) |
36 / 200 |
./genBig 100000 200 chain(链,n = 10⁵) |
200 / 200 |
★★★ 第二行才是这一步真正的重点:数据大到 10 万个点,它照样一个都不错 ——
因为随机树的深度只有 34,深度差几乎从不超过 31,而 LOG = 5 正好覆盖到 31。
⇒ 抓它要的不是「更大」,是「更深」。 而「更深」这件事,你顺手造的随机树给不了你,得自己去造链。
★ 于是这一章的两条线在这里合成了一条: 树的形状不只是「暴力慢不慢」的旋钮,也是「bug 现不现形」的旋钮。
12★★ 生成器:七个档位,而后面两个旋钮全是负分
| 档位 | 相对上一档拧了什么 | ①Root | ②Same | ③Last | ④Dep | ⑤Log | 试金石 |
|---|---|---|---|---|---|---|---|
| 0(顺手写法) | 随机树、编号不打乱、1 号当根 | ★ 0 | 208 | 232 | 241 | 0 | 241 |
| 1 | ★ 编号打乱 + 根随机 | 233 | 231 | 212 | 272 | 0 | 272 |
| 2 | ★ 形状混着来(链 / 星 / 二叉 / 毛毛虫) | 218 | 239 | 195 | 274 | 0 | 274 |
| 3 ★★ 最终档 | ★ 询问里塞「祖先对」 | 220 | 231 | ⚠ 150 | ⚠ 254 | 0 | 254 |
| 4 | 对照 = 3 − 编号打乱和根随机 | ★ 0 | 208 | 149 | 221 | 0 | 221 |
| 5 | 对照 = 3 − 形状 | 220 | 240 | 158 | 258 | 0 | 258 |
| 6 | 对照 = 3 − 祖先对询问 | 218 | 239 | 195 | 274 | 0 | 274 |
「顺手写法」是两件事同时发生的:
- 造树时让父亲的编号一定比自己小(因为
for i = 2..n: fa[i] = 随便挑一个更早的点最好写); - 让 1 号点当根(因为不用想)。
⇒ 于是 wrongRootOne(咬定根是 1 号)一轮都抓不到 —— 它和正解在这种数据上是同一份程序。
对照档 4 把这一处撤掉,它立刻又变回 0:这一处改动是它唯一的活路。
★ 第 27 章树形 DP 一模一样地栽过一次(那一章的 wrongRoot 也是精确的 0)。
⚠ 而这两件顺手事必须一起拧才有用:只打乱编号、或者只随机根,它都会被抓到 ——
真正让它隐身的,是两个顺手写法凑在一起。
看档位 2 和档位 3 相对上一档的变化:
| 旋钮 | ①Root | ②Same | ③Last | ④Dep |
|---|---|---|---|---|
| 形状(1 → 2) | 233 → 218 | 231 → 239 | 212 → 195 | 272 → 274 |
| 祖先对(2 → 3) | 218 → 220 | 239 → 231 | 195 → 150 | 274 → 254 |
四列里有五处是掉的,尤其 ③wrongLast 从 212 一路掉到 150 ——
因为祖先对的询问根本不进第 ② 段循环,而 ③ 的 bug 就在那一段里。
⇒ 那为什么还留着?理由不是抓获率,是覆盖面(第 27 章档位 3、第 35 章「值域压小」的同款账)。 而这一章的覆盖面理由,比前面任何一章都硬 —— 看下面那张表。
第 39 章立过一条规矩:动笔前问一句「这个性质随机数据送不送」。 这一章给它加了半句 —— 还得问「在多大的数据上」。
本机实测(随机树,10 万个随机询问):
n |
最大深度 | ★ 「一个点是另一个点的祖先」 | 「LCA 恰好是根」 |
|---|---|---|---|
| 12(对拍那种规模) | 7 | ★ 59.6% | 30.1% |
| 100 | 10 | 12.6% | 3.9% |
| 1 000 | 18 | 2.2% | 0.21% |
| 10 000 | 32 | 0.42% | 0.056% |
| 100 000(真实规模) | 34 | ★ 0.042% | 0.013% |
从 59.6% 一路掉到 0.042%,差 1400 倍。
⇒ 于是「祖先对」这个旋钮的账是这样算的:
- 在对拍的小数据上它几乎是白送的 —— 所以加了它,抓获率反而掉;
- 可在真实规模上,随机询问一个祖先对都碰不到 ——
你要是只靠对拍就上考场,
wrongNoSame那一类 bug 你从来没有真正测过。
★★★ 「对拍抓到了」不等于「真实规模上也覆盖到了」。 这两件事在这一章第一次被量成了两个差 1400 倍的数。
13★ 倍增不只是 LCA:同一张表,换个东西记
小题:树的每条边带一个权。
q个询问,每次给u v k,输出两个数: ①u往上数第k个祖先是谁(不存在输出 0);②u到v路径上最大的那条边权。
输入
7 4 1 1 2 5 1 3 9 2 4 2 2 5 7 3 6 1 6 7 8 4 5 1 4 7 3 5 5 2 4 4 9
输出
2 7 0 9 1 0 0 0
第二行 0 9:4 的深度只有 2,问它第 3 个祖先 —— 没有,输出 0;
而 4 到 7 的路径是 4→2→1→3→6→7,边权 2、5、9、1、8,最大的是 9。
第 ① 问就是「对齐深度」那一段单独拿出来:把 k 拆成二进制,该跳哪几位跳哪几位。
第 ② 问只多记了一个东西:
mx[k][v] = 从 v 往上跳 2^k 步,这一段路上最大的边权
mx[k][v] = max( mx[k-1][v], mx[k-1][ up[k-1][v] ] ) // 和 up 一模一样的递推然后把 LCA 那两段循环原样跑一遍,沿途 max 一下就行。
⇒ 倍增是一类技术,不是一道模板题。
凡是「沿着一条链往上走、而且能合并」的东西 —— 最大值、最小值、和、gcd —— 都能这么记。
⚠ 但「能合并」是有条件的:这里的 max 可交换可结合,换成「路径上第二大的边权」就不行了。
// ★★ 倍增不只是 LCA:同一张表,换个东西记//// 题面(章末那道小题):树的每条边带一个权 w。q 个询问,每次给 u v k,输出两个数:// ① u 往上数第 k 个祖先是谁(不存在输出 0);// ② u 到 v 的路径上,最大的那条边权是多少(u == v 时没有边,输出 0)。//// ★ 这两问和 LCA 用的是**同一张 up 表**,只是多记了一个东西:// mx[k][v] = 从 v 往上跳 2^k 步,这一段路上最大的边权// mx[k][v] = max( mx[k-1][v], mx[k-1][ up[k-1][v] ] ) // 和 up 一模一样的递推//// ⇒ ① 第 k 个祖先:把 k 拆成二进制,该跳哪几位跳哪几位 —— 这就是「对齐深度」那一段单独拿出来;// ② 路径最大边权:把 LCA 的两段循环原样跑一遍,沿途 max 一下就行。//// ★ 这一节想说明的只有一句话:**倍增是一类技术,不是一道模板题。**// 凡是「沿着一条链往上走、而且能合并」的东西(最大值、最小值、和、gcd…)都能这么记。// ⚠ 但「能合并」是有条件的:这里的 max 可交换可结合,换成「路径上第二大」就不行了。
#include <bits/stdc++.h>using namespace std;
const int MAXN = 200005;const int LOG = 18;
int up[LOG][MAXN], mx[LOG][MAXN];int dep[MAXN];vector<pair<int, int>> g[MAXN];int n, q, root;
void build() { vector<char> vis(n + 1, 0); queue<int> que; dep[root] = 0; up[0][root] = root; mx[0][root] = 0; vis[root] = 1; que.push(root); while (!que.empty()) { int u = que.front(); que.pop(); for (size_t i = 0; i < g[u].size(); i++) { int v = g[u][i].first, w = g[u][i].second; if (vis[v]) continue; vis[v] = 1; dep[v] = dep[u] + 1; up[0][v] = u; mx[0][v] = w; que.push(v); } } for (int k = 1; k < LOG; k++) for (int v = 1; v <= n; v++) { up[k][v] = up[k - 1][up[k - 1][v]]; mx[k][v] = max(mx[k - 1][v], mx[k - 1][up[k - 1][v]]); // ★ 和 up 同一个递推 }}
/** u 往上第 k 个祖先,没有就返回 0 */int kth(int u, int k) { if (k > dep[u]) return 0; // ⚠ 跳出树外了 for (int b = 0; b < LOG; b++) if ((k >> b) & 1) u = up[b][u]; return u;}
/** u 到 v 路径上的最大边权 */int pathMax(int u, int v) { int res = 0; if (dep[u] < dep[v]) swap(u, v); int d = dep[u] - dep[v]; for (int k = 0; k < LOG; k++) // ① 对齐深度,顺手 max if ((d >> k) & 1) { res = max(res, mx[k][u]); u = up[k][u]; } if (u == v) return res; for (int k = LOG - 1; k >= 0; k--) // ② 一起往上跳,顺手 max if (up[k][u] != up[k][v]) { res = max(res, max(mx[k][u], mx[k][v])); u = up[k][u]; v = up[k][v]; } return max(res, max(mx[0][u], mx[0][v])); // ⚠ 最后那一步(到 LCA)也算}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cin >> n >> q >> root; for (int i = 0; i < n - 1; i++) { int u, v, w; cin >> u >> v >> w; g[u].push_back(make_pair(v, w)); g[v].push_back(make_pair(u, w)); } build(); for (int i = 0; i < q; i++) { int u, v, k; cin >> u >> v >> k; cout << kth(u, k) << ' ' << pathMax(u, v) << '\n'; } return 0;}点「运行 ▶」看结果
300 轮对拍实测(标准答案是 extBrute.cpp,一格一格爬):
| 故意写错的地方 | 顺手档 | ★ 最终档 |
|---|---|---|
① extWrongMax:对齐深度那一段忘了 max |
277 / 300 | ⚠ 271 / 300 |
② extWrongKth:忘了判「跳出树外」 |
201 / 300 | ★ 260 / 300 |
★ 最终档对 ① 是 −6,对 ② 是 +59(它把 k 有时故意取得比深度还大)——
净胜,所以留下。
extWrongKth 少写了一句 if (k > dep[u]) return 0;。
因为我们把根的父亲设成了它自己,跳过头既不会越界也不会崩 —— 它会安安静静停在根上,然后把根当成答案交出去。
★★ 省掉的边界判断,迟早要还。 那个约定确实让主线代码干净了(不用到处判「跳出树外了吗」), 但它同时把一类错误从崩溃变成了错答案 —— 崩溃你一眼就看见,错答案要靠对拍。 这笔账两头都要知道,不能只记住「这样写方便」。
14这一章没讲的
| 做法 | 预处理 | 单次查询 | 本书讲了吗 |
|---|---|---|---|
| 一格一格往上爬 | O(n) |
O(深度) |
★ 本章第 2 步(它就是暴力) |
| ★ 倍增 | O(n log n) |
O(log n) |
★ 本章 |
| 欧拉序 + ST 表 | O(n log n) |
★ O(1) |
✗ —— 要先讲 ST 表,本书 0 命中 |
| Tarjan(离线 + 并查集) | O(n α) |
均摊 O(α) |
✗ —— 必须离线(先读完所有询问) |
| 树链剖分 | O(n) |
O(log n),常数极小 |
✗ —— 更长,但能干的事也多得多 |
★ 选倍增当主线的理由只有一条:它顺手就能推广(第 13 步那两个小题就是证明), 而且它和第 42 章快速幂共用同一个直觉。别的做法更快,但只解一道题。
★ 顺带一句考场实话:n 和 q 都不大(比如 10⁴ 以内)时,
第 2 步那份暴力就够了 —— 它十行,写不错。第 3 步已经量给你看了。
15自测
- 洛谷 P3379 【模板】最近公共祖先(LCA) —— ★ 就是这一章那道题的原题(只问 LCA,不问距离)。n 和 q 都到 5×10^5 —— 第 2 步那份暴力正好过不了
- 洛谷 P5903 【模板】树上 k 级祖先 —— ★ 第 13 步那一问的原题。⚠ 它卡到了倍增过不去(正解是长链剖分),但拿倍增拿部分分是标准操作
- 洛谷 P1967 [NOIP 2013 提高组] 货车运输 —— ★★ 最大生成树 + 树上路径最小边权 —— 第 34 章和第 13 步那半节的合体,倍增在这道题里是主角
- 洛谷 P2420 让我们异或吧 —— 树上路径异或和:先求出每个点到根的异或和,答案就是 x[u] ^ x[v] —— ★ 连 LCA 都不用,因为异或自己抵消。练「先想清楚要不要 LCA」
- 洛谷 P3128 [USACO15DEC] Max Flow P —— ⚠ 提高组:树上差分 + LCA。★ 这是 LCA 最常见的第二个用法,而树上差分本书没讲 —— 正好自己补一块
- 洛谷 P1395 会议 —— 入门难度的树上题(求树的重心),不需要 LCA。★ 放在这儿是想说明:不是每道树上题都要上倍增
- ★ 把「往上跳多少步」拆成二进制 ——
up[k][v] = up[k-1][up[k-1][v]], 和第 42 章快速幂是同一件事换了个主语。查询是两段完全不同的循环: 先对齐深度,再「只在不一样时」一起跳,最后再上一步。 - ⚠⚠ 树的形状决定一切。 同一份暴力,随机树上平均爬 40.50 格、链上 60 573.56 格
(差 1496 倍);而
n = 10⁵的随机树深度只有 34 —— 所以「暴力慢」这句话在你顺手造的树上是站不住的,建表爆栈那件事也一样。 - ★★★ 「对拍抓到了」不等于「真实规模上覆盖到了」。
LOG开小的那个 bug,对拍 0 / 300、随机的 10 万点大数据 0 / 200, 只有又大又深的数据才抓得到;而「一个点是另一个点的祖先」这个性质, 小树上白送 59.6%,10 万点上只剩 0.042%。每一条都要问一句:在多大的数据上?