堆的双向映射
食用指南:
对该算法程序编写以及踩坑点很熟悉的同学可以直接跳转到代码模板查看完整代码
只有基础算法的题目会有关于该算法的原理,实现步骤,代码注意点,代码模板,代码误区的讲解
非基础算法的题目侧重题目分析,代码实现,以及必要的代码理解误区
题目描述:
-
维护一个集合,初始时集合为空,支持如下几种操作:
I x,插入一个数 x;
PM,输出当前集合中的最小值;
DM,删除当前集合中的最小值(数据保证此时的最小值唯一);
D k,删除第 k 个插入的数;
C k x,修改第 k 个插入的数,将其变为 x;
现在要进行 N 次操作,对于所有第 2 个操作,输出当前集合的最小值。输入格式
第一行包含整数 N。
接下来 N 行,每行包含一个操作指令,操作指令为 I x,PM,DM,D k 或 C k x 中的一种。输出格式
对于每个输出指令 PM,输出一个结果,表示当前集合中的最小值。
每个结果占一行。数据范围
1≤N≤105
−109≤x≤109
数据保证合法。输入样例:
8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM
输出样例:
-10
6 -
题目来源:https://www.acwing.com/problem/content/841/
题目分析:
-
本题涉及堆的四种操作:插入 & 显示 & 删除 & 修改
-
单看四个操作哪个也不难,难在查询第k个插入的数
-
一方面要使用数组记录第k个插入的数在堆中的节点序号
o2h[i] = j;表示第i个插入的数在堆中是第j个节点, -
另一方面由于堆本身经常发生父子节点值交换,
导致第k个插入的数在堆中的序号发生变化
所以需要开数组记录每个值对应的插入顺序
h2o[i] = j;表示堆中第i个节点的值是第j个插入的数 -
此处一定要搞清楚:
堆的节点序号本身是不变的,1永远是树根,2*i和2*i+1永远是子节点值的插入顺序也是一定的
我们改变的是值和节点序号的对应关系
因此改变了插入顺序和节点序号的对应关系
算法原理:
模板算法:
- 传送门:静态堆
双向映射:
1. 存储形式:
- 堆:heap[N] & len
- 堆中节点对应插入序号:h2o[N]
- 插入序号对应堆中节点:o2h[N]
2. 何时h2o和o2h发生变化?
- 若每次在堆中输入一个数,而该数在堆的位置不发生变化
则h2o o2h都是单增数列 - 由于down()和up()导致堆中的节点发生交换,而导致o2h和h2o发生变化
- 所以我们只需要研究节点交换时,两个节点的o2h和h2o怎么变化即可
3. h2o和o2h如何变化?
- 当i & j两个节点发生值交换时
- 节点对应的插入顺序发生了变化:swap(h2o[i], h2o[j]);
- 插入顺序对应的堆中节点发生了变化:
h2o[i]表示节点i的插入顺序,h2o[j]表示节点j的插入顺序
swap(o2h[h2o[i]], o2h[h2o[j]]);
4. up & down针对的对象:
- up() 和 down()针对的是堆中的节点
当第i个节点和第len个节点发生值交换时,
我们要下沉或上升的还是第i个节点 - 当删除了堆顶元素之后,我们将最后一个元素置于堆顶,但是还是从堆顶开始down()
- 当删除了某个元素后,我们将最后一个元素至于该元素原本的节点,从原本节点开始up() / down()
所以我们有必要记录被删除数的原本节点位置
代码实现:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int heap[N], len;
int h2o[N],o2h[N], order;
void heap_swap(int i, int j){
swap(heap[i], heap[j]);
//也可先swap(o2h[h2o[i]], o2h[h2o[j]]);
swap(h2o[i], h2o[j]);
swap(o2h[h2o[i]], o2h[h2o[j]]);
}
void down(int i){
int j = i;
if (2*i <= len && heap[j]>heap[2*i]) j = 2*i;
if (2*i+1 <= len && heap[j]>heap[2*i+1]) j = 2*i+1;
if (j != i){
heap_swap(i ,j);
down(j);
}
}
void up(int i){
int j = i/2;
if (j >0 && heap[i]<heap[j]){
heap_swap(i, j);
up(j);
}
}
int main(){
int m = 0;
cin >>m;
while(m--){
string s;
cin >>s;
if (s == "I"){
int x = 0;
cin >>x;
len++;
order++;
heap[len] = x;
h2o[len] = order;
o2h[order] = len;
up(len);
}else if (s == "PM"){
cout <<heap[1] <<endl;
}else if(s == "DM"){
heap_swap(len, 1);
len--;
//此处虽然也是删除,但是肯定从堆顶开始down(),不必记录
down(1);
}else if (s == "D"){
int k = 0;
cin >>k;
//交换前记录被删除的值原本对应的节点位置,之后从该节点up / down
int tmp = o2h[k];
heap_swap(len, o2h[k]);
len--;
up(tmp);
down(tmp);
}else{
int k = 0, x = 0;
cin >>k >>x;
heap[o2h[k]] = x;
down(o2h[k]);
up(o2h[k]);
}
}
}
代码误区:
1 .描述五大操作及其注意事项:
- 堆内增加节点:初始化该节点的值对应的插入顺序以及该插入顺序对应的节点位置
- 堆内最值:输出堆顶
- 堆内修改节点值:该节点的值的插入顺序不变,该顺序对应的节点位置也不变,从原节点down() & up()即可
- 堆内删除最值节点:将最后一个节点值和堆顶值交换,同时对应的插入顺序指向的节点位置指向堆顶,堆顶位置指向的插入顺序就是最后一个节点的插入顺序
- 删除第k个插入的节点:事先保持该节点位置,该节点和最后一个节点交换值&插入顺序&插入顺序指向的节点位置,从删除节点开始up()/down()
2. 最出错点:
- 删除节点对应值时,不暂存节点位置
本篇感想:
- 到本篇结束,写完了数据结构以及算法基础的内容,明日开始图论
- 图论预计小20篇,涵盖图的存储,dfs bfs,最短距离5大算法,最小生成树2大算法,二分图判定,最大匹配数。
本周能讲完图论的话,本周共讲小30讲,和上周篇数相近。 - 由于带权并查集和本文不好理解,所以耗费了很多时间。
包括现在也觉得本讲讲的不是很清晰,日后有时间再来简化本讲吧 - 看完本篇博客,恭喜已登 《筑基境-初期》
距离登仙境不远了,加油