可并堆嘛,顾名思义,就是它可以实现合并两个堆仅消耗$O(logn)$的时间复杂度。而普通的堆要消耗$O(n)$或$O(nlogn)$。
左偏树是一种优秀的可并堆,它同时具有堆和左偏的性质。其中左偏指从当前节点的左儿子出发到最近的一个“出口”的距离比从右儿子出发要长(或者相等)。
“出口”,就是可以到达的一颗空树啦,如果一个节点没有或只有一个儿子,那它就是一个“出口”。
为此我们为每个的维护一个离最近出口的距离值dis
显然,由于左偏性,一点的$dis$等于它右儿子的$dis+1$($tr[x].dis=tr[tr[x].rc].dis+1$)
左偏树最重要的操作就是merge,其用递归的方式实现合并。
假设我们要合并两个堆$X$,$Y$,节点$x$,$y$是他们的根节点,试图把$x$的右子树和$y$合并后,把这颗合并后的新堆作为$x$的右儿子。但是需要满足(小根)堆的性质,所以先保证$x$的权值小于$y$的权值。然后维护左偏性质,如果左子树的$dis$比右子树的小,那么两个儿子交换。最后上传$dis$给$x$
维护左偏性质可使合并的复杂度为$O(logn)$。
可以知道,合并的时间复杂度取决于$dis$(因为如果有“出口”,Merge()就直接返回单节点了)。因为合并时我们都选择右儿子与另一个堆进行合并,根据左偏性得知,左儿子的$dis$比右儿子的$dis$大。那么可以使用与启发式合并类似的证明方法得知,向下递归的次数最多不超过$O(logn)$次。
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<cstdio> #include<queue> #include<ctime> #include<cstdlib> using namespace std; const int MXN=100005; namespace LTr{//Leftist Tree,左偏树 struct Node{ int fa,lc,rc,able; int dis,val; }tr[MXN]; int n; void Create(int x,int val){tr[x]=(Node){0,0,0,1,0,val};}//新建节点 int Root(int x){//找根,但这种方式很暴力,可能被构造O(n)T掉,所以保险建议用并查集 if(tr[x].fa) return Root(tr[x].fa); else return x; } int Comb(int x,int y){//合并 if(!x||!y) return x+y; if(tr[x].val>tr[y].val//保证小根堆 ||(tr[x].val==tr[y].val&&x>y)) swap(x,y); tr[x].rc=Comb(tr[x].rc,y);//递归合并 if(tr[x].rc) tr[tr[x].rc].fa=x; if(tr[tr[x].lc].dis<tr[tr[x].rc].dis)//保证左偏 swap(tr[x].lc,tr[x].rc); tr[x].dis=tr[tr[x].rc].dis+1;//更新dis return x; } //For User void Init(int myn){n=myn;Create(0,0);} bool IsAble(int x){return tr[x].able;}//是否已被删除 void Merge(int x,int y){Comb(Root(x),Root(y));} int Top(int x){return tr[Root(x)].val;}//返回x所在堆的堆顶值 void Pop(int x){//弹出x所在堆的堆顶 x=Root(x); int lc=tr[x].lc,rc=tr[x].rc; tr[lc].fa=tr[rc].fa=0;//更新其儿子的父节点为空 tr[x]=(Node){0,0,0,0,0,0};//标记该点已经不存在 Merge(lc,rc);//合并左右节点 } } int n,qn; int main(){ cin>>n>>qn; LTr::Init(n); for(int i=1;i<=n;i++){ int val;scanf("%d",&val); LTr::Create(i,val); } for(int i=1;i<=qn;i++){ int type,x,y;scanf("%d",&type); switch(type){ case 1:{ scanf("%d%d",&x,&y); if(LTr::IsAble(x)&<r::IsAble(y)&<r::Root(x)!=LTr::Root(y)) LTr::Merge(x,y); break;} case 2:{ scanf("%d",&x); if(LTr::IsAble(x)){ printf("%d ",LTr::Top(x)); LTr::Pop(x); }else printf("-1 "); break;} } } return 0; }
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<cstdio> #include<queue> #include<ctime> #include<cstdlib> using namespace std; const int MXN=100005; namespace LTr{//Leftist Tree,左偏树 struct Node{ int jmp,lc,rc,able; int dis,val; }tr[MXN]; int n; void Create(int x,int val){tr[x]=(Node){x,0,0,1,0,val};}//新建节点 int Root(int x){//并查集维护jmp找根 if(tr[x].jmp==x) return tr[x].jmp; return tr[x].jmp=Root(tr[x].jmp); } int Comb(int x,int y){//合并 if(!x||!y) return x+y; if(tr[x].val>tr[y].val//保证小根堆 ||(tr[x].val==tr[y].val&&x>y)) swap(x,y); tr[x].rc=Comb(tr[x].rc,y);//递归合并 if(tr[x].rc) tr[tr[x].rc].jmp=x; if(tr[tr[x].lc].dis<tr[tr[x].rc].dis)//保证左偏 swap(tr[x].lc,tr[x].rc); tr[x].dis=tr[tr[x].rc].dis+1;//更新dis return x; } //For User void Init(int myn){n=myn;Create(0,0);} bool IsAble(int x){return tr[x].able;}//是否已被删除 void Merge(int x,int y){Comb(Root(x),Root(y));} int Top(int x){return tr[Root(x)].val;}//返回x所在堆的堆顶值 void Pop(int x){//弹出x所在堆的堆顶 x=Root(x); int lc=tr[x].lc,rc=tr[x].rc; tr[lc].jmp=lc;tr[rc].jmp=rc;//一定要加,否则会在被删除的点和新根中反复横跳 tr[x]=(Node){Comb(lc,rc),0,0,0,0,0}; //合并(注意用的Comb()而不是Merge())左右儿子,标记该点已经不存在,同时把jmp改到合并后的根上 } } int n,qn; int main(){ cin>>n>>qn; LTr::Init(n); for(int i=1;i<=n;i++){ int val;scanf("%d",&val); LTr::Create(i,val); } for(int i=1;i<=qn;i++){ int type,x,y;scanf("%d",&type); switch(type){ case 1:{ scanf("%d%d",&x,&y); if(LTr::IsAble(x)&<r::IsAble(y)&<r::Root(x)!=LTr::Root(y)) LTr::Merge(x,y); break;} case 2:{ scanf("%d",&x); if(LTr::IsAble(x)){ printf("%d ",LTr::Top(x)); LTr::Pop(x); }else printf("-1 "); break;} } } return 0; }