0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P5903,日期见页头。两边不一致时信原站。
题目背景
本题仅作为长链剖分求树上 k 级祖先评测用,不保证卡掉了其他复杂度不正确的做法。
题目描述
给定一棵 n 个点的有根树。
有 q 次询问,第 i 次询问给定 xᵢ, kᵢ,要求点 xᵢ 的 kᵢ 级祖先,答案为 ansᵢ。
特别地,ans₀ = 0。
本题中的询问将在程序内生成。给定一个随机种子 s 和一个随机函数 get(x):
#define ui unsigned int
ui s;
inline ui get(ui x) {
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
return s = x;
}
你需要按顺序依次生成询问。设 dᵢ 为点 i 的深度,其中根的深度为 1。
对于第 i 次询问,xᵢ = ((get(s) xor ansᵢ₋₁) mod n) + 1,kᵢ = (get(s) xor ansᵢ₋₁) mod d_{xᵢ}。
输入格式
第一行三个整数 n, q, s。
第二行 n 个整数 f₁…fₙ,其中 fᵢ 表示 i 的父亲。特别地,若 fᵢ = 0,则 i 为根。
输出格式
一行一个整数,表示 xor 从 i = 1 到 q 的 i × ansᵢ。
数据范围
- 对于 20% 的数据,
n, q ≤ 10³。 - 对于 50% 的数据,
n, q ≤ 10⁵。 - 对于 100% 的数据,
2 ≤ n ≤ 5×10⁵,1 ≤ q ≤ 5×10⁶,1 ≤ s < 2³²。
时限 3 秒,内存 512000 KB(500 MiB)。
输入输出样例
输入
6 3 7 5 5 2 2 0 3
输出
1
x₁ = 4,k₁ = 1,ans₁ = 2;x₂ = 6,k₂ = 3,ans₂ = 5;x₃ = 3,k₃ = 0,ans₃ = 3。
⇒ 1×2 xor 2×5 xor 3×3 = 2 xor 10 xor 9 = 1。
★ 注意第三问:k₃ = 0 —— 0 级祖先就是它自己。
1★★★ 题面第一行就把话挑明了:这道题的正解不是倍增
「本题仅作为长链剖分求树上
k级祖先评测用,不保证卡掉了其他复杂度不正确的做法。」
出题人要收的是长链剖分:预处理 O(n log n)、每次询问 O(1)。
而倍增是每次询问 O(log n) —— q 到 5×10⁶ 时它慢一个 log。
⇒ 所以这一页不假装能教你写长链剖分(那超出了本书的范围),它做的是另一件更有用的事: 把「倍增在这道题上还剩多少余量」量出来 —— 而量完之后的答案很有意思: 看树长什么样(第 ③ 步)。
★ 而这道题真正会咬人的四处,一处都不在「怎么跳」上:
| 题面写在哪儿 | 后果 | |
|---|---|---|
⚠⚠ 随机函数必须一字不差(ui 是 unsigned) |
题面把代码原样给了你 | 换成 int ⇒ 在算另一串询问 |
⚠⚠ 强制在线(xor ansᵢ₋₁) |
那两个公式里 | 不能离线、不能排序、不能分组 |
| ⚠ 根的深度是 1 | 「其中根的深度为 1」 | ★★ 写成 0 ⇒ mod d[x] 是一次除以零 |
⚠ 输出要 long long |
i × ansᵢ |
最大 5×10⁶ × 5×10⁵ = 2.5×10¹² |
// P5903【模板】树上 K 级祖先 —— 用倍增写(★ 而题面自己说了:这道题的正解不是它)//// ⚠⚠ 题目背景第一行就把话挑明了:// 「**本题仅作为长链剖分求树上 k 级祖先评测用,不保证卡掉了其他复杂度不正确的做法。**」// ⇒ 出题人想收的是 **O(1) 查询**的长链剖分(预处理 O(n log n)、每次询问 O(1))。// 倍增是 **O(log n) 查询** ⇒ `q` 到 5×10⁶ 时它慢一个 log。// ⇒ 这一页要回答的就是那句话的分量:**倍增在这道题上到底还剩多少余量**(解析页第 ③ 步)。//// ★ 而这道题真正会咬人的四处,一处都不在「怎么跳」上:// ① ⚠⚠ **询问是程序内生成的,而且强制在线** —— `x_i` 和 `k_i` 都要异或上 `ans_{i−1}`// ⇒ **不能离线排序**,也不能先把 q 个询问读进来。// ② ⚠⚠ **那个随机函数必须一字不差地照抄**:`ui` 是 **unsigned**,// 三次移位是 `<<13 / >>17 / <<5`,而且 `get` 会**把 s 改掉**(`return s = x`)。// ③ ⚠ **根的深度是 1**(题面写死的)—— 因为 `k_i` 是对 `d[x_i]` 取模,// 根的深度要是写成 0,那儿就是一次**除以零**。// ④ ⚠⚠ **输出要 `long long`**:`i × ans_i` 最大 5×10⁶ × 5×10⁵ = **2.5×10¹²**。//// ⚠⚠ 存法**不能**照搬 [P3379](/sol/p3379/) 那一页量出来的结论(解析页第 ③ 步量了):// 那道题求 LCA 要**把 20 层全比一遍** ⇒ `up[MAXN][LOG]`(同一个点的 20 层挨在一起)赢 10 倍;// 而这道题只读 `k` 的那几个二进制位 —— 随机树上 `k ≤ 31` ⇒ **只碰得到最低 5 层**// ⇒ 反过来是 `up[LOG][MAXN]`(同一层的点挨在一起,只碰 10 MB)赢,端到端 0.77 vs 1.17 秒。// ⇒ ★★ 「换个存法快多少」的主语不是这张表,是**你会去读它的哪一部分**。
#include <bits/stdc++.h>using namespace std;
typedef unsigned int ui;
const int MAXN = 500005;const int LOG = 20;
int up[LOG][MAXN];int dep[MAXN]; // ⚠ ③ 根的深度是 1int fa[MAXN];int n, root;long long q;ui s;
/** ⚠ ② 题面给的随机函数,一个字都不能改 */inline ui getRnd(ui x) { x ^= x << 13; x ^= x >> 17; x ^= x << 5; return s = x;}
/* ---- 快读:输入只有 n + 3 个整数,可那也有 3.4 MB ---- */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 long long readInt() { int c = gc(); long long x = 0; while (c < '0' || c > '9') c = gc(); while (c >= '0' && c <= '9') { x = x * 10 + (c - '0'); c = gc(); } return x;}
int main() { n = (int)readInt(); q = readInt(); s = (ui)readInt(); vector<vector<int> > son(n + 1); for (int i = 1; i <= n; i++) { fa[i] = (int)readInt(); if (fa[i] == 0) root = i; else son[fa[i]].push_back(i); }
/* BFS 求 dep 和 up[·][0](⚠ 父亲编号不保证更小,所以不能顺着 1..n 递推)*/ { vector<int> que; que.reserve(n); dep[root] = 1; // ⚠ ③ 根的深度是 1 up[0][root] = root; // 根的父亲设成它自己,跳过头就停在根 que.push_back(root); for (size_t i = 0; i < que.size(); i++) { int u = que[i]; for (size_t j = 0; j < son[u].size(); j++) { int v = son[u][j]; 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]];
long long ans = 0; // ⚠ ④ i × ans_i 最大 2.5×10¹² ui last = 0; // ans_0 = 0 for (long long i = 1; i <= q; i++) { int x = (int)(((getRnd(s) ^ last) % (ui)n) + 1); ui k = (getRnd(s) ^ last) % (ui)dep[x]; int u = x; for (int b = 0; b < LOG; b++) // 把 k 拆成二进制,该跳哪几位就跳哪几位 if ((k >> b) & 1u) u = up[b][u]; last = (ui)u; ans ^= i * (long long)u; } printf("%lld\n", ans); return 0;}点「运行 ▶」看结果
2第一版:一步一步往上爬 k 次
这道题的四个坑(随机函数 / 强制在线 / 深度从 1 / long long)两版都得对,
而「怎么找 k 级祖先」这一步暴力压根没得错。
⇒ 于是对拍抓到的每一次不一致,都精确地指向那四个坑之一。
3★★★ 这道题最反直觉的一条:随机树上,「一步一步爬」比倍增还快
// P5903 的两笔账:两条路各做多少次基本动作(机器无关),以及那张表的两种存法差多少//// 用法:./p5903Count <csv|table> [q] < 一份输入(q 不给就用输入里的)//// ★ 这道题的 `k_i` 是**对深度取模**摇出来的 ⇒ 平均要跳的步数直接由树的形状决定:// 随机树深度只有 40 出头 ⇒ 平均爬 20 步;一条链就是平均爬 n/4 = 12.5 万步。// ⇒ 「一步一步爬」和「倍增」谁快,在这道题上**由数据说了算**。
#include <bits/stdc++.h>#include <sys/time.h>using namespace std;
typedef unsigned int ui;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]; // 转置([P3379](/sol/p3379/) 那一页量出来更快的那种)static int dep[MAXN], fa[MAXN];static int n, root;static long long q;static ui s;
static inline ui getRnd(ui x) { x ^= x << 13; x ^= x >> 17; x ^= x << 5; return s = x; }
int main(int argc, char** argv) { string mode = argc > 1 ? argv[1] : "table"; if (scanf("%d %lld %u", &n, &q, &s) != 3) return 0; if (argc > 2) q = atoll(argv[2]); ui s0 = s;
vector<vector<int> > son(n + 1); for (int i = 1; i <= n; i++) { if (scanf("%d", &fa[i]) != 1) return 0; if (fa[i] == 0) root = i; else son[fa[i]].push_back(i); } { vector<int> que; que.reserve(n); dep[root] = 1; fa[root] = root; upA[0][root] = root; que.push_back(root); for (size_t i = 0; i < que.size(); i++) { int u = que[i]; for (size_t j = 0; j < son[u].size(); j++) { int v = son[u][j]; dep[v] = dep[u] + 1; upA[0][v] = u; que.push_back(v); } } } for (int k = 1; k < LOG; k++) for (int v = 1; v <= n; v++) upA[k][v] = upA[k - 1][upA[k - 1][v]]; for (int v = 1; v <= n; v++) for (int k = 0; k < LOG; k++) upB[v][k] = upA[k][v];
int maxDep = 0; for (int i = 1; i <= n; i++) maxDep = max(maxDep, dep[i]);
/* ① 机器无关:暴力要爬多少步、倍增要跳多少次(先把询问摇一遍,顺手数出来) */ long long climb = 0, jumps = 0; { s = s0; ui last = 0; for (long long i = 1; i <= q; i++) { int x = (int)(((getRnd(s) ^ last) % (ui)n) + 1); ui k = (getRnd(s) ^ last) % (ui)dep[x]; climb += k; jumps += __builtin_popcount(k); int u = x; for (int b = 0; b < LOG; b++) if ((k >> b) & 1u) u = upB[u][b]; last = (ui)u; } }
/* ② 三条路各跑一遍,只量查询那一段 */ long long ansA = 0, ansB = 0, ansC = 0; double msA, msB, msC; { s = s0; ui last = 0; double t = nowMs(); for (long long i = 1; i <= q; i++) { int x = (int)(((getRnd(s) ^ last) % (ui)n) + 1); ui k = (getRnd(s) ^ last) % (ui)dep[x]; int u = x; for (int b = 0; b < LOG; b++) if ((k >> b) & 1u) u = upA[b][u]; last = (ui)u; ansA ^= i * (long long)u; } msA = nowMs() - t; } { s = s0; ui last = 0; double t = nowMs(); for (long long i = 1; i <= q; i++) { int x = (int)(((getRnd(s) ^ last) % (ui)n) + 1); ui k = (getRnd(s) ^ last) % (ui)dep[x]; int u = x; for (int b = 0; b < LOG; b++) if ((k >> b) & 1u) u = upB[u][b]; last = (ui)u; ansB ^= i * (long long)u; } msB = nowMs() - t; } if (climb <= 2000000000LL) { // 爬得动才量,链上就别指望了 s = s0; ui last = 0; double t = nowMs(); for (long long i = 1; i <= q; i++) { int x = (int)(((getRnd(s) ^ last) % (ui)n) + 1); ui k = (getRnd(s) ^ last) % (ui)dep[x]; int u = x; for (ui c = 0; c < k; c++) u = fa[u]; last = (ui)u; ansC ^= i * (long long)u; } msC = nowMs() - t; } else { msC = -1; ansC = ansA; }
char buf[64]; vector<pair<string, string> > out; auto putD = [&](const string& k, double x, int p = 1) { snprintf(buf, sizeof(buf), "%.*f", p, x); out.push_back(make_pair(k, string(buf))); }; out.push_back(make_pair("n", to_string(n))); out.push_back(make_pair("q", to_string(q))); out.push_back(make_pair("maxdep", to_string(maxDep))); out.push_back(make_pair("climb", to_string(climb))); out.push_back(make_pair("jumps", to_string(jumps))); out.push_back(make_pair("same", string((ansA == ansB && ansA == ansC) ? "1" : "0"))); putD("ms_A", msA); putD("ms_B", msB); putD("ms_climb", msC); putD("ratio_AB", msB > 0 ? msA / msB : 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(" %-10s %s\n", out[i].first.c_str(), out[i].second.c_str()); return 0;}点「运行 ▶」看结果
| 树的形状 | 最大深度 | ✗ 暴力爬多少步 | ★ 倍增跳多少次 | ✗ 暴力 | ★ 倍增 |
|---|---|---|---|---|---|
rand |
32 | 32 850 000 | ★ 8 827 429 | ★ 0.40 秒 | 0.53 秒 |
binary |
19 | 42 366 214 | 9 692 713 | ★ 0.48 秒 | 0.62 秒 |
chain |
500 000 | ⚠ 625 126 801 694 | 42 984 954 | ⚠ 跑不完 | 5.45 秒 |
⇒ ★★★ 随机树上:倍增做的活少 3.7 倍,秒表却慢 1.3 倍。
又一次两把尺子打架 —— 而这次原因很具体:
暴力那 6.57 步是在一个 2 MB 的 fa[] 里追指针,
倍增那 1.77 次跳是往一张 40 MB 的表里随机摸,外加每次询问都要把 20 位扫一遍。
⚠⚠ 而 chain 那一行把它整个翻过来:两把尺子的差距从 3.7 倍变成 14 543 倍,
暴力那 6.25×10¹¹ 步彻底跑不完。
⇒ ★★ 主语从头到尾只有一个:这棵树的深度。
(kᵢ 是对深度取模摇出来的 ⇒ 平均要爬深度的一半。)
| 顶格形状 | ★ 倍增(端到端) | 余量 |
|---|---|---|
rand |
0.53 秒 | 5.7 倍 |
binary |
0.62 秒 | 4.8 倍 |
chain |
⚠ 5.45 秒 | ✗ 超时 1.8 倍 |
⇒ 这就是题目背景那句话的分量:倍增不是「一定过不去」,是「链上过不去」。 ★ 而洛谷这道题的数据到底有没有链,题面没说(「不保证卡掉了其他复杂度不正确的做法」)—— 这一页量的是题面顶格的最坏形状,那才是能写下来的结论。
| 在随机树上死的是 | 在链上死的是 | |
|---|---|---|
| P3379(问 LCA) | —— | ✗ 暴力(爬 833 亿步) |
这道题(问 k 级祖先) |
—— | ✗ 倍增(5.45 秒 / 时限 3 秒) |
⇒ ★★ 同一条尺子(树的形状)连着两道题决定生死,而两次死的是不同的算法 —— [P3379] 上暴力在随机树里比倍增还快 7.5 倍,这道题上暴力在随机树里比倍增还快 1.3 倍, 两次都是那句「随机树的深度只有 log 级」在起作用(第 51 章第 3 步)。
P3379 那一页量出:把 up[LOG][MAXN] 换成 up[MAXN][LOG],
顶格星形树上查询快 10.66 倍。同一个动作搬到这道题上,反而变慢一半:
| 顶格形状 | 最大深度 | k 最多几个二进制位 |
A = up[LOG][MAXN](★ 正解用的) |
B = up[MAXN][LOG] |
A÷B |
|---|---|---|---|---|---|
rand |
32 | 5 | ★ 0.53 秒 | 1.06 秒 | 0.50 |
binary |
19 | 5 | ★ 0.62 秒 | 1.17 秒 | 0.53 |
chain |
500 000 | 19 | 5.45 秒 | 5.65 秒 | 0.96 |
(端到端、七轮交替取中位数。⚠ 这一格必须这么量:这台机器上「往 40 MB 的表里随机摸」 单跑一次能从 0.53 秒飘到 1.39 秒,而七轮交替里 A 轮轮都赢 —— 结论只敢用「谁赢」这一层。)
⇒ ★★★ 主语是「查询会读到哪几层」:
- [P3379] 求 LCA 时第 ② 段要把 20 层全比一遍 ⇒ 40 MB 全都要碰 ⇒ B(同一个点的 20 层挨在一起)赢;
- 这道题只读
k的那几个二进制位,而随机树的k ≤ 31⇒ 只碰得到最低 5 层 ⇒ A(同一层的点挨在一起)只碰 10 MB,反而赢。
★ 而 chain 那一行是这条解释的自检:深度到 5×10⁵ ⇒ 20 层全碰得到 ⇒ 两版打平(0.96)。
⇒ ★★ 「换个存法快多少」这句话,主语不是这张表,是「你会去读它的哪一部分」。
4⚠ 四个坑,而每一个都写在题面上
第 51 章第 7 步那份 fast.cpp 里写的是 dep[root] = 0 —— 那是对的,
因为那道题只拿 dep 去算深度差。
而这道题的题面写死了「根的深度为 1」,因为 kᵢ 是对 d[xᵢ] 取模摇出来的:
dep[root] = 0 ⇒ k = r % 0 ⇒ SIGFPE,程序直接崩⇒ ★★★ 这是第 52 章那条「上一章的正确写法就是这一章的 bug」 最直白的一次 —— 而且这次连「错得悄无声息」都不是,它是当场崩。
5★ 对拍:四个错法 + 一份「一律输出 0」
| 档位 | ①Int | ②Dep0 | ③Off | ④Int32 | 试金石 |
|---|---|---|---|---|---|
0 顺手写法(随机小树、q = 5~30) |
299 | 299 | 300 | ★ 0 | 299 |
| 1 链 | 299 | 300 | 299 | ★ 0 | 300 |
| 2 星形 | 300 | 300 | 298 | ★ 0 | 299 |
| 3 最终档(形状随机 + q 大一些) | 300 | 300 | 300 | ★ 0 | 300 |
★★★ 前三列几乎满格 —— 因为它们都在算另一串询问,一步走岔就再也回不来。 ⇒ ★ 这也说明这道题的对拍很便宜:随手造一棵小树就能把三个坑全抓住。
⚠⚠ 而 ④Int32 那一列是四个精确的 0,并且这个 0 是能算出来的:
i × ansᵢ 要越过 2³¹ 需要 i ≥ 2³¹ / n,而对拍的 n ≤ 30
⇒ 得 i ≥ 7.2 × 10⁷ —— 而这一档的 q 最多 60。
| 那条线在哪儿 | |
|---|---|
对拍(n ≤ 30,q ≤ 60) |
i ≥ 71 582 789 ⇒ 结构上永远够不着 |
题面顶格(n = 5×10⁵) |
i ≥ 4295 ⇒ q 到四千多就开始错 |
⇒ ★★ 加轮数没用,加 q 也没用 —— 得同时把 n 和 q 都放大。
顶格 rand 上实测:正解 1418135787338,int 版 796579658。
6★ 哪一版就已经能过了
| 数据档 | ✗ 一步一步爬 | ★ 倍增 |
|---|---|---|
20%(n, q ≤ 10³) |
✓ 秒过 | ✓ |
50%(n, q ≤ 10⁵) |
⚠ 随机树 0.01 秒,链上 8.75 秒 | ✓ 0.04 秒 |
100%(n ≤ 5×10⁵,q ≤ 5×10⁶) |
✗ 链上外推约 2200 秒 | ⚠ 随机树 0.53 秒,链上 5.45 秒 |
⇒ ★★ 暴力稳拿 20 分(n, q ≤ 10³ 时最坏也只爬 5×10⁵ 步);
倍增稳拿 50 分,满分则取决于「那 10 个测试点里有没有深树」。
★ 而题目背景那句话已经把答案写了一半:出题人没打算卡别的做法,
他只是把 q 开到 5×10⁶,让 O(1) 和 O(log n) 之间那个 log 显出来。
这道题第一次把「输入很小、工作量巨大」摆到台面上 ——
输入只有 n + 3 个整数(3.4 MB),输出只有一个数,
而中间要在线地做 5×10⁶ 次询问。
⇒ 于是这一页的每一个坑(随机函数、强制在线、深度从 1、long long)
都长在「怎么把询问摇出来」上,一个都不在「怎么找祖先」上。