堆(优先队列)
堆是一种树形结构,树的根是堆顶,堆顶始终保持为所有元素的最优值。有大根堆和小根堆,大根堆的根节点是最大值,小根堆的根节点是最小值。堆一般用二叉树实现,称为二叉堆。
堆的存储方式
堆的操作
empty
返回堆是否为空
top
直接返回根节点的值,时间复杂度
push
将新元素添加在数组最后面,若它比父节点小则不断与其父节点交换,使得堆重新满足父节点比子节点存储的数都要小(自下而上),时间复杂度
pop
弹出根节点,并让堆依然符合原来的性质。首先交换根节点和数组中最后一个元素,再去掉最后一个元素。若新根节点比子节点大,则不断与较小子节点交换,直到重新满足条件(自上而下),时间复杂度
例:P3378 【模板】堆
由此,给出二叉堆的模板实现:
参考代码
#include <cstdio> #include <algorithm> using namespace std; const int MAXN = 1e6 + 5; int heap[MAXN], len; void push(int x) { heap[++len] = x; int i = len; while (i > 1 && heap[i] < heap[i / 2]) { swap(heap[i], heap[i / 2]); i /= 2; } } void pop() { heap[1] = heap[len--]; int i = 1; while (i * 2 <= len) { int son = i * 2; if (son < len && heap[son + 1] < heap[son]) son++; if (heap[son] < heap[i]) { swap(heap[son], heap[i]); i = son; } else break; } } int main() { int n; scanf("%d", &n); while (n--) { int op; scanf("%d", &op); if (op == 1) { int x; scanf("%d", &x); push(x); } else if (op == 2) printf("%d\n", heap[1]); else pop(); } return 0; }
例:P1177 【模板】排序
输入
分析:利用堆也是可以做排序的,先把所有的元素 push 进去,然后每次取出堆顶(最小值)输出并弹出堆顶,直到堆空为止,这种排序方法称为堆排序。
参考代码
#include <cstdio> #include <algorithm> using namespace std; const int MAXN=100005; struct Heap { int a[MAXN],cnt; void push(int x) { // 压入 a[++cnt]=x; int i=cnt; while (i>1 && a[i]<a[i/2]) { swap(a[i/2],a[i]); i/=2; } } void pop() { // 删除 a[1]=a[cnt--]; int i=1; while (i*2<=cnt) { int son=i*2; if (son<cnt && a[son+1]<a[son]) son++; if (a[son]<a[i]) { swap(a[son],a[i]); i=son; } else break; } } int top() { return a[1]; } }; Heap h; int main() { int n,x; scanf("%d",&n); for (int i=1;i<=n;i++) { scanf("%d",&x); h.push(x); } for (int i=1;i<=n;i++) { printf("%d ",h.top()); h.pop(); } return 0; }
堆排序整体的时间复杂度是
优先队列
C++ 提供了优先队列这个数据结构,也就是 STL 中的 priority_queue
,底层就是由堆实现的。要使用优先队列,需要包含 queue
头文件,优先队列支持的基础操作如下:
priority_queue<int> q
新建一个保存int
型变量的优先队列q
,默认是大根堆priority_queue<int, vector<int>, greater<int>> q
新建一个小根堆q.top()
优先队列查询最大值(或者是最小值)q.pop()
将最大值(最小值)弹出队列q.push(x)
将x
加入优先队列
和大多数 STL 容器一样,可以使用 q.empty()
判断它是否为空,用 q.size()
获取它的大小。
例:P3378 【模板】堆
用 STL 的优先队列来写这道题代码更加简洁。
// STL 优先队列 #include <cstdio> #include <algorithm> #include <queue> using namespace std; priority_queue<int, vector<int>, greater<int>> q; // 小根堆 int main() { int n; scanf("%d", &n); // 操作次数 while (n--) { int op, x; scanf("%d", &op); if (op == 1) { scanf("%d", &x); q.push(x); } else if (op == 2) printf("%d\n", q.top()); else q.pop(); } return 0; }
例:P2168 [NOI2015] 荷马史诗
一部《荷马史诗》中有
解题思路
哈夫曼编码的变形。每次从堆中选出权重最小的
需要注意的是,每次合并都会减少 k-1-(n-1)%(k-1)
个权重为 long long
类型。
参考代码
#include <cstdio> #include <queue> #include <algorithm> using namespace std; typedef long long LL; const int N = 100005; LL w[N]; struct Node { LL val; int depth; }; struct NodeCompare { // 定义Node比较类 bool operator()(const Node &a, const Node &b) { // 权重相同时,高度小的优先出队 return a.val != b.val ? a.val > b.val : a.depth > b.depth; } }; int main() { int n, k; scanf("%d%d", &n, &k); priority_queue<Node, vector<Node>, NodeCompare> q; for (int i = 1; i <= n; i++) { scanf("%lld", &w[i]); q.push({w[i], 1}); // 读入结点(叶节点) } if ((n - 1) % (k - 1) != 0) { // 有一次合并结点数量不足k个 for (int i = 1; i <= k - 1 - (n - 1) % (k - 1); i++) q.push({0, 1}); // 需要补若干个权重为0的结点 } LL ans = 0; while (q.size() != 1) { LL sum = 0; int maxh = 0; for (int i = 1; i <= k; i++) { // 从堆中取k个最小的 Node tmp = q.top(); q.pop(); sum += tmp.val; // 新结点加上子结点权重 maxh = max(maxh, tmp.depth); // 最大深度 } ans += sum; // 更新总长度 q.push({sum, maxh + 1}); // 合并后的结点放回堆中 } printf("%lld\n%lld\n", ans, q.top().depth - 1); // 编码长度是哈夫曼树的高度减1 return 0; }
例:P2085 最小函数值
题目给定了若干个二次函数,由于
朴素想法
暴力计算每个函数值
朴素的想法是对于每个函数都计算前
优化思路
注意函数的取值是单调递增的,因此实际上可以看作是给定
参考代码
#include <cstdio> #include <queue> #include <vector> using namespace std; const int N = 10005; int a[N], b[N], c[N]; struct Node { int idx, x, f; }; struct NodeCompare { bool operator()(const Node &lhs, const Node &rhs) const { return lhs.f > rhs.f; } }; priority_queue<Node, vector<Node>, NodeCompare> q; int fn(int idx, int x) { return a[idx] * x * x + b[idx] * x + c[idx]; } int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 0; i < n; i++) { scanf("%d%d%d", &a[i], &b[i], &c[i]); } for (int i = 0; i < n; i++) q.push({i, 1, fn(i, 1)}); for (int i = 0; i < m; i++) { Node t = q.top(); q.pop(); printf("%d ", t.f); q.push({t.idx, t.x + 1, fn(t.idx, t.x + 1)}); } return 0; }
#include <cstdio> #include <queue> #include <vector> using namespace std; const int N = 10005; int a[N], b[N], c[N]; struct Node { int idx, x, f; bool operator<(const Node &other) const { return f > other.f; } }; priority_queue<Node> q; int fn(int idx, int x) { return a[idx] * x * x + b[idx] * x + c[idx]; } int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 0; i < n; i++) { scanf("%d%d%d", &a[i], &b[i], &c[i]); } for (int i = 0; i < n; i++) q.push({i, 1, fn(i, 1)}); for (int i = 0; i < m; i++) { Node t = q.top(); q.pop(); printf("%d ", t.f); q.push({t.idx, t.x + 1, fn(t.idx, t.x + 1)}); } return 0; }
例:P1631 序列合并
解题思路
可以发现,最小和一定是
考虑到这一点,我们不妨把
参考代码
#include <cstdio> #include <algorithm> #include <queue> using namespace std; typedef long long LL; const int N = 100005; int a[N], b[N], ans[N]; struct Index { int x, y; }; struct IndexCompare { bool operator()(const Index& idx1, const Index& idx2) const { return a[idx1.x] + b[idx1.y] > a[idx2.x] + b[idx2.y]; } }; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) scanf("%d", &a[i]); for (int i = 1; i <= n; i++) scanf("%d", &b[i]); priority_queue<Index, vector<Index>, IndexCompare> q; for (int i = 1; i <= n; i++) q.push({i, 1}); for (int i = 1; i <= n; i++) { Index tmp = q.top(); q.pop(); ans[i] = a[tmp.x] + b[tmp.y]; q.push({tmp.x, tmp.y + 1}); } for (int i = 1; i <= n; i++) printf("%d%c", ans[i], i == n ? '\n' : ' '); return 0; }
对顶堆
如果把大根堆想成一个上宽下窄的三角形,把小根堆想成一个上窄下宽的三角形,那么对顶堆就可以具体地被想象成一个“陀螺”或者一个“沙漏”,通过这两个堆的上下组合,我们可以把一组数据分别加入到对顶堆中的大根堆和小根堆,以维护我们不同的需要。
根据数学中不等式的传递原理,假如一个集合 A 中的最小元素比另一个集合 B 中的最大元素还要大,那么就可以断定: A 中的所有元素都比 B 中元素大。所以,我们把小根堆“放在”大根堆“上面”,如果小根堆的堆顶元素比大根堆的堆顶元素大,那么小根堆的所有元素要比大根堆的所有元素大。
例如给定
我们可以这样解决问题:把大根堆的元素个数限制成
- 插入:若插入的元素小于大根堆堆顶元素,则将其插入大根堆,否则将其插入小根堆
- 维护:当大根堆的大小大于
时,不断将大根堆堆顶元素取出并插入小根堆,直到大根堆的大小等于 ;当大根堆的大小小于 时,不断将小根堆堆顶元素取出并插入大根堆,直到大根堆的大小等于 - 查询第
小的元素:大根堆堆顶元素 - 删除第
小的元素:删除大根堆堆顶元素
同理,对顶堆还可以用于解决其他“第
例:P1168 中位数
解题思路
使用两个堆,大根堆维护较小的数,小根堆维护较大的数。这样一来,小根堆的堆顶是较大的数中最小的,大根堆的堆顶是较小的数中最大的。
而求中位数只需要在保证两个堆中元素大小关系的同时,控制两个堆的大小尽可能平衡,这样其中一个堆的堆顶元素即为中位数。
参考代码
#include <cstdio> #include <queue> #include <vector> using namespace std; int main() { int n; scanf("%d", &n); priority_queue<int> big; priority_queue<int, vector<int>, greater<int>> small; for (int i = 1; i <= n; i++) { int x; scanf("%d", &x); small.push(x); if (i % 2 == 1) { while (!big.empty() && small.top() < big.top()) { int st = small.top(); small.pop(); int bt = big.top(); big.pop(); small.push(bt); big.push(st); } int st = small.top(); small.pop(); big.push(st); printf("%d\n", big.top()); } } return 0; }
例:P1801 黑匣子
解题思路
控制对顶堆中的大根堆的元素数目伴随着
参考代码
#include <cstdio> #include <queue> #include <iostream> using namespace std; const int N = 200005; int a[N]; priority_queue<int, vector<int>, greater<int>> h; priority_queue<int> ans; int main() { int m, n; scanf("%d%d", &m, &n); for (int i = 1; i <= m; i++) scanf("%d", &a[i]); int pre = 0; int idx = 0; while (n--) { int u; scanf("%d", &u); for (int i = pre + 1; i <= u; i++) ans.push(a[i]); while (ans.size() > idx) { h.push(ans.top()); ans.pop(); } ans.push(h.top()); h.pop(); printf("%d\n", ans.top()); pre = u; idx++; } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?