0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P3378,日期见页头。两边不一致时信原站。
题目描述
给定一个数列,初始为空,请支持下面三种操作:
- 给定一个整数
x,请将x加入到数列中。 - 输出数列中最小的数。
- 删除数列中最小的数(如果有多个数最小,只删除 1 个)。
输入格式
第一行是一个整数,表示操作的次数 n。
接下来 n 行,每行表示一次操作。每行首先有一个整数 op 表示操作类型。
- 若
op = 1,则后面有一个整数x,表示要将x加入数列。 - 若
op = 2,则表示要求输出数列中的最小数。 - 若
op = 3,则表示删除数列中的最小数。如果有多个数最小,只删除1个。
输出格式
对于每个操作 2,输出一行一个整数表示答案。
数据规模与约定
- 对于
30%的数据,保证n ≤ 15。 - 对于
70%的数据,保证n ≤ 10⁴。 - 对于
100%的数据,保证1 ≤ n ≤ 10⁶,1 ≤ x < 2³¹,op ∈ {1, 2, 3}。
时限 1 秒,内存 512 MB。
输入输出样例
输入
5 1 2 1 5 2 3 2
输出
2 5
★ 五步:插 2、插 5、问最小(2)、删最小、再问(5)。 ⚠ 那句「如果有多个数最小,只删除 1 个」样例里一次都没考到 —— 第 ⑤ 步会说它值多少钱。
1算法本身,本章第 5、6、7 步已经讲完了
// P3378【模板】堆 —— ★ 这一版就能 AC(手写小根堆,正文第 5、6 步原样搬过来)//// 和正文那道题的差别只有两处,**都不在算法上**:// ① 没有「最后一行把剩下的倒出来」那一问(正文是自己加的,为的是让 bug 露头);// ② 规模从 2×10⁵ 涨到 **10⁶**,而时限只有 1 秒 ⇒ 读入那笔账要算(第 ⑥ 步)。//// ⚠ 题面写的是 `1 ≤ x < 2^31` —— **恰好是 int 的正数半边,一个值都不剩**:// int 的上限 2147483647 就是 2^31 − 1,所以 int 够用,**余量为 0**。// ⇒ 任何「拿一个大常数当哨兵」的写法在这道题上都是错的(第 ④ 步 p3378Inf.cpp)。
#include <bits/stdc++.h>using namespace std;
const int MAXN = 1000006;int a[MAXN]; // ★ a[1..sz] 才是堆,a[0] 空着不用int sz = 0;
/** 上浮:比爸爸小就往上(空穴法,每层只有一次赋值) */void up(int i) { int x = a[i]; while (i > 1 && a[i >> 1] > x) { a[i] = a[i >> 1]; i >>= 1; } a[i] = x;}
/** 下沉:两个儿子里挑**小**的那个换 */void down(int i) { int x = a[i]; while ((i << 1) <= sz) { int c = i << 1; if (c < sz && a[c + 1] < a[c]) c++; // ⚠ c < sz 才有右儿子 if (a[c] >= x) break; a[i] = a[c]; i = c; } a[i] = x;}
int main() { ios::sync_with_stdio(false); // ★ 第 ⑥ 步量过:这一句在这道题上值 3.4 倍 cin.tie(nullptr);
int n; if (!(cin >> n)) return 0;
string out; for (int i = 0; i < n; i++) { int op; cin >> op; if (op == 1) { int x; cin >> x; a[++sz] = x; up(sz); } else if (op == 2) { out += to_string(a[1]); // 堆顶就是最小值,O(1) out += '\n'; } else { a[1] = a[sz--]; // 拿最后一格填到根上,再沉下去 if (sz) down(1); } } cout << out; return 0;}点「运行 ▶」看结果
正文那道题就是这一道多问了一句(最后把剩下的按序倒出来)。 所以这一页不重讲算法,它要做的是另外三件事:
① 把「那我维护一个有序数组不就行了」放到考场规模上量一遍(第 ③ 步)—— ★★★ 而结论会拐个弯:照题面顶格随机撒 10⁶ 个操作,有序数组比正解还快。 ② 把题面那半行
1 ≤ x < 2³¹乘一遍(第 ④ 步); ③ 数一数这道题要读多少字节(第 ⑥ 步)。
2第一反应:拿一个变量记着最小值
// ✗ 第一反应:「不就是求最小吗,用一个变量记着就行了」//// 插入时 mn = min(mn, x),查询就输出 mn —— 前两问都对。// ⚠ 到第三个操作(删除最小)就露馅了:**删掉之后 mn 该变成谁?这一版答不上来。**// (它只能把 mn 重新设回 INF,于是后面的查询全错。)//// ★★ 这一版存在的意义是把这道题真正的难点摆出来:// 难的不是「求最小」,是「**删掉最小之后还要能接着求最小**」——// 一个变量记不住第二小是谁,而重新扫一遍就是 O(n)。//// ⚠ 顺带演示题面那句 `x < 2^31` 的分量:这里的 INF 写的是 0x3f3f3f3f = 1 061 109 567,// 它**比一半以上的合法 x 还小**(第 ④ 步细说)。
#include <bits/stdc++.h>using namespace std;
const int INF = 0x3f3f3f3f;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; if (!(cin >> n)) return 0;
int mn = INF; string out; for (int i = 0; i < n; i++) { int op; cin >> op; if (op == 1) { int x; cin >> x; mn = min(mn, x); } else if (op == 2) { out += to_string(mn); out += '\n'; } else mn = INF; // ⚠ 删掉之后只能认输 } cout << out; return 0;}点「运行 ▶」看结果
插入时 mn = min(mn, x),查询输出 mn —— 前两个操作它完全正确。
到第三个操作(删除最小)就答不上来了:删掉之后,第二小是谁?
一个变量记不住,而重新扫一遍是 O(n)。
「求最小」本身不难(一个变量、一次扫描都行)。 难的是删掉最小之后还要能接着求最小 —— 而且要来 10⁶ 次。
⇒ 这正是本章第 3、4 步那句话的考场版: 一点结构都不攒的话,每次都得重新找一遍。
★ 顺带,它在样例上打出来的那个 1061109567 不是随便一个数 ——
它就是 0x3f3f3f3f,第 ④ 步整节都在说它。
3★★★ 「那我维护一个有序数组」—— 顶格随机上它比正解还快
A 机 · WSL2 · 8 核 / 7 G · 2026-08-31 · 独占 · 顶格 n = 10⁶ |
堆的最大 / 平均大小 | 有序数组搬动的元素个数 | 秒表(时限 1 秒) |
|---|---|---|---|
| ⚠ 三种操作各三分之一,全随机 | 908 / 237 | 156 207 595 | ★ 0.05 秒 |
| ★ 前一半全插入,后一半删查交替 | 500 000 / 312 500 | ★ 156 186 236 672 | ★ 10.94 秒 ⇒ 超时 10.9 倍 |
| ⇒ 两档之比 | 551 倍 | ★ 1000.0 倍 | 219 倍 |
★★★ 顺手照题面顶格随机跑一遍,得到的结论是「有序数组 0.05 秒,比手写堆的 0.07 秒还快」。 而机理是能证的,两行:
三种操作等概率 ⇒ 堆的大小是一条在 0 处反射的随机游走, 它的量级是 √n ≈ 1000,不是
n。实测最大 908、平均 237 —— 一次都没大起来过。 ⇒ 有序数组每次只搬两百来个元素,那当然快。
⇒ 这是「顶格 ≠ 最坏」在本书最锋利的一次:前面几轮撞见的都是 「暴力在顶格随机上跑得完」,这一次是错的选择在顶格随机上反而更快。
搬动的元素个数是机器无关的(两次跑逐位相同),它给的比值是 1000.0; 秒表给的是 219。差在哪儿说得清:
一次搬 30 万个元素的 memmove,摊到每个元素上比一次搬 200 个便宜得多 (连续内存、能向量化)。⇒ 次数和秒表量的从来不是同一件事,两个都要报。
⚠ 而那个 10.94 秒没有写成断言(一条要跑 11 秒)——
断言钉的是算出来的搬动数 156 186 236 672。
★ 「算的和跑的是同一个量」配了自检:档 4 上真跑一遍和用树状数组算一遍
必须给出同一个数(p3378Count.cpp 里的 same,两条路一行代码都不共享)。
⇒ 想复现那 11 秒:./p3378Count full。
4⚠ 题面那半行 1 ≤ x < 2³¹:int 恰好够,而且一个哨兵都不剩
题面的上限 2³¹ − 1 |
2 147 483 647 |
int 的上限 |
2 147 483 647 ⇒ ★ 恰好够,余量为 0 |
而 0x3f3f3f3f(人人都在用的那个 INF) |
1 061 109 567 ⇒ ⚠ 比一半的合法 x 还小 |
⇒ ★★★ 这道题上任何「拿一个大常数当哨兵」的写法都是错的 ——
和第 35 章 P1886 是同一个形状:那道题 aᵢ ∈ [−2³¹, 2³¹)
把 int 的 42.9 亿个值全用光了,这道题用光的是正数那一半。
那个写法本身很聪明:整片数组预填 INF,于是 down() 里不用再判 sz ——
走到堆外面碰到的一定是 INF,「它比谁都大」,循环自己就停了。
⚠ 而这道题把「它比谁都大」变成了假话:一个 20 亿的数往下沉时,
会觉得旁边那个 10.6 亿的空位更小,于是把哨兵换上来 —— 堆里凭空多出一个 1061109567。
5★★★ 值域这一个旋钮,把两个 bug 推向相反的方向
300 轮(n 随机 8~20) |
有重复的数 | 有 x > 10.6 亿 |
✗ set 吃重复 | ✗ INF 哨兵 | ✗ 大根堆 | ✗ 一个变量 |
|---|---|---|---|---|---|---|
| 档 0:值域 1~10(顺手写的小数据) | 268 | ★ 0 | 41 | ★ 0 | 239 | 152 |
| ⚠ 档 1:值域 1~10⁹(顺手写的「大」数据) | ★ 0 | ★ 0 | ★ 0 | ★ 0 | 244 | 160 |
| ★ 档 2:照题面 1 ~ 2³¹−1 | ★ 0 | 296 | ★ 0 | 89 | 244 | 225 |
| ★ 档 3:照题面 + 删除密集 | ★ 0 | 289 | 0 | 111 | 229 | 240 |
★★★ 档 1 是最差的一档:两个 bug 一个都抓不到。
而它恰恰是顺手写生成器时最容易写下的那一行 —— rng() % 1000000000。
理由是一句算术:
10⁹ 比
0x3f3f3f3f= 1 061 109 567 小 5.8% ⇒ 第一层触发条件永远不成立; 而值域一大,十来个数里撞出重复的概率又约等于 0 ⇒ 另一个 bug 的第一层也不成立。
⇒ ★★ 同一个旋钮(值域)把两个 bug 推向相反方向 (第 23 章 P1049 那条的第七次), 而它们中间那一档谁都抓不到 —— 这是本书第一次量到「两头都要,中间是空的」。
| 第一层(触发条件成立) | 真被抓 | 比 | |
|---|---|---|---|
| set 吃重复(档 0) | 268 | 41 | 6.5 倍 |
| INF 哨兵(档 2) | 296 | 89 | 3.3 倍 |
两层之间差的那一截都说得清:重复的数还得真的影响某一次输出或删除;
x > 10.6 亿的数还得真的在某次 down() 里沉到堆的边界上。
⇒ 「≡」是量出来的运气,不是一条规律 —— 写不成就老实报两层。
四个错法里样例挡住了两个(大根堆打出 5 2、一个变量打出 2 1061109567),
放过了两个(set / INF 哨兵)。
⇒ 挡住的两个都是「每一组都错」型,放过的两个都是「偶尔才错」型 ——
而真正让你 WA 在第 7 个点上的,恰恰是后一种。
6⚠ 和算法无关的那一条:这道题要读 12.5 MB
输入最大的形状是「n = 10⁶ 行全是插入」:2 × 10⁶ 个整数、12 482 560 字节(11.9 MB)。
| A 机 · WSL2 · 2026-08-31 · 独占 · 读同一份 11.9 MB | 毫秒 |
|---|---|
默认 cin(同步开着) |
280 |
★ 关同步的 cin |
55 |
scanf |
75 |
手写 fread 快读 |
★ 16 |
★★ 结论不是「一定要写快读」 —— 算法那一头只要 30 毫秒,
就算用最慢的默认 cin,端到端也只有 300 毫秒出头,离 1 秒还远。
⇒ 这道题上读入优化值 4~5 倍,但不写也过得去。
⚠ 而这条结论跨题不能搬:第 6 章 P2367 要读 2 × 10⁷ 个数、时限 1 秒,
那道题连 scanf 都不够。倍数跨题几乎不变,分数线画在绝对时间上。
★ 顺带又一次:关同步的 cin 比 scanf 还快(55 vs 75)。
7度量程序、读入台架和生成器
8一页纸
| 算法 | 本章第 5~7 步原样搬过来,这一页不重讲 |
| ★ 哪一版能过 | 手写堆 31 ms、priority_queue 23 ms、multiset 195 ms —— 三种都能过 |
| ★★★ 顶格随机会骗人 | 同样 10⁶ 次操作,只换形状:有序数组 0.05 秒 → 10.94 秒(搬动数差 1000 倍) |
| ⇒ 机理能证 | 三种操作等概率 ⇒ 堆的大小是在 0 处反射的随机游走,量级 √n(实测最大 908) |
| ★★ 题面那半行 | 1 ≤ x < 2³¹ ⇒ int 恰好够、余量为 0,0x3f3f3f3f 比一半的 x 还小 |
| ★★★ 值域这个旋钮 | 把两个 bug 推向相反方向,而顺手写的 1e9 那一档两个都抓不到 |
| ⚠ 两层触发条件 | 268 → 41(6.5 倍)、296 → 89(3.3 倍)——都写不成 ≡ |
| ⚠ 读入 | 11.9 MB:默认 cin 280 ms / 关同步 55 / scanf 75 / 快读 16 ⇒ 值 4~5 倍,但不写也过 |