题单 · 习题解析

洛谷 P10471 最大异或对 The XOR Largest Pair

★★★ 三道题连起来看,Trie 的骨架一个字没变、挂在节点上的东西一路在减:[P8306](/sol/p8306/) 挂一个计数、[P2580](/sol/p2580/) 挂一个三态、**这道题什么都不挂,只剩「这条边在不在」**;★★★ 而这一页最值钱的一条不在对拍表里:**顶格随机数据上,五个待测版本里三个是对的,连「恒输出 2³¹−1」的试金石都满分** —— 因为那一档的正确答案就是 2³¹−1 本身,而这是能算的(`C(10⁵,2) / 2³¹ ≈ 2.3` ⇒ 约九成的随机顶格数据顶到天花板)⇒ **所有「只会高估」的错法自动变对**,[「一致有两种」](/sol/p1746/)在这儿是「都顶到了天花板」,⚠ 而这正是所有人第一时间会造的那一档;★★ 唯一真正会让你 WA 的是**一个整数**:`0 ≤ Aᵢ < 2³¹` 是 **31 位**不是 30 位,而顺手写的生成器爱写 `% 10⁹`(10⁹ < 2³⁰)⇒ 「只做 30 位」在那一档是**结构性的精确的 0**(和 [P4551](/sol/p4551/) 那个坑一字不差,连生成器盲区都一样);★★★ 第三条:**「先全插完再逐个查询」看着破坏了「i ≠ j」,其实一次都不会错**(两行能证:答案 ≥ 0,而自己配自己只贡献 0),⚠ 而这个「精确的 0」配了自检 —— 把问题换成「最**小**异或对」当场全错,★★ 可**值域 0~15 那一档连自检自己都失效**(必有重复 ⇒ 最小异或对本来就是 0);★★ 顺带一条**反过来的「顶格 ≠ 最坏」**:两两枚举的循环体一个分支都没有 ⇒ **耗时只取决于 N、和数据形状毫无关系**(2.54 / 2.57 秒),而「更快」的 01-Trie 反倒因节点数不同差了 4 倍(0.08 / 0.02 秒);★ 而暴力顶格 **2.54 秒 / 时限 1 秒**——只超 2.5 倍,换算过去就是「N 小于约 6.3 万它就能过」,这正是讨论区里满屏「n 方过十万」的那个区间

原题:洛谷 P10471出自 第 50 章 Trie(字典树):一堆字符串摆成一棵树 的题单题面本地存档:2026-09-10
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

给定 N 个整数 A₁, A₂, ⋯, A_N 中选出两个进行异或计算,得到的结果最大是多少?

输入格式

第一行一个整数 N,第二行 N 个整数 A₁, A₂, ⋯, A_N

输出格式

一个整数表示答案。

数据范围

对于所有测试数据,2 ≤ N ≤ 10⁵,保证 0 ≤ Aᵢ < 2³¹

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

输入输出样例

输入

3
1 2 3

输出

3

三个数两两异或:1^2 = 31^3 = 22^3 = 1 ⇒ 最大是 3

1★ 这就是本章第 11 步那道小题的原题,一个字都没改

★★ 一句话:一个整数也是一个「长度固定的 01 字符串」

第 50 章第 11 步把这件事讲完了: 把 x 从第 30 位写到第 0 位,它就是一个长度正好 31 的 01 串。 于是「和 x 异或最大」= 「在 01-Trie 上,每一位都尽量走反面」—— 因为高位那一下值 2³⁰,比后面所有位加起来还多。

⇒ 这一页要写的,是原题题面上那两处本章正文没有的东西: ① N10⁵ ⇒ 两两枚举正好卡在及格线外边; ② 0 ≤ Aᵢ < 2³¹31 位,⚠ 而顺手写的生成器几乎一定造不到第 30 位。

p10471.cpp★ 正解:01-Trie + 从高位往低位贪心
// P10471 最大异或对 —— 正解:01-Trie + 从高位往低位贪心,O(31 N)
//
// ★ 这就是[第 50 章第 11 步](/ch/50-trie/)那道小题的原题,一个字都没改:
// **把一个整数看成一个长度固定的「01 字符串」**(从第 30 位写到第 0 位),
// 于是「找一个和 x 异或最大的数」就是「在 Trie 上顺着**每一位都尽量走反面**」。
//
// ⚠⚠ 两处会咬人的地方,都不在 Trie 上:
// ① **多少位**:题面写 `0 ≤ Aᵢ < 2³¹` ⇒ 最高位是第 **30** 位,一共 **31** 位。
// 写成 30 位(第 29 位起)就把最高位整个丢了 —— 而随机小数据一个字都不会错。
// ② 贪心的每一步**必须先看反面那条边在不在**,不在就只能走本侧 ——
// 少了这一句,`u` 会掉到 0 号节点(根)上去,后面的位全在读别人的家底。
//
// ★ 而「边插边查」还是「先全插完再查」这件事,看着像个坑,其实**一次都不会错** ——
// 理由和自检写在解析页第 ⑤ 步。
#include <bits/stdc++.h>
using namespace std;
const int BITS = 31; // ★ 0 ≤ Aᵢ < 2³¹ ⇒ 第 30 位到第 0 位,一共 31 位
const int MAXN = 100005 * BITS + 5; // 每个数最多新开 31 个节点
int ch[MAXN][2];
int tot;
void insertNum(int x) {
int u = 0;
for (int b = BITS - 1; b >= 0; b--) {
int k = (x >> b) & 1;
if (!ch[u][k]) ch[u][k] = ++tot;
u = ch[u][k];
}
}
/** 树里已经有的数当中,和 x 异或最大的那个异或值 */
int askMax(int x) {
int u = 0, res = 0;
for (int b = BITS - 1; b >= 0; b--) {
int k = (x >> b) & 1;
if (ch[u][k ^ 1]) { res |= 1 << b; u = ch[u][k ^ 1]; } // ⚠ 先看反面在不在
else u = ch[u][k];
if (!u) break; // 树是空的(只有第一个数会走到这儿)
}
return res;
}
int main() {
int n;
if (scanf("%d", &n) != 1) return 0;
int ans = 0;
for (int i = 0; i < n; i++) {
int x;
if (scanf("%d", &x) != 1) return 0;
if (i) ans = max(ans, askMax(x)); // ★ 边插边查 ⇒ 天然只和「前面的数」配对
insertNum(x);
}
printf("%d\n", ans);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2★★ 第一版:两两枚举 —— 而它离及格线只差 2.5 倍

p10471Brute.cpp✗ 第一版:三行,一个坑都没有
★★★ 本机实测:顶格 2.54 秒 / 时限 1 秒 —— 也就是说 N ≈ 6.3 万它就过了
做多少次基本动作(顶格 N = 10⁵ 秒表(3 次取中位数)
✗ 两两枚举 C(N,2) = 4 999 950 000 2.54 秒
★ 01-Trie N × 31 × 2 = 6 200 000 0.08 秒
倍数 806 倍 32 倍

⇒ 它确实超时,可只超出 2.5 倍 —— 换算过去就是: 只要数据的 N 小于 约 6.3 万10⁵ / √2.54),两两枚举就能过。

⚠ 而这道题的讨论区里,「n 方过十万」「数据太水」这类帖子是置顶的那一批。 ★ 这一页量的是题面顶格;洛谷那份数据实际有多大,题面没说,这一页也不猜。 ⇒ 但两件事是确定的:按题面写下来的最坏情况它过不去, 而它离过去只差 2.5 倍 —— 这正是这类题最容易让人赌一把的区间。

★★ 一条反过来的「顶格 ≠ 最坏」:两两枚举根本没有「最坏形状」

本书量过很多次「同样顶格,换个形状差几十倍」。这道题上它翻面了

顶格 N = 10⁵,只换数据形状 ✗ 两两枚举 ★ 01-Trie
随机 31 位 2.54 秒 0.08 秒
所有数共用 16 位高位前缀 2.57 秒 0.02 秒

⇒ 两两枚举的循环体里一个分支都没有(一次异或、一次取 max) ⇒ 它的耗时只取决于 N,和数据长什么样毫无关系; 而「更快」的那一版反倒差了 4 倍 —— 因为 Trie 的节点数是随数据变的 (见第 ④ 步那张表)。

3⚠⚠ 第一个坑:`< 2³¹` 是 31 位,而顺手写的生成器造不到那一位

p10471Bit30.cpp✗ 错法①:只做 30 位
★★ 它是「结构性的精确的 0」的教科书例子

它算的是:把每个数的第 30 位抹掉之后的最大异或对。 ⇒ 于是只要数据里所有数都 < 2³⁰,它一分不扣。

而 2³⁰ = 1 073 741 824 —— ⚠ 顺手写的生成器最爱写 rng() % 1000000000, 10⁹ 正好在它下面。⇒ 那一档它是结构性的精确的 0(对拍加多少轮都没用)。

生成器的值域 ①Bit30 被抓(300 轮) 顶格 10⁵ 上正解的答案
% 10⁹(顺手) 0 1 073 741 823 = 2³⁰ − 1
& 0x7fffffff(题面顶格) 299 2 147 483 647 = 2³¹ − 1

⇒ 这和 P4551 上那个坑一字不差(那道题的边权也是 0 ≤ w < 2³¹), ⇒ ★ 而两页的生成器盲区也一模一样 —— 说明这不是某个人手滑,是 % 10⁹ 这个习惯本身。

4★ 内存这笔账:静态数组 23.7 MiB,而真用到多少由数据说了算

p10471Count.cpp★ 节点数 / 次数 / 以及那个「精确的 0」的自检
// P10471 的三笔账:节点数 / 内存、两条路各做多少次基本动作、以及那个「精确的 0」的自检
//
// 用法:./p10471Count <csv|table> < 一份输入
//
// ★ 第三件事最要紧:p10471Self(先把 N 个数全插完再逐个查询)**一次都不会错**,
// 而「一个反例都没有」和「这段代码根本没在跑」输出上一模一样
// ([第 19 章 P2240](/sol/p2240/) 那条规矩)。
// ⇒ 所以这一份顺手把**同一处改动**放到另一个问题上验一遍:
// 求「最**小**异或对」时,自己配自己就是 0,那时候它当场全错。
#include <bits/stdc++.h>
using namespace std;
const int BITS = 31;
int main(int argc, char** argv) {
string mode = argc > 1 ? argv[1] : "table";
int n;
if (scanf("%d", &n) != 1) return 0;
vector<int> a(n);
for (int i = 0; i < n; i++) if (scanf("%d", &a[i]) != 1) return 0;
/* ① Trie 的节点数(= 所有数的不同「二进制前缀」个数)—— 排序之后数相邻的 LCP 就行 */
long long nodes = 0;
{
vector<int> v(a);
sort(v.begin(), v.end());
long long prevLen = -1;
int prev = 0;
for (size_t i = 0; i < v.size(); i++) {
if (i == 0) { nodes += BITS; prev = v[0]; prevLen = BITS; continue; }
int d = prev ^ v[i];
int common = BITS; // 和上一个数共用多少位高位前缀
for (int b = BITS - 1; b >= 0; b--) if ((d >> b) & 1) { common = BITS - 1 - b; break; }
nodes += BITS - common;
prev = v[i];
(void)prevLen;
}
}
/* ② 两条路各做多少次基本动作 */
long long brutePairs = (long long)n * (n - 1) / 2;
long long trieSteps = (long long)n * BITS * 2; // 每个数插一遍 + 查一遍,各 31 步
/* ③ ★ 那个「精确的 0」的自检:最大 vs 最小,各算「允许自己配自己」和「不允许」两种 */
int maxNo = 0, maxYes = 0;
int minNo = INT_MAX, minYes = INT_MAX;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) {
int v = a[i] ^ a[j];
if (i != j) { maxNo = max(maxNo, v); minNo = min(minNo, v); }
maxYes = max(maxYes, v);
minYes = min(minYes, v);
}
vector<pair<string, long long> > out;
out.push_back(make_pair("n", n));
out.push_back(make_pair("nodes", nodes));
out.push_back(make_pair("mib_x100", (long long)(nodes * 2 * 4 * 100.0 / 1048576.0)));
out.push_back(make_pair("brute_pairs", brutePairs));
out.push_back(make_pair("trie_steps", trieSteps));
out.push_back(make_pair("ratio", trieSteps ? brutePairs / trieSteps : 0));
out.push_back(make_pair("max_no", maxNo));
out.push_back(make_pair("max_yes", maxYes));
out.push_back(make_pair("max_same", maxNo == maxYes ? 1 : 0));
out.push_back(make_pair("min_no", minNo));
out.push_back(make_pair("min_yes", minYes));
out.push_back(make_pair("min_same", minNo == minYes ? 1 : 0));
if (mode == "csv") for (size_t i = 0; i < out.size(); i++) printf("%s,%lld\n", out[i].first.c_str(), out[i].second);
else for (size_t i = 0; i < out.size(); i++) printf(" %-14s %lld\n", out[i].first.c_str(), out[i].second);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
p10471GenBig.cpp★ 顶格生成器:rand / low / share / small
★★ 节点数 = 所有数的「不同二进制前缀」个数(本机实测,2026-09-10)
顶格 N = 10⁵,形状 Trie 节点数 算出来的 节点 × 2 × 4 实测峰值 正解耗时 正解答案
rand(随机 31 位) 1 550 410 11.8 MiB 16.0 MiB 0.08 秒 2 147 483 647
low(值域 10⁹) 1 439 739 11.0 MiB 15.2 MiB 0.07 秒 1 073 741 823
share(共用 16 位高位) 115 299 0.9 MiB 5.1 MiB 0.02 秒 65 535
small(值域 0~15) 57 0.0 MiB 4.2 MiB 0.01 秒 15

★ 静态数组要开 100005 × 31 个节点 ⇒ 3 100 160 × 2 × 4 = 23.7 MiB, 而题面给 512 MiB —— ⇒ 这道题的内存有 21.6 倍余量,一点都不紧 (对照隔壁 P8306:709.5 MiB / 1024 MiB,余量只有 1.4 倍)。

⚠ 而随机 31 位的节点数只有上界的一半(1.55M vs 3.1M): 10⁵ 个数在最上面那十六七层是挤在一起的(2¹⁷ > 10⁵), 只有再往下才开始各走各的。⇒ ★ 「不同前缀的个数」这句话,在 01-Trie 上照样成立。

5★★★ 一个「看着像 bug、其实一次都不会错」的写法 —— 而它配了自检

p10471Self.cpp★ 先把 N 个数全插完,再逐个查询
★★ 两行证明

正解是「边插边查」:查 a[i] 的时候树里只有 a[0..i−1] ⇒ 天然满足题面那句「选出两个」。 把顺序换成「先全插完再查」,a[i] 查询时它自己也在树上,贪心完全可能选中它自己。

可它一次都不会错:

   ① 题面保证 N ≥ 2  ⇒ 真正的答案是某一对 i < j 的异或值,而异或值 ≥ 0
   ② 自己配自己只贡献 x ^ x = 0
   ⇒ max(…, 0) 改不了任何东西

实测:四档 1200 轮,被抓 0 次。

★★★ 而「精确的 0」必须配自检 —— 否则它和「这段代码没在跑」长得一模一样

自检的做法就是第 19 章 P2240 那条规矩: 拿同一处改动去做一件已知会错的事,看它是不是真的会错。

这里换一个问题就够了:把「最大异或对」换成「最小异或对」。 那时候「自己配自己 = 0」就是全局最小,一改就废:

生成器档位 最大:不许自配 / 允许自配 最小:不许自配 / 允许自配
0(顺手,值域 10⁹) 一样 ✓ 23 572 596 / 0
1(顶格 2³¹) 一样 ✓ 21 229 279 / 0
2(值域 0~15) 一样 ✓ 0 / 0(一样)
3(最终档) 一样 ✓ 9 163 / 0

⇒ ★★ 而档 2 那一行连自检自己都失效了 —— 值域只有 16 个数、N ≥ 5 ⇒ 必然有两个数相同 ⇒ 最小异或对本来就是 0。 ⇒ ★★★ 自检也要检查它自己是不是空壳第 16 章 P1002 那条的又一次)。

6★ 对拍:四个错法 + 一份「恒输出 2³¹−1」

p10471Blind.cpp✗ 错法②:不判反面分支在不在,悄悄掉回根
p10471Low.cpp✗ 错法③:从低位往高位贪心
p10471Adj.cpp✗ 错法④:排序后只比相邻两个
p10471Const.cpp★ 试金石:一律输出 2147483647
p10471Set.cpp★ 第三条路:哈希表,从高位把答案一位一位试出来
p10471Gen.cpp★ 生成器:四个档位
★★★ 300 轮 × 四档(本机实测,2026-09-10)
档位 ①Bit30 ②Blind ③Low ④Adj ★Self 试金石
0 顺手(N = 5~30,值域 % 10⁹ 0 300 259 266 0 300
1 值域顶格 [0, 2³¹) 299 300 249 280 0 300
2 值域 0~15 0 103 4 147 0 300
3 最终档 298 300 259 276 0 300

★ 两个 0 的性质完全不同,而这一页两种都齐了:

  • ①Bit30 的 0 是结构性的(值域够不到第 30 位,加轮数没用);
  • ★Self 的 0 是答案永远对(换任何数据都是 0,而上一步给了它的证明和自检)。

⚠ 而 ③Low 在档 2 只有 4 / 300:值域只有 16 个数、N ≥ 5 ⇒ 十六个值多半全都出现过 ⇒ 两种贪心顺序都能凑到 15,谁先谁后不重要了。

★★★ 而这一页最值钱的一条,在对拍表外面:顶格随机数据上,五个版本里三个是对的

把上面那些版本喂给题面顶格的随机数据N = 10⁵,值域 [0, 2³¹)):

①Bit30 ②Blind ③Low ④Adj ★Self 试金石(恒输出 2³¹−1)
顶格随机上答对了吗

⇒ 原因只有一句:那一档的正确答案就是 2³¹−1 本身。

★ 而这不是巧合,是能算的:10⁵ 个随机的 31 位数里, 「存在两个数正好按位互补」的期望对数是 C(10⁵,2) / 2³¹ ≈ 2.3 ⇒ 大约九成的随机顶格数据,答案顶到天花板。 ⇒ 于是所有「只会高估、不会低估」的错法自动变对,而「什么都不算」也满分。

⇒ ★★★ 「一致有两种:都算对了,和都没算」在这一页是 「都顶到了天花板」 —— ⚠ 而这正是所有人第一时间会去造的那一档数据。

7★ 哪一版就已经能过了

★★ 结论
版本 能过吗 数字
✗ 两两枚举 一分不给 顶格 2.54 秒 / 1 秒;⚠ 这道题一个部分分档都没有
01-Trie(正解) 0.08 秒16.0 MiB / 512 MiB
★ 哈希表逐位试 0.09 秒 —— 和 Trie 一行代码都不共享,正好当顶格对拍的参照物

★ 而这道题会让你 WA 的,从头到尾只有一个整数< 2³¹31 位。 ⇒ 剩下三个错法(掉回根 / 贪反了顺序 / 只比相邻)都是「没想清楚贪心凭什么对」, 而它们在顶格随机数据上有两个是看不出来的

★ 一句话带走

这一章的 Trie,到这道题上连计数都不挂了 —— 节点上什么都没有,只剩「这条边在不在」。 ⇒ 而三道题连起来看:P8306 挂一个计数、P2580 挂一个三态、 这道题什么都不挂。骨架一个字没变,变的一直是挂在上面的东西。