题解: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

【核心思想】

  1. 问题分析:需要维护一个支持动态插入、删除最小值、按插入顺序删除指定元素、按插入顺序修改指定元素的集合。普通堆只支持前三种操作,但按插入顺序定位并删除/修改任意元素需要知道该元素在堆中的当前位置。关键观察是:通过维护堆下标与插入序号的双向映射,可以实现 \(O(\log n)\) 的任意元素删除和修改。

  2. 算法选择

    • 小根堆 + 双射映射:使用数组 heap 存储堆结构,同时维护两个映射数组:
      • pth[k]:第 \(k\) 个插入的元素在堆中的当前下标
      • htp[i]:堆中下标为 \(i\) 的元素是第几个插入的
    • 自定义交换heap_swap 在交换堆元素值的同时,同步更新两个映射数组,保持双射关系
  3. 关键步骤

    • 插入 I x
      • hsize++inum++(插入序号计数器)
      • 新元素放到堆尾:heap[hsize] = x
      • 建立映射:pth[inum] = hsizehtp[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] = x
      • down(pos)up(pos) 调整
    • 向上调整 up(u):当父节点值 \(>\) 当前节点值时,与父节点交换并继续向上
    • 向下调整 down(u):与左右子节点中的较小者交换并继续向下
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N \log N)\),每次插入、删除、修改均为 \(O(\log N)\)
    • 空间复杂度:\(O(N)\),堆数组和两个映射数组
  5. 双射映射模拟堆的核心思想

    • 逻辑地址与物理地址分离:"第 \(k\) 个插入的元素"是逻辑标识(固定不变),而它在堆中的位置会随调整操作不断变化。双射映射实现了从逻辑标识到物理位置的高效定位
    • 原子化交换操作heap_swap 不仅交换堆中的数值,还同步交换两个映射关系,确保任何堆结构调整后映射依然正确
    • 调整的完备性:修改元素值后,新值可能大于或小于原值,因此需要同时执行 downup(或判断后只执行一个),确保堆性质恢复
    • 删除的通用性D kDM 都复用同一套"交换到堆尾+缩小堆+调整"的逻辑,通过 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
posted @ 2026-07-02 22:46  团爸讲算法  阅读(4)  评论(0)    收藏  举报