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 = 3、1^3 = 2、2^3 = 1 ⇒ 最大是 3。
1★ 这就是本章第 11 步那道小题的原题,一个字都没改
第 50 章第 11 步把这件事讲完了:
把 x 从第 30 位写到第 0 位,它就是一个长度正好 31 的 01 串。
于是「和 x 异或最大」= 「在 01-Trie 上,每一位都尽量走反面」——
因为高位那一下值 2³⁰,比后面所有位加起来还多。
⇒ 这一页要写的,是原题题面上那两处本章正文没有的东西:
① N 到 10⁵ ⇒ 两两枚举正好卡在及格线外边;
② 0 ≤ Aᵢ < 2³¹ ⇒ 31 位,⚠ 而顺手写的生成器几乎一定造不到第 30 位。
// 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;}点「运行 ▶」看结果
2★★ 第一版:两两枚举 —— 而它离及格线只差 2.5 倍
做多少次基本动作(顶格 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 位,而顺手写的生成器造不到那一位
它算的是:把每个数的第 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,而真用到多少由数据说了算
// 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;}点「运行 ▶」看结果
顶格 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、其实一次都不会错」的写法 —— 而它配了自检
正解是「边插边查」:查 a[i] 的时候树里只有 a[0..i−1] ⇒ 天然满足题面那句「选出两个」。
把顺序换成「先全插完再查」,a[i] 查询时它自己也在树上,贪心完全可能选中它自己。
可它一次都不会错:
① 题面保证 N ≥ 2 ⇒ 真正的答案是某一对 i < j 的异或值,而异或值 ≥ 0
② 自己配自己只贡献 x ^ x = 0
⇒ max(…, 0) 改不了任何东西实测:四档 1200 轮,被抓 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」
| 档位 | ①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 位。
⇒ 剩下三个错法(掉回根 / 贪反了顺序 / 只比相邻)都是「没想清楚贪心凭什么对」,
而它们在顶格随机数据上有两个是看不出来的。