阶段 11 · 树上进阶 · 第 51 章提高组 S

LCA 与倍增:把「往上跳多少步」拆成二进制

★ 关键一步和第 42 章快速幂是同一件事,只换了个主语 —— 那里拆的是指数,这里拆的是步数。⚠ 而这一章的转折点在第 3 步:暴力在随机树上一点都不慢,本机实测它比倍增还快,只有在链上才差 1496 倍。

需要先学:第 29 章 图的存储:三种存法的对比与选型第 42 章 快速幂与取模例题:给一棵树和一个根,回答 q 次「这两个点的最近公共祖先是谁、它们离多远」建议用时:155 分钟

1一句话问题

给一棵 n 个点的树,以及指定的根 rootn ≤ 5×10⁵)。

q 个询问,每次给两个点 uv,输出两个数: 它们的最近公共祖先(LCA),以及 uv距离(走几条边)。

输入

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 底下挂着 232 底下挂着 453 底下挂着 66 底下挂着 7

  • 45 的 LCA 是 2,走 2 步;
  • 47 的 LCA 是根 1,走 5 步;
  • 24 —— 一个是另一个的祖先,那时 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暴力:一步一步往上爬

brute.cpp暴力:把 u 的祖先链做记号,让 v 往上撞
// 标准答案:一步一步往上爬 —— 而且用的是和正解**完全不同**的两条路
//
// ★ 这一份故意不建 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

它是这道题最直白的翻译,也是这一章的标准答案。 ★ 注意它dep 数组都没建:LCA 靠「做记号」找,距离靠「数了几步」数出来 —— 和正解那两件事(倍增表、距离公式)没有一处重合

3⚠⚠ 转折点:它在随机树上一点都不慢

按惯例,这一步该实测「暴力有多慢」。可这一章又量不出来 —— 而且原因和第 48 章(KMP)一模一样:你顺手造的数据太温柔了。

count.cpp换一把尺子:数「往上爬了多少格」
秒表在小数据上全是 0.00 秒,而「爬了几格」是能数出来的、换台机器也不变的东西。顺带把树的最大深度和平均深度也量出来 —— 那才是这一章真正的主角。
// 换一把尺子:数「往上爬了多少步」
//
// ★ 为什么需要它:这一章的秒表在小数据上全是 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

本机实测(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 章连着三章那条的第四次 —— 而这一章的旋钮只有一个:树的形状。

同题对比:暴力:一格一格爬 vs 倍增
⚠ 这个生成器默认造的是「随机树」—— 你会看到左边(暴力)赢。想看到倍增赢,得自己去 genBig.cpp 里把形状改成 chain。这一步就是这一章的整个论点。
暴力:一格一格爬
倍增

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 章的快速幂是同一件事,只换了个主语

第 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 步那个样例说的事。

倍增表:up[k][v] = 从 v 往上跳 2^k 步落在谁头上
第 1 / 23 步
1234567
v=1v=2v=3v=4v=5v=6v=7
k=0·······
k=1·······
k=2·······
up[k][v] 的意思是「从 v 出发往上跳 2^k 步,落在谁头上」。先填第 0 层。

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正解

fast.cpp正解:倍增,预处理 O(n log n)、单次查询 O(log n)
// 正解:倍增求 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

8动画二:看那两段跳

★ 第一组(问 4 和 7)看完整的两段。 ★ 换到「祖先对:问 2 和 4」那一组 —— 对齐深度之后就结束了,第 ② 段根本没进。 ★ 再换到「一条链」,看深度差 5 是怎么拆成 101₂ 两下跳完的。

查询:先把深的那个提上来,再一起往上跳
第 1 / 7 步
1234567
u = 7(第 3 层)
v = 4(第 2 层)
深度差 = 1(二进制 1)
这一步跳了
LCA =
问 4 和 7 的 LCA。先让深的那个上来:7 在第 3 层,4 在第 2 层,差 1 层(二进制 1)。

9⚠ 建表那一趟:为什么必须用 BFS

depup[0] 要把整棵树走一遍。用递归 DFS 写最顺手,但这一章不能用 —— 因为这道题的最坏形状是一条链,而链正是递归最怕的东西。

deep.cpp两种建表方式:递归 DFS 会压多少层栈
页面上这组只有 6 个点,两种都没问题。下面那张表是本地拿 ./genBig <n> 1 chain 1 造的链跑出来的。
// ⚠ 把「建表那趟不能用递归 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

本机实测./genBig <n> 1 chain 1,链,根是随机挑的):

n 这棵树的最大深度 递归 DFS 建表 BFS 建表
10 万 64 578 压了 64 579 层,没炸
20 万 110 416 压了 110 417 层,没炸
25 万 211 936 段错误
50 万 431 264 段错误
⚠ 和第 30 章是同一个坑,只是这一次躲不开

本机默认栈 8 MB,一个栈帧几十个字节 —— 二十来万层正好用完。

第 30 章讲图上 DFS 时量过一模一样的账(50 万点的图,285 380 层就段错误)。 不同的是:那一章的最坏情况要碰运气,这一章的最坏情况就是题目的标准数据 —— 一条 50 万点的链,是出题人一定会造的。

建表一律用 BFS(或者手写栈)。 ★ 两条路算出来的 dep 一模一样,差别只在「谁来记住还没做完的事」: 递归让操作系统的栈记(8 MB 封顶),BFS 让你自己的队列记(队列在堆上,堆大得多)。

10★ 对拍:五个写错的版本,外加一份试金石

对拍器
★ 生成器不给档位时跑的就是最终档(第 12 步那张表里的档位 3):编号打乱 + 根随机 + 形状混着来 + 询问里塞祖先对。
// 正解:倍增求 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 随机就抓(差一层)
wrongLogLOG 写死成 5 ★★★ 0 / 300 对拍原理上抓不到 —— 见下一步
五个错误版本 + 一份试金石(点开看)
wrongDepth.cpp④ 距离少乘了 2
wrongRootOne.cpp① 咬定根就是 1 号点
wrongNoSame.cpp② 忘了判 u == v
wrongLast.cpp③ 跳完忘了再上一步
wrongLog.cpp⑤ ★ LOG 开小了 —— 小数据上它是对的
wrongAlwaysRoot.cpp★ 试金石:咬定 LCA 永远是根

★ 每一份的文件头都写着「它靠什么现形」—— 那不是注释,那是下一步调生成器的依据。

11★★★ LOG 那一课:对拍看不见,随机的大数据也看不见

LOG 要满足 2^LOG > n。写死成 5 会怎样?

在深度不到 32 的树上,它一个字节都不错。

于是它成了本书第三个「对拍看不见」的错法 —— 而原因和前两个都不一样

为什么对拍抓不到
第 48 章 wrongSlow 只影响复杂度,答案永远对
第 50 章 wrongMemset 只影响复杂度,答案永远对
★ 本章 wrongLog 在你对拍的那个规模上,它是真的对
⚠⚠ 于是「造一组大数据跑一次」也还不够 —— 本机实测
拿什么数据去打它 200 个询问里错了几个
对拍(./genn ≤ 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
★★★ 第一行:顺手写法里,两个顺手凑在一起,把 ①Root 藏成了精确的 0

「顺手写法」是两件事同时发生的:

  • 造树时让父亲的编号一定比自己小(因为 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 倍的数。

gen.cpp(七个档位)五个错法各要什么,文件头一条条写着

13★ 倍增不只是 LCA:同一张表,换个东西记

小题:树的每条边带一个权。q 个询问,每次给 u v k,输出两个数: ① u 往上数第 k 个祖先是谁(不存在输出 0);② uv 路径上最大的那条边权。

输入

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 94 的深度只有 2,问它第 3 个祖先 —— 没有,输出 0; 而 47 的路径是 4→2→1→3→6→7,边权 2、5、9、1、8,最大的是 9

★★ 这两问和 LCA 用的是同一张表

第 ① 问就是「对齐深度」那一段单独拿出来:把 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 可交换可结合,换成「路径上第二大的边权」就不行了。

ext.cpp倍增:up 表旁边再挂一张 mx 表
// ★★ 倍增不只是 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

300 轮对拍实测(标准答案是 extBrute.cpp,一格一格爬):

故意写错的地方 顺手档 ★ 最终档
extWrongMax:对齐深度那一段忘了 max 277 / 300 ⚠ 271 / 300
extWrongKth:忘了判「跳出树外」 201 / 300 260 / 300

★ 最终档对 ① 是 −6,对 ② 是 +59(它把 k 有时故意取得比深度还大)—— 净胜,所以留下。

⚠ 第 ② 个错法,是第 4 步那个「方便约定」的账单

extWrongKth 少写了一句 if (k > dep[u]) return 0;

因为我们把根的父亲设成了它自己,跳过头既不会越界也不会崩 —— 它会安安静静停在根上,然后把根当成答案交出去。

★★ 省掉的边界判断,迟早要还。 那个约定确实让主线代码干净了(不用到处判「跳出树外了吗」), 但它同时把一类错误从崩溃变成了错答案 —— 崩溃你一眼就看见,错答案要靠对拍。 这笔账两头都要知道,不能只记住「这样写方便」。

拓展那一节的三份代码(点开看)
extBrute.cpp标准答案:一格一格爬
extWrongMax.cpp① 对齐深度那一段忘了 max
extWrongKth.cpp② 忘了判「跳出树外」
extGen.cpp两个档位

14这一章没讲的

⚠ 求 LCA 还有别的路,本书只讲了倍增
做法 预处理 单次查询 本书讲了吗
一格一格往上爬 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 章快速幂共用同一个直觉。别的做法更快,但只解一道题。

★ 顺带一句考场实话:nq 都不大(比如 10⁴ 以内)时, 第 2 步那份暴力就够了 —— 它十行,写不错。第 3 步已经量给你看了。

15自测

自测清单0 / 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。★ 放在这儿是想说明:不是每道树上题都要上倍增
这一章我卡在哪(过一个月回来看,这几行比整章正文都值钱)
这一章记住三句话
  1. 把「往上跳多少步」拆成二进制 —— up[k][v] = up[k-1][up[k-1][v]], 和第 42 章快速幂是同一件事换了个主语。查询是两段完全不同的循环: 先对齐深度,再「只在不一样时」一起跳,最后再上一步
  2. ⚠⚠ 树的形状决定一切。 同一份暴力,随机树上平均爬 40.50 格、链上 60 573.56 格 (差 1496 倍);而 n = 10⁵ 的随机树深度只有 34 —— 所以「暴力慢」这句话在你顺手造的树上是站不住的,建表爆栈那件事也一样。
  3. ★★★ 「对拍抓到了」不等于「真实规模上覆盖到了」。 LOG 开小的那个 bug,对拍 0 / 300、随机的 10 万点大数据 0 / 200, 只有又大又深的数据才抓得到;而「一个点是另一个点的祖先」这个性质, 小树上白送 59.6%,10 万点上只剩 0.042%。每一条都要问一句:在多大的数据上?