题单 · 习题解析

洛谷 P4551 最长异或路径

★★★ 这道题**一个字符串哈希都用不上** —— 它是这一章的对照组,题单把它放进来是为了说明「把东西变成一个数」之后路还能往哪走:这一章的半步在这儿只有一句「`d[u] = 根到 u 的边权异或` ⇒ 路径异或 = `d[u] ^ d[v]`」(根到 LCA 那段走了两遍,`x ^ x = 0` 自己抵消),剩下的是第 50 章的 01-Trie;★★★ 而最容易挂的地方是**上一章的正确写法**:[第 52 章树上差分](/ch/52-tree-diff/)的路径和是 `s[u] + s[v] − 2·s[lca]`(减两遍),照猫画虎写成 `d[u] ^ d[v] ^ d[lca]` 就等于**把那一段又加了回去** ⇒ **加法要减两遍,异或一遍都不用减**,⚠ 而官方唯一那组样例**放过它**(最优那一对的 LCA 正好是根,`d[lca] = 0`);★★ 第二个坑是**数位数**:`0 ≤ w < 2³¹` 是 **31 位**(最高第 30 位),写成 30 位就丢最高位 —— ⚠ 而顺手写的生成器爱把边权取成 `rng() % 1000` ⇒ 那一档是**结构性的精确的 0**(0 / 300 / 300,而且触发 ≡ 抓获:「答案 ≥ 2³⁰」的轮数一格不差);★ 「要不要 long long」是一句话:答案 ≤ 2³¹−1 ⇒ **`int` 恰好够、余量为 0**,而顶格随机实测**答案就是 2147483647 本身**(和 [P1886](/sol/p1886/) / [P3378](/sol/p3378/) 同形);★★ 「递归会不会爆栈」量出来了:这份 DFS 每层约 **64 字节**,门槛在 **13.0 万 ~ 13.1 万层**之间,而顶格链是 10 万层 ⇒ **能活,余量只有 1.30 倍**(对照 [P5318](/sol/p5318/) 那份精简递归 48 字节 / 17.4 万层)⇒ 顶格题一律写迭代,赌赢了也只赢一点点;★ 顶格那一档的参照物是**按位分治**(同样 O(31n),和 Trie 一行不共享),因为两两异或要 5×10⁹ 对跑不完;⚠ 这道题**一个部分分档都没有** ⇒ 暴力一分不给

⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

给定一棵 n 个点的带权树,结点下标从 1 开始到 n。求树中所有异或路径的最大值

异或路径指树上两个结点之间唯一路径上的所有边权的异或值。

输入格式

第一行一个整数 n,表示结点数。

接下来 n − 1 行,给出 u, v, w,分别表示树上的 u 点和 v 点有连边,边的权值是 w

输出格式

一行,一个整数表示答案。

数据范围

1 ≤ n ≤ 10⁵0 < u, v ≤ n0 ≤ w < 2³¹

时限 1 秒,内存 524288 KB(512 MB)。

输入输出样例

输入

4
1 2 3
2 3 4
2 4 6

输出

7

当两个结点分别是 1、3 时,答案是 7 = 3 XOR 4,取最大值。

1★★★ 这道题一个字符串哈希都用不上 —— 它是这一章的第二个对照组

★★ 题单注解自己说了它为什么在这儿

第 49 章的题单在这道题后面写着:

「⚠ 提高组:它其实是第 50 章 01-Trie 的题。放在这儿是想说明 『把东西变成一个数』之后,路还能往哪儿走。」

⇒ 这一章教的是那半步:把一个复杂的东西压成一个整数,让「比较」变成 O(1)。

  • P3370:把一个串变成一个数;
  • P1381:把一个单词变成一张表里的下标;
  • ★ 这道题:把树上的一条路径变成一个数。

★★★ 而这半步在这道题上只有一句话,证明只有一行:

   令 d[u] = 根到 u 的所有边权的异或
   ⇒ u 到 v 的路径异或 = d[u] XOR d[v]

为什么:路径 = (根→u) 和 (根→v) 拼起来,两条里根到 LCA 那一段被走了两遍, 而 x XOR x = 0它自己抵消了

⇒ 于是题目变成:n 个数里挑两个,异或最大。剩下的就是第 50 章的 01-Trie。

p4551.cpp★ 正解:d[u] + 01-Trie,O(31n)
// P4551 最长异或路径 —— 正解:把「树上路径」变成「一个数」,再上 01-Trie。
//
// ★★ 这道题挂在第 49 章的题单里,可它**一个字符串哈希都用不上** ——
// 题单注解自己写着:「它其实是第 50 章 01-Trie 的题。放在这儿是想说明
// 『把东西变成一个数』之后,路还能往哪儿走。」
// ⇒ 这一章教的是那**半步**:把一个复杂的东西压成一个整数,让「比较」变成 O(1)。
// 这道题把同一半步用在**树上的路径**上。
//
// ★★★ 那半步只有一句话,而且证明只有一行:
// 令 d[u] = 根到 u 的所有边权的异或。
// 则 u 到 v 的路径异或 = d[u] ^ d[v]。
// 为什么:路径 = (根→u) ⊕ (根→v),两条里**根到 LCA 那一段被走了两遍**,
// 而 `x ^ x = 0` ⇒ 它自己抵消了。
// ⚠⚠ 注意这和[第 52 章树上差分](/ch/52-tree-diff/)那套**求和**的公式**不一样**:
// 求和是 `s[u] + s[v] − 2·s[lca]`(要减两遍),异或是**一遍都不用减**。
// 照搬那个公式、写成 `d[u] ^ d[v] ^ d[lca]`,就是 p4551Lca.cpp。
//
// ⇒ 剩下的问题变成:**n 个数里挑两个,异或最大**。01-Trie 从高位往低位贪心,O(31n)。
//
// ⚠ 两处要数清楚的:
// ① 题面写 `0 ≤ w < 2^31` ⇒ 最高位是**第 30 位**(0 基),要跑 30..0 共 **31** 位。
// 写成 29..0 就丢最高位(p4551Bit.cpp)。
// ② `d[u]` 和答案都 < 2^31 ⇒ `int` **恰好装得下,余量为 0**(和[第 37 章 P3378](/sol/p3378/) 同形)。
//
// ⚠ 建树用迭代 DFS:顶格 n = 10⁵ 排成一条链,递归**能活但只剩不到两倍余量**(见 p4551Rec.cpp)。
#include <bits/stdc++.h>
using namespace std;
const int BITS = 31; // 第 30 位到第 0 位
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<int> head(n + 1, -1), nxt(2 * (n > 0 ? n : 1)), to(2 * (n > 0 ? n : 1)), wt(2 * (n > 0 ? n : 1));
int ec = 0;
for (int i = 0; i < n - 1; i++) {
int u, v, w;
cin >> u >> v >> w;
to[ec] = v; wt[ec] = w; nxt[ec] = head[u]; head[u] = ec++;
to[ec] = u; wt[ec] = w; nxt[ec] = head[v]; head[v] = ec++;
}
/* ① 迭代 DFS 求 d[] */
vector<int> d(n + 1, 0);
vector<char> vis(n + 1, 0);
vector<int> st;
st.push_back(1);
vis[1] = 1;
while (!st.empty()) {
int u = st.back();
st.pop_back();
for (int e = head[u]; e != -1; e = nxt[e]) {
int v = to[e];
if (vis[v]) continue;
vis[v] = 1;
d[v] = d[u] ^ wt[e];
st.push_back(v);
}
}
/* ② 01-Trie:n 个数里挑两个异或最大 */
vector<array<int, 2>> ch(1);
ch[0] = { 0, 0 };
for (int i = 1; i <= n; i++) {
int cur = 0;
for (int b = BITS - 1; b >= 0; b--) {
int t = (d[i] >> b) & 1;
if (!ch[cur][t]) { ch.push_back({ 0, 0 }); ch[cur][t] = (int)ch.size() - 1; }
cur = ch[cur][t];
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
int cur = 0, val = 0;
for (int b = BITS - 1; b >= 0; b--) {
int t = (d[i] >> b) & 1;
if (ch[cur][t ^ 1]) { val |= 1 << b; cur = ch[cur][t ^ 1]; }
else cur = ch[cur][t];
}
ans = max(ans, val);
}
cout << ans << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2★★★ 而这道题最容易挂的地方,是上一章的正确写法

★★ 求和要减两遍,异或一遍都不用减

第 52 章树上差分那套求和的公式是所有人都背过的:

   路径和 = s[u] + s[v] − 2·s[lca]        ← 根到 LCA 那一段被算了两遍,减两遍

⇒ 于是「异或版」顺手就写成 d[u] ^ d[v] ^ d[lca] —— 照猫画虎,减一遍

★★ 可正确答案是一遍都不减x XOR x = 0,那一段自己就没了。 多异或一个 d[lca],等于把根到 LCA 那一段又加了回去

⇒ ★★★ 第 52 章那条「上一章的正确写法可能就是这一章的 bug」又一次 —— 而这一次两者之间隔着的不是题目,是运算加法要减两遍,异或一遍都不用减。

⚠ 而官方那唯一一组样例挡不住它:样例里最优的那一对是 1 和 3, 它们的 LCA 就是根(d[1] = 0)⇒ 多异或一个 0,什么都没变,照样打出 7。 ★ 触发条件:最优的那一对的 LCA 不是根 —— 三档实测被抓 122 / 135 / 201(共 300 轮)。

p4551Lca.cpp✗ 错法一:照搬树上差分的「减 LCA」
// P4551 —— ✗ 错法二:照搬[第 52 章树上差分](/ch/52-tree-diff/)那套**求和**的公式。
//
// ★ 那一章的路径和是:`s[u] + s[v] − 2·s[lca]`(根到 LCA 那一段被算了两遍,要减掉两遍)。
// ⚠ 于是「异或版」顺手就写成 `d[u] ^ d[v] ^ d[lca]` —— 照猫画虎,减一遍。
//
// ★★ 而正确答案是**一遍都不减**:`x ^ x = 0`,那一段自己就抵消了。
// ⇒ 多异或一个 `d[lca]`,等于**把根到 LCA 那一段又加了回去**。
//
// ⇒ ★★★ [第 52 章](/ch/52-tree-diff/)那条「上一章的正确写法可能就是这一章的 bug」又一次 ——
// 而这一次两章之间隔着的不是题目,是**运算**:加法要减两遍,异或一遍都不用。
//
// ⚠ 注意这一版**慢得多**(它要真的求 LCA,而且是 O(n²) 枚举点对),
// 所以只在小数据上当错法用 —— 这一页要看的是它**答什么**,不是它跑多快。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<vector<pair<int, int>>> g(n + 1);
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));
}
vector<int> d(n + 1, 0), fa(n + 1, 0), dep(n + 1, 0);
vector<char> vis(n + 1, 0);
vector<int> st(1, 1);
vis[1] = 1;
while (!st.empty()) {
int u = st.back(); st.pop_back();
for (size_t k = 0; k < g[u].size(); k++) {
int v = g[u][k].first;
if (vis[v]) continue;
vis[v] = 1; d[v] = d[u] ^ g[u][k].second; fa[v] = u; dep[v] = dep[u] + 1; st.push_back(v);
}
}
auto lca = [&](int x, int y) {
while (dep[x] > dep[y]) x = fa[x];
while (dep[y] > dep[x]) y = fa[y];
while (x != y) { x = fa[x]; y = fa[y]; }
return x;
};
int ans = 0;
for (int i = 1; i <= n; i++)
for (int j = i + 1; j <= n; j++) ans = max(ans, d[i] ^ d[j] ^ d[lca(i, j)]); // ⚠ 多异或了一个
cout << ans << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

3⚠ 第二个坑:「w < 2³¹」是 31 位,不是 30 位

★ 一句要数清楚的话,和一档结构性的 0

题面写的是 0 ≤ w < 2³¹

⚠ 「小于 2³¹」很容易被读成「30 位」—— 而它其实是 31 位: 最大的 w2³¹ − 1 = 2147483647,二进制是 31 个 1,最高位是第 30 位(0 基)。 ⇒ 01-Trie 要跑 b = 30, 29, …, 0,一共 31 层。

★ 触发条件精确到一句话:存在一对 (u, v) 使 d[u] XOR d[v] 的第 30 位是 1

⚠⚠ 而顺手写的生成器最爱把边权取成 rng() % 1000 这种小数 ⇒ 那一档是结构性的精确的 0(三档实测 0 / 300 / 300)。 ⇒ 第 46 章 P1100 那条的又一次:顺手写的上界正好卡在 bug 的下面。

★ 顺带把「要不要 long long」问完 —— 答案是 int 恰好够,余量为 0

w < 2³¹ ⇒ 每个 d[u] < 2³¹,异或不会让它变大 ⇒ 答案 ≤ 2³¹ − 1 = 2147483647int 恰好装得下,一个格子都不剩。

⚠ 而这不是纸上谈兵:顶格随机边权跑一遍,答案就是 2147483647 本身n = 10⁵ 个 31 位随机数里,两两异或几乎必然凑得出全 1)。

⇒ 和第 35 章 P1886(题面把 int 的 42.9 亿个值全用光)、 第 37 章 P33781 ≤ x < 2³¹,余量为 0)是同一个形状: 题面把 int 的值域用满,多一位都不给。

p4551Bit.cpp✗ 错法二:只做 30 位,丢掉最高位
// P4551 —— ✗ 错法一:位数数错了,只做 30 位(第 29 位到第 0 位)。
//
// ★ 题面写的是 `0 ≤ w < 2^31`。
// ⚠ 「小于 2³¹」很容易被读成「30 位」—— 而它其实是 **31 位**(第 30 位到第 0 位,
// 最大的 w 是 2³¹ − 1 = 2147483647,二进制是 31 个 1)。
//
// ★ 触发条件精确到一句话:**存在一对 (u, v) 使 `d[u] ^ d[v]` 的第 30 位是 1**。
// ⚠ 而顺手写的生成器最爱把边权取成 `rng() % 1000` 这种小数
// ⇒ 那一档是**结构性的精确的 0**([第 46 章 P1100](/sol/p1100/) 那条:
// **顺手写的上界正好卡在 bug 的下面**)。
#include <bits/stdc++.h>
using namespace std;
const int BITS = 30; // ⚠ 少了一位
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<int> head(n + 1, -1), nxt(2 * (n > 0 ? n : 1)), to(2 * (n > 0 ? n : 1)), wt(2 * (n > 0 ? n : 1));
int ec = 0;
for (int i = 0; i < n - 1; i++) {
int u, v, w;
cin >> u >> v >> w;
to[ec] = v; wt[ec] = w; nxt[ec] = head[u]; head[u] = ec++;
to[ec] = u; wt[ec] = w; nxt[ec] = head[v]; head[v] = ec++;
}
vector<int> d(n + 1, 0);
vector<char> vis(n + 1, 0);
vector<int> st(1, 1);
vis[1] = 1;
while (!st.empty()) {
int u = st.back(); st.pop_back();
for (int e = head[u]; e != -1; e = nxt[e]) {
int v = to[e];
if (vis[v]) continue;
vis[v] = 1; d[v] = d[u] ^ wt[e]; st.push_back(v);
}
}
vector<array<int, 2>> ch(1);
ch[0] = { 0, 0 };
for (int i = 1; i <= n; i++) {
int cur = 0;
for (int b = BITS - 1; b >= 0; b--) {
int t = (d[i] >> b) & 1;
if (!ch[cur][t]) { ch.push_back({ 0, 0 }); ch[cur][t] = (int)ch.size() - 1; }
cur = ch[cur][t];
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
int cur = 0, val = 0;
for (int b = BITS - 1; b >= 0; b--) {
int t = (d[i] >> b) & 1;
if (ch[cur][t ^ 1]) { val |= 1 << b; cur = ch[cur][t ^ 1]; }
else cur = ch[cur][t];
}
ans = max(ans, val);
}
cout << ans << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

4★ 两把尺子:n² 和 31n 差 806 倍

p4551Count.cpp★ 数「基本操作」+ 把「int 够不够」坐实
// P4551 —— 三条路各做多少次基本操作,以及「int 到底够不够」那笔账。
//
// ★ 三条路:
// ✗ 两两异或 C(n,2) 次 —— 顶格 10⁵ ⇒ **5×10⁹**
// ★ 01-Trie 31 n 次插入 + 31 n 次查询 ⇒ 顶格 **6.2×10⁶**
// ★ 按位分治 同样 O(31 n),一行代码都不和 Trie 共享(顶格那一档的参照物)
//
// ★★ 而「要不要 long long」是一句除法就问完的事:
// 题面 `0 ≤ w < 2^31` ⇒ 每个 d[u] < 2^31,异或不会变大 ⇒ 答案 ≤ 2³¹ − 1
// ⇒ **`int` 恰好装得下,余量为 0**(和[第 37 章 P3378](/sol/p3378/)、
// [第 35 章 P1886](/sol/p1886/) 是同一个形状:题面把 int 的值域用满,一个格子都不剩)。
// ⚠ 而这份程序顺手把它坐实:顶格随机边权下,**答案就是 2147483647 本身**。
//
// 用法:./p4551Count [table|csv]
#include <bits/stdc++.h>
using namespace std;
static string padLeft(const string& s, int width) {
int disp = 0;
for (unsigned char c : s) { if ((c & 0xC0) == 0x80) continue; disp += (c < 0x80) ? 1 : 2; }
return string(max(0, width - disp), ' ') + s;
}
static long long ops = 0;
static int trieMax(const vector<int>& d) {
vector<array<int, 2>> ch(1);
ch[0] = { 0, 0 };
for (size_t i = 0; i < d.size(); i++) {
int cur = 0;
for (int b = 30; b >= 0; b--) {
ops++;
int t = (d[i] >> b) & 1;
if (!ch[cur][t]) { ch.push_back({ 0, 0 }); ch[cur][t] = (int)ch.size() - 1; }
cur = ch[cur][t];
}
}
int ans = 0;
for (size_t i = 0; i < d.size(); i++) {
int cur = 0, val = 0;
for (int b = 30; b >= 0; b--) {
ops++;
int t = (d[i] >> b) & 1;
if (ch[cur][t ^ 1]) { val |= 1 << b; cur = ch[cur][t ^ 1]; }
else cur = ch[cur][t];
}
ans = max(ans, val);
}
return ans;
}
static int bruteMax(const vector<int>& d) {
int ans = 0;
for (size_t i = 0; i < d.size(); i++)
for (size_t j = i + 1; j < d.size(); j++) { ops++; ans = max(ans, d[i] ^ d[j]); }
return ans;
}
int main(int argc, char** argv) {
string mode = argc > 1 ? argv[1] : "table";
const int NS[3] = { 1000, 4000, 16000 };
long long tb[3][2];
int ansSame = 1;
for (int k = 0; k < 3; k++) {
mt19937 rg(20260909u + k);
vector<int> d(NS[k]);
for (int i = 0; i < NS[k]; i++) d[i] = (int)(rg() & 0x7fffffffu);
ops = 0; int a1 = bruteMax(d); tb[k][0] = ops;
ops = 0; int a2 = trieMax(d); tb[k][1] = ops;
if (a1 != a2) ansSame = 0;
}
/* 顶格:答案顶到 int 的上限本身 */
mt19937 rg(20260909u);
vector<int> big(100000);
for (int i = 0; i < 100000; i++) big[i] = (int)(rg() & 0x7fffffffu);
int bigAns = trieMax(big);
if (mode == "table") {
printf("★ 基本操作次数(n 个数里挑两个异或最大)\n\n");
printf(" %-8s %s %s\n", "n", padLeft("✗ 两两异或", 18).c_str(), padLeft("★ 01-Trie(31 位)", 22).c_str());
for (int k = 0; k < 3; k++) printf(" %-8d %18lld %22lld\n", NS[k], tb[k][0], tb[k][1]);
printf(" n 每翻 4 倍:两两异或 ×%.1f、×%.1f(O(n²) 的签名);Trie ×%.1f、×%.1f(线性)\n",
(double)tb[1][0] / tb[0][0], (double)tb[2][0] / tb[1][0],
(double)tb[1][1] / tb[0][1], (double)tb[2][1] / tb[1][1]);
printf(" ⇒ 顶格 n = 10⁵:两两异或 %lld 次,Trie %lld 次 —— 差 %.0f 倍\n\n",
100000LL * 99999 / 2, 62LL * 100000,
(100000.0 * 99999 / 2) / (62.0 * 100000));
printf("★ 「int 够不够」:题面 0 ≤ w < 2³¹ ⇒ 答案 ≤ 2³¹ − 1 = 2147483647\n");
printf(" 顶格随机边权实测答案:%d ⇒ %s\n", bigAns,
bigAns == 2147483647 ? "★ 正好顶到 int 的上限,余量为 0" : "没顶到");
printf(" 两两异或 ↔ Trie 三档答案一致:%s\n", ansSame ? "是" : "否");
} else {
for (int k = 0; k < 3; k++) printf("n%d,%lld %lld\n", NS[k], tb[k][0], tb[k][1]);
printf("ansSame,%d\nbigAns,%d\n", ansSame, bigAns);
printf("grow,%.1f %.1f %.1f %.1f\n",
(double)tb[1][0] / tb[0][0], (double)tb[2][0] / tb[1][0],
(double)tb[1][1] / tb[0][1], (double)tb[2][1] / tb[1][1]);
}
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★ 「n 个数里挑两个异或最大」的两条路
n ✗ 两两异或 ★ 01-Trie(31 位)
1000 499 500 62 000
4000 7 998 000 248 000
16000 127 992 000 992 000
n 每翻 4 倍 ×16.0O(n²) 的签名) ×4.0(线性)

⇒ 顶格 n = 10⁵:两两异或 4 999 950 000 次,Trie 6 200 000 次 —— 差 806 倍

★ 而顶格的秒表:正解 0.06 秒 / 时限 1 秒(链形和随机树一样),余量 16 倍。 ⚠ 这道题一个部分分档都没有O(n²) 那条路一分不给 (和第 48 章 P4391 一样)。

p4551Div.cpp★ 另一条正确的路:按位分治(顶格那一档的参照物)
★ 顶格对拍的参照物不必是暴力

顶格 n = 10⁵ 上两两异或要 5×10⁹ 对,跑不完 ⇒ 拿暴力当参照物这条路断了。

★ 而按位分治是另一条完全不同的路(从高位往下把当前这堆数按「这一位是 0 还是 1」 分成两半,两半都非空 ⇒ 这一位一定能取到 1),同样 O(31n)和 01-Trie 一行代码都不共享。 ⇒ 第 19 章 P1803 立的那条规矩,换一道题又用上一次。

5★★ 递归会不会爆栈:顶格活着,可余量只有 1.3 倍

★★★ 这道题的顶格链是 10 万层,而这台机器的门槛是 13 万

第 29 章 P5318 立过的规矩:「递归会不会爆栈」不是写法的属性, 是「栈上限 ÷ 每层字节数」这道除法 —— 同一台机器上, 函数体里多一句 string 拼接,每层就从 48 字节涨到 341 字节、能递归的层数从 17 万掉到 2.4 万。

⇒ 所以只能量。把这道题的 DFS 写成递归(p4551Rec.cpp),在上一档档往上试 (A 机 · WSL2 · ulimit -s = 8192 KB · 2026-09-09):

链的长度 递归版
n = 10⁵题面顶格 ★ 活着,答案正确
n = 1.3×10⁵ 活着
n = 1.31×10⁵ 段错误(退出码 139)
n = 2×10⁵ ✗ 段错误

⇒ 门槛在 13.0 万 ~ 13.1 万层之间,每层约 64 字节(8192 KB ÷ 13 万)。 ★ 顶格 10 万层能活,但余量只有 1.30 倍

⇒ ★★ 结论还是那句:顶格题一律写迭代,别赌 —— 赌赢了也只赢一点点。 (对照:第 29 章 P5318 那份精简递归是 48 字节 / 17.4 万层, 第 30 章 B3625 独立量到同一个数。这一份多了三个数组下标, 每层从 48 涨到 64 字节,门槛就从 17.4 万掉到 13.0 万。)

p4551Rec.cpp★ 递归版:答案对,顶格活着但余量 1.3 倍

6★ 对拍:三档 × 300 轮

p4551Gen.cpp★ 生成器:边权的值域是这一页唯一的旋钮
// P4551 数据生成器(对拍用)。用法:./p4551Gen <seed> [档位] [n],不给档位就是**最终档 2**。
//
// ★ 每个错法靠什么现形:
// ①只做 30 位 ← 要「**某一对的异或第 30 位是 1**」
// ⚠ 而顺手写的边权是 `rng() % 1000` 这种小数 ⇒ 那一档是**结构性的 0**
// ②照搬树上差分 ← 只要「最优的那一对的 LCA 不是根」,随机就抓
//
// 档位:
// 0 ★ 顺手写法:随机树,边权 0~999 —— ① 在这儿一次都碰不到
// 1 ★ 边权照题面顶格(0 ≤ w < 2^31)—— ① 这才有得谈
// 2 ★★ 最终档 = 1 + 一半轮次造**链**(LCA 一定不是根,而且顺带压递归深度)
// 3 ★ 顶格链(配合 `n`):给「递归会不会爆栈」和顶格秒表用
#include <bits/stdc++.h>
using namespace std;
static mt19937 rng;
static unsigned ru(unsigned m) { return rng() % m; }
int main(int argc, char** argv) {
unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1u;
int level = argc > 2 ? atoi(argv[2]) : 2;
int n = argc > 3 ? atoi(argv[3]) : 0;
rng.seed(seed * 2654435761u + 555u);
if (n <= 0) n = (level == 3) ? 100000 : 6 + (int)ru(15);
bool chain = (level == 3) || (level == 2 && (ru(2) == 0));
printf("%d\n", n);
for (int v = 2; v <= n; v++) {
int u = chain ? v - 1 : 1 + (int)ru((unsigned)(v - 1));
unsigned w;
if (level == 0) w = ru(1000);
else { w = rng(); w &= 0x7fffffffu; } // 0 ≤ w < 2^31
printf("%d %d %u\n", u, v, w);
}
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
p4551Brute.cpp★ 参照物:两两异或

(参照物是两两异或;正解 vs 按位分治:900 轮 0 组不一致)

档位 ✗ 只做 30 位 ✗ 照搬「减 LCA」
0 ★ 顺手写法:随机树,边权 0~999 0 122
1 ★ 边权照题面顶格(0 ≤ w < 2³¹ 300 135
2 ★★ 最终档 = 1 + 一半轮次造 300 201
★★ 这张表读出来两条
  1. ★★★ 「只做 30 位」在顺手那一档是结构性的精确的 0 —— 边权只有 0~999(10 位)⇒ 第 30 位永远是 0 ⇒ 那个 bug 一次都碰不到。 ⚠ 而它的触发条件 ≡ 抓获数:「答案 ≥ 2³⁰」的轮数和被抓的轮数一格不差 (0 / 300 / 300)。 ⇒ ★ 救法只有一个:照抄题面那行数据范围0 ≤ w < 2³¹),别自己缩小。

  2. 「减 LCA」那一列随档位单调上升 122 → 135 → 201 —— 旋钮是「最优的那一对的 LCA 离根有多远」, 而造链是把它推上去最直接的办法(链上任意两点的 LCA 就是编号小的那个)。 ⚠ 顺带:造链这一档同时把递归深度也压满了 —— 一个档位干了两件事

7★ 哪一版就已经能过了

★★ 这一页真正的收获:「把东西变成一个数」之后,工具箱换了一层
版本 结果 说明
p4551.cpp(01-Trie + 迭代 DFS) AC 顶格 0.06 秒,余量 16 倍
p4551Div.cpp(按位分治) AC 同样 O(31n),顶格 0.07 秒
p4551Rec.cpp(递归 DFS) 能过,余量 1.3 倍 顶格链 10 万层,门槛 13.0 万
✗ 两两异或 TLE 5×10⁹ 对,而这道题一个部分分档都没有
✗ 只做 30 位 WA ⚠ 顺手写的对拍抓不到(结构性的 0)
✗ 照搬「减 LCA」 WA 官方样例放过(最优那一对的 LCA 正好是根)

⇒ ★★★ 一句话带走:这一章教会你「把东西压成一个数」, 可压完之后要用什么工具,是另一件事。 串压成数 ⇒ 排序 / 哈希表(P3370); 路径压成数 ⇒ 按位的数据结构(01-Trie / 按位分治)。 ⇒ 而这两件事之间没有任何公共代码 —— 共享的只是那句「变成一个数」。