1. 程式人生 > >絕對是全網最好的Splay 入門詳解——洛谷P3369&BZOJ3224: Tyvj 1728 普通平衡樹 包教包會

絕對是全網最好的Splay 入門詳解——洛谷P3369&BZOJ3224: Tyvj 1728 普通平衡樹 包教包會

平衡樹是什麼東西想必我就不用說太多了吧。

百度百科:

    一個月之前的某天晚上,yuli巨佬為我們初步講解了Splay,當時接觸到了平衡樹裡的旋轉等各種騷操作,感覺非常厲害。而第二天我調Splay的模板竟然就搞了一天,最後還是失敗告終,只能CV了事,而Splay也成了我心中的一個心結,一直沒法解決。在西安集訓的時候也沒有去自己親自地把Splay調出來AC,後來又面臨期末考試,直到今天,我再一次地嘗試將Splay調出來,又花了2個多小時的時間。這個過程是非常痛苦的,翻了無數篇部落格,看了無數題解,我才勉強打出了適合我自己的Splay模板。

  看到AC的那一瞬間,心裡面是五味雜陳,雖然我是如此垃圾,現在我才自己將這樣的模板A掉,但看了網上那麼多篇部落格,學了很多種方法,但適合我自己的我卻花了很長的時間才完成。所以我希望寫一篇部落格,真真正正地手把手教大家Splay(僅僅是模板),讓學Splay的神犇們少走一些彎路。這也許就是我這一篇部落格的意義所在吧。

來看洛谷的題面:

基本操作簡介:

維護的陣列:

size[]:子樹的大小

cnt[]:某個節點出現的次數

fa[]:某個節點它的父親節點

val[]:某個節點對應的權值

lc[]:該節點的左兒子

rc[]:該節點的右兒子

變數:

root 根

tot 統計節點數

函式:

clear:用於刪除節點時的清空

程式碼如下:

 

push:隨時需要更新

程式碼如下:

 

  我們看到插入,刪除等操作,就自然而然地想到用平衡樹來解決它,而要完成這樣的操作,最重要的核心步驟就是旋轉。而旋轉又分為Zig右旋和Zag左旋

  大家看這張圖還是比較清晰明確的,其實旋轉可以這樣理解,一家人,x是兒子,y是x的爸爸,而z是x的爺爺,有一天,一種神奇的力量改變了他們之間的這種關係,本來是兒子的x變成了它本身父親y的父親,而x的爺爺z就變成了x的新父親。

  按照上圖,我們將x右旋,x的父親y此時就變成了它的右兒子。而x本身已經有一個右兒子了,這個右兒子就通過旋轉變成了y的左兒子。而左旋的話就同理可得,x的父親y就變成了他的左兒子,而本身x的左兒子又變成了它原來父親y的右兒子。這樣在旋轉過後整個結構仍然滿足之前的平衡性質。

  旋轉zig和zag的程式碼我都習慣分開寫,兩個的實現起來都差不多,比較好理解。

zig:

zag同zig也是基本相同:

  zig和zag在這裡已經講明白了,那麼就應該是核心Splay伸展操作了,也是基於zig和zag之上的,就是將我們要操作的那個點x旋轉到指定節點(一般是根節點為保證複雜度)的位置。但我們在這裡還需要有一些分類討論。

  首先看x的爺爺z是否已經在我們需要的節點上了,如果在的話,x就只需要旋轉兩次就可以到指定節點的下方。

那麼我們現在就需要看一看x,y,z之間的相對位置。

1:若x是y的左兒子,y是z的左兒子(借用某大佬部落格中的圖片,感謝)

此時x,y,z在同一條直線上,所以我們要將x和y都旋轉zig-zig

程式碼片段:

 2:若x是y的右兒子,y是z的右兒子,即也是在同一條直線上,那麼結果和第一種情況相同,只不過應該是zag-zag

同樣給出程式碼:

那當它們不再同一條直線上,又怎麼辦呢,圖示已經給出來了,我們只需要轉x就可以了。

顯而易見的引出第三種情況x是y的右兒子,y是z的左兒子,所以我們先將x左旋,變成y的左兒子,再將x右旋到目標節點,這樣就是zag-zig,程式碼如下:

第4中情況也就可以類比然後得出,x是y的左兒子,y是z的右兒子,這樣就將x先右旋,再將其左旋,即為zig-zag操作,程式碼如下:

需要轉兩次的4種情況講完了,那轉一次就更簡單了,如上圖,y是目標節點,x轉一下就登天了,如果x是y的右兒子,那就左旋,反之,如果是左兒子,那麼他就右旋。

程式碼如下:

如果最後連x的父親都是0了,那它肯定就轉到根節點上了,大功告成!

完整程式碼如下:

接下來是insert操作:

如果此時根節點為空,說明樹也為空,那麼就新開一個節點,當前的節點就作為根節點。

程式碼如下:

此時如果已經有元素在樹上了,我們就需要按照平衡樹的性質(左兒子的值小於父親,右兒子的值大於父親)將其插入進去。從根節點開始往下找合適的位置。最後千萬不要忘了更新。

我們需要插入的值如果之前已經有了,那麼我們只需要將這個值出現的次數加1即可。再把這個節點旋到根。程式碼如下:

按一般的情況,就是以平衡樹的性質插入即可。如果走到了空節點,將它插入,還需要更新其父親的資訊

維護兩個變數,也可以理解為在樹上的兩個指標,一個是指向當前節點(從根節點開始),另一個是指向當前節點的父親,最後還要將操作的節點旋到根。

程式碼如下:

完整的insert操作如下:

好啦,接下來又是與插入操作對應的刪除操作。

先求出該點對應的排名,因為之後會用到,此時x已經在根節點root上了。

如果這個點早就出現過,即cnt[x]>1,那麼cnt[x]-1,直接刪除就得了。

程式碼如下:

 針對一些特殊的情況,還需要繁雜的分類討論,這是刪除操作的難點所在。

1:沒有左兒子和右兒子,孤身一人,直接清空即可。

程式碼如下:

 

2:沒有左兒子,更新現在的根節點,刪去老的根節點即可。

現在的樹就是這樣:

 

程式碼如下:

3:沒有右兒子,同2操作。

樹是這樣的:

程式碼如下:

一般的情況:

在這種情況下,我們需要求出根節點的前驅(小於x點中最大的數)然後將前驅節點Splay到根,這樣也同時能夠保證樹的平衡。整棵右子樹就成了新的根的右兒子,這樣的話原來的老根就排除出外了,直接clear即可。

程式碼如下:

刪除操作的完整版:

 查詢排名:

這其實很好的利用到了平衡樹的性質。我們同樣用一個now指標在樹上找,如果值比它小就走左子樹,大的話就往右子樹走,走的時候累加左子樹的size大小。若有多個重複的數,還要在排名中加上該數出現的次數。

程式碼如下圖所示:

 

查詢排名為k的數實際上就與排名的查詢一個道理,但細節上也有不同。

如果當前點有左子樹,並且x比左子樹的大小小的話,即向左子樹尋找;
否則,向右子樹尋找:先判斷是否有右子樹,然後記錄右子樹的大小以及當前點的大小(都為權值),用於判斷是否需要繼續向右子樹尋找。如果當前點有左子樹,並且x比左子樹的大小小的話,即向左子樹尋找。

前驅和後繼的查詢道理就非常簡單,我的程式碼也簡單易懂,直接把查詢的點插入作為根節點,從根節點向下找,最後刪除即可。

Splay其實還是很簡單的是不是。

完整程式碼見下方:

#include<bits/stdc++.h> 
using namespace std;
const int maxn=1e6+7;
int fa[maxn],lc[maxn],rc[maxn],val[maxn],cnt[maxn],size[maxn];
int root,tot;
int n,opt,x;
void clear(int x){
    size[x]=fa[x]=lc[x]=rc[x]=val[x]=cnt[x]=0;
}
void push(int x){
    size[x]=size[lc[x]]+size[rc[x]]+cnt[x];
}
void zig(int x){//右旋 
    if(!fa[x]) return;
    int y=fa[x],z=fa[y];
    if(z){
        if(lc[z]==y) lc[z]=x;
        else rc[z]=x;
    } 
    fa[x]=z;
    fa[y]=x;
    fa[rc[x]]=y;
    lc[y]=rc[x];
    rc[x]=y;
    push(y);
    push(x);
}
void zag(int x){//左旋 
    if(!fa[x]) return;
    int y=fa[x],z=fa[y];
    if(z){
        if(lc[z]==y) lc[z]=x;
        else rc[z]=x;
    }
    fa[x]=z;
    fa[y]=x;
    fa[lc[x]]=y;
    rc[y]=lc[x];
    lc[x]=y;
    push(y);
    push(x);
}
void splay(int x,int rt){//伸展 
    rt=fa[rt];
    int y,z;
    while(fa[x]!=rt){
        y=fa[x];
        z=fa[y];
        if(z&&z!=rt){
            if(lc[z]==y&&lc[y]==x){
                zig(y);
                zig(x);
            }
            else if(lc[z]==y&&rc[y]==x){
                zag(x);
                zig(x);
            }
            else if(rc[z]==y&&lc[y]==x){
                zig(x);
                zag(x);
            }
            else{
                zag(y);
                zag(x);
            }
        }
        else if(lc[y]==x){
            zig(x);
        }   
        else{
               zag(x);
        }   
    }
    if(!fa[x]) root=x;
}
void insert(int v){//插入 
    if(!root){
        tot++;
        lc[tot]=rc[tot]=fa[tot]=0;
        val[tot]=v;
        cnt[tot]=1;
        size[tot]=1;
        root=tot;
        return;
    }
    int now=root,f=0;
    while(1){
        if(val[now]==v){
            cnt[now]++;
            push(now);
            push(f);
            splay(now,root);
            break;
        }
        f=now;
        if(val[now]<v) now=rc[now];
        else now=lc[now];
        if(!now){
            tot++;
            val[tot]=v;
            cnt[tot]=size[tot]=1;
            fa[tot]=f;
            if(val[f]<v) rc[f]=tot;
            else lc[f]=tot;
            lc[tot]=rc[tot]=0;
            push(f);
            splay(tot,root);
            break;
        }
    }
}
int find1(int v){//查詢v的排名 
    int now=root,ans=0;
    while(1){
        if(v<val[now]) now=lc[now];
        else{
            ans+=size[lc[now]];
            if(v==val[now]){
                splay(now,root);
                return ans+1;
            }
            ans+=cnt[now];
            now=rc[now];
        }
    }
}
int find2(int x){//查詢排名為x數的值 
    int now=root;
    while(1){
        if(lc[now]&&x<=size[lc[now]]) now=lc[now];
        else{
            int ans=size[lc[now]]+cnt[now];
            if(ans>=x) return val[now];    
            x-=ans;
            now=rc[now];
        }
    }
}
int pre(){//查詢前驅和後繼 
    int now=lc[root];
    while(rc[now]) now=rc[now];
    return now;    
}
int nex(){
    int now=rc[root];
    while(lc[now]) now=lc[now];
    return now;
}
void del(int x){//刪除某個數 
    find1(x);
    if(cnt[root]>1){
        cnt[root]--;
        push(root);
        return;
    }
    if(!lc[root]&&!rc[root]){
        clear(root);
        root=0;
        return;
    }
    if(!lc[root]){
        int oldroot=root;
        root=rc[root];
        fa[root]=0;
        clear(oldroot);
        return;
    }
    if(!rc[root]){
        int oldroot=root;
        root=lc[root];
        fa[root]=0;
        clear(oldroot);
        return;
    }
    int lrt=pre();
    int oldroot=root;
    splay(lrt,root);
    fa[rc[oldroot]]=root;
    rc[root]=rc[oldroot];
    clear(oldroot);
    push(root);
} 
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d%d",&opt,&x);
        if(opt==1) insert(x);
        if(opt==2) del(x);
        if(opt==3) printf("%d\n",find1(x));
        if(opt==4) printf("%d\n",find2(x));
        if(opt==5) insert(x),printf("%d\n",val[pre()]),del(x);
        if(opt==6) insert(x),printf("%d\n",val[nex()]),del(x);
    }
    return 0;
}