版權聲明:轉載請註明出處:http://blog.csdn.net/clove_unique
首先聲明:萬分感謝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(用於刪除之後)
- inline void clear(int x){
- ch[x][0]=ch[x][1]=f[x]=cnt[x]=key[x]=size[x]=0;
- }
【get操作】:判斷當前點是它父結點的左兒子還是右兒子
- inline int get(int x){
- return ch[f[x]][1]==x;
- }
- inline void update(int x){
- if (x){
- size[x]=cnt[x];
- if (ch[x][0]) size[x]+=size[ch[x][0]];
- if (ch[x][1]) size[x]+=size[ch[x][1]];
- }
- }
【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一下當前點和各個父結點的各個值
【代碼】
- inline void rotate(int x){
- int old=f[x],oldf=f[old],which=get(x);
- ch[old][which]=ch[x][which^1];f[ch[old][which]]=old;
- f[old]=x;ch[x][which^1]=old;
- f[x]=oldf;
- if (oldf)
- ch[oldf][ch[oldf][1]==old]=x;
- update(old);update(x);
- }
【splay操作】
其實splay只是rotate的發展。伸展操作只是在不停的rotate,一直到達到目標狀態。如果有一個確定的目標狀態,也可以傳兩個參。此代碼直接splay到根。
splay的過程中需要分類討論,如果是三點一線的話(x,x的父親,x的祖父)需要先rotate x的父親,否則需要先rotate x本身(否則會形成單旋使平衡樹失衡)
- inline void splay(int x){
- for (int fa;(fa=f[x]);rotate(x))
- if (f[fa])
- rotate((get(x)==get(fa)?fa:x));
- root=x;
- }
【insert操作】
其實插入操作是比較簡單的,和普通的二叉查找樹基本一樣。
step 1:如果root=0,即樹爲空的話,做一些特殊的處理,直接返回即可。
step 2:按照二叉查找樹的方法一直向下找,其中:
如果遇到一個結點的關鍵字等於當前要插入的點的話,我們就等於把這個結點加了一個權值。因爲在二叉搜索樹中是不可能出現兩個相同的點的。並且要將當前點和它父親結點的各項值更新一下。做一下splay。
如果已經到了最底下了,那麼就可以直接插入。整個樹的大小要+1,新結點的左兒子右兒子(雖然是空)父親還有各項值要一一對應。並且最後要做一下他父親的update(做他自己的沒有必要)。做一下splay。
- inline void insert(int v){
- 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;}
- int now=root,fa=0;
- while (1){
- if (key[now]==v){
- cnt[now]++;update(now);update(fa);splay(now);break;
- }
- fa=now;
- now=ch[now][key[now]<v];
- if (now==0){
- sz++;
- ch[sz][0]=ch[sz][1]=0;key[sz]=v;size[sz]=1;
- cnt[sz]=1;f[sz]=fa;ch[fa][key[fa]<v]=sz;
- update(fa);
- splay(sz);
- break;
- }
- }
- }
【find操作】查詢x的排名
初始化:ans=0,當前點=root
和其它二叉搜索樹的操作基本一樣。但是區別是:
如果x比當前結點小,即應該向左子樹尋找,ans不用改變(設想一下,走到整棵樹的最左端最底端排名不就是1嗎)。
如果x比當前結點大,即應該向右子樹尋找,ans需要加上左子樹的大小以及根的大小(這裏的大小指的是權值)。
不要忘記了再splay一下
- inline int find(int v){
- int ans=0,now=root;
- while (1){
- if (v<key[now])
- now=ch[now][0];
- else{
- ans+=(ch[now][0]?size[ch[now][0]]:0);
- if (v==key[now]) {splay(now);return ans+1;}
- ans+=cnt[now];
- now=ch[now][1];
- }
- }
- }
初始化:當前點=root
和上面的思路基本相同:
如果當前點有左子樹,並且x比左子樹的大小小的話,即向左子樹尋找;
否則,向右子樹尋找:先判斷是否有右子樹,然後記錄右子樹的大小以及當前點的大小(都爲權值),用於判斷是否需要繼續向右子樹尋找。
- inline int findx(int x){
- int now=root;
- while (1){
- if (ch[now][0]&&x<=size[ch[now][0]])
- now=ch[now][0];
- else{
- int temp=(ch[now][0]?size[ch[now][0]]:0)+cnt[now];
- if (x<=temp)
- return key[now];
- x-=temp;now=ch[now][1];
- }
- }
- }
【求x的前驅(後繼),前驅(後繼)定義爲小於(大於)x,且最大(最小)的數】
這類問題可以轉化爲將x插入,求出樹上的前驅(後繼),再將x刪除的問題。
其中insert操作上文已經提到。
【pre/next操作】
這個操作十分的簡單,只需要理解一點:在我們做insert操作之後做了一遍splay。這就意味着我們把x已經splay到根了。求x的前驅其實就是求x的左子樹的最右邊的一個結點,後繼是求x的右子樹的左邊一個結點(想一想爲什麼?)
- inline int pre(){
- int now=ch[root][0];
- while (ch[now][1]) now=ch[now][1];
- return now;
- }
- inline int next(){
- int now=ch[root][1];
- while (ch[now][0]) now=ch[now][0];
- return now;
- }
【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新根。
- inline void del(int x){
- int whatever=find(x);
- if (cnt[root]>1) {cnt[root]--;return;}
- //Only One Point
- if (!ch[root][0]&&!ch[root][1]) {clear(root);root=0;return;}
- //Only One Child
- if (!ch[root][0]){
- int oldroot=root;root=ch[root][1];f[root]=0;clear(oldroot);return;
- }
- else if (!ch[root][1]){
- int oldroot=root;root=ch[root][0];f[root]=0;clear(oldroot);return;
- }
- //Two Children
- int leftbig=pre(),oldroot=root;
- splay(leftbig);
- f[ch[oldroot][1]]=root;
- ch[root][1]=ch[oldroot][1];
- clear(oldroot);
- update(root);
- return;
- }
【總結】
平衡樹的本質其實是二叉搜索樹,所以很多操作是基於二叉搜索樹的操作。splay的本質是rotate,旋轉其實只是爲了保證二叉搜索樹的平衡性。
所有的操作一定都滿足二叉搜索樹的性質,所有改變父子關係的操作一定要update。
關鍵是理解rotate,splay的原理以及每一個操作的原理。
所有的操作均來自bzoj3224 普通平衡樹 附鏈接:http://www.lydsy.com/JudgeOnline/problem.php?id=3224
完整代碼:http://blog.csdn.net/clove_unique/article/details/50636361