配对堆
在stl里的priority——queue是一个很好用的堆,但是假如说我们要将两个堆去进行合并的话,普通的堆的时间和空间复杂度都大大超标,这时候我们就有了可合并堆,我个人认为配对堆会好些一点,所以就放弃左偏堆了(
基本原理
我们知道一个多叉树的存树方式 是父亲指向儿子,我们的配对堆其实也是一个多叉树,但是它的存树方式是儿子左兄弟,也就是我们用一个结构体去存堆
struct heap{
int r;//right
int s;//son
int x;//value
};
int fa[100];
如果有需要还可以加上并查集去判断每一个点是否处在一个堆中(最好还是加上)
merge
合并的复杂度是o(1) 是不是很难得,我们只用找到两个数所在堆的堆顶,之后如果有0就返回非0那个,假如是小根堆的话就把大的堆放在小的下面,然后更新一下就OK
void ins(int x,int y){
a[y].r=a[x].s;
a[x].s=y;
fa[y]=x;
}
int merge(int x,int y){//这里加return是为了后面的pop
x=find(x);
y=find(y);
if(!x||!y) return x+y;//有一个为0就返回非零那个
if(x==y) return x;//如果相同就返回其中一个
if(a[x].x>a[y].x) swap(x,y);
ins(x,y);
return x;
}
pop
这里就来了配对堆的核心,也是复杂度的保证,我们假如要弹出堆顶,那么我们首先需要的就是把堆顶的儿子一个一个拿出来合并对吧,但是这样的效率太慢,我们每一次拿出来两个去进行合并,这样的话我们就保证了下一次弹出时的复杂度,这是一种均摊复杂度的思想
void kill(int x){
int root=find(x);
a[root].s=a[x].r;
a[x].r=0;
}
void del(int rr){
if(!a[rr].s) return 0;//么有儿子就返回0
int x=a[rr].s;
int y=a[x].r;
kill(x);//将这个点拿出来
if(y) kill(y);
return merge(merge(x,y),del(rr));
//返回合并后的新的堆顶
}
void pop(int x){
int root=find(x);
int r=del(root);
fa[root]=r;//这个点以及被删掉了,所以收以前所有只想他的店都要指向新的root
fa[r]=r;//注意这一句,要不然会死循环
return;
}
push
这个操作也很简单,新加一个点就等于新建一个堆之后把这个和之前的合并
void push(int x,int y){//把x插入y所在的堆里面
a[x].s=a[x].r=0;
y=find(y);
return merge(x,y);
}
top
这个就是返回堆顶的一个操作
int top(int x){
int root=find(x);
return a[root].x;
}
模板
#include<bits/stdc++.h>
using namespace std;
int n,m;//堆的个数和操作个数
//这里以小根堆为例
int root;
struct heap{
int s;//儿子
int x;//值
int r;//左兄弟,这就是他的特色
};//这里加入有很多堆其实不用在意,可以用并查集去维护
heap a[1000010];
int fa[1000010];
int find(int x){
if(fa[x]==x) return x;
else return fa[x]=find(fa[x]);
}
void ins(int x,int y){ a[y].r=a[x].s;a[x].s=y;fa[y]=x;}//这就是把y的左兄弟变为x的儿子,之后再把a的大儿子变为y
void kill(int x) {
root=find(x);
a[root].s=a[x].r;
a[x].r=0;
}//把根节点的儿子变为x的左兄弟,之后再把x的左兄弟变为null 把这个点单独拿出来,之后合并
int merge(int x,int y){//int 是为了返回新的顶的值,在以后有用
if(x==y) return x;//如果是同一个堆的话 就返回一个不管就欧克了
if(!x||!y) return x+y;//如果有一个为0 就不用合并了
if(a[x].x>a[y].x) swap(x,y);
ins(x,y);//小的变成堆顶,之后返回堆顶的值
return x;
}//合并两个堆 ,相比较两个堆顶的权重,将较大的放在小的下面
/*void push(int x){
a[x].s=a[x].r=0;
root=merge(root,x);
}//插入一个节点,很简单就是建一个堆之后merge
*/
bool broken[1000010];
int del(int rr){
if(!a[rr].s) return 0;//如果没有儿子就 return 0
int x=a[rr].s;//第一个儿子拿出来
int y=a[x].r;//第二个孩子拿出来
kill(x);//从这个树里面单独拎出来
if(y) kill(y);//如果有y这个点就kill
return merge(merge(x,y),del(rr)); //不断的将删除的节点的儿子们给合并,就可以保证之后的均摊复杂度
}
void pop(int x){
root=find(x);
int r=root;
root=del(root);//将根节点变为合并后的节点
fa[r]=root;//这个点删除掉了,那么所有指向它的点的父亲都应该变成合并后的那个
fa[root]=root;//把堆顶的父亲变为自己
broken[r]=true;
return;
} //弹出堆顶
int top(int x){
root=find(x);
return a[root].x;
}
int main(){
//freopen("P3377_11.in","r",stdin);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i].x);
a[i].r=a[i].s=0;
fa[i]=i;
}
int op,u,v;
for(int i=1;i<=m;i++){
scanf("%d%d",&op,&u);
if(op==1){
scanf("%d",&v);
if(!broken[u] && !broken[v])
merge( find(u),find(v) );
}
else if(op==2) {
if(broken[u]==true){
printf("-1\n");
}
else {
printf("%d\n",top(u));
pop(u);
}
}
}
return 0;
}
小结
这是一个不错的数据结构,可以优化dij,但是好像还要加一个decrease_key的操作,这个以后有时间就去补一下,下次见