1. 程式人生 > >可並堆之左偏樹

可並堆之左偏樹

左偏樹

可並堆,就是支援合併,並且滿足堆性質的二叉樹,先不管如何來實現合併,假設我們已經維護好了這樣的資料結構,如何來實現堆應該有的操作:
插入:插入一個節點可以看成把一個單個節點的堆(單點顯然滿足堆性質)和堆進行合併
刪除根:可以看成把根的左子樹和右子樹合併
所以,可並堆只需要一個操作:合併,就可以實現所有堆應該有的操作。
左偏樹:
左偏樹最重要的性質正如它的名字——它是左偏的,也就是說,左偏樹的任意節點都滿足它的左子樹的節點個數小於右子樹的節點個數,這個性質有什麼用呢?如果我們從根節點開始,不停的遞迴右子樹,次數一定小於log2(n),因為每次遞迴右子樹,由於右子樹的節點個數一定小於左子樹,可以看成節點個數/2。
這個性質對於合併就有很大的好處,由於這個性質,合併時只要不停的用右子樹去合併,單次合併的複雜度就可以看成是log2(n)的。
合併:
合併時,考慮將樹x和樹y合併,顯然,合併後的樹的根節點一定是兩個樹的根節點中值較小的那個,為了方便操作,預設將樹x的根節點作為根,也就是說,每次都把y與x的右子樹合併,y與x的右子樹合併後的根節點就是樹x的右兒子,還要滿足左偏性質,如果合併後右子樹的節點個數大於左子樹的節點個數,就交換左右子樹,最後更新當前子樹的大小。
程式碼如下:

int merge(int x,int y){
    if((!x)||(!y))return x|y;
    if(hep[y]<hep[x])swap(x,y);
    int son=merge(hep[x].r,y);
    hep[x].r=son;
    if(hep[hep[x].r].num>hep[hep[x].l].num)swap(hep[x].r,hep[x].l);
    hep[x].num=hep[hep[x].l].num+1+hep[hep[x].r].num;
    return x;
}

下面來看一道題目(摘自洛谷):
一開始有N個小根堆,每個堆包含且僅包含一個數。接下來需要支援兩種操作:

操作1: 1 x y 將第x個數和第y個數所在的小根堆合併(若第x或第y個數已經被刪除或第x和第y個數在用一個堆內,則無視此操作)

操作2: 2 x 輸出第x個數所在的堆最小數,並將其刪除(若第x個數已經被刪除,則輸出-1並無視刪除操作)

這題顯然就是左偏樹的模板題,但是有一點需要注意,在實際運用中,左偏樹還需要維護每個節點所在的堆的堆頂,馬上就會想到並查集來維護,但是,刪除操作怎麼辦?並查集並不天然支援刪除某個節點,好在這題只需要刪除堆頂就可以,顯然,合併操作時只要將需要合併的兩個堆的fa中將要作為兒子節點的fa修改為最終的根節點就可以。
但是,刪除一個節點要如何辦到?由於刪除一個根節點後可能成為根節點的只有兩個——它的兩個兒子,所以,只要把會成為新的根節點的fa更新為它自己,再把原來的根節點的fa修改為新的根節點即可。
程式碼如下:

                int k=merge(hep[x].l,hep[x].r);
                fa[x]=k;fa[k]=k;
                vis[x]=0;

其中的vis是用來標記某個節點是否被刪除。
本題程式碼:

#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 500006
using namespace std;
struct data{
    int l,r,s,num,id;
    bool operator <(const data&b)const{
        return s<b.s||(s==b.s&&id<b.id);
    }
}hep[maxn];
int n,m,fa[maxn];
bool vis[maxn];
int merge(int x,int y){
    if((!x)||(!y))return x|y;
    if(hep[y]<hep[x])swap(x,y);
    int son=merge(hep[x].r,y);
    hep[x].r=son;
    if(hep[hep[x].r].num>hep[hep[x].l].num)swap(hep[x].r,hep[x].l);
    hep[x].num=hep[hep[x].l].num+1+hep[hep[x].r].num;
    return x;
}
int get(int x){return fa[x]==x?x:fa[x]=get(fa[x]);}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)scanf("%d",&hep[i].s),hep[i].num=1,hep[i].id=i,fa[i]=i;
    memset(vis,1,sizeof(vis));
    for(int i=1,p,x,y;i<=m;i++){
        scanf("%d",&p);
        if(p==1){
            scanf("%d%d",&x,&y);
            if((!vis[x])||(!vis[y]))continue;
            x=get(x);y=get(y);
            if(hep[x]<hep[y])fa[y]=x;
                        else fa[x]=y;
            if(x!=y)merge(x,y);
        }else{
            scanf("%d",&x);
            if(vis[x]){
                x=get(x);
                printf("%d\n",hep[get(x)].s);
                int k=merge(hep[x].l,hep[x].r);
                fa[x]=k;fa[k]=k;
                vis[x]=0;
            }else printf("-1\n");
        }
    }
    return 0;
}