题单 · 习题解析

洛谷 P1469 找筷子

★★★ 和第 46 章正文是同一道题,可题面那行「空间限制 8 Mb」把正文第 7 步那张表翻了面 —— 正文写着「✓ 能过」的排序版,在真题上顶格实测 **41.9 MiB**(限制 8 MiB,超 5.2 倍)当场 MLE,而异或版是 **3.7 MiB** 且那 3.7 全是进程启动开销;★★ 而这一页六个版本里**四个的答案完全正确**(排序 MLE / 默认 cin / 一次性读整个文件 MLE),对拍四档 1200 轮**三列全零** ⇒ **对拍验的是「算得对不对」,从来不验「装不装得下、跑不跑得完」**;★★★ 「把落单读成只出现一次」那个错法**官方样例一测就死**(样例里 `2` 出现 3 次),而顺手写的生成器(k 对 + 1 根落单)**结构上一辈子造不出那种输入** ⇒ 四档里三档是精确的 0 —— [「样例比对拍还狠」](/sol/p1217/)的第三次;★★ 读入那一关:顶格 94.1 MB,默认 `cin` **2075 毫秒 / 时限 2000**(只输 3.75%)⇒ 结论只能写成「它比关同步慢六倍多,而你根本没有六倍可以浪费」;★★ 快读缓冲区这个旋钮**两头都撞过了** —— [P1923](/sol/p1923/) 是开小了少读一截,这道题是开大了 MLE,出路同一个:用窗口别用桶;★ 两个 WA 版本「触发 ≡ 抓获」八格一个不差

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

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

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

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

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

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

题目描述

经过一段时间的紧张筹备,电脑小组的「RP 餐厅」终于开业了,这天,经理 LXC 接到了一个定餐大单, 可把大家乐坏了!员工们齐心协力按要求准备好了套餐正准备派送时,突然碰到一个棘手的问题:筷子!

CX 小朋友找出了餐厅中所有的筷子,但遗憾的是这些筷子长短不一,而我们都知道筷子需要长度一样的 才能组成一双,更麻烦的是 CX 找出来的这些筷子数量为奇数,但是巧合的是,这些筷子中只有一只筷子 是落单的,其余都成双,善良的你,可以帮 CX 找出这只落单的筷子的长度吗?

输入格式

第一行是一个整数,表示筷子的数量 n

第二行有 n 个整数,第 i 个整数表示第 i 根筷子的长度 aᵢ

输出格式

输出一行一个整数表示答案。

数据规模与约定

  • 对于 30% 的数据,保证 n ≤ 10⁵
  • 对于 100% 的数据,保证 1 ≤ n ≤ 10⁷ + 11 ≤ 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 章拿这道题当例题,用的是一份改小过的题面。两边并排看:

第 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 来说
nint 存下来再排序 (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 分)

⚠⚠ 实测顶格峰值 41.9 MiB —— 而对拍、样例、`-Wall` 全都看不见它

./p1469Count mem sort 真跑一遍顶格 n = 10⁷+1getrusage 读峰值):

版本 自己要了多少(峰值 − 起步) 顶格峰值 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 毫秒减掉会得出完全相反的结论」 是同一个形状:一个加性开销会把小数字整个吃掉,却在大数字上完全隐形 —— 而这一次还多一层:那个加性开销自己也是会变的。

p1469Sort.cpp✗ 排序版:答案对,顶格 41.9 MiB ⇒ MLE
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 但别急着删它 —— 题面那句「30% 的数据 n ≤ 10⁵」是出题人递过来的分数

n ≤ 10⁵ 时把数据存下来只要 0.38 MiB,排序也就几毫秒。 ⇒ 这一版稳拿 30 分,而且它是考场上十分钟就能写完的第一反应。

「出题人把暴力那一档也写好了」的又一次。 ★ 而这道题的分档只有一档 ——「30%」和「100%」之间没有中间地带, 所以真正的问题只有一个:你要不要为那 70 分去想第二个办法。

3⚠ 第 ② 版:同样是排序,而它还漏了一行 —— 触发条件是一句话

★ 「前面全成对 ⇒ 落单的排在最后」,这一行掉了就恒输出 0

排序之后一对一对地看,a[i] != a[i+1] 的第一个 a[i] 就是答案。 可循环条件是 i + 1 < n,而 n 是奇数 ⇒ 最后一根永远没人跟它比。 落单的那根正好最长时,循环走到头一次都没触发,ans 还是初值 0。

★ 触发条件只有一层,而且是一句能写下来的话:落单的那个长度 == 最大值 ⇒ 抓获数应当 满足这句话的轮数。四档实测(每档 300 轮):

档位 落单的 == 最大值 ✗ 真被抓
0 ★ 顺手写的(18 对、长度 120) 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。

p1469Last.cpp✗ 错法一:漏了「落单的排在最后」那一行

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

p1469Once.cpp✗ 错法二:输出「只出现一次」的那个 —— 样例一测就死
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 而顺手写的生成器一辈子造不出这一档 —— 这是「样例比对拍还狠」的第三次

所有人写这道题的生成器都是同一个套路: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 = 0a ^ 0 = a、顺序无所谓), 第 46 章第 5 步已经讲透了,这里不重复。

这一页要补的是真题题面逼出来的两处写法

  1. 一个数组都不开。 异或天生就是「边读边算」,顶格 n = 10⁷+1 时它自己申请的内存是 0.0 MiB (峰值就等于起步,和 n 完全无关)。 ⇒ 在 8 MiB 这条线下,这不是「更优雅」,是唯一能过的形状
  2. ⚠⚠ 快读的缓冲区只开 64 KiB。 顶格输入约 94 MB(实测 98 707 677 字节), 而缓冲区是要算进那 8 MiB 的 —— 开到 4 MiB,加上进程本身那 3.7 MiB 就已经在和限制赛跑了。 ⇒ 要的是「读完就续」的窗口,不是「一次读完」的桶(下一步就是那个反面教材)。
p1469.cpp★ 正解:64 KiB 窗口快读 + 一个 int
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

6⚠ 读入这一关:题面那句提醒值多少 —— 而默认 cin 正好压在线上

p1469Read.cpp四种读法,顶格 n = 10⁷+1,每档 3 次取中位数
// 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;
}
点一下即可编辑
输出
点「运行 ▶」看结果
⚠⚠ 本机实测:默认 cin 2075 毫秒 / 时限 2000 —— 所以这句结论不能写成「它一定 TLE」

(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

⇒ ★★ 两头都撞过了,而出路是同一个:用「读完就续」的窗口,别用「一次读完」的桶。 ★ 窗口版还有一个白送的好处:它根本不去算「输入有多大」这件事

p1469Whole.cpp✗ 错法三:一次性 fread 整个文件(16 MiB 的桶)
p1469Cin.cpp✗ 错法四:算法完全正确,只是没关同步

8★★★ 对拍:这一页六个版本里,四个的答案是完全正确的

p1469Gen.cpp(四档)生成器:顺手写的 / 落单的最长 / 落单的出现 3 次 / 长度放到 10⁹
⚠⚠ 四档 × 六个版本 —— 三列全零,而那三列正是这道题真正会挂的地方
档位(每档 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

★★★ 三条读得出来的结论:

  1. ★★★ 这张表最值钱的是那三列 0。 排序版、默认 cin 版、整个文件版的答案永远正确 —— 一万轮也抓不到。而它们恰恰是这道题上最容易挂的三种死法(MLE / TLE / MLE)。 ⇒ 「样例是个『一测就死』的过滤器,而它筛的是『答案错』」的又一次: ★ 对拍也一样。它验的是「算得对不对」,从来不验「装不装得下、跑不跑得完」。
  2. ★★ 两个 WA 版本的「触发 ≡ 抓获」八格一个不差(见第 ③ ④ 步那两张表)—— 因为它们的触发条件都只有一层,而且都是一句能写下来的话。
  3. 两个 0 的性质完全不同:「漏最后一行」在档 3 是概率低(1/500,加轮数能救); 「只出现一次」在档 0 / 1 / 3 是结构性的(那种输入根本造不出来,加一百万轮还是 0)。 ⇒ 「对拍 0 次有两种原因,造两档就能分开」的又一次。

9★ 哪一版就已经能过了

p1469Count.cpp本页所有数字的出处(内存账 + 触发线)
// 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⁷ + 1
static 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 那条说「并排放着、语气一样的两句提醒,可能一句是命门、 一句是噪声」;这道题是另一个极端 —— 两句都是命门,而且指的是两件完全不同的事。 读题时把这两句当成数据范围的一部分,比读懂算法要紧。