C++ 竞赛训练营第三课:STL 核心容器之 priority_queue(优先队列)(竞赛向)

一、课程导航 🚀
- 🎯 竞赛视角:priority_queue 的 "效率核心"------ 为什么是贪心 / TopK 问题的最优解?
- 📚 核心特性:底层堆结构与优先级规则(大根堆 / 小根堆)
- 🔧 竞赛高频操作:构造配置、增删查改与接口实战
- ⚡ 竞赛进阶用法:自定义优先级(结构体 / 多字段排序)
- 📝 真题实战:贪心算法、TopK 问题、Dijkstra 算法优化
- ❌ 竞赛避坑指南:优先级配置错误、效率陷阱规避
- 🔖 下节预告:STL 核心容器之 map/unordered_map(哈希表的竞赛用法)
二、核心知识点与竞赛实战
🎯 1. 竞赛视角:priority_queue 的 "效率核心"
在竞赛中,priority_queue(优先队列)是 "动态维护最值" 的神器,核心价值在于:
- 自动排序:插入元素时自动按优先级排序,队首始终是最值(大根堆默认最大,小根堆默认最小);
- 高效操作:插入(push)、删除最值(pop)均为 O (log n) 时间,远超手动遍历找最值的 O (n);
- 场景适配:完美匹配贪心算法(每次选当前最优)、TopK 问题(维护前 k 个最值)、图算法(Dijkstra 最短路径)等竞赛高频题型,是 "空间换时间" 的典型高效方案。
竞赛场景适配:
- 贪心算法:哈夫曼编码、任务调度、最小代价合并、区间覆盖;
- TopK 问题:第 k 大元素、前 k 个高频元素、数据流中的中位数;
- 图算法:Dijkstra 最短路径(优先队列优化)、最小生成树辅助。
📚 2. 核心特性:底层堆结构与优先级规则
2.1 底层原理与核心规则
- 底层实现:基于堆结构(完全二叉树),无需手动维护排序,容器自动调整;
- 核心规则:优先级驱动存取,队首永远是 "优先级最高" 的元素(非 FIFO,区别于普通 queue);
- 默认行为:大根堆(priority_queue<int> pq),队首为最大值。
2.2 大根堆与小根堆(竞赛必备配置)
|-----|------------|----------------------------------------------------------|------------------------|
| 类型 | 核心特性(队首元素) | 竞赛配置代码(直接套用) | 适用场景 |
| 大根堆 | 最大值优先 | priority_queue<int> pq; | 找最大元素、贪心选最大项 |
| 小根堆 | 最小值优先 | priority_queue<int, vector<int>, greater<int>> pq; | 找最小元素、TopK 问题、Dijkstra |
2.3 与 queue 的核心区别(竞赛选型关键)
|----------------|------------|--------|-------------------|---------------------|
| 容器 | 存取规则 | 队首元素 | 核心操作复杂度 | 竞赛适用场景 |
| queue | FIFO(先进先出) | 最早入队元素 | push/pop O(1) | BFS、层序遍历、滑动窗口(普通) |
| priority_queue | 优先级优先 | 最值元素 | push/pop O(log n) | 贪心、TopK、Dijkstra 算法 |
🔧 3. 竞赛高频操作:构造配置与接口实战
priority_queue接口与普通 queue 高度兼容,竞赛中仅需掌握 "核心 4 接口 + 2 构造",简洁高效:
3.1 核心构造方式(竞赛直接套用)
cpp
// 1. 大根堆(默认,最常用)
priority_queue<int> pq1;
// 2. 小根堆(竞赛高频,必须掌握)
priority_queue<int, vector<int>, greater<int>> pq2;
// 3. 用已有数组/vector初始化(快速构造堆)
vector<int> nums = {3,1,4,1,5};
priority_queue<int> pq3(nums.begin(), nums.end()); // 大根堆,初始元素为nums所有值
// 4. 结构体自定义优先级(见4.1节)
3.2 核心接口(竞赛必备,O (log n) 复杂度)
cpp
priority_queue<int> pq;
pq.push(x); // 插入元素x,自动调整堆结构(O(log n))
pq.pop(); // 删除队首最值元素(O(log n)),无返回值!
pq.top(); // 访问队首最值元素(O(1)),必须先判空
pq.empty(); // 判断队列是否为空(O(1)),访问top/pop前必用
pq.size(); // 获取元素个数(O(1)),竞赛中用于边界判断(如TopK中控制堆大小)
3.3 竞赛关键提醒(避坑第一)
- 无返回值陷阱:pop()仅删除队首元素,不返回值(区别于 vector),需先top()获取再pop();
- 判空必做:top()/pop()前必须用empty()判断,否则空容器访问会导致 RE(运行时错误);
- 小根堆配置:第三个参数greater<int>不可省略,且需包含<vector>头文件(通常#include<bits/stdc++.h>已涵盖)。
⚡ 4. 竞赛进阶用法:自定义优先级(结构体 / 多字段排序)
竞赛中常需对结构体(如学生、任务、边)按自定义规则排序,priority_queue支持通过 "仿函数" 或 "重载运算符" 实现,以下是竞赛最常用的 "仿函数配置法":
4.1 结构体自定义优先级(高频真题场景)
示例场景:按学生成绩降序排序(成绩相同按年龄升序)
cpp
#include <iostream>
#include <priority_queue>
using namespace std;
// 定义结构体(竞赛中直接复制使用)
struct Student {
int score; // 成绩
int age; // 年龄
};
// 定义仿函数(自定义优先级规则)
struct Cmp {
// 重载()运算符:返回true时,a的优先级低于b(即b排在队首)
bool operator()(const Student& a, const Student& b) {
if (a.score != b.score) {
return a.score < b.score; // 成绩降序(分数高的优先)
} else {
return a.age > b.age; // 成绩相同,年龄升序(年龄小的优先)
}
}
};
// 竞赛中使用配置
priority_queue<Student, vector<Student>, Cmp> pq;
核心规则:仿函数中return a < b表示 "a 的优先级低于 b",即 b 排在队首(与sort的比较器逻辑相反,注意区分)。
4.2 pair 类型的优先级(竞赛简化用法)
pair默认按 "first 元素降序,first 相同则 second 降序" 排序,竞赛中可直接利用这一特性,无需自定义仿函数:
cpp
// 示例:pair<int, int>(first=分数,second=年龄),按分数降序、年龄升序
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
// 注:greater<pair<int,int>> 会让pair按first升序,如需降序可省略(默认大根堆)
📝 5. 真题实战:竞赛高频题模板(直接套用)
例题 1:TopK 问题 ------ 数组中的第 k 个最大元素(LeetCode 215,入门题)
题目描述:给定整数数组和 k,返回数组中第 k 个最大元素(无需排序整个数组)。
核心思路:小根堆维护前 k 个最大元素,堆顶即为第 k 大元素(O (n log k) 高效解)。
竞赛代码(模板):
cpp
#include <iostream>
#include <vector>
#include <priority_queue>
using namespace std;
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> pq; // 小根堆
for (int x : nums) {
pq.push(x);
if (pq.size() > k) { // 堆大小超过k,删除最小值(保证堆内是前k大)
pq.pop();
}
}
return pq.top(); // 堆顶是第k大元素
}
int main() {
vector<int> nums = {3,2,1,5,6,4};
int k = 2;
cout << findKthLargest(nums, k) << endl; // 输出5
return 0;
}
例题 2:贪心算法 ------ 最小代价合并石头(竞赛经典)
题目描述:有 n 堆石头,每次合并两堆,代价为两堆石头数量之和,求合并所有堆的最小代价。
核心思路:小根堆每次选最小的两堆合并,合并后将新堆入堆,重复至只剩一堆(哈夫曼编码思想)。
竞赛代码(模板):
cpp
#include <iostream>
#include <vector>
#include <priority_queue>
using namespace std;
int minCost(vector<int>& stones) {
priority_queue<int, vector<int>, greater<int>> pq(stones.begin(), stones.end());
int cost = 0;
while (pq.size() > 1) {
int a = pq.top(); pq.pop();
int b = pq.top(); pq.pop();
int sum = a + b;
cost += sum;
pq.push(sum);
}
return cost;
}
int main() {
vector<int> stones = {4,3,2,1};
cout << minCost(stones) << endl; // 输出19(1+2=3→3+3=6→6+4=10,总3+6+10=19)
return 0;
}
例题 3:图算法 ------Dijkstra 最短路径(优先队列优化)
题目描述:给定有向带权图,求从起点到所有节点的最短路径。
核心思路:用小根堆维护 "当前节点到起点的最短距离",每次选距离最小的节点扩展,更新邻接节点距离(O (m log n),m 为边数,n 为节点数)。
竞赛代码(模板):
cpp
#include <iostream>
#include <vector>
#include <priority_queue>
#include <climits>
using namespace std;
const int INF = INT_MAX;
int main() {
int n, m, start; // n节点数,m边数,start起点
cin >> n >> m >> start;
vector<vector<pair<int, int>>> adj(n+1); // adj[u]存储(u, v, w):u到v的边权w
for (int i = 0; i < m; ++i) {
int u, v, w;
cin >> u >> v >> w;
adj[u].emplace_back(v, w);
}
vector<int> dist(n+1, INF); // dist[i]:起点到i的最短距离
dist[start] = 0;
// 小根堆:存储(当前距离, 节点),按距离升序
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;
pq.emplace(0, start);
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (d > dist[u]) continue; // 已找到更短路径,跳过
for (auto [v, w] : adj[u]) {
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
pq.emplace(dist[v], v);
}
}
}
for (int i = 1; i <= n; ++i) {
if (dist[i] == INF) cout << "INF ";
else cout << dist[i] << " ";
}
return 0;
}
❌ 6. 竞赛避坑指南:常见错误与效率陷阱
6.1 常见错误(最易丢分)
- 错误 1:小根堆配置遗漏参数(如写成priority_queue<int, greater<int>> pq)------ 缺少底层容器参数vector<int>,编译报错;
- 错误 2:自定义优先级时逻辑颠倒(如想按成绩降序,却写成return a.score > b.score)------ 仿函数返回true表示 a 优先级低于 b,需注意与sort比较器的区别;
- 错误 3:空容器调用top()/pop()------ 竞赛中必须先判空(if (!pq.empty())),否则直接 RE;
- 错误 4:混淆top()和front()------priority_queue无front()接口,访问队首用top()(与 queue 区别)。
6.2 效率陷阱(避免超时)
- 错误:对大规模数据(n>1e5)使用priority_queue时,频繁调用size()判断 ------ 虽size()是 O (1),但需避免不必要的循环判断,可通过 "堆内元素特性" 终止(如 Dijkstra 中跳过已处理节点);
- 正确:TopK 问题中,堆大小严格控制在 k 以内(超过则pop()),避免 O (n log n) 开销,保证 O (n log k) 效率;
- 注意:结构体自定义优先级时,尽量用 "仿函数" 而非 lambda(部分编译器对 lambda 支持不佳,竞赛中优先仿函数保证兼容性)。
🔖 7. 下节预告:STL 核心容器之 map/unordered_map(哈希表的竞赛用法)
- 核心特性:键值对存储、查找 O (1)(unordered_map)与有序性(map);
- 竞赛高频用法:快速查找、统计频率、映射关系(如字符串→整数、id→属性);
- 核心区别:map(红黑树,有序,O (log n))与 unordered_map(哈希表,无序,O (1))的选型;
- 真题适配:两数之和、字母异位词、最长连续序列、哈希表优化动态规划。
下节课,我们将聚焦哈希表的竞赛实战 ------ 如何用 map/unordered_map 解决 "快速查找" 类高频题,掌握 O (1) 查找的核心效率优势!
