题单 · 习题解析

洛谷 P1168 中位数

★★ 和隔壁 [P1801](/sol/p1801/) 是**同一招的两个节奏**:那道题的 k 由输入说了算、要手动搬人,这道题**每读两个数 k 才 +1**,一句「保持两边一样高」就够了;★ 而这道题的分档**给暴力留了 40 分**(N ≤ 3000 时排序 0.11 秒 / 时限 1 秒),⚠ 而同一个暴力在 P1801 上**一分都拿不到** ⇒ **「暴力值多少分」是「暴力 × 那道题分档」的属性**;★★★ 这一页最值钱的是**作者的草稿被实测打回**:把平衡塞进 `if (i & 1)` 里(偶数项不平衡)**1500 轮一次都没错** —— 因为**一次「匀一个人」把高度差改的是 2 不是 1**,插两个人之后只可能是 +3/+1/−1,一次匀正好都修得好;★★ 而反过来「只写单向平衡」每一步都平衡却错 265~297/300 ⇒ **要紧的是一次动作改了多少,不是什么时候改**;★★ 「单调不升」那一档是**能证的精确的 0**(新来的数永远 ≤ L 的堆顶,那半句话从没被用到);⚠ 题面自相矛盾(描述说非负、输入格式说正整数),数据范围说了算 —— 实测是**噪声**,而照题面 0~10⁹ 随机 **300 轮一个 0 都造不出来**

原题:洛谷 P1168出自 第 37 章 堆与 priority_queue 的题单题面本地存档:2026-08-31
⚠ 先自己写一遍,再往下看

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

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第一反应:每到一个奇数项,把前面那一段排一遍序

p1168Sort.cpp答案永远对,O(n² log n) —— 20% 那一档随便过
// 第一反应:每到一个奇数项,就把前面那一段抄出来排一遍序,取正中间那个
//
// 答案永远对,`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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2★ 换成 nth_element —— 而这道题的分档,给暴力留了 40 分

p1168Nth.cpp去掉一个 log(第 12 章的快速选择)—— 40% 那一档 0.02 秒
★★ 同一个「每次重扫一遍」的暴力,在这道题上值 40 分,在隔壁那道上值 0 分
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.cpp★ 这一版就能 AC(顶格 0.02 秒 / 时限 1 秒)
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 同一招的两个节奏 —— 题单把这两道排在一起,恰恰是因为它们不一样
P1801 黑匣子 ★ 这道题
k(要问第几小)怎么长大 每来一次 GET 就 +1,而中间插了几个由输入说了算 每读两个数才 +1,节奏是固定的
于是代码里 手动R 的堆顶搬给 Lk 长大那一下) ★ 只要一句「保持两边一样高」,k 自己就跟上了
两个堆的分工 L 装最小的 k 个,R 装其余 L 装较小的一半,R 装较大的一半

「对顶堆」不是一个能背下来的模板 —— 它是「两个堆顶背靠背卡住一条缝」这个想法, 而那条缝在哪儿、怎么往前挪,每道题都要重新读一遍题面。

4⚠ 和算法无关的那一条:只在奇数项输出

p1168Even.cpp✗ 每一项都输出了(官方样例:7 行 vs 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

题面写的是「对于前奇数项求中位数」,输出 ⌊(N+1)/2⌋ 行。 读快了就变成「对每一项求中位数」—— 而偶数项的中位数本来就没有定义。 ★ 四个档 300 / 300 / 300 / 300,每一组都错,所以样例一测就死。

5★★★ 「平衡」这一步的三个版本 —— 其中一个是作者的草稿被实测打回来

p1168Bal.cpp✗ 平衡条件差一:多出来的那一个该归谁,记反了(样例挡住)
p1168One.cpp✗ 平衡只写了单向 —— 只管「L 太高」,忘了「R 太高」
p1168Late.cpp⚠ 把平衡塞进 if (i & 1) 里 —— 看着像 bug,1500 轮一次都没错
★★★ 对拍 300 轮 × 五个档
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。 ⇒ 「样例是一测就死的过滤器」这条规律在这一页走到了它的极限: 这组样例把所有真错法都筛掉了。

⚠⚠ 那个「平衡挪进 if」的五个 0 —— 本页作者的草稿在这儿被打回来了

草稿上写的是:「偶数项那一步没平衡 ⇒ 两边高度差会到 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⚠ 题面自己矛盾了一处 —— 而它是噪声

★ 「非负整数」还是「正整数」?用第 12 章那个判据称一称
题面的三处说法
题目描述 「给定一个长度为 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度量程序和生成器

p1168Count.cpp度量程序(本页除耗时表外的数字都出自它)
p1168Gen.cpp(七个档位)数据生成器

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 合法);实测是噪声