P3378 【模板】堆
图片失效致歉
题目描述
给定一个数列,初始为空,请支持下面三种操作:
- 给定一个整数 x x x,请将 x x x 加入到数列中。
- 输出数列中最小的数。
- 删除数列中最小的数(如果有多个数最小,只删除 1 1 1 个)。
输入格式
第一行是一个整数,表示操作的次数 n n n。
接下来 n n n 行,每行表示一次操作。每行首先有一个整数 o p op op 表示操作类型。
- 若 o p = 1 op = 1 op=1,则后面有一个整数 x x x,表示要将 x x x 加入数列。
- 若 o p = 2 op = 2 op=2,则表示要求输出数列中的最小数。
- 若 o p = 3 op = 3 op=3,则表示删除数列中的最小数。如果有多个数最小,只删除 1 1 1 个。
输出格式
对于每个操作 2 2 2,输出一行一个整数表示答案。
输入输出样例 #1
输入 #1
5
1 2
1 5
2
3
2
输出 #1
2
5
说明/提示
【数据规模与约定】
- 对于 30 % 30\% 30% 的数据,保证 n ≤ 15 n \leq 15 n≤15。
- 对于 70 % 70\% 70% 的数据,保证 n ≤ 10 4 n \leq 10^4 n≤104。
- 对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 10 6 1 \leq n \leq 10^6 1≤n≤106, 1 ≤ x < 2 31 1 \leq x \lt 2^{31} 1≤x<231, o p ∈ { 1 , 2 , 3 } op \in \{1, 2, 3\} op∈{1,2,3}。
学习二叉堆的第一步:先找模版题
第二步:查资料oi wiki
万事先暴力,根据题目可以想到准备一个数组,插入时放到末尾,输出最小数时打擂台,时间复杂度是 O(n) ,删除时同理,插入为 O ( 1 ) O(1) O(1) ,算下来最坏情况每次操作都输出最小数为 O ( n 2 ) O(n^{2}) O(n2) ,也就是大约要运行 1e+12 次
正解部分
二叉堆是怎么做到在 O ( n l o g n ) O(n log n) O(nlogn) 插入, O ( n l o g n ) O(nlogn) O(nlogn) 删除,求最值只用 O ( 1 ) O(1) O(1) 的?
如图,这是一个二叉树(大根堆为例):
所以我们可以得知大根堆的特点:
-
根绝对是这棵树的最大值
-
子节点比父结点要小
-
如果转化为一维平面,那么子节点的下标/2=父节点的下标(根没有父节点)
一维平面示例图:
| x | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| wx | 8 | 4 | 7 | 1 | 2 | 6 | 3 | 0 |
(二叉堆在数组中的储存方式)
1.top
还是以大根堆为例,无论是小根堆还是大根堆,他们的最值都是根(也就是数组里的w1)
代码实现:
cpp
int top(){
return w[1];
}
2.push(a)操作
先将a放入二叉堆的末尾:
接着在与自己的父结点比较,如果 w[x]>w[x/2](即父结点小于子结点),就交换
所以一整个添加的过程如下:
代码如下:
cpp
void xiufu_up(int x){
if(x==1||x[x]<w[x/2]){
return;
}//已是根 或者 自己到了极限
swap(w[x],w[x/2]);
xiufu_up(x/2);//继续查找
}
void push(int x){
w[++now]=x;//now=当前层数
xiufu_up(now);//修复
}
3.pop()操作
所谓皇帝轮流做明年到我家,如果删除了一个结点,那么自己的子结点就要上来一个
我们可以想到一个思路:
从被删除的节点往下搜索,每次都判断自己的节点哪个更大,就让那个节点"替位",然而那个节点的位置又空了,所以再向子结点遍历... 整个过程很像递归:
5的节点3 4,4顶 -> 4的节点3 2,3顶 ->3的节点2,1。2顶
2返回(叶子结点)-> 3返回 -> 4返回 ->完成
代码实现:
cpp
void xiufu_down(int x){
if(x*2>tot){
return;
}
int tmp=x*2;
if(x*2+1<=now&&w[x*2]>w[x*2+1]){
tmp++;
}//自己的左结点比右结点大,则左结点登上宝座
if(w[x]<w[tmp]){
swap(w[x],w[tmp]);
xiufu_down(tmp);
}
}
void pop(){
swap(w[1],w[now--]);
xiufu_down();
}
我知道你有思路了,但你先别抄,这是大根堆的,原题讲的是最小的数,所以这道题是小根堆,前面的介绍学习只是让大家认识二叉堆的。所以我们现在换一种思路去做会有不同的发现:
绝对绝对绝对的正解(小根堆)?)
小根堆与大根堆截然不同:
-
根绝对是这棵树的最小值
-
子节点比父结点要大
-
如果转化为一维平面,那么子节点的下标/2=父节点的下标(根没有父节点)
1.push and xiufu_up()
有了大根堆的思路我们也能写出代码:
cpp
void xiufu(int x){
if(x==1||a[x]>a[x/2]){
return;
}//如果为根或者比自己的父结点大(符合从小到大)
swap(a[x],a[x/2]);
xiufu(x/2);//继续以父结点(子节点)的位置修复
}//修复堆
void push(int x){
a[++now]=x;//要++now
xiufu(now);//自底向上
}//添加数
2.pop and xiufu_down()
实际上大根堆转化为小根堆不难,就是反过来嘛 找规律
cpp
void xiufupop(int x){
if(x*2>now){
return;
}//自己是叶子
int tmp=x*2;
if(x*2+1<=now&&a[x*2]>a[x*2+1]){
tmp++;
//tmp=x*2+1
//变更为右节点
}
if(a[x]>a[tmp]){
swap(a[x],a[tmp]);
xiufupop(tmp);
}
}//修复
void pop(){
swap(a[1],a[now--]);
xiufupop(1);
}
AC代码(手动模拟法)
请勿"ctrl+c,ctrl+v"学
cpp
#include<bits/stdc++.h>
using namespace std;
int a[1000001];
int now;//当前层数
int n,op,x;
int top(){
return a[1];
}
void xiufu(int x){
if(x==1||a[x]>a[x/2]){
return;
}//如果为根或者比自己的父结点大(符合从小到大)
swap(a[x],a[x/2]);
xiufu(x/2);//继续以父结点(子节点)的位置修复
}//修复堆
void push(int x){
a[++now]=x;//要++now
xiufu(now);//自底向上
}//添加数
void xiufupop(int x){
if(x*2>now){
return;
}//自己是叶子
int tmp=x*2;
if(x*2+1<=now&&a[x*2]>a[x*2+1]){
tmp++;
//tmp=x*2+1
//变更为右节点
}
if(a[x]>a[tmp]){
swap(a[x],a[tmp]);
xiufupop(tmp);
}
}//修复
void pop(){
swap(a[1],a[now--]);
xiufupop(1);
}
int main(){
cin>>n;
now=0;
for(int i=1;i<=n;i++){
cin>>op;
if(op==1){
cin>>x;
push(x);
}else if(op==2){
cout<<top()<<"\n";
}else{
pop();
}
}
return 0;
}
STL容器做法
c++提供了 priority_queue 的 STL 容器,具体用法可以去搜:
AC 代码( STL 法):
cpp
#include<bits/stdc++.h>
using namespace std;
int n;
int op,x;
int main(){
cin>>n;
priority_queue<int,vector<int>,greater<int>>q;
for(int i=1;i<=n;i++){
cin>>op;
if(op==1){
cin>>x;
q.push(x);
}else if(op==2){
cout<<q.top()<<"\n";
}else{
q.pop();
}
}
return 0;
}