0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1469,日期见页头。两边不一致时信原站。
题目描述
经过一段时间的紧张筹备,电脑小组的「RP 餐厅」终于开业了,这天,经理 LXC 接到了一个定餐大单, 可把大家乐坏了!员工们齐心协力按要求准备好了套餐正准备派送时,突然碰到一个棘手的问题:筷子!
CX 小朋友找出了餐厅中所有的筷子,但遗憾的是这些筷子长短不一,而我们都知道筷子需要长度一样的 才能组成一双,更麻烦的是 CX 找出来的这些筷子数量为奇数,但是巧合的是,这些筷子中只有一只筷子 是落单的,其余都成双,善良的你,可以帮 CX 找出这只落单的筷子的长度吗?
输入格式
第一行是一个整数,表示筷子的数量 n。
第二行有 n 个整数,第 i 个整数表示第 i 根筷子的长度 aᵢ。
输出格式
输出一行一个整数表示答案。
数据规模与约定
- 对于 30% 的数据,保证
n ≤ 10⁵。 - 对于 100% 的数据,保证
1 ≤ n ≤ 10⁷ + 1,1 ≤ aᵢ ≤ 10⁹。
提示
- 请注意数据读入对程序效率造成的影响。
- 请注意本题的空间限制为 8 Mb。
时限 2 秒,内存 8192 KB(8 MiB —— ⚠ 按 KB 读,别顺手写成 8 MB 以外的数)。
输入输出样例
输入
9 2 2 1 3 3 3 2 3 1
输出
2
九根筷子。⚠⚠ 数一数每个长度出现了几次:2 出现 3 次、3 出现 4 次、1 出现 2 次。
落单的那一根是某根长度为 2 的筷子 —— 而长度 2 本身出现了三次。
★ 这组样例是照着题面那句话精心挑的,第 ④ 步会看到它一测就打死一个错法。
1★ 这道题的两个关卡,题面自己写在【提示】里了 —— 而它们都不在算法上
第 46 章拿这道题当例题,用的是一份改小过的题面。两边并排看:
| 第 46 章正文那份 | ★ 真题 P1469 | |
|---|---|---|
| 筷子数 | m ≤ 2×10⁶+1 |
n ≤ 10⁷+1(多 5 倍) |
| 长度 | ≤ 10⁸ |
≤ 10⁹ |
| 时限 | 1 秒 | 2 秒 |
| 内存 | 256 MiB | ★★★ 8192 KB = 8 MiB(少 32 倍) |
⇒ ★★★ 正文第 7 步那张四行表,换到真题上有两行要翻面。
正文写着「O(n log n) 排序:0.179 秒 / 11.5 MiB / ✓ 能过」——
那句话在那份题面上是对的;在真题上它是一个 MLE。
⇒ 这一页真正在讲的事只有一句:这道题的关卡是内存和读入,不是算法。 而题面末尾那两句提醒(「注意读入效率」「空间限制 8 Mb」),逐句对应这两个关卡。
| 做法 | 要多少内存 | 对 8 MiB 来说 |
|---|---|---|
把 n 个 int 存下来再排序 |
(10⁷+1) × 4 B = 38.15 MiB |
✗ 超 4.77 倍 |
int 值域桶 cnt[10⁹+1] |
3.73 GiB | ✗ 超 477 倍 |
char 值域桶(只记奇偶) |
954 MiB | ✗ 超 119 倍 |
map<int,int> 计数 |
约 229 MiB(5×10⁶ 个节点 × 48 B) | ✗ 超 28 倍 |
| ★ 异或 | 一个 int |
✓ 和 n 完全无关 |
★★ 这五行一行程序都不用跑就能算完,而它们已经把这道题的解法选定了。 ⇒ 第 45 章那把尺子的另一头:估内存和估时间是同一个动作, 而这道题上先撑不住的是内存。
2第 ① 版:存下来排序 —— 答案完全正确,而它一分都拿不到(不,它拿 30 分)
./p1469Count mem sort 真跑一遍顶格 n = 10⁷+1(getrusage 读峰值):
| 版本 | 它自己要了多少(峰值 − 起步) | 顶格峰值 | 8 MiB 的限制 |
|---|---|---|---|
| ★ 异或版(边读边算) | ★ 0.0 MiB —— 一个字节都不要 | = 起步 | ✓ |
| ✗ 排序版 | 38.2 MiB(正好是 (10⁷+1) × 4 字节) |
41.9 MiB(⚠ 独占、从 shell 里量的) | ✗ 超 4.8 倍 |
⚠⚠ 它的输出逐字节正确。样例过、四档 1200 轮对拍 0 次不一致、编译零警告。 ⇒ 「答案对但跑不完」只能靠算发现的又一次 —— 而这一次连「数次数」都用不上,它是一句乘法。
第一跤:断言写成「异或版峰值 3.7 MiB < 8 MiB」,check:viz 当场翻红 ——
同一个二进制、同一份环境变量,从 shell 里跑起步是 3.7 MiB,被 node 起来是 8.4 MiB。
⇒ 那条断言于是得出了「异或版也 MLE」这种结论。改成量差值(峰值 − 起步)之后
--ch=46 单跑绿了:异或版 0.0 MiB,排序版 38.2 MiB。
第二跤:换成「排序版顶格峰值在 38~46 MiB」——--ch=46 单跑是 41.9 / 42.0,
可在全量里(那会儿 node 自己的 rss 已经 680 MB)又红了。
⇒ ★★★ 两跤合起来才是完整的结论:ru_maxrss 这个量里有两截都不属于被测程序 ——
一截取决于谁 spawn 它,另一截取决于那一刻机器有多忙。
⇒ 所以这一页最后只钉了一个数:异或版自己要了 0.0 MiB(它什么都没申请,
所以峰值恒等于起步,跨宿主一定成立);
★★ 而排序版那 38.15 MiB 是算出来的,不是量出来的——(10⁷+1) × 4 字节,一句乘法。
⇒ ★★ 这和第 34 章 P3366 那条「不把读入那 19 毫秒减掉会得出完全相反的结论」 是同一个形状:一个加性开销会把小数字整个吃掉,却在大数字上完全隐形 —— 而这一次还多一层:那个加性开销自己也是会变的。
// P1469 ✗ 排序版:**答案是对的** —— 它挂在题面那行「空间限制 8 Mb」上//// ★ 它就是[第 46 章正文第 3 步](/ch/46-bitwise/)那份 sortPair.cpp,正文里写的是「✓ 能过」。// 那句话没错 —— 因为正文那道题是 m ≤ 2×10⁶+1、内存 **256 MiB**。// 换到真题(n ≤ 10⁷+1、内存 **8 MiB**)上:// (10⁷+1) 个 int = 40 000 004 字节 = **38.15 MiB**,而限制是 8 MiB// ⇒ 光是「把数据存下来」这一步就超了 **4.8 倍**,排序还没开始。//// ⚠⚠ 这一版是这一页的主角,因为它演示的是一整类事故:// **答案对、样例过、对拍 300 轮 0 次不一致,而它在评测机上一分都拿不到。**// ⇒ 「答案对但跑不完(装不下)」只能靠**算**发现([第 20 章 P5019](/sol/p5019/) 那条)。#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<int> a(n); // ⚠ 就是这一行:n = 10⁷+1 时它要 38.15 MiB for (int i = 0; i < n; i++) cin >> a[i]; sort(a.begin(), a.end()); for (int i = 0; i + 1 < n; i += 2) if (a[i] != a[i + 1]) { cout << a[i] << '\n'; return 0; } cout << a[n - 1] << '\n'; // ★ 前面全成对 ⇒ 落单的排在最后(p1469Last 少的就是这一行) return 0;}点「运行 ▶」看结果
n ≤ 10⁵ 时把数据存下来只要 0.38 MiB,排序也就几毫秒。
⇒ 这一版稳拿 30 分,而且它是考场上十分钟就能写完的第一反应。
⇒ 「出题人把暴力那一档也写好了」的又一次。 ★ 而这道题的分档只有一档 ——「30%」和「100%」之间没有中间地带, 所以真正的问题只有一个:你要不要为那 70 分去想第二个办法。
3⚠ 第 ② 版:同样是排序,而它还漏了一行 —— 触发条件是一句话
排序之后一对一对地看,a[i] != a[i+1] 的第一个 a[i] 就是答案。
可循环条件是 i + 1 < n,而 n 是奇数 ⇒ 最后一根永远没人跟它比。
落单的那根正好最长时,循环走到头一次都没触发,ans 还是初值 0。
★ 触发条件只有一层,而且是一句能写下来的话:落单的那个长度 == 最大值 ⇒ 抓获数应当 ≡ 满足这句话的轮数。四档实测(每档 300 轮):
| 档位 | 落单的 == 最大值 | ✗ 真被抓 | |
|---|---|---|---|
| 0 ★ 顺手写的(1 |
71 | 71 | ★ 1.0 |
| 1 ⚠ 专门档:落单的正好最长 | 300 | 300 | ★ 1.0 |
| 2 ⚠ 专门档:落单的那个长度出现 3 次 | 71 | 71 | ★ 1.0 |
| 3 ★ 长度放到 10⁹(400~600 对) | 0 | ★ 0 | ★ 1.0 |
⇒ ★★ 四格一个不差。而档 3 那个 0 不是「结构上抓不到」,是概率低:
落单的恰好最长的概率约 1 / 不同长度的个数 ≈ 1/500,300 轮抓不到很正常。
⇒ 「对拍 0 次有两种原因」:这一次是值域太大把它稀释掉了,
而档 1 那个专门档一行就把它顶到 300。
4⚠⚠ 第 ③ 版:把「落单」读成了「只出现一次」—— 而官方样例一测就死
这两句话差得很远:
「其余都成双」⇒ 别的长度出现的是偶数根(2 根、4 根、6 根都行); 「只有一只筷子落单」⇒ 落单那个长度出现的是奇数根 —— 可以是 1 根, 也可以是 3 根(一双 + 那只落单的)。
⇒ 于是「用 map 数一遍,输出计数为 1 的那个」是错的。
★★★ 而官方样例正是照着这句话造的:
9
2 2 1 3 3 3 2 3 1
长度 2 出现 3 次 <-- 答案
长度 3 出现 4 次
长度 1 出现 2 次整组数据里没有任何一个长度只出现一次 ⇒ 这一版当场打出 0。
// P1469 ✗ 错法二:「落单」读成了「只出现一次」//// ⚠⚠ 题面写的是「这些筷子中**只有一只筷子是落单的**,其余都成双」——// 它说的是**筷子**落单,不是**长度**只出现一次。// 同一个长度完全可以出现 3 根(一双 + 那只落单的),别的长度也可以出现 4 根(两双)。//// ★★★ 而**官方样例就是照着这个坑造的**:// 9 / 2 2 1 3 3 3 2 3 1 ⇒ 2 出现 3 次、3 出现 4 次、1 出现 2 次,答案是 2// ⇒ 这一版在样例上找不到任何「只出现一次」的长度,当场打出 0。//// ★ 它靠什么现形:**落单的那个长度出现次数 ≥ 3**。// ⚠ 而顺手写的生成器(k 对 + 1 根落单)**一辈子造不出这一档** ⇒ 档 0 是**结构性的精确的 0**。// ⇒ 「[样例比对拍还狠](/sol/p1217/)」的又一次,而这次原因很具体:// 出题人是照着题面那句话挑的样例,随机生成器不会。#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; if (!(cin >> n)) return 0; map<int, int> cnt; for (int i = 0; i < n; i++) { int x; cin >> x; cnt[x]++; } for (auto& kv : cnt) if (kv.second == 1) { cout << kv.first << '\n'; return 0; } // ⚠ 就是这个 == 1 cout << 0 << '\n'; return 0;}点「运行 ▶」看结果
所有人写这道题的生成器都是同一个套路:挑 k 个不同的长度各来一对,再加一根落单的。
⇒ 落单的那个长度必然只出现一次 ⇒ 这个错法在档 0 是结构性的精确的 0。
| 档位 | 落单的长度出现 ≥ 3 次 | ✗ 真被抓 |
|---|---|---|
| 0 ★ 顺手写的 | ★ 0 | ★ 0(结构性的) |
| 1 落单的正好最长 | ★ 0 | ★ 0(同一个结构) |
| 2 ⚠ 专门档:给落单的那个长度再加一对 | 300 | 300 |
| 3 长度放到 10⁹ | ★ 0 | ★ 0 |
⇒ ★★★ 四档里三档是 0,而官方样例一测就死。 这是「样例比对拍还狠」的第三次(前两次是第 8 章 P2249、 第 41 章 P1217),★ 而这一次的原因说得最干净: 出题人是照着题面那句话挑的样例,而随机生成器只会照着「最顺手的那种结构」造。
⇒ 能救它的不是加轮数(加到一万轮还是 0),是回去把题面那句话再读一遍,然后照着它加一档。
5★ 正解:异或 —— 而它有两处是被那 8 MiB 逼出来的
异或为什么能一口气把成对的消掉(a ^ a = 0、a ^ 0 = a、顺序无所谓),
第 46 章第 5 步已经讲透了,这里不重复。
这一页要补的是真题题面逼出来的两处写法:
- ★ 一个数组都不开。 异或天生就是「边读边算」,顶格
n = 10⁷+1时它自己申请的内存是 0.0 MiB (峰值就等于起步,和n完全无关)。 ⇒ 在 8 MiB 这条线下,这不是「更优雅」,是唯一能过的形状。 - ⚠⚠ 快读的缓冲区只开 64 KiB。 顶格输入约 94 MB(实测 98 707 677 字节), 而缓冲区是要算进那 8 MiB 的 —— 开到 4 MiB,加上进程本身那 3.7 MiB 就已经在和限制赛跑了。 ⇒ 要的是「读完就续」的窗口,不是「一次读完」的桶(下一步就是那个反面教材)。
// P1469 找筷子 —— 正解:一个变量,一个循环,边读边算//// ★★★ 这道题和[第 46 章正文](/ch/46-bitwise/)那道是同一道,但**题面那两行限制不一样**:// 正文那份是「m ≤ 2×10⁶+1、长度 ≤ 10⁸、内存 256 MiB」,// 真题是 「n ≤ 10⁷+1、a_i ≤ 10⁹、内存 **8192 KB = 8 MiB**、时限 2 秒」。// ⇒ 正文第 7 步那张四行表上「✓ 能过」的排序版,在真题上是 **MLE**(见 p1469Sort.cpp)。//// ⚠ 所以这一份有两处是被那 8 MiB 逼出来的,不是为了炫技:// ① **一个数组都不开** —— 异或天生就是边读边算,峰值内存和 n 无关;// ② 快读的缓冲区只开 **64 KiB**。⚠⚠ 顶格输入约 94 MB(随机数据实测 98 707 677 字节;// 每个数都顶到 10 位那种极端情形约 110 MB),// 「一次性 fread 整个文件」那种常见模板在这道题上直接 MLE(见 p1469Whole.cpp),// 而缓冲区哪怕开到 4 MiB,加上进程本身那 ~4 MiB 就已经在和 8 MiB 赛跑了。// ⇒ 要的是「读完就续」的**窗口**,不是「一次读完」的桶。#include <cstdio>using namespace std;
static char buf[1 << 16]; // ⚠ 64 KiB —— 这道题里缓冲区大小是有上限的static size_t bpos = 0, blen = 0;static inline int gc() { if (bpos == blen) { blen = fread(buf, 1, sizeof(buf), stdin); bpos = 0; if (!blen) return EOF; } return buf[bpos++];}static inline int readInt() { int c = gc(), x = 0; while (c != EOF && (c < '0' || c > '9')) c = gc(); for (; c >= '0' && c <= '9'; c = gc()) x = x * 10 + (c - '0'); return x;}
int main() { int n = readInt(); int res = 0; // ★ 初值必须是 0:它是异或的单位元(a ^ 0 = a) for (int i = 0; i < n; i++) res ^= readInt(); printf("%d\n", res); return 0;}点「运行 ▶」看结果
6⚠ 读入这一关:题面那句提醒值多少 —— 而默认 cin 正好压在线上
// P1469 的第一道门槛:把 10⁷+1 个数读进来(题面【提示】自己点了名的那件事)//// 用法:./p1469Read [n] 人话版(默认题面顶格 n = 10⁷+1)// ./p1469Read [n] csv 给 check:viz 用//// ★ 先算一笔账([第 10 章 P1271](/sol/p1271/) 那把尺子:**字节数**,不是数的个数):// n = 10⁷+1 个数、每个 a_i ≤ 10⁹ ⇒ 平均 9 位多 + 一个分隔符 ≈ 每个数 10 字节// ⇒ 输入约 **94 MB**(本文件实测 98 707 677 字节)。这是全书最大的一次读入// ([第 12 章 P1923](/sol/p1923/) 那次是 48 MB)。//// ⚠⚠ 而这道题的内存限制是 **8192 KB = 8 MiB** ⇒ 那 94 MB **不许整个进内存**。// 所以这张表里第 ④ 行是「fread **窗口**」(64 KiB,读完就续),// 不是网上最流行的「一次性 fread 整个文件」—— 那一份在这道题上光缓冲区就超 11.8 倍。//// ⚠ 这份程序自己造数据写进临时文件,再 freopen 回来读 —— 不需要喂输入。// ⚠ 「同步开着」那一趟必须排最前面(关掉同步之后 cin 会预读,换文件会读到残渣)。// ★★ 每一档跑 3 次取中位数,而且断言只钉**倍数**、不钉秒数// —— 硬规矩第 5 条,代价见 [P1923](/sol/p1923/) 那两次翻红。#include <bits/stdc++.h>#include <chrono>using namespace std;using namespace std::chrono;
static char path[64];
static void makeData(int nn) { snprintf(path, sizeof(path), "/tmp/p1469-bench-%d.txt", (int)getpid()); FILE* f = fopen(path, "w"); if (!f) { fprintf(stderr, "写不出临时文件\n"); exit(1); } mt19937 rng(20260906u); fprintf(f, "%d\n", nn); for (int i = 0; i < nn; i++) fprintf(f, "%u%c", (unsigned)(rng() % 1000000000u) + 1, i + 1 == nn ? '\n' : ' '); fclose(f);}static void reopenIn() { if (!freopen(path, "r", stdin)) { fprintf(stderr, "读不回临时文件\n"); exit(1); } }
static char ibuf[1 << 16]; // ★ 64 KiB 的**窗口**(正解用的就是这个大小)static size_t ipos = 0, ilen = 0;static inline int gc() { if (ipos == ilen) { ilen = fread(ibuf, 1, sizeof(ibuf), stdin); ipos = 0; if (!ilen) return EOF; } return ibuf[ipos++];}static inline int readIntFast() { int c = gc(), x = 0; while (c != EOF && (c < '0' || c > '9')) c = gc(); for (; c >= '0' && c <= '9'; c = gc()) x = x * 10 + (c - '0'); return x;}
int main(int argc, char** argv) { int n = (argc > 1) ? atoi(argv[1]) : 10000001; bool csv = (argc > 2 && string(argv[2]) == "csv"); makeData(n);
long long bytes = 0; { FILE* f = fopen(path, "rb"); fseek(f, 0, SEEK_END); bytes = ftell(f); fclose(f); } double mb = (double)bytes / 1048576.0;
const int REP = 3; double rep[4][REP]; int res[4] = {0, 0, 0, 0}; const char* NAME[4] = { "cin 默认(同步开着)", "cin 关同步", "scanf", "fread 窗口快读(64 KiB)" }; auto median3 = [](double* a) { double x = a[0], y = a[1], z = a[2]; return max(min(x, y), min(max(x, y), z)); };
for (int r = 0; r < REP; r++) { reopenIn(); auto t0 = steady_clock::now(); int s = 0; int nn; cin >> nn; for (int i = 0; i < n; i++) { int x; cin >> x; s ^= x; } rep[0][r] = duration<double, milli>(steady_clock::now() - t0).count(); res[0] = s; }
ios::sync_with_stdio(false); cin.tie(nullptr); for (int r = 0; r < REP; r++) { reopenIn(); auto t0 = steady_clock::now(); int s = 0; int nn; cin >> nn; for (int i = 0; i < n; i++) { int x; cin >> x; s ^= x; } rep[1][r] = duration<double, milli>(steady_clock::now() - t0).count(); res[1] = s; }
for (int r = 0; r < REP; r++) { reopenIn(); auto t0 = steady_clock::now(); int s = 0; int nn; if (scanf("%d", &nn) != 1) return 1; for (int i = 0; i < n; i++) { int x; if (scanf("%d", &x) != 1) return 1; s ^= x; } rep[2][r] = duration<double, milli>(steady_clock::now() - t0).count(); res[2] = s; }
for (int r = 0; r < REP; r++) { reopenIn(); ipos = ilen = 0; auto t0 = steady_clock::now(); int s = 0; int nn = readIntFast(); (void)nn; for (int i = 0; i < n; i++) s ^= readIntFast(); rep[3][r] = duration<double, milli>(steady_clock::now() - t0).count(); res[3] = s; }
remove(path); double ms[4]; for (int i = 0; i < 4; i++) ms[i] = median3(rep[i]); bool same = (res[0] == res[1] && res[1] == res[2] && res[2] == res[3]);
if (csv) { printf("n,%d\n", n); printf("bytes,%lld\n", bytes); printf("mb,%.1f\n", mb); for (int i = 0; i < 4; i++) printf("ms%d,%.0f\n", i, ms[i]); /* ★★ 断言只钉**倍数**和**量级带**,不钉秒数,更不钉「有没有跨过 2000」—— 默认 cin 实测就在 2000 上下(余量不到 5%),那种布尔量会被并行负载晃翻 ([P1923](/sol/p1923/) 两次翻红换来的教训)。 */ printf("ratioSync,%.2f\n", ms[0] / ms[1]); // 默认 cin / 关同步 printf("ratioScanf,%.2f\n", ms[0] / ms[2]); // 默认 cin / scanf printf("ratioFast,%.2f\n", ms[0] / ms[3]); // 默认 cin / 快读 printf("syncBand,%d\n", (ms[0] > 1200.0 && ms[0] < 6000.0) ? 1 : 0); // 默认 cin 在「秒」这个量级上 printf("othersFit,%d\n", (ms[1] < 500.0 && ms[2] < 500.0 && ms[3] < 500.0) ? 1 : 0); printf("same,%d\n", same ? 1 : 0); printf("wholeMiB,%.1f\n", mb); // 一次性读完至少要这么大的缓冲区 printf("limitMiB,%.1f\n", 8192.0 / 1024.0); return 0; }
printf("P1469 读入这一关:n = %d,输入 %lld 字节(%.1f MB),时限 2 秒\n\n", n, bytes, mb); for (int i = 0; i < 4; i++) printf(" %s:%.0f 毫秒(%s)\n", NAME[i], ms[i], ms[i] < 2000.0 ? "够" : "★ 不够"); printf("\n 四种读法结果一致:%s\n", same ? "是" : "★★ 否(有一份少读了)"); printf(" 默认 cin / 关同步 = %.2f 倍;/ scanf = %.2f 倍;/ 快读 = %.2f 倍\n", ms[0] / ms[1], ms[0] / ms[2], ms[0] / ms[3]); printf(" ⚠ 默认 cin 正好压在时限线上(余量不到 5%%)⇒ 别把结论写成「它一定 TLE」,\n"); printf(" 该写的是「它比关同步慢六倍多,而你根本没有六倍可以浪费」。\n"); printf("\n ⚠ 「一次性 fread 整个文件」那种模板:缓冲区至少要 %.1f MiB,而内存限制是 8.0 MiB\n", mb); return 0;}点「运行 ▶」看结果
(A 机 · WSL2 · Linux 6.18-microsoft · 2026-09-06 · 独占;顶格 n = 10⁷+1,输入 94.1 MB)
| 读法 | 耗时 | 相对默认 cin |
|---|---|---|
cin 默认(同步开着) |
★ 2075 毫秒 | 1.00 |
cin 关同步 |
318 毫秒 | 6.5 倍 |
scanf |
455 毫秒 | 4.6 倍 |
★ fread 窗口快读(64 KiB) |
79 毫秒 | 26 倍 |
★★★ 注意第一行那个数:2075 对 2000,只输 3.75%。 ⇒ 按硬规矩第 5 条,这种余量的秒数不能拿去写断言,也不该拿去写结论 (P1923 那条断言就是这么被并行负载翻过两次的)。
⇒ 该写的结论是这一句:它比关同步慢六倍多,而你根本没有六倍可以浪费。 ★ 而后三行的余量分别是 6.3 / 4.4 / 25 倍 —— 差别不在「够不够」,在「你还剩多少余量去写别的」。
7⚠⚠ 那份流传最广的快读模板,在这道题上是个陷阱
网上抄得最多的快读长这样:开一个大数组,fread 一口气把整个文件读进来,然后在内存里扫指针。
它在绝大多数题上确实最快 —— 而这道题的输入约 94 MB,内存限制 8 MiB。
⇒ 光缓冲区就要超 11.8 倍。而且它还会一声不吭地少读一截: 读满就不再续,后面那几十 MB 的筷子根本没被异或进去,输出一个看着挺正常的错数。
| 缓冲区 | 后果 | |
|---|---|---|
| 第 12 章 P1923 | 开小了(33.5 MB 的桶吃 49.4 MB 的输入) | ⚠ 少读一截,所有版本一起错、对拍全绿 |
| ★ 本题 | 开大了(≥ 94 MiB 才够) | ⚠ MLE |
⇒ ★★ 两头都撞过了,而出路是同一个:用「读完就续」的窗口,别用「一次读完」的桶。 ★ 窗口版还有一个白送的好处:它根本不去算「输入有多大」这件事。
8★★★ 对拍:这一页六个版本里,四个的答案是完全正确的
| 档位(每档 300 轮) | ✗ 排序(MLE) | ✗ 漏最后一行 | ✗ 只出现一次 | ✗ 默认 cin(TLE) | ✗ 整个文件(MLE) |
|---|---|---|---|---|---|
| 0 ★ 顺手写的 | 0 | 71 | ★ 0 | 0 | 0 |
| 1 落单的正好最长 | 0 | 300 | ★ 0 | 0 | 0 |
| 2 落单的出现 3 次 | 0 | 71 | 300 | 0 | 0 |
| 3 长度放到 10⁹ | 0 | ★ 0 | ★ 0 | 0 | 0 |
★★★ 三条读得出来的结论:
- ★★★ 这张表最值钱的是那三列 0。 排序版、默认
cin版、整个文件版的答案永远正确 —— 一万轮也抓不到。而它们恰恰是这道题上最容易挂的三种死法(MLE / TLE / MLE)。 ⇒ 「样例是个『一测就死』的过滤器,而它筛的是『答案错』」的又一次: ★ 对拍也一样。它验的是「算得对不对」,从来不验「装不装得下、跑不跑得完」。 - ★★ 两个 WA 版本的「触发 ≡ 抓获」八格一个不差(见第 ③ ④ 步那两张表)—— 因为它们的触发条件都只有一层,而且都是一句能写下来的话。
- ⚠ 两个 0 的性质完全不同:「漏最后一行」在档 3 是概率低(1/500,加轮数能救); 「只出现一次」在档 0 / 1 / 3 是结构性的(那种输入根本造不出来,加一百万轮还是 0)。 ⇒ 「对拍 0 次有两种原因,造两档就能分开」的又一次。
9★ 哪一版就已经能过了
// P1469 解析页上所有数字的出处。// ./p1469Count 人话版(内存账 + 触发线)// ./p1469Count csv 给 check:viz 用// ./p1469Count mem xor ★ 真跑一遍顶格的异或版,打「峰值 − 起步」// ./p1469Count mem sort ★ 真跑一遍顶格的排序版,打「峰值 − 起步」//// ⚠⚠⚠ 为什么这里报的是**差值**而不是峰值(2026-09-06 撞出来的):// `ru_maxrss` 里有一截是「进程起步」,而**那一截不是程序的属性 —— 它取决于谁 spawn 它**。// 同一个二进制、同一份环境变量:从 shell 里跑起步 **3.7 MiB**,被 node 起来是 **8.4 MiB**。// ⇒ 拿峰值写断言,`check:viz` 里就会得出「异或版也 MLE」这种结论(真踩过一次)。// ⇒ 稳的是**差值**:它就是这份程序自己申请的那些内存,跨宿主不变。//// 三件事:// ① ★★★ **内存账** —— 这道题真正的关卡。题面写的是「8192 KB」,按 KB 读就是 **8 MiB**// ([第 41 章那条](/sol/p1865/):转录题面时时限和内存都要连单位一起抄)。// 四种做法各要多少,全是一句乘法,动手之前就能算完。// ② 两个错法的**触发线**,以及官方样例落在哪一侧。// ③ 题面那句「对于 30% 的数据 n ≤ 10⁵」——⇒ 出题人把暴力那一档也写好了// ([第 24 章 P1776](/sol/p1776/) 那条)。#include <bits/stdc++.h>#include <sys/resource.h>using namespace std;
static double peakMiB() { struct rusage ru; getrusage(RUSAGE_SELF, &ru); return (double)ru.ru_maxrss / 1024.0; // Linux 上 ru_maxrss 的单位是 KiB}
static const long long NMAX = 10000001LL; // 题面顶格 n = 10⁷ + 1static const long long VMAX = 1000000000LL; // 题面顶格 a_i ≤ 10⁹static const double LIMIT = 8192.0 / 1024.0; // 8192 KB = 8.0 MiB
int main(int argc, char** argv) { string mode = argc > 1 ? argv[1] : "";
if (mode == "mem") { string which = argc > 2 ? argv[2] : "xor"; double before = peakMiB(); mt19937 rng(20260906u); if (which == "sort") { vector<int> a((size_t)NMAX); for (long long i = 0; i < NMAX; i++) a[(size_t)i] = (int)(rng() % 1000000000u) + 1; sort(a.begin(), a.end()); double now = peakMiB(); printf("sort 版(把 %lld 个 int 存下来再排序):自己要了 %.1f MiB" "(峰值 %.1f − 起步 %.1f);限制 %.1f MiB ⇒ %s\n", NMAX, now - before, now, before, LIMIT, now - before > LIMIT ? "★ MLE" : "够"); } else { int res = 0; for (long long i = 0; i < NMAX; i++) res ^= (int)(rng() % 1000000000u) + 1; double now = peakMiB(); printf("xor 版(边读边算,一个数组都不开):自己要了 %.1f MiB" "(峰值 %.1f − 起步 %.1f);限制 %.1f MiB ⇒ %s(res=%d)\n", now - before, now, before, LIMIT, now - before > LIMIT ? "★ MLE" : "够", res); } return 0; }
/* ---------- ① 内存账:四种做法,全是一句乘法 ---------- */ double mSort = (double)NMAX * 4.0 / 1048576.0; // 存 n 个 int double mBucketInt = (double)(VMAX + 1) * 4.0 / 1073741824.0; // int 桶(GiB) double mBucketChar = (double)(VMAX + 1) / 1048576.0; // char 桶(MiB) double mMap = (double)((NMAX + 1) / 2) * 48.0 / 1048576.0; // map 节点按 48 字节算 double mSub = (double)100000 * 4.0 / 1048576.0; // 30% 那一档存下来要多少
/* ---------- ② 触发线:官方样例 ---------- */ int sample[] = {2, 2, 1, 3, 3, 3, 2, 3, 1}; map<int, int> c; for (int v : sample) c[v]++; int lone = 0, mx = 0; for (auto& kv : c) { if (kv.second % 2) lone = kv.first; mx = max(mx, kv.first); } int loneCnt = c[lone];
if (mode == "csv") { printf("memSort,%.2f\n", mSort); printf("memBucketInt,%.2f\n", mBucketInt); printf("memBucketChar,%.0f\n", mBucketChar); printf("memMap,%.0f\n", mMap); printf("memSub,%.2f\n", mSub); printf("limit,%.1f\n", LIMIT); printf("overSort,%.2f\n", mSort / LIMIT); printf("sampleLone,%d\n", lone); printf("sampleLoneCnt,%d\n", loneCnt); printf("sampleMax,%d\n", mx); printf("sampleOnceDies,%d\n", loneCnt >= 3 ? 1 : 0); printf("sampleLastDies,%d\n", lone == mx ? 1 : 0); printf("subFits,%d\n", mSub < LIMIT ? 1 : 0); return 0; }
printf("P1469 的关卡在内存上:题面写「8192 KB」= %.1f MiB,n 顶格 %lld,a_i ≤ %lld\n\n", LIMIT, NMAX, VMAX); printf(" ✗ 存下来再排序 %.2f MiB ⇒ 超 %.2f 倍(正文第 3 步说「能过」的那一份)\n", mSort, mSort / LIMIT); printf(" ✗ int 值域桶 %.2f GiB\n", mBucketInt); printf(" ✗ char 值域桶 %.0f MiB\n", mBucketChar); printf(" ✗ map 计数 约 %.0f MiB(按每个节点 48 字节算)\n", mMap); printf(" ★ 异或 一个 int —— 和 n 完全无关\n\n"); printf(" ★ 而题面「30%% 的数据 n ≤ 10⁵」那一档:存下来只要 %.2f MiB ⇒ 排序版稳拿 30 分\n\n", mSub); printf(" 官方样例:落单的是 %d(出现 %d 次),最大值是 %d\n", lone, loneCnt, mx); printf(" ⇒ 「只出现一次」那个错法:%s\n", loneCnt >= 3 ? "★ 一测就死" : "放过"); printf(" ⇒ 「漏了落单在最后」那个错法:%s\n", lone == mx ? "★ 一测就死" : "⚠ 放过(落单的不是最大值)"); return 0;}点「运行 ▶」看结果
| 写法 | 顶格自己要了多少内存 | 顶格读入 | 交上去 |
|---|---|---|---|
| ✗ 存下来排序 | 38.2 MiB(峰值 41.9) | — | ✗ MLE(★ 但 30% 那一档稳拿 30 分) |
| ✗ 漏了最后一行 | 38.2 MiB | — | ✗ WA + MLE |
| ✗ 只出现一次 | 更大(map) |
— | ✗ WA + MLE(★ 样例就死) |
✗ 异或 + 默认 cin |
0.0 MiB | 2075 毫秒 | ⚠ 压在 2000 毫秒的线上 |
| ✗ 异或 + 整个文件 | ★ ≥ 94 MiB(缓冲区) | 最快 | ✗ MLE + 少读一截 |
| ★ 异或 + 64 KiB 窗口 | ★ 0.0 MiB | 79 毫秒 | ★ AC(余量 25 倍) |
⇒ ★★★ 这道题的算法只有三行,而这一页六个版本里四个的答案是对的。 真正分出胜负的两件事,题面已经替你写在【提示】里了:
「请注意数据读入对程序效率造成的影响。」 「请注意本题的空间限制为 8 Mb。」
⇒ ★★ 第 14 章 P1747 那条说「并排放着、语气一样的两句提醒,可能一句是命门、 一句是噪声」;这道题是另一个极端 —— 两句都是命门,而且指的是两件完全不同的事。 读题时把这两句当成数据范围的一部分,比读懂算法要紧。