题单 · 习题解析

洛谷 P3379 【模板】最近公共祖先(LCA)

★★★ 倍增本身[第 51 章](/ch/51-lca/)已经讲完,这一页量出来的是它的**两个主语** —— 「暴力慢不慢」的主语是**树的形状**、「倍增快不快」的主语是**那张表的下标顺序**,而这两句话**光看复杂度一个都看不出来**;★★★ 题面末尾那行「2021/10/4 加了两组数据卡掉了暴力跳」翻译过来就是「在那之前这道模板题的数据里没有链」——同样顶格 `N = M = 5×10⁵`,暴力在**链**上要爬 **833 亿**步、在**随机树**上只要 **1175 万**步(**差 7091 倍**),⚠ 而在顶格随机树上暴力只花 0.20 秒、**比正解还快 7.5 倍**;★★★ 而真正吃掉时限的**不是读入**(10⁶ 行、2×10⁶ 个整数,快读只要 **20 毫秒 = 时限的 1%**),是**那张 40 MB 的表怎么摆**:把 `up[LOG][MAXN]` 换成 `up[MAXN][LOG]`(算法一个字不改、**读表次数一个不差**、答案逐字节相同),顶格星形树上查询从 **1303 毫秒**掉到 **116 毫秒(10.66 倍)** ⇒ 又一次[两把尺子打架](/sol/p1074/),而这次机理一句话说得完:**差的不是做了多少活,是那些活离得多远**;⚠⚠ 而**建表那一侧正好反过来**(A 连续写 22.4 ms、B 跨 80 字节 160.6 ms,慢 7.2 倍)⇒ **同一个下标顺序在两侧的最优选择是相反的**;⇒ ★★ 于是这道题有**两条分数线**:会不会倍增 / 那张表怎么摆 —— 照抄正文那份**能过,可最坏形状上只剩 1.15 倍余量**(1.74 / 2 秒),转置之后是 **10.0 倍**;★★★ 外加一张三种 0 同框的对拍表:**「WA 但档位够不着」(LOG 取小了,要深度 > 131072 而对拍 n ≤ 30)/「RE」(递归建表,顶格链 431 264 层段错误)/「TLE」(默认 cin)** 四档全是精确的 0 ⇒ **看到一列 0,先问它是哪一种:加轮数、换档位、换尺子,三种救法完全不同**;★ 而「忘了判 `u == v`」有一半被「根的父亲是它自己」抹平了(LCA 恰好是根时它自愈)⇒ 触发条件是两层的,**官方样例整组放过它**

原题:洛谷 P3379出自 第 51 章 LCA 与倍增:把「往上跳多少步」拆成二进制 的题单题面本地存档:2026-09-11
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库(原题那张图也存了)。

转录自洛谷 P3379,日期见页头。两边不一致时信原站。

题目描述

如题,给定一棵有根多叉树,请求出指定两个点之间最近的公共祖先。

输入格式

第一行包含三个正整数 N, M, S,分别表示树的结点个数、询问的个数和树根结点的序号

接下来 N−1 行每行包含两个正整数 x, y,表示 x 结点和 y 结点之间有一条直接连接的边 (数据保证可以构成树)。

接下来 M 行每行包含两个正整数 a, b,表示询问 a 结点和 b 结点的最近公共祖先。

输出格式

输出包含 M 行,每行包含一个正整数,依次为每一个询问的结果。

数据范围

  • 对于 30% 的数据,N ≤ 10M ≤ 10
  • 对于 70% 的数据,N ≤ 10000M ≤ 10000
  • 对于 100% 的数据,1 ≤ N, M ≤ 5×10⁵1 ≤ x, y, a, b ≤ N不保证 a ≠ b

⚠ 题面末尾还有一行:「2021/10/4 数据更新 @fstqwq:应要求加了两组数据卡掉了暴力跳。」

时限 2 秒,内存 524288 KB(512 MiB)。

输入输出样例

输入

5 5 4
3 1
2 4
5 1
1 4
2 4
3 2
3 5
1 2
4 5

输出

4
4
1
4
4

该树结构如下(根是 4):

P3379 样例的树结构

第一次询问 2, 44;第二次 3, 24;第三次 3, 51; 第四次 1, 24;第五次 4, 54

1★ 骨架第 51 章已经写完了 —— 这一页做的是「把题面那几行数字乘一遍」

★★ 乘完之后有三处和正文不一样

第 51 章第 7 步那份 fast.cpp 就是照这道题写的(只是它多打了一个距离)。 所以这一页不重讲倍增是什么,它只做一件事:把真题题面上每一行数字对一遍

正文那份 这道真题
「暴力慢不慢」 第 3 步量的是随机树:一点都不慢 ⚠⚠ 题面末尾写着「加了两组数据卡掉了暴力跳
建表怎么走 第 9 步说了要 BFS N 到 5×10⁵,一条链递归下去必爆栈
谁在吃时限 —— ★★★ 不是读入,是那张 40 MB 的表怎么摆(第 ⑤ 步)

★ 而题面自己点名了一处:「不保证 a ≠ b —— 那正是正文 wrongNoSame.cpp 那个坑; ⚠ 还点名了一处很多人会看漏的:根是输入给的 S,不是 1 号点

p3379.cpp★ 正解:倍增 + BFS 建表 + 快读
// P3379【模板】最近公共祖先(LCA)—— 正解:倍增,预处理 O(n log n)、单次查询 O(log n)
//
// ★ 骨架就是[第 51 章](/ch/51-lca/)第 7 步那份 `fast.cpp`,一个字都没改(那道题就是照这道题写的)。
// 这一页要做的是把**真题题面上那几行数字**一行一行乘一遍,而乘完之后有三处和正文不一样:
//
// ① ⚠⚠ **暴力过不去了** —— 正文第 3 步量的是「随机树上暴力一点都不慢」,
// 而这道题的题面末尾写着:**「2021/10/4 数据更新 @fstqwq:应要求加了两组数据卡掉了暴力跳。」**
// ⇒ 那句「不慢」的主语是**随机树**,而出题人专门补了链。
// ② ⚠ **建表必须 BFS**:`n` 到 5×10⁵,一条链递归下去必爆栈(正文第 9 步)。
// ③ ⚠⚠ **真正吃掉时限的不是读入,是那张 40 MB 的表怎么摆** ——
// `n + m` 到 10⁶ 行、约 2×10⁶ 个整数,看着像一道读入题,
// 可快读实测只要 **20 毫秒**(时限 2 秒的 1%);
// 而 `up[LOG][MAXN]` 这个下标顺序在顶格星形树上光查询就要 **1162 毫秒**
// —— 把两个下标换个顺序(`p3379T.cpp`)是 **113 毫秒**。解析页第 ⑤ 步量了这件事。
//
// ⚠ 而题面自己点名了一处:**「不保证 a ≠ b」** —— 那正是正文 `wrongNoSame.cpp` 那个坑。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 500005;
const int LOG = 20; // 2^19 = 524288 > 5×10⁵,取 20 留一格
int up[LOG][MAXN];
int dep[MAXN];
int head_[MAXN], nxt_[MAXN * 2], to_[MAXN * 2], ecnt;
int n, m, root;
inline void addEdge(int u, int v) { to_[++ecnt] = v; nxt_[ecnt] = head_[u]; head_[u] = ecnt; }
/* ---- 快读:2×10⁶ 个整数,这道题上它是分数线的一部分 ---- */
static char ibuf[1 << 22];
static size_t ilen = 0, ipos = 0;
inline int gc() {
if (ipos == ilen) { ilen = fread(ibuf, 1, sizeof(ibuf), stdin); ipos = 0; if (!ilen) return -1; }
return ibuf[ipos++];
}
inline int readInt() {
int c = gc(), x = 0;
while (c < '0' || c > '9') c = gc();
while (c >= '0' && c <= '9') { x = x * 10 + (c - '0'); c = gc(); }
return x;
}
/** BFS 一趟求出 dep 和 up[0],再把 up 的其余层推出来 */
void build() {
vector<char> vis(n + 1, 0);
vector<int> que;
que.reserve(n);
dep[root] = 0;
up[0][root] = root; // ⚠ 根的父亲是它自己
vis[root] = 1;
que.push_back(root);
for (size_t i = 0; i < que.size(); i++) {
int u = que[i];
for (int e = head_[u]; e; e = nxt_[e]) {
int v = to_[e];
if (vis[v]) continue;
vis[v] = 1;
dep[v] = dep[u] + 1;
up[0][v] = u;
que.push_back(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; // ⚠ 一个是另一个的祖先(题面明说「不保证 a ≠ b」)
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 正下方,再上一步才是答案
}
/* ---- 快写 ---- */
static char obuf[1 << 22];
static size_t opos = 0;
inline void writeInt(int x) {
if (opos + 16 > sizeof(obuf)) { fwrite(obuf, 1, opos, stdout); opos = 0; }
char t[12];
int k = 0;
if (!x) t[k++] = '0';
while (x) { t[k++] = (char)('0' + x % 10); x /= 10; }
while (k) obuf[opos++] = t[--k];
obuf[opos++] = '\n';
}
int main() {
n = readInt(); m = readInt(); root = readInt();
for (int i = 0; i < n - 1; i++) {
int u = readInt(), v = readInt();
addEdge(u, v);
addEdge(v, u);
}
build();
for (int i = 0; i < m; i++) {
int u = readInt(), v = readInt();
writeInt(lca(u, v));
}
fwrite(obuf, 1, opos, stdout);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2★★★ 第一版:一步一步往上爬 —— 而它是被出题人「点名」干掉的

p3379Brute.cpp✗ 第一版:先提到一样深,再一起往上走
★★★「暴力一点都不慢」那句话的主语是「随机树」,而出题人 2021 年补了链

第 51 章第 3 步量过一件反直觉的事:随机树的深度只有 log 级, 于是「一步一步爬」在随机数据上根本慢不起来。这道题把那句话的主语摆到了台面上:

顶格 N = M = 5×10⁵,只换树的形状 最大深度 ✗ 暴力一共要爬多少步
star(一个根挂满儿子) 2 999 997
binary(完全二叉树) 34 15 943 301
rand(父亲 = 随便一个更早的点) 41 11 753 034
cater(毛毛虫) 215 632 41 669 061 988
chain(一条链) 431 264 83 337 125 198

⇒ ★★★ 同样是「顶格」,链上要爬的步数是随机树的 7091 倍。

本机实测(把规模缩到 N = M = 5×10⁴,也就是 1% 的工作量):链上 2.93 秒 ⇒ 按 O(NM) 外推回顶格是 约 293 秒,而时限 2 秒 —— 超时 147 倍。 ⚠ 而在顶格随机树上,暴力只要 0.20 秒 —— 比正解还快 7.5 倍。

⇒ ★★ 题面那行「2021/10/4 加了两组数据卡掉了暴力跳」,翻译过来就是: 在那之前,这道模板题的数据里没有链。

★ 顺带一条:那个「爬多少步」不用真去爬也能数出来

p3379Count.cpp 要报这个数,可在顶格的链上真去爬要 8.33×10¹⁰ 步。 其实一行就能算:那个循环走的步数恰好等于 uv 的距离 —— 对齐阶段走 |dep u − dep v| 步,一起走那段两人各走 min − dep[lca] 步, 加起来正好是 dist(u,v)。⇒ 拿倍增求一次 LCA 就把它数完了。

3⚠ 三个题面上写着的坑

p3379Root1.cpp✗ 错法①:无视题面给的 S,从 1 号点建树
p3379Same.cpp✗ 错法②:忘了在两段循环之间判 u == v
p3379Log.cpp✗ 错法③:LOG 取成 17
★★ 而「忘了判 u == v」有一半被根的自环救掉了

少了那一句,第 ② 段循环一次都不会跳,于是返回 up[0][u] —— 正确答案的父亲

⚠ 可正解里写着「根的父亲是它自己」(up[0][root] = root) ⇒ 当 LCA 恰好是根的时候,「父亲」就是它本人,这个 bug 自己被抹平了。

★ 官方样例正好演了这件事:五个询问里有三个的 LCA 是根 4, 另外两个(3 23 5)不是祖先对 ⇒ 样例整组放过它。 ⇒ 触发条件因此是两层的:存在祖先对,而且那个祖先不是根。

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

N 到 5×10⁵ ⇒ 一条链的深度可以到 5×10⁵ − 1,而 2^17 = 131072 不够。 可要抓住它,数据得又大又深

最大深度 LOG = 17 够吗
对拍那种小数据(n ≤ 30 ≤ 29 ✓ 够
顶格随机树n = 5×10⁵ 41
顶格完全二叉树 34 ✓ 够
顶格 431 264 不够

⇒ ★★★ 这就是第 51 章第 11 步那一课的真题版: 它在对拍的四个档上全是精确的 0,在顶格随机树上也是 0 —— 只有链能抓到它。

4⚠ 两个「答案完全正确、只是过不去」的版本

p3379Dfs.cpp✗ 错法④:递归 DFS 建表(顶格链上段错误)
p3379Cin.cpp✗ 错法⑤:默认的 cin / cout
★★ 本机实测
顶格 rand 顶格 chain
★ 正解 1.50 秒 0.67 秒
✗ 递归建表 和正解逐字节相同 段错误(退出码 139)
✗ 默认 cin / cout 1.78 秒 1.32 秒

★ 递归那一版的深度就是树的深度:顶格链 431 264 层(上面那张表)。 ⚠ 而「几层会炸」依赖 ulimit -s,换台机器就变 ⇒ 这一页只钉「深度是多少」这个机器无关的数,不钉「几层会炸」。

⚠⚠ 而默认 cin 那一版最值钱的是它没有证明的那件事:它多花 0.28 秒, 在随机树上(1.78 / 2 秒)还过得去 —— 可正解在最坏形状(star)上本来就只剩 0.26 秒余量 ⇒ 加上去就只剩 0.13 秒(1.87 / 2 秒,第 ⑦ 步那张表)。

5★★★ 真正吃掉时限的:那张 40 MB 的表,两个下标换个顺序差 10 倍

p3379Count.cpp★ 分段计时 + 数「那张表被读了多少次」
它把同一份数据喂给两种存法,两版的每一步都一模一样 —— 连读表的次数都一个不差。
// P3379 的三笔账:读入 / 建表 / 查询各花多少,以及**那张表被读了多少次**(机器无关)
//
// 用法:./p3379Count <csv|table> < 一份输入
//
// ★ 它把同一份数据喂给**两种存法**:
// A = up[LOG][MAXN](正文那份,固定 k 时 v 连续)
// B = up[MAXN][LOG](转置,固定 v 时 k 连续)
// 两版的**每一步都一模一样**,连「读了多少次 up」都一个不差 ——
// ⇒ 于是这一份能把[两把尺子打架](/sol/p1074/)量得干干净净:
// **次数完全相同,秒表差好几倍,而差的是「那些活离得多远」。**
#include <bits/stdc++.h>
#include <sys/time.h>
using namespace std;
static double nowMs() { struct timeval tv; gettimeofday(&tv, nullptr); return tv.tv_sec * 1000.0 + tv.tv_usec / 1000.0; }
const int MAXN = 500005;
const int LOG = 20;
static int upA[LOG][MAXN];
static int upB[MAXN][LOG];
static int dep[MAXN];
static int head_[MAXN], nxt_[MAXN * 2], to_[MAXN * 2], ecnt;
static int n, m, root;
static long long reads; // 读了多少次 up[][]
static char ibuf[1 << 22];
static size_t ilen = 0, ipos = 0;
static inline int gc() {
if (ipos == ilen) { ilen = fread(ibuf, 1, sizeof(ibuf), stdin); ipos = 0; if (!ilen) return -1; }
return ibuf[ipos++];
}
static inline int readInt() {
int c = gc(), x = 0;
while (c < '0' || c > '9') c = gc();
while (c >= '0' && c <= '9') { x = x * 10 + (c - '0'); c = gc(); }
return x;
}
static void bfs() {
vector<char> vis(n + 1, 0);
vector<int> que;
que.reserve(n);
dep[root] = 0; upA[0][root] = root; vis[root] = 1; que.push_back(root);
for (size_t i = 0; i < que.size(); i++) {
int u = que[i];
for (int e = head_[u]; e; e = nxt_[e]) {
int v = to_[e];
if (vis[v]) continue;
vis[v] = 1; dep[v] = dep[u] + 1; upA[0][v] = u; que.push_back(v);
}
}
}
template <bool COUNT>
static int lcaA(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
int d = dep[u] - dep[v];
for (int k = 0; k < LOG; k++) { if (COUNT) reads++; if ((d >> k) & 1) u = upA[k][u]; }
if (u == v) return u;
for (int k = LOG - 1; k >= 0; k--) { if (COUNT) reads += 2; if (upA[k][u] != upA[k][v]) { u = upA[k][u]; v = upA[k][v]; } }
return upA[0][u];
}
static int lcaB(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
int d = dep[u] - dep[v];
for (int k = 0; k < LOG; k++) if ((d >> k) & 1) u = upB[u][k];
if (u == v) return u;
for (int k = LOG - 1; k >= 0; k--) if (upB[u][k] != upB[v][k]) { u = upB[u][k]; v = upB[v][k]; }
return upB[u][0];
}
int main(int argc, char** argv) {
string mode = argc > 1 ? argv[1] : "table";
double t0 = nowMs();
n = readInt(); m = readInt(); root = readInt();
for (int i = 0; i < n - 1; i++) {
int u = readInt(), v = readInt();
to_[++ecnt] = v; nxt_[ecnt] = head_[u]; head_[u] = ecnt;
to_[++ecnt] = u; nxt_[ecnt] = head_[v]; head_[v] = ecnt;
}
vector<pair<int, int> > qs(m);
for (int i = 0; i < m; i++) { qs[i].first = readInt(); qs[i].second = readInt(); }
double tRead = nowMs() - t0;
bfs();
int maxDep = 0;
for (int i = 1; i <= n; i++) maxDep = max(maxDep, dep[i]);
/* 建表:两种存法各一次 */
double t1 = nowMs();
for (int k = 1; k < LOG; k++) for (int v = 1; v <= n; v++) upA[k][v] = upA[k - 1][upA[k - 1][v]];
double buildA = nowMs() - t1;
for (int v = 1; v <= n; v++) upB[v][0] = upA[0][v];
double t2 = nowMs();
for (int k = 1; k < LOG; k++) for (int v = 1; v <= n; v++) upB[v][k] = upB[upB[v][k - 1]][k - 1];
double buildB = nowMs() - t2;
/* 查询:两种存法各一次;顺手把「读了多少次 up」数出来(两版必然相同) */
long long sumA = 0, sumB = 0;
double t3 = nowMs();
for (int i = 0; i < m; i++) sumA += lcaA<false>(qs[i].first, qs[i].second);
double queryA = nowMs() - t3;
double t4 = nowMs();
for (int i = 0; i < m; i++) sumB += lcaB(qs[i].first, qs[i].second);
double queryB = nowMs() - t4;
reads = 0;
for (int i = 0; i < m; i++) lcaA<true>(qs[i].first, qs[i].second);
/* 暴力「一步一步爬」要走多少步 —— 机器无关
★ 不用真去爬:那个循环走的步数**恰好等于 u 到 v 的距离**
(对齐阶段走 |dep u − dep v| 步,一起走那段两人各走 min − dep[lca] 步,加起来正好是 dist)。
⇒ 于是顶格的链上也能一秒钟数完,而真去爬要 2.5×10¹¹ 步。 */
long long climb = 0;
for (int i = 0; i < m; i++) {
int u = qs[i].first, v = qs[i].second;
int l = lcaB(u, v);
climb += (long long)(dep[u] - dep[l]) + (dep[v] - dep[l]);
}
char buf[64];
vector<pair<string, string> > out;
auto put = [&](const string& k, double x, int prec = 1) {
snprintf(buf, sizeof(buf), "%.*f", prec, x); out.push_back(make_pair(k, string(buf)));
};
out.push_back(make_pair("n", to_string(n)));
out.push_back(make_pair("m", to_string(m)));
out.push_back(make_pair("maxdep", to_string(maxDep)));
out.push_back(make_pair("same", string(sumA == sumB ? "1" : "0")));
out.push_back(make_pair("reads", to_string(reads)));
out.push_back(make_pair("climb", to_string(climb)));
put("ms_read", tRead);
put("ms_buildA", buildA);
put("ms_buildB", buildB);
put("ms_queryA", queryA);
put("ms_queryB", queryB);
put("ratio_query", queryB > 0 ? queryA / queryB : 0, 2);
if (mode == "csv") for (size_t i = 0; i < out.size(); i++) printf("%s,%s\n", out[i].first.c_str(), out[i].second.c_str());
else for (size_t i = 0; i < out.size(); i++) printf(" %-12s %s\n", out[i].first.c_str(), out[i].second.c_str());
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
p3379T.cpp★ 只把 up[LOG][MAXN] 换成 up[MAXN][LOG]
★★★ 顶格分段实测(本机,2026-09-11,3 次取中位数)
顶格 N = M = 5×10⁵ 读入 建表 A / B 查询 A / B 查询 A÷B
star 19.3 ms 13.0 / 47.0 1303.0 / 116.3 10.66
rand 20.6 ms 14.2 / 48.4 1038.9 / 172.4 6.63
binary 19.7 ms 15.0 / 48.5 1191.4 / 174.0 6.85
cater 19.0 ms 21.0 / 111.7 1193.9 / 502.8 2.37
chain 21.1 ms 22.4 / 160.6 537.8 / 370.1 1.45

(A = up[LOG][MAXN],正文那份;B = up[MAXN][LOG],只把两个下标换个顺序。)

⇒ ★★★ 这道题看着像一道读入题(10⁶ 行、2×10⁶ 个整数), 可快读只花 20 毫秒 —— 时限的 1%。真正吃掉时限的是查询,而查询的成本取决于那张表怎么摆。

★★ 而 star 那一行最能说明问题:树只有两层、一次跳都不跳, 可 A 版照样要 1303 毫秒 —— 因为第 ② 段那 20 层比较,每一层都要跨 2 MB 去读两个数; B 版把同一个点的 20 层放在一起,一条 cache line 就装下了。

★★★ 两把尺子又打架,而这次原因说得干干净净
读了多少次 up[][] 查询耗时(star
A = up[LOG][MAXN] 29 999 880 1303.0 ms
B = up[MAXN][LOG] 29 999 880(一个不差) 116.3 ms

同样的算法、同样的步数、同样的答案(1200 轮 + 五种顶格形状逐字节相同),秒表差 10.66 倍。

这是第 16 章 P1074(次数一样、秒表差 9.5 倍)那条的又一次, ★ 而这次机理是能一句话说完的:差的不是做了多少活,是那些活离得多远。

⚠⚠ 而建表那一侧正好反过来:A 的两层循环是连续写(chain 上 22.4 ms), B 是每次跨 80 字节(160.6 ms,慢 7.2 倍)。 ⇒ ★★ 同一个下标顺序,在「建表」和「查询」两侧的最优选择是相反的 —— 而这道题查询那边赢得多得多(m = n,且查询要读 20 层、建表只写一遍)。

6★ 对拍:五个错法 + 一份「一律输出根」

p3379AllRoot.cpp★ 试金石:什么都不算,一律输出根 S
p3379Gen.cpp★ 生成器:四个档位
★★★ 300 轮 × 四档(本机实测,2026-09-11)
档位 ①Root1 ②Same ③Log ④Dfs ⑤Cin 试金石
0 顺手写法(随机树、根固定为 1、询问随机两点) 0 275 0 0 0 295
1 + 编号打乱 + 根随机 274 290 0 0 0 300
2 + 一半询问取「祖先对」 0 298 0 0 0 298
3 最终档 = 1 + 2 + 形状随机 258 299 0 0 0 299

★★★ 这张表最值钱的是右边那三列:四档全是精确的 0,而三个 0 是三件完全不同的事

它到底怎么了 对拍为什么看不见
③Log 会答错(WA) 档位到不了那条线:要深度 > 131072,而对拍的 n ≤ 30
④Dfs 会崩(RE) 答案永远对 —— 它只是栈不够
⑤Cin 会超时(TLE) 答案永远对 —— 它只是慢

⇒ ★★★ 一张表里同时出现「WA 但档位够不着」「RE」「TLE」三种 0 —— 比第 48 章 P3435(两类并排)和 P2375(同一行藏三种)都齐。 ⇒ 看到一列全是 0,先问它是哪一种:加轮数、换档位、换尺子,三种救法完全不同。

★ 而 ①Root1 在顺手档是结构性的精确的 0:那一档的根就是 1 —— ⚠ 而「根定成 1」几乎是写树上题生成器的本能。

7★ 哪一版就已经能过了

★★ 结论:照抄正文那份就能过 —— 但要知道它的余量只剩 1.05 倍

顶格五种形状里最难受的那一档(本机实测,时限 2 秒):

版本 最坏那一档 余量
✗ 一步一步爬 chain 约 293 秒(外推) 超时 147 倍
✗ 递归建表 chain 段错误
✗ 默认 cin / cout star 1.87 秒 1.07 倍
★ 正解(照抄正文那份) star 1.74 秒 1.15 倍
★★ 把表转置(up[MAXN][LOG] star 0.20 秒 10.0 倍

⇒ ★★★ 这道题有两条分数线:第一条在「会不会倍增」上,第二条在「那张表怎么摆」上, 而第二条一个算法字都不用改

★ 题面那两个部分分档(N,M ≤ 10 的 30% / N,M ≤ 10⁴ 的 70%)是给暴力留的: 10⁴ × 10⁴ 在链上是 10⁸ 步,本机 0.1 秒量级 ⇒ 暴力稳拿 70 分

★ 一句话带走

倍增本身第 51 章已经讲完,这一页量出来的是它的两个「主语」 —— 「暴力慢不慢」的主语是树的形状(链上 7091 倍), 「倍增快不快」的主语是那张表的下标顺序(星形上 10.66 倍)。 ⇒ 而这两句话,光看复杂度一个都看不出来