课程 · C++ 速查 · STL 专题 · 第 7 块

priority_queue:每次拿走最大的那个

学会堆的四个动作,以及「最小的先出」时那串三个参数的模板怎么写。

第 7 块(共 12 块已写好)8 条动作3 处陷阱2 道例题2 道练习4 份可运行代码

读这一页之前,先知道读完要能做什么

  • 写出来大根堆的 push / top / pop,以及「取出来用」的固定两句
  • 写出来小根堆:priority_queue<int, vector<int>, greater<int>>
  • 说清它不能按下标取、也不能遍历 —— 要按顺序拿出来就一个个 pop

这是什么

4 段

一句话:priority_queue 是一个「每次都能拿走最大那个」的容器(大根堆)。

★★ 四个动作就够:push 放进去、top 看顶上那个、pop 扔掉顶上那个、empty 空了吗。⚠ top 和 pop 是两件事 —— top 只是看,pop 才是扔。

★★★ 默认是大根。题目要「最小的先出」(合并果子、Dijkstra 都是),就把三个参数写全:priority_queue<int, vector<int>, greater<int>> q; —— 最后那个 greater<int>() 就是第 1 块讲的比较器,只是方向反了。

⚠ 它不能按下标取、也不能遍历:堆只保证顶上那个是对的,底下那些什么顺序标准不作承诺。要按顺序全拿出来,就一个个 pop —— 这本身就是堆排序。

完整动作表

8 条

复杂度写的是这个动作自己的代价,不是整个程序的。「这一栏最容易错的地方」那一列是重点。

写法干什么代价
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();
★★★ 默认「大」,要「小」就把三个参数写全 —— 第 37 章 3 行那版就是这么写的。

② 取值是两句:先 `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()。

本书哪一章真的用到它

4 处

⚠ 下表里的文件路径和那串 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>()

pq-basic.cpp

给一串整数,用两种堆各出队一遍:默认的大根堆(出来是从大到小)、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
大根堆的顶是最大的 9,弹出来从大到小;小根堆的顶是 1,弹出来从小到大 —— 同一串数,只差 greater<int>()。
先想一想

★ 先自己手算:这串数从大到小排是什么、从小到大排是什么,再对照输出看哪行是哪种写法。⚠ 取值时想清楚 top 和 pop 各管一件事。

pq-basic.cpp参考代码
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. priority_queue<int> q; 不写第三个参数 ⇒ 大根,q.top() 是当前最大的那个(这组数里是 9)。
  2. 一个个弹出来打印:while (!q.empty()) { cout << q.top(); q.pop(); } —— 结果从大到小,这就是堆排序。
  3. ★ 换成 priority_queue<int, vector<int>, greater<int>>,同一串数弹出来就是从小到大,top() 变成 1。
  4. ⚠ 每次取值都是两句:先 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;

例题 ② 合并果子:每次取最小的两堆

pq-merge.cpp

有 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 是当时堆里最小的两个,不是输入顺序的前两个。
先想一想

★ 想清楚三件事:① 用大根还是小根;② 合出来的那堆要放回去;③ 一共要合几次。

pq-merge.cpp参考代码
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. 开小根堆,把 n 堆全 push 进去。
  2. ★★ 循环 n - 1 次:取两个就是两套「看一眼、扔一眼」—— a = q.top(); q.pop(); b = q.top(); q.pop();
  3. 合出来的 c = a + b 要再 push 回去,否则后面就轮不到它。
  4. ⚠ 代价会越累越大,用 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 个

pq-kth.cpp

给 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 大时该怎么办。

pq-kth.cpp参考答案
记住这一句

★★ 弹掉 k − 1 个,顶上就是第 k 大。⚠ 弹掉的那些是白扔的,所以循环次数和「取第 k 个」差一次。

用到的写法:priority_queue<int> q; q.top() q.pop() q.size()

练习 ② 按花费优先:pair 直接往堆里放

pq-pair.cpp

有 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 分别管什么。

pq-pair.cpp参考答案
记住这一句

★★★ 「按什么优先」就是一个比较器 —— 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。