史上最詳盡平衡樹(splay樹)講解

首先聲明:萬分感謝gty大哥的幫助!這年頭能找到簡單易懂的數組版平衡樹模板只能靠學長了!


變量聲明:f[i]表示i的父結點,ch[i][0]表示i的左兒子,ch[i][1]表示i的右兒子,key[i]表示i的關鍵字(即結點i代表的那個數字),cnt[i]表示i結點的關鍵字出現的次數(相當於權值),size[i]表示包括i的這個子樹的大小;sz爲整棵樹的大小,root爲整棵樹的根。

再介紹幾個基本操作:

【clear操作】:將當前點的各項值都清0(用於刪除之後)

  1. inline void clear(int x){  
  2.      ch[x][0]=ch[x][1]=f[x]=cnt[x]=key[x]=size[x]=0;  
  3. }  

【get操作】:判斷當前點是它父結點的左兒子還是右兒子
  1. inline int get(int x){  
  2.      return ch[f[x]][1]==x;  
  3. }  
【update操作】:更新當前點的size值(用於發生修改之後)
  1. inline void update(int x){  
  2.      if (x){  
  3.           size[x]=cnt[x];  
  4.           if (ch[x][0]) size[x]+=size[ch[x][0]];  
  5.           if (ch[x][1]) size[x]+=size[ch[x][1]];  
  6.      }  
  7. }  
下面boss來了:

【rotate操作圖文詳解】


這是原來的樹,假設我們現在要將D結點rotate到它的父親的位置。

step 1:

找出D的父親結點(B)以及父親的父親(A)並記錄。判斷D是B的左結點還是右結點。

step 2:

我們知道要將Drotate到B的位置,二叉樹的大小關係不變的話,B就要成爲D的右結點了沒錯吧?

咦?可是D已經有右結點了,這樣不就衝突了嗎?怎麼解決這個衝突呢?

我們知道,D原來是B的左結點,那麼rotate過後B就一定沒有左結點了對吧,那麼正好,我們把G接到B的左結點去,並且這樣大小關係依然是不變的,就完美的解決了這個衝突。


這樣我們就完成了一次rotate,如果是右兒子的話同理。step 2的具體操作:

我們已經判斷了D是B的左兒子還是右兒子,設這個關係爲K;將D與K關係相反的兒子的父親記爲B與K關係相同的兒子(這裏即爲D的右兒子的父親記爲B的左兒子);將D與K關係相反的兒子的父親即爲B(這裏即爲把G的父親記爲B);將B的父親即爲D;將D與K關係相反的兒子記爲B(這裏即爲把D的右兒子記爲B);將D的父親記爲A。

最後要判斷,如果A存在(即rotate到的位置不是根的話),要把A的兒子即爲D。

顯而易見,rotate之後所有牽涉到變化的父子關係都要改變。以上的樹需要改變四對父子關係,BG DG BD AB,需要三個操作(BG BD AB)。

step 3:update一下當前點和各個父結點的各個值

【代碼】

  1. inline void rotate(int x){  
  2.      int old=f[x],oldf=f[old],which=get(x);  
  3.      ch[old][which]=ch[x][which^1];f[ch[old][which]]=old;  
  4.      f[old]=x;ch[x][which^1]=old;  
  5.      f[x]=oldf;  
  6.      if (oldf)  
  7.           ch[oldf][ch[oldf][1]==old]=x;  
  8.      update(old);update(x);  
  9. }  

【splay操作】

其實splay只是rotate的發展。伸展操作只是在不停的rotate,一直到達到目標狀態。如果有一個確定的目標狀態,也可以傳兩個參。此代碼直接splay到根。

splay的過程中需要分類討論,如果是三點一線的話(x,x的父親,x的祖父)需要先rotate x的父親,否則需要先rotate x本身(否則會形成單旋使平衡樹失衡)

  1. inline void splay(int x){  
  2.      for (int fa;(fa=f[x]);rotate(x))  
  3.           if (f[fa])  
  4.                rotate((get(x)==get(fa)?fa:x));  
  5.      root=x;  
  6. }  

【insert操作】

其實插入操作是比較簡單的,和普通的二叉查找樹基本一樣。

step 1:如果root=0,即樹爲空的話,做一些特殊的處理,直接返回即可。

step 2:按照二叉查找樹的方法一直向下找,其中:

如果遇到一個結點的關鍵字等於當前要插入的點的話,我們就等於把這個結點加了一個權值。因爲在二叉搜索樹中是不可能出現兩個相同的點的。並且要將當前點和它父親結點的各項值更新一下。做一下splay。

如果已經到了最底下了,那麼就可以直接插入。整個樹的大小要+1,新結點的左兒子右兒子(雖然是空)父親還有各項值要一一對應。並且最後要做一下他父親的update(做他自己的沒有必要)。做一下splay。

  1. inline void insert(int v){  
  2.      if (root==0) {sz++;ch[sz][0]=ch[sz][1]=f[sz]=0;key[sz]=v;cnt[sz]=1;size[sz]=1;root=sz;return;}  
  3.      int now=root,fa=0;  
  4.      while (1){  
  5.           if (key[now]==v){  
  6.                cnt[now]++;update(now);update(fa);splay(now);break;  
  7.           }  
  8.           fa=now;  
  9.           now=ch[now][key[now]<v];  
  10.           if (now==0){  
  11.                sz++;  
  12.                ch[sz][0]=ch[sz][1]=0;key[sz]=v;size[sz]=1;  
  13.                cnt[sz]=1;f[sz]=fa;ch[fa][key[fa]<v]=sz;  
  14.                update(fa);  
  15.                splay(sz);  
  16.                break;  
  17.           }  
  18.      }  
  19. }  

【find操作】查詢x的排名

初始化:ans=0,當前點=root

和其它二叉搜索樹的操作基本一樣。但是區別是:

如果x比當前結點小,即應該向左子樹尋找,ans不用改變(設想一下,走到整棵樹的最左端最底端排名不就是1嗎)。

如果x比當前結點大,即應該向右子樹尋找,ans需要加上左子樹的大小以及根的大小(這裏的大小指的是權值)。

不要忘記了再splay一下

  1. inline int find(int v){  
  2.      int ans=0,now=root;  
  3.      while (1){  
  4.           if (v<key[now])  
  5.                now=ch[now][0];  
  6.           else{  
  7.                ans+=(ch[now][0]?size[ch[now][0]]:0);  
  8.                if (v==key[now]) {splay(now);return ans+1;}  
  9.                ans+=cnt[now];  
  10.                now=ch[now][1];  
  11.           }  
  12.      }  
  13. }  
【findx操作】找到排名爲x的點

初始化:當前點=root

和上面的思路基本相同:

如果當前點有左子樹,並且x比左子樹的大小小的話,即向左子樹尋找;

否則,向右子樹尋找:先判斷是否有右子樹,然後記錄右子樹的大小以及當前點的大小(都爲權值),用於判斷是否需要繼續向右子樹尋找。

  1. inline int findx(int x){  
  2.      int now=root;  
  3.      while (1){  
  4.           if (ch[now][0]&&x<=size[ch[now][0]])  
  5.                now=ch[now][0];  
  6.           else{  
  7.                int temp=(ch[now][0]?size[ch[now][0]]:0)+cnt[now];  
  8.                if (x<=temp)  
  9.                     return key[now];  
  10.                x-=temp;now=ch[now][1];  
  11.           }  
  12.      }  
  13. }  

【求x的前驅(後繼),前驅(後繼)定義爲小於(大於)x,且最大(最小)的數】

這類問題可以轉化爲將x插入,求出樹上的前驅(後繼),再將x刪除的問題。

其中insert操作上文已經提到。

【pre/next操作】

這個操作十分的簡單,只需要理解一點:在我們做insert操作之後做了一遍splay。這就意味着我們把x已經splay到根了。求x的前驅其實就是求x的左子樹的最右邊的一個結點,後繼是求x的右子樹的左邊一個結點(想一想爲什麼?)

  1. inline int pre(){  
  2.      int now=ch[root][0];  
  3.      while (ch[now][1]) now=ch[now][1];  
  4.      return now;  
  5. }  
  6.   
  7. inline int next(){  
  8.      int now=ch[root][1];  
  9.      while (ch[now][0]) now=ch[now][0];  
  10.      return now;  
  11. }  

【del操作】

刪除操作是最後一個稍微有點麻煩的操作。

step 1:隨便find一下x。目的是:將x旋轉到根。

step 2:那麼現在x就是根了。如果cnt[root]>1,即不只有一個x的話,直接-1返回。

step 3:如果root並沒有孩子,就說名樹上只有一個x而已,直接clear返回。

step 4:如果root只有左兒子或者右兒子,那麼直接clear root,然後把唯一的兒子當作根就可以了(f賦0,root賦爲唯一的兒子)

剩下的就是它有兩個兒子的情況。

step 5:我們找到新根,也就是x的前驅(x左子樹最大的一個點),將它旋轉到根。然後將原來x的右子樹接到新根的右子樹上(注意這個操作需要改變父子關係)。這實際上就把x刪除了。不要忘了update新根。

  1. inline void del(int x){  
  2.      int whatever=find(x);  
  3.      if (cnt[root]>1) {cnt[root]--;return;}  
  4.      //Only One Point  
  5.      if (!ch[root][0]&&!ch[root][1]) {clear(root);root=0;return;}  
  6.      //Only One Child  
  7.      if (!ch[root][0]){  
  8.           int oldroot=root;root=ch[root][1];f[root]=0;clear(oldroot);return;  
  9.      }  
  10.      else if (!ch[root][1]){  
  11.           int oldroot=root;root=ch[root][0];f[root]=0;clear(oldroot);return;  
  12.      }  
  13.      //Two Children  
  14.      int leftbig=pre(),oldroot=root;  
  15.      splay(leftbig);  
  16.      f[ch[oldroot][1]]=root;  
  17.      ch[root][1]=ch[oldroot][1];  
  18.      clear(oldroot);  
  19.      update(root);  
  20.      return;  
  21. }  


【總結】

平衡樹的本質其實是二叉搜索樹,所以很多操作是基於二叉搜索樹的操作。

splay的本質是rotate,旋轉其實只是爲了保證二叉搜索樹的平衡性。

所有的操作一定都滿足二叉搜索樹的性質,所有改變父子關係的操作一定要update。

關鍵是理解rotate,splay的原理以及每一個操作的原理。

所有的操作均來自bzoj3224 普通平衡樹  附鏈接:http://www.lydsy.com/JudgeOnline/problem.php?id=3224

完整代碼:http://blog.csdn.net/clove_unique/article/details/50636361

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章