0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库(原题那张图也存了)。
转录自洛谷 P3379,日期见页头。两边不一致时信原站。
题目描述
如题,给定一棵有根多叉树,请求出指定两个点之间最近的公共祖先。
输入格式
第一行包含三个正整数 N, M, S,分别表示树的结点个数、询问的个数和树根结点的序号。
接下来 N−1 行每行包含两个正整数 x, y,表示 x 结点和 y 结点之间有一条直接连接的边
(数据保证可以构成树)。
接下来 M 行每行包含两个正整数 a, b,表示询问 a 结点和 b 结点的最近公共祖先。
输出格式
输出包含 M 行,每行包含一个正整数,依次为每一个询问的结果。
数据范围
- 对于 30% 的数据,
N ≤ 10,M ≤ 10。 - 对于 70% 的数据,
N ≤ 10000,M ≤ 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):

第一次询问 2, 4 ⇒ 4;第二次 3, 2 ⇒ 4;第三次 3, 5 ⇒ 1;
第四次 1, 2 ⇒ 4;第五次 4, 5 ⇒ 4。
1★ 骨架第 51 章已经写完了 —— 这一页做的是「把题面那几行数字乘一遍」
第 51 章第 7 步那份 fast.cpp 就是照这道题写的(只是它多打了一个距离)。
所以这一页不重讲倍增是什么,它只做一件事:把真题题面上每一行数字对一遍。
| 正文那份 | 这道真题 | |
|---|---|---|
| 「暴力慢不慢」 | 第 3 步量的是随机树:一点都不慢 | ⚠⚠ 题面末尾写着「加了两组数据卡掉了暴力跳」 |
| 建表怎么走 | 第 9 步说了要 BFS | ⚠ N 到 5×10⁵,一条链递归下去必爆栈 |
| 谁在吃时限 | —— | ★★★ 不是读入,是那张 40 MB 的表怎么摆(第 ⑤ 步) |
★ 而题面自己点名了一处:「不保证 a ≠ b」 —— 那正是正文 wrongNoSame.cpp 那个坑;
⚠ 还点名了一处很多人会看漏的:根是输入给的 S,不是 1 号点。
// 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;}点「运行 ▶」看结果
2★★★ 第一版:一步一步往上爬 —— 而它是被出题人「点名」干掉的
第 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¹⁰ 步。
其实一行就能算:那个循环走的步数恰好等于 u 到 v 的距离 ——
对齐阶段走 |dep u − dep v| 步,一起走那段两人各走 min − dep[lca] 步,
加起来正好是 dist(u,v)。⇒ 拿倍增求一次 LCA 就把它数完了。
3⚠ 三个题面上写着的坑
少了那一句,第 ② 段循环一次都不会跳,于是返回 up[0][u] —— 正确答案的父亲。
⚠ 可正解里写着「根的父亲是它自己」(up[0][root] = root)
⇒ 当 LCA 恰好是根的时候,「父亲」就是它本人,这个 bug 自己被抹平了。
★ 官方样例正好演了这件事:五个询问里有三个的 LCA 是根 4,
另外两个(3 2 和 3 5)不是祖先对 ⇒ 样例整组放过它。
⇒ 触发条件因此是两层的:存在祖先对,而且那个祖先不是根。
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⚠ 两个「答案完全正确、只是过不去」的版本
顶格 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 倍
// 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;}点「运行 ▶」看结果
顶格 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★ 对拍:五个错法 + 一份「一律输出根」
| 档位 | ①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★ 哪一版就已经能过了
顶格五种形状里最难受的那一档(本机实测,时限 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 倍)。 ⇒ 而这两句话,光看复杂度一个都看不出来。