题解:AcWing 839 模拟堆
【题目来源】
AcWing:839 模拟堆 - AcWing题库
【题目描述】
维护一个集合,初始时集合为空,支持如下几种操作:
(1)I x,插入一个数x;
(2)PM,输出当前集合中的最小值;
(3)DM,删除当前集合中的最小值(数据保证此时的最小值唯一);
(4)D k,删除第k个插入的数;
(5)C k x,修改第k个插入的数,将其变为x;
现在要进行N次操作,对于所有第2个操作,输出当前集合的最小值。
【输入】
第一行包含整数N。接下来N行,每行包含一个操作指令,操作指令为 I x,PM,DM,D k 或 C k x 中的一种。
【输出】
对于每个输出指令PM,输出一个结果,表示当前集合中的最小值。每个结果占一行。
【输入样例】
8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM
【输出样例】
-10
6
【核心思想】
-
问题分析:需要维护一个支持动态插入、删除最小值、按插入顺序删除指定元素、按插入顺序修改指定元素的集合。普通堆只支持前三种操作,但按插入顺序定位并删除/修改任意元素需要知道该元素在堆中的当前位置。关键观察是:通过维护堆下标与插入序号的双向映射,可以实现 \(O(\log n)\) 的任意元素删除和修改。
-
算法选择:
- 小根堆 + 双射映射:使用数组
heap存储堆结构,同时维护两个映射数组:pth[k]:第 \(k\) 个插入的元素在堆中的当前下标htp[i]:堆中下标为 \(i\) 的元素是第几个插入的
- 自定义交换:
heap_swap在交换堆元素值的同时,同步更新两个映射数组,保持双射关系
- 小根堆 + 双射映射:使用数组
-
关键步骤:
- 插入
I x:hsize++,inum++(插入序号计数器)- 新元素放到堆尾:
heap[hsize] = x - 建立映射:
pth[inum] = hsize,htp[hsize] = inum up(hsize)向上调整
- 查询最小值
PM:输出heap[1] - 删除最小值
DM:heap_swap(1, hsize)交换堆顶与堆尾hsize--,down(1)向下调整
- 删除第 \(k\) 个插入的元素
D k:- 通过
pth[k]找到该元素在堆中的下标 \(pos\) heap_swap(pos, hsize),hsize--down(pos)和up(pos)调整(只需执行其一,但两者都执行也无害)
- 通过
- 修改第 \(k\) 个插入的元素
C k x:- 通过
pth[k]找到堆中下标 \(pos\) heap[pos] = xdown(pos)和up(pos)调整
- 通过
- 向上调整
up(u):当父节点值 \(>\) 当前节点值时,与父节点交换并继续向上 - 向下调整
down(u):与左右子节点中的较小者交换并继续向下
- 插入
-
时间/空间复杂度:
- 时间复杂度:\(O(N \log N)\),每次插入、删除、修改均为 \(O(\log N)\)
- 空间复杂度:\(O(N)\),堆数组和两个映射数组
-
双射映射模拟堆的核心思想:
- 逻辑地址与物理地址分离:"第 \(k\) 个插入的元素"是逻辑标识(固定不变),而它在堆中的位置会随调整操作不断变化。双射映射实现了从逻辑标识到物理位置的高效定位
- 原子化交换操作:
heap_swap不仅交换堆中的数值,还同步交换两个映射关系,确保任何堆结构调整后映射依然正确 - 调整的完备性:修改元素值后,新值可能大于或小于原值,因此需要同时执行
down和up(或判断后只执行一个),确保堆性质恢复 - 删除的通用性:
D k和DM都复用同一套"交换到堆尾+缩小堆+调整"的逻辑,通过heap_swap统一处理映射更新 - 适用于需要支持"按标识定位并修改/删除任意元素"的动态优先队列场景,是标准堆数据结构的扩展版本
【解题思路】

【算法标签】
堆排序
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n; // n操作指令数量
int heap[N], hsize; // heap堆,从下标1开始放数据。hsize堆大小
// point-to-heap[i]: 第i个插入元素在堆中下标是pth[i],堆中下标是i的元素
int pth[N], htp[N]; // 第i个元素 -> 堆下标; 堆下标 -> 第i个元素
// 堆数组heap中下标是a,b
void heap_swap(int a, int b)
{
swap(pth[htp[a]], pth[htp[b]]);
swap(htp[a], htp[b]);
swap(heap[a], heap[b]);
}
// 节点u与左右儿子比较
void down(int u) //调整第u个元素,与其左右子节点比较
{
int t=u;
if (u*2<=hsize && heap[u*2]<heap[t]) //根大要调整
t = u*2;
if (u*2+1<=hsize && heap[u*2+1]<heap[t]) //根大要调整
t = u*2+1;
if (t!=u) {
heap_swap(u, t); //子节点t移到下标u变为根节点
down(t); //父节点u移到下标t处,继续与新儿子
}
}
// 节点u向上比较(与父节点比较)生成小根堆
void up(int u)
{
while (u/2>=1 && heap[u/2]>heap[u]) {
heap_swap(u/2, u);
u = u/2;
}
}
int main()
{
int inum=0; //第inum次插入
char op[5];
cin >> n; // n操作指令数量
while (n--) {
int k, x;
cin >> op;
// strcmp比较两个字符串大小,相等返回0
if (!strcmp(op, "I")) { // I x, 插入一个数x
cin >> x;
hsize++; inum++;
pth[inum]=hsize; htp[hsize]=inum;
heap[hsize]=x;
up(hsize);
}
else if (!strcmp(op, "PM")) // PM输出当前集合中的最小值
cout << heap[1] << endl;
else if (!strcmp(op, "DM")) { // DM删除当前集合中的最小值
heap_swap(1, hsize);
hsize--;
down(1); // 与子节点比生成小根堆
}
else if (!strcmp(op, "D")) { // D k 删除第k个插入的数
cin >> k;
k = pth[k]; // 第k个插入的数在堆中下标
heap_swap(k, hsize);
hsize--;
down(k); //堆调整
up(k);
}
else { // C k x, 修改第k个插入的数,将其变为x
cin >> k >> x;
k = pth[k]; // 第k个插入的数在堆中下标
heap[k] = x;
down(k); // 堆调整
up(k);
}
}
return 0;
}
【运行结果】
8
I -10
PM
-10
I -10
D 1
C 2 8
I 6
PM
6
DM
浙公网安备 33010602011771号