0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1168,日期见页头。两边不一致时信原站。
题目描述
给定一个长度为 N 的非负整数序列 A,对于前奇数项求中位数。
输入格式
第一行一个正整数 N。
第二行 N 个正整数 A₁…A_N。
输出格式
共 ⌊(N + 1) / 2⌋ 行,第 i 行为 A₁…A_{2i−1} 的中位数。
数据规模与约定
对于 20% 的数据,N ≤ 100;
对于 40% 的数据,N ≤ 3000;
对于 100% 的数据,1 ≤ N ≤ 10⁵,0 ≤ Aᵢ ≤ 10⁹。
时限 1 秒,内存 128 MB。
输入输出样例
输入
7 1 3 5 7 9 11 6
输出
1 3 5 6
★ N = 7 ⇒ 输出 ⌊8/2⌋ = 4 行:A₁ / A₁₋₃ / A₁₋₅ / A₁₋₇ 的中位数。
⚠ 第 7 个数读了,但没有第 5 行 —— 偶数项不问。
输入
7 3 1 5 9 8 7 6
输出
3 3 5 6
★ 第二组样例:同样 7 个数,只是顺序被打乱了。
1第一反应:每到一个奇数项,把前面那一段排一遍序
// 第一反应:每到一个奇数项,就把前面那一段抄出来排一遍序,取正中间那个//// 答案永远对,`O(n² log n)`。题面「20% 的数据 N ≤ 100」那一档随便过,// 顶格 `N = 10⁵` 要排 5 万次、每次平均 5 万个数 —— 第 ② 步量了它到底有多远。
#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 + 1); for (int i = 1; i <= n; i++) cin >> a[i];
string out; for (int i = 1; i <= n; i += 2) { vector<int> v(a.begin() + 1, a.begin() + 1 + i); sort(v.begin(), v.end()); out += to_string(v[i / 2]); out += '\n'; } cout << out; return 0;}点「运行 ▶」看结果
2★ 换成 nth_element —— 而这道题的分档,给暴力留了 40 分
| A 机 · WSL2 · 8 核 / 7 G · 2026-08-31 · 独占 | 每次排序 | 每次 nth_element | ★ 对顶堆 |
|---|---|---|---|
N = 100(题面 20% 档) |
0.00 秒 | 0.00 秒 | 0.00 秒 |
N = 3000(题面 40% 档) |
★ 0.11 秒 | ★ 0.02 秒 | 0.00 秒 |
N = 20 000 |
3.78 秒 | 0.72 秒 | 0.00 秒 |
N = 10⁵(顶格) |
超过 60 秒 | 14.77 秒 | ★ 0.02 秒 |
时限 1 秒 ⇒ 这道题上,连最笨的「每次排一遍」也稳拿 40 分。 ⚠ 而隔壁 P1801 的 30% 档要 3.73 秒 —— 同一个暴力,那道题一分都拿不到。 ⇒ ★★ 「暴力能拿多少分」不是暴力的属性,是「暴力 × 那道题的分档」的属性, 每道题都得自己乘一遍。
★ 机器无关的那把尺子(要碰多少个元素,Σ 奇数项):
N |
3000 | 20 000 | 10⁵ |
|---|---|---|---|
| 碰到的元素个数 | 2 250 000 | 100 000 000 | ★ 2 500 000 000 |
规模 ×6.7 ⇒ 工作量 ×44.4,规模 ×5 ⇒ ×25 —— 一个不差的 O(N²)。
而对顶堆在顶格只做 N = 10⁵ 次堆操作,本机 4 毫秒(不含 I/O)。
3★ 正解:对顶堆 —— 和上一道题同一招,节奏不一样
// P1168 中位数 —— ★ 这一版就能 AC:**对顶堆**(和隔壁 [P1801] 同一招,节奏不一样)//// L:大根堆,装着较小的那一半;R:小根堆,装着较大的那一半。// 始终保持 |L| == |R| 或者 |L| == |R| + 1// ⇒ 读到第 i 个数(i 为奇数)时 |L| = (i+1)/2,**L 的堆顶就是中位数**。//// ============ 和 P1801 的差别:k 长大的节奏 ============// [P1801] 每来一次 GET,k 就 +1(而 ADD 来几个由输入说了算);// 这道题**每读两个数,k 才 +1** —— 所以这里不用手动搬人,// 「保持两边一样高」这一条自己就把 k 顶上去了。// ⇒ 同一招,两个节奏。**题单把它俩排在一起,恰恰是因为它们不一样。**//// ⚠ 只在**奇数项**输出,一共 ⌊(N+1)/2⌋ 行 —— N 是偶数时,最后那个数读进来了但不问。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; if (!(cin >> n)) return 0;
priority_queue<int> L; // 较小的一半(大根堆) priority_queue<int, vector<int>, greater<int>> R; // 较大的一半(小根堆) string out;
for (int i = 1; i <= n; i++) { int x; cin >> x; if (L.empty() || x <= L.top()) L.push(x); else R.push(x);
if (L.size() > R.size() + 1) { R.push(L.top()); L.pop(); } // L 高了,匀一个过去 else if (L.size() < R.size()) { L.push(R.top()); R.pop(); } // R 高了,匀一个回来
if (i & 1) { out += to_string(L.top()); out += '\n'; } // ★ 只在奇数项报 } cout << out; return 0;}点「运行 ▶」看结果
| P1801 黑匣子 | ★ 这道题 | |
|---|---|---|
k(要问第几小)怎么长大 |
每来一次 GET 就 +1,而中间插了几个由输入说了算 |
★ 每读两个数才 +1,节奏是固定的 |
| 于是代码里 | 得手动把 R 的堆顶搬给 L(k 长大那一下) |
★ 只要一句「保持两边一样高」,k 自己就跟上了 |
| 两个堆的分工 | L 装最小的 k 个,R 装其余 |
L 装较小的一半,R 装较大的一半 |
⇒ 「对顶堆」不是一个能背下来的模板 —— 它是「两个堆顶背靠背卡住一条缝」这个想法, 而那条缝在哪儿、怎么往前挪,每道题都要重新读一遍题面。
4⚠ 和算法无关的那一条:只在奇数项输出
// ✗ 错法一:每一项都输出了 —— 题面要的是 ⌊(N+1)/2⌋ 行,只问**奇数项**//// 「对于前奇数项求中位数」这句话,读快了就变成「对每一项求中位数」。// ⚠ 偶数项的中位数本身是没定义的(两个数夹在中间),这一版打的是 L 的堆顶 —— 偏小的那个。// ★ 官方样例 N = 7:正解 4 行,它 7 行,**一测就死**。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; if (!(cin >> n)) return 0;
priority_queue<int> L; priority_queue<int, vector<int>, greater<int>> R; string out; for (int i = 1; i <= n; i++) { int x; cin >> x; if (L.empty() || x <= L.top()) L.push(x); else R.push(x); if (L.size() > R.size() + 1) { R.push(L.top()); L.pop(); } else if (L.size() < R.size()) { L.push(R.top()); R.pop(); } out += to_string(L.top()); // ⚠ 少了 if (i & 1) out += '\n'; } cout << out; return 0;}点「运行 ▶」看结果
题面写的是「对于前奇数项求中位数」,输出 ⌊(N+1)/2⌋ 行。
读快了就变成「对每一项求中位数」—— 而偶数项的中位数本来就没有定义。
★ 四个档 300 / 300 / 300 / 300,每一组都错,所以样例一测就死。
5★★★ 「平衡」这一步的三个版本 —— 其中一个是作者的草稿被实测打回来
| 300 轮 | ★ 前提:「把人从 R 匀回 L」被用到 | ✗ 每项都输出 | ✗ 平衡差一 | ✗ 只写单向 | ⚠ 平衡挪进 if |
|---|---|---|---|---|---|
| 档 0:顺手(值域 1~10) | 278 | 300 | 300 | 265 | ★ 0 |
档 1:值域照题面 0~10⁹ |
288 | 300 | 300 | ★ 288 | ★ 0 |
| ★ 档 2:单调不降 | 300 | 300 | 300 | 297 | ★ 0 |
| ★★ 档 3:单调不升 | ★ 0 | 300 | 300 | ★ 0 | ★ 0 |
| 档 6:专门造 0(值域 0~3) | 269 | 300 | 300 | 217 | ★ 0 |
★★ 档 3 那个 0 是能证的:输入单调不升 ⇒ 每个新来的 x 都 ≤ L.top() ⇒ 一路只往 L 堆
⇒ 「把人从 R 匀回 L」那半句话一次都没被用到,少写它当然一点事都没有。
⇒ ⚠ 而单调不降那一档是 297 / 300 —— 两个形状看着对称,对这个 bug 却是天壤之别
(「为一个 bug 精心造的档位,正是另一个的盲区」的方向版)。
★ 第一层和抓获数在档 1 上一个不差(288 ≡ 288),档 3 上 0 ≡ 0,另外三档差一到两成。
⚠ 顺带一句关于官方样例:三个错法(每项都输出 / 平衡差一 / 只写单向)样例全挡住了,
唯一溜过去的是「平衡挪进 if」—— 而它恰恰根本不是 bug。
⇒ 「样例是一测就死的过滤器」这条规律在这一页走到了它的极限:
这组样例把所有真错法都筛掉了。
草稿上写的是:「偶数项那一步没平衡 ⇒ 两边高度差会到 2 ⇒ 那儿只有一个 if,一次只匀一个人 ⇒ 会错」。
实测五个档 1500 轮,一次都没错。 而理由是一句算术:
★★ 一次「匀一个人」让高度差改变的是 2,不是 1(一边少一个、另一边多一个)。 从「刚平衡完」的
|L| − |R| = 1出发,插两个人之后只可能是 +3 / +1 / −1 —— 而这三种情况,一次匀人(+3 → +1、−1 → +1)或者不匀(+1),正好都修得好。
★ 而这个「精确的 0」配了自检:上表第一列就是它 ——
「高度差真的到过 +3 或 −1」的轮数是 278 / 288 / 300 / 0 / 269,不是 0
⇒ 那批数据问得出这个问题,只是问出来的答案是「它对」。
⇒ ★★★ 于是这一页给出了一对干净的对照: 跳过平衡的那一版一次都不错,每一步都平衡的那一版(少半个方向)错得一塌糊涂 —— 要紧的从来不是「什么时候平衡」,是「一次动作把高度差改了多少」。
6⚠ 题面自己矛盾了一处 —— 而它是噪声
| 题面的三处说法 | |
|---|---|
| 题目描述 | 「给定一个长度为 N 的非负整数序列」 |
| 输入格式 | 「第二行 N 个正整数」 |
| ★ 数据规模 | 0 ≤ Aᵢ ≤ 10⁹ |
⇒ 以数据范围为准:0 是合法输入。那它重要吗?
用第 12 章那个判据——造一档专门违反它 / 触及它,看有没有任何一版的行为变了:
输入里真的出现过 0 的轮数 |
|
|---|---|
档 1(值域 0 ~ 10⁹,照题面随机) |
★ 0 / 300 |
档 6(值域 0 ~ 3,专门造 0) |
272 / 300 |
而档 6 上没有任何一版的对错关系变了(上表最后一行和档 0 是同一个形状)
⇒ 这句话在这道题上是噪声:这一页所有写法都不拿 0 当哨兵,堆里存什么值都一样。
⚠ 但值得记一笔的是上面那个 0 / 300:照题面 0 ~ 10⁹ 随机,
300 轮里一个 0 都造不出来。⇒ 万一哪天真有个写法拿 0 当空标记,
顺手写的生成器结构上抓不到它。
7度量程序和生成器
8一页纸
| ★ 哪一版能过 | 对顶堆(顶格 0.02 秒 / 时限 1 秒);⚠ 而排序暴力稳拿 40 分 |
| ★★ 暴力值多少分 | 这道题 40 分、隔壁 P1801 0 分 —— 主语是「暴力 × 那道题的分档」 |
| ★ 机器无关的尺子 | Σ 奇数项:225 万 / 1.0 亿 / 25 亿,规模 ×5 ⇒ 工作量 ×25 |
| ★★ 和 P1801 的差别 | k 长大的节奏:那道题由输入说了算,这道题每两个数一次,所以不用手动搬人 |
| ⚠ 和算法无关 | 只在奇数项输出,共 ⌊(N+1)/2⌋ 行(样例一测就死) |
| ★★★ 草稿被打回 | 「平衡挪进 if (i & 1)」一次都不错 —— 一次匀人把高度差改 2,+3/+1/−1 全修得好 |
| ★★ 反过来的那个 | 「只写单向平衡」每一步都平衡,却错 265~297 / 300 ⇒ 要紧的是改了多少,不是何时改 |
| ★★ 一个能证的 0 | 单调不升那一档,「从 R 匀回 L」一次没被用到 ⇒ 少写它是精确的 0 |
| ⚠ 题面矛盾 | 「非负」vs「正整数」⇒ 数据范围说了算(0 合法);实测是噪声 |