读这一页之前,先知道读完要能做什么
- 写出来大根堆的
push/top/pop,以及「取出来用」的固定两句 - 写出来小根堆:
priority_queue<int, vector<int>, greater<int>> - 说清它不能按下标取、也不能遍历 —— 要按顺序拿出来就一个个
pop
这是什么
一句话:priority_queue 是一个「每次都能拿走最大那个」的容器(大根堆)。
★★ 四个动作就够:push 放进去、top 看顶上那个、pop 扔掉顶上那个、empty 空了吗。⚠ top 和 pop 是两件事 —— top 只是看,pop 才是扔。
★★★ 默认是大根。题目要「最小的先出」(合并果子、Dijkstra 都是),就把三个参数写全:priority_queue<int, vector<int>, greater<int>> q; —— 最后那个 greater<int>() 就是第 1 块讲的比较器,只是方向反了。
⚠ 它不能按下标取、也不能遍历:堆只保证顶上那个是对的,底下那些什么顺序标准不作承诺。要按顺序全拿出来,就一个个 pop —— 这本身就是堆排序。
完整动作表
复杂度写的是这个动作自己的代价,不是整个程序的。「这一栏最容易错的地方」那一列是重点。
| 写法 | 干什么 | 代价 |
|---|---|---|
priority_queue<int> q; | 开一个大根堆(默认方向) ★ q.top() 永远是当前最大的那个 | O(1) |
q.push(x) | 放进堆里 ★★ 四个动作里最常用的一个 | O(log n) |
q.top() | 看顶上那个(不扔) ⚠ 空堆上调它会出事 —— 先判 q.empty() | O(1) |
q.pop() | 扔掉顶上那个(不给返回值) ★★★ 要用那个值:先 top() 存下来,再 pop() —— 两句连着写 | O(log n) |
q.empty() / q.size() | 空了吗 / 里面几个 ⚠ 和 int 比大小要写 (int)q.size() | O(1) |
priority_queue<int, vector<int>, greater<int>> q; | 小根堆:最小的在顶上 ★★★ 这一串背下来 —— Dijkstra、合并果子每次都要写 | O(1) |
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> q; | 按 pair 排的小根堆:first 小的先出,first 相同自动比 second★ 「花费小的优先」就这么写,不用自己写比较器;写 greater<>(C++14)也一样 | O(log n) |
while (!q.empty()) { ... q.pop(); } | 把堆里的东西从大到小全拿出来(小根堆就是从小到大) ★ 这就是「没有遍历」的那个替代写法 | O(n log n) |
⭐ 这一节最容易错的地方(3 处)
这一节排在例题前面是故意的:读到一个坑,再去看正确的例子,记得住。 下面每一条都是「编译得过去、答案可能是错的」那种 —— 它们才是 OI 里真正扣分的地方。
① 默认是大根 —— 要「小的先出」,那一长串不能省
priority_queue<int> q 给你的是大根:top() 永远是最大的那个。
★ 程序照常跑、编译零警告、答案整个是反的 —— 而「取出来是最大」看上去还挺合理,特别不容易起疑。大根堆 top() = 9
依次弹出: 9 9 7 5 4 2 2 1// 要最小的先出:
priority_queue<int, vector<int>, greater<int>> q;
// 记不住那一长串也行:存负数进去,取出来再取负
q.push(-x);
int smallest = -q.top();② 取值是两句:先 `top()` 再 `pop()`,顺序不能反
pop() 只负责扔、它不给任何值;top() 只负责看、它不扔。所以「取出最大的」永远是连着的两句。
★ 写反了(先 pop 再 top)拿到的是下一个元素,答案悄悄错;只写 pop 忘了 top,值根本没取到;★ 只写 top 忘了 pop 最要命 —— 顶上那个永远弹不掉,while 变成死循环,跑出来是 TLE,而不是报错。
⚠ int x = q.pop(); 这种写法编译器会拦下来(pop 返回 void),那反而是最幸运的一种。long long x = q.top(); // 先看
q.pop(); // 再扔③ 空堆上调 `top()` / `pop()` —— 不报错,读垃圾值
vector 的 a[i] 一样:越界不报错。空堆上 top() 会去读一块不属于它的内存 —— 拿到的可能是 0,也可能是别的变量的值。
★ 于是「看起来对」的循环换组数据就崩,或者一直转下去。while (!q.empty()) {
use(q.top()); // 先判空,再看顶上
q.pop();
}empty(),判了再 top()。本书哪一章真的用到它
⚠ 下表里的文件路径和那串 probe 是逐字验过的(check:docs 第 ⑳ 条)—— 改了那份代码这儿会红,正是要的。点章号直接去看那一页。
| 章 | 书里的哪一份代码 | 用它来干什么 |
|---|---|---|
| 第 19 章 | code/19-greedy-sorting/p1090Heap.cpp | ★★ 合并果子 —— 例题 ② 的原题,小根堆那串模板逐字一样 |
| 第 37 章 | code/37-heap/heapKth.cpp | ★ 默认的大根堆:第 k 大就是一个个弹掉(练习 ① 同款) |
| 第 37 章 | code/37-heap/stl.cpp | ★★ 第 37 章专门有一份「改用 STL 的那 3 行」,三个模板参数逐个标了名字 |
| 第 32 章 | code/32-dijkstra/fast.cpp | ★★ Dijkstra 的小根堆:pair 直接往里放(练习 ② 同款) |
例题(2 道)
每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。先自己读题想一遍,再看代码。
例题 ① 大根堆和小根堆:只差一个 greater<int>()
给一串整数,用两种堆各出队一遍:默认的大根堆(出来是从大到小)、greater<int> 的小根堆(出来是从小到大),再看看它们的 top() 各是什么。
★ 这一题是 priority_queue 的定性题:同一串数,方向只差那第三个参数。
n。第二行 n 个整数。top(),以及依次弹出的序列。输入
8 5 2 9 1 7 2 9 4
输出
大根堆 top() = 9 依次弹出: 9 9 7 5 4 2 2 1 小根堆 top() = 1 依次弹出: 1 2 2 4 5 7 9 9
greater<int>()。★ 先自己手算:这串数从大到小排是什么、从小到大排是什么,再对照输出看哪行是哪种写法。⚠ 取值时想清楚 top 和 pop 各管一件事。
// STL 专题 · priority_queue —— 大根堆和小根堆,只差一个 greater<int>()//// ★ 默认是**大根**:top() 永远是最大的那个。依次弹出来就是**从大到小**。//// 要「最小的先出」,把三个模板参数写全://// priority_queue<int, vector<int>, greater<int>> q;// ~~~~~~~~~ ~~~~~~~~~~~~~~~~// 元素类型 比较器(← 第 0 块讲的那个,方向反过来)//// ⚠ 取值永远是两句:先 top()(看一眼),再 pop()(扔掉)。pop() 自己不给任何值。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; cin >> n; vector<int> a(n); for (int &t : a) cin >> t;
// ① 默认的大根堆 priority_queue<int> big; for (int t : a) big.push(t); cout << "大根堆 top() = " << big.top() << "\n依次弹出:"; while (!big.empty()) { cout << ' ' << big.top(); // ★ 先看 big.pop(); // ★ 再扔 } cout << "\n\n";
// ② 小根堆:三个参数写全 priority_queue<int, vector<int>, greater<int>> small; for (int t : a) small.push(t); cout << "小根堆 top() = " << small.top() << "\n依次弹出:"; while (!small.empty()) { cout << ' ' << small.top(); small.pop(); } cout << '\n';
return 0;}点「运行 ▶」看结果
priority_queue<int> q;不写第三个参数 ⇒ 大根,q.top()是当前最大的那个(这组数里是 9)。- 一个个弹出来打印:
while (!q.empty()) { cout << q.top(); q.pop(); }—— 结果从大到小,这就是堆排序。 - ★ 换成
priority_queue<int, vector<int>, greater<int>>,同一串数弹出来就是从小到大,top()变成 1。 - ⚠ 每次取值都是两句:先
q.top()(看),再q.pop()(扔)——pop()自己不给任何值。
★★ 默认大根;要小根就把三个参数写全。这串模板不用理解,背下来就行 —— 第 32 章 Dijkstra 每次都要写。
用到的写法:priority_queue<int> q; q.push(x) q.top() q.pop() priority_queue<int, vector<int>, greater<int>> q;
例题 ② 合并果子:每次取最小的两堆
有 n 堆果子,每次把最小的两堆合成一堆,代价是这两堆之和。问最小总代价。
★ 「每次取最小的两堆」就是小根堆的字面意思 —— 整个算法三句话,全在上面的动作表里。
n。第二行 n 个整数(每堆的大小)。输入
4 4 5 1 4
输出
第 1 步:1 + 4 = 5 总代价 5 第 2 步:4 + 5 = 9 总代价 14 第 3 步:5 + 9 = 14 总代价 28 最后剩 14 这一堆,总代价 28
1 + 4 = 5(代价 5)→ 4 + 5 = 9(累计 14)→ 5 + 9 = 14(累计 28)。⚠ 第二步取的 4 和 5 是当时堆里最小的两个,不是输入顺序的前两个。★ 想清楚三件事:① 用大根还是小根;② 合出来的那堆要放回去;③ 一共要合几次。
// STL 专题 · priority_queue —— 合并果子:每次取最小的两堆//// ★ 「每次取最小的」就是小根堆的字面意思,整个算法只有三句://// a = q.top(); q.pop(); // 取第一小// b = q.top(); q.pop(); // 取第二小// q.push(a + b); // 合出来的那堆再放回去//// ⚠ 取**两个**就是两套「top + pop」—— 只取一个,答案就全错了。// ⚠ 这一题的代价会越加越大,用 long long。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; cin >> n; priority_queue<long long, vector<long long>, greater<long long>> q; for (int i = 0; i < n; i++) { long long x; cin >> x; q.push(x); }
long long total = 0; int step = 1; // ★ 一直做到只剩一堆(顶上那个) while (q.size() > 1) { long long a = q.top(); q.pop(); long long b = q.top(); q.pop(); long long c = a + b; total += c; cout << "第 " << step++ << " 步:" << a << " + " << b << " = " << c << " 总代价 " << total << "\n"; q.push(c); }
cout << "最后剩 " << q.top() << " 这一堆,总代价 " << total << "\n"; return 0;}点「运行 ▶」看结果
- 开小根堆,把
n堆全push进去。 - ★★ 循环
n - 1次:取两个就是两套「看一眼、扔一眼」——a = q.top(); q.pop(); b = q.top(); q.pop(); - 合出来的
c = a + b要再push回去,否则后面就轮不到它。 - ⚠ 代价会越累越大,用
long long;循环条件写q.size() > 1—— 只剩一堆就停。
★★★ 「每次取最小 / 最大」的题,八成就是堆 + 这三句。第 37 章的堆、第 32 章的 Dijkstra 用的都是它。
用到的写法:priority_queue<int, vector<int>, greater<int>> q; q.push(x) q.top() q.pop()
练习(2 道)
只给题面、样例和一句提示,答案是折叠着的 —— 先自己写一版。
练习 ① 第 k 大:弹掉 k−1 个
给 n 个整数和 k,输出第 k 大的数(从大到小数第 k 个)。
★ 大根堆最经典的一招:顶上永远是最大的,弹掉 k − 1 个,顶上就是答案。
n k。第二行 n 个整数。k − 1 个数、第 k 大的值,以及堆里还剩几个。输入
7 3 3 9 4 7 5 1 8
输出
弹掉: 9 8 第 3 大 = 7 (堆里还剩 5 个没动)
★ 大根堆的顶是最大。想清楚弹几次才轮到第 k 大(弹 1 次轮到第 2 大……)。⚠ 还要想清楚 k 和 n 的关系 —— k 比 n 大时该怎么办。
★★ 弹掉 k − 1 个,顶上就是第 k 大。⚠ 弹掉的那些是白扔的,所以循环次数和「取第 k 个」差一次。
练习 ② 按花费优先:pair 直接往堆里放
有 n 个任务,每个有花费和编号。反复取出花费最小的那个;花费相同则编号小的先出。
★ 这一题练的是「带第二关键字的优先级」—— 不用写比较器,pair + greater 就够。
n。之后 n 行,每行是 花费 编号。输入
5 30 5 10 2 10 7 20 1 30 3
输出
按花费从小到大取: 1. 花费 10 编号 2 2. 花费 10 编号 7 3. 花费 20 编号 1 4. 花费 30 编号 3 5. 花费 30 编号 5
★ pair 默认比 first(大的在上),first 相同时自动比 second。⚠ 想清楚 pair 里谁放前面、套不套 greater 分别管什么。
★★★ 「按什么优先」就是一个比较器 —— greater<pair<int,int>> 和第 1 块讲的是同一件事,只是写成了模板参数。
用到的写法:priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> q; q.top() q.pop()
← 上一块:queue / stack / deque:一头动还是两头动 · 回 STL 专题目录 · 下一块:map:键 → 值的一张表 →
⚠ 本页说的是 STL 怎么用。算法本身(Dijkstra、单调栈、状压……)在53 章正课里,而每一章末尾都会告诉你它用到了哪些 STL。