题解:洛谷 P3378 堆
【题目来源】
【题目描述】
给定一个数列,初始为空,请支持下面三种操作:
- 给定一个整数 \(x\),请将 \(x\) 加入到数列中。
- 输出数列中最小的数。
- 删除数列中最小的数(如果有多个数最小,只删除 \(1\) 个)。
【输入】
第一行是一个整数,表示操作的次数 \(n\)。
接下来 \(n\) 行,每行表示一次操作。每行首先有一个整数 \(op\) 表示操作类型。
- 若 \(op = 1\),则后面有一个整数 \(x\),表示要将 \(x\) 加入数列。
- 若 \(op = 2\),则表示要求输出数列中的最小数。
- 若 \(op = 3\),则表示删除数列中的最小数。如果有多个数最小,只删除 \(1\) 个。
【输出】
对于每个操作 2,输出一行一个整数表示答案。
【输入样例】
5
1 2
1 5
2
3
2
【输出样例】
2
5
【核心思想】
-
问题分析:维护一个动态变化的整数集合,支持三种操作:插入一个元素、查询当前集合中的最小值、删除当前集合中的最小值(若有多个则只删除一个)。这是优先队列(堆) 的经典应用场景,需要高效地支持插入、获取最小值和删除最小值操作。由于操作次数 \(n\) 可能较大(通常 \(10^5\) 级别),每次操作需在对数时间内完成。
-
算法选择:
- 二叉堆(小根堆):使用数组模拟完全二叉树,维护堆性质——任意节点的值不大于其子节点(小根堆)。插入元素时放在末尾并向上调整(上浮),删除最小值时移除堆顶并用最后一个元素替代后向下调整(下沉)。每次操作时间复杂度 \(O(\log n)\)。
- C++ STL
priority_queue:直接使用标准库提供的优先队列,默认为大根堆,通过greater<int>可得到小根堆,代码简洁且可靠。
-
关键步骤:
- 读入操作次数 \(n\)。
- 初始化小根堆:
priority_queue<int, vector<int>, greater<int>> q;。 - 循环处理每条操作:
- 操作 1:读入 \(x\),执行
q.push(x)。 - 操作 2:输出堆顶元素
q.top()(最小值)。 - 操作 3:执行
q.pop(),删除堆顶元素。
- 操作 1:读入 \(x\),执行
- 注意:操作 2 和操作 3 前应确保堆非空(题目保证有效操作)。
-
时间/空间复杂度:
- 时间复杂度:\(O(n \log n)\),每次堆操作 \(O(\log n)\)。
- 空间复杂度:\(O(n)\),存储堆中元素。
-
堆(优先队列)的核心思想:
- 完全二叉树结构:堆是一棵完全二叉树,使用数组存储,父子节点通过下标关系快速定位(左子节点 \(2i\),右子节点 \(2i+1\))。
- 堆性质维护:插入和删除后通过向上/向下调整,保证根节点始终为最小值,从而 \(O(1)\) 获取最值。
- 堆排序衍生:堆结构支持动态插入删除,适用于实时获取最值的场景,是优先队列的底层实现。
- 适用场景:任务调度、Dijkstra 算法、动态中位数等需要频繁提取最值的问题。
【算法标签】
普及 #堆排序
【代码详解】
#include <bits/stdc++.h>
using namespace std;
int n; // 操作次数
priority_queue<int, vector<int>, greater<int>> q; // 小根堆(最小值优先)
int main()
{
// 输入操作次数
cin >> n;
// 处理每个操作
for (int i = 1; i <= n; i++)
{
int op, x;
cin >> op; // 输入操作类型
if (op == 1) // 操作1:插入元素
{
cin >> x;
q.push(x); // 将x插入堆中
}
else if (op == 2) // 操作2:查询最小值
{
cout << q.top() << endl; // 输出堆顶元素(最小值)
}
else if (op == 3) // 操作3:删除最小值
{
q.pop(); // 删除堆顶元素(最小值)
}
}
return 0;
}
【运行结果】
5
1 2
1 5
2
2
3
2
5
浙公网安备 33010602011771号