嗯好的今天我們來談談cosplay
splay是一種操作,是一種調整二叉排序樹的操作,但是它並不會時時刻刻保持一個平衡,因為它會根據每一次操作把需要操作的點旋轉到根節點上
所謂二叉排序樹,就是滿足對樹中的任意一個節點,它左子樹上的任意一個值比它的值小,右子樹上的任意一個值比它的值大的一棵二叉樹 ;至於平衡:是一棵空樹或任意節點的左右兩個子樹的深度差的絕對值不超過1(from:百度百科) 看圖:
不平衡: 平衡:
可以觀察到這兩棵樹都是滿足⑥<⑤<④<③<①<②的大小關系,只是改變每個點的相對位置不同。
然后呢,splay的真正含義就出來了:通過很多很多次變換把要處理的點放到根節點上。與此同時,我們把這種變換叫做旋轉。旋轉的精髓就是在不破壞樹中的大小順序的同時改變節點的位置。旋轉是splay的核心(雖然大部分排序樹的核心操作都是旋轉),大概思路和treap是差不多的(沒學過treap然后爆發出狂笑),但對於splay相對來說懶一點的是它轉的時候是自動判斷左旋還是右旋。每一次旋轉,我們把被旋轉的這個點向上轉一層(也就是轉到它父節點的位置上)
比如說要旋轉⑤節點: 可以看看旋轉之后的樣子:
因為⑤<④<③,所以要把⑤旋轉后應該是③的左子樹(也就是④的位置),但是⑧始終是大於④的所以⑧的位置是不變的 (④的右子樹),然后因為⑤>④>⑦,所以④和⑦都應該是⑤的右子樹而且⑦應該是④的左子樹(因為是一棵二叉樹),然后可以發現與⑥,⑧沒有任何關系,真正改變的就只有圖中的紅線(表示各個節點間的父子關系)。這就是旋轉的步驟。
但如果⑤是④的右子樹呢?其實是差不多的,就是左右翻轉一下。再想想這個圖,自己推一遍:
好的,那么旋轉的普遍規律就可以看出來了:整個旋轉的操作中改變的只是被旋轉點、它的父節點、以及它的某一棵子樹,到底是哪一棵子樹是根據被旋轉點與它父親的關系來決定的:如果這個點是它父節點的左子樹,那么應該調整這個點的右子樹;如果是右子樹,則相反。
(有兩個很棒的動圖來表示旋轉)
接下來打出代碼。在那之前,我們先定義數組:
son[i][0]是指點i的左節點編號,son[i][1]是右節點編號
root表示當前根節點的編號
sz是指整個樹的值的種數,同時用於新節點插入時的編號(可以類比時間戳)
siz[i]表示以i為頂點的樹的值的個數,要計算重復出現的值(包括i節點自己)
key[i]表示節點i的值是多少
fa[i]表示節點i的父節點編號
cnt[i]表示節點i的值出現了多少次(數量cnt,siz[i]和種類sz是兩個東西不要搞混了)
因為旋轉的方式是由它是左子樹還是右子樹決定的,所以我們可以先寫一個函數來判斷:
int get(int x){ return son[fa[x]][1]==x;//如果它父節點的右兒子編號等於它那么返回1(右節點),否則返回0(是左節點) }
接下來就是旋轉了:
void rotate(int x){ int f=fa[x],ff=fa[f],w=get(x);//父節點、祖父節點、是父節點的左子樹還是右子樹 son[f][w]=son[x][w^1];//x節點的另一個子樹放給原本x節點的位置 fa[son[f][w]]=f;//更新x節點的另一個子樹的父節點 son[x][w^1]=f;//將父節點接到x節點的另一個子樹上 fa[f]=x; fa[x]=ff;//f、x位置互換后更新祖父節點 if(ff){//父節點不是根節點(根節點的父節點為0) son[ff][son[ff][1]==f]=x; } update(f); update(x); }
注意這里的^位運算,意思是兩位不同時返回1,相同時返回0,這里^1可以快速找出另一個兒子(0^1=1,1^1=0)
這里的update是指旋轉之后由於位置的改變而引起的種類總數的變化,因為每一個節點值的出現的次數是沒有改變的,其實很簡單就是左子樹種類加上右子樹種類:
void update(int x){ if(x!=0){//如果是根節點,旋轉時種類數始終不變 siz[x]=cnt[x];//自身值的出現次數 if(son[x][0])//如果有左子樹 siz[x]+=siz[son[x][0]]; if(son[x][1])//如果有右子樹 siz[x]+=siz[son[x][1]]; } }
這只是一次操作,我們前面說了我們是把要處理的點旋轉到根節點上,那么怎么做呢?循環就行了。但是還有一種特殊情況,由於這種情況都滿足被旋轉點和父節點都是左節點或者是右兒子,我們姑且稱它為三點一線,先看圖:
比如說我們這里要splay④,如果直接把④一直旋轉到根節點的話就會是這樣:
可以看見③還是①的左節點,相當於只是改變了④和①的關系,專業一點就是說形成了單旋使平衡樹失衡。而解決的方法就是在出現三點一線時先旋轉它的父節點避免單旋,正確的應該是這樣:
void splay(int x){ for(int f;f=fa[x];rotate(x)){//注意旋轉的始終是x if(fa[f]){//可能存在三點一線 rotate(get(x)==get(f)?f:x);//三點一線情況判斷 } } }
接下來是splay的插入:由於在旋轉的時候,我們是建立在這棵樹是有序的前提下的,而要保證這個前提,就需要從這棵樹的建立開始就讓它有序,所以,我們在插入的時候就必須按照排序樹的規定來插入,也就是說每次插入都是從根節點開始比較大小,直到找到這個值或者是找到樹的底部(這個值沒有出現過):
void insect(int v){ if(sz==0){//如果這棵樹是空樹,插入點應該為根節點 sz++; son[1][1]=son[1][0]=fa[1]=0; siz[1]=cnt[1]=1; root=1; key[1]=v; return; } int now=root,f=0;//now表示現在查找到節點編號,f表示當前節點的父節點 while(1){ if(key[now]==v){//如果這個值已經在樹中,那么它出現的次數增加 cnt[now]++; update(now);//更新相關點 update(f); splay(now);//將插入的點旋轉到根節點,便於下一次可能的操作 break; } f=now; now=son[now][v>key[now]];//依照節點值查找位置,如果大於當前值v>key[now]=1,則在右子樹范圍內,反之亦然 if(now==0){//樹中無這個值,查找到樹的底端,新建一個子節點 sz++;//新節點編號 son[sz][1]=son[sz][0]=0;//新節點初始化 fa[sz]=f; siz[sz]=cnt[sz]=1; son[f][v>key[now]]=sz;//判斷新節點是左節點還是右節點並更新父節點 key[sz]=v; update(f);//更新數量 splay(sz);//旋轉到根節點 break; } } }
接下來是查找一個值在樹中排從小到大第幾(是比它小的有多少個)(如果為了方便理解也可以說是從大到小求倒數第幾),原理很簡單,因為排序樹的性質,我們可以知道只要是在這個節點的左邊的都是比它小的,那么我們就可以利用之前記錄好的siz來快速求出:(注意是數量,要加上重復的,比如說1,2,2,3,這個數列中,3排第4)
int find(int v){ int ans=0,now=root;//ans記錄已經有多少比它小的點,now表示正在尋找的節點的編號 while(1){ if(v<key[now]){//如果當前節點的值大於v,那么當前節點的左子樹不完全小於v,繼續向當前節點的左子樹尋找 now=son[now][0]; } else{//當前節點的左子樹上的值必然全部小於v ans+=(son[now][0]!=0?siz[son[now][0]]:0);//如果有左子樹則直接加上左子樹的數量 if(v==key[now]){//如果當前節點的值等於v,則右子樹上不可能有比它小的數,所有比它小的數已經找完 splay(now);//下一次可能的操作 return ans+1;//有1個數比它小那么它應該是第2,以此類推,要+1 } ans+=cnt[now];//key[now]<v的情況,除了它的左兒子還要加上它自身的數量 now=son[now][1];//右子樹中可能存在比v小的值,所以在右子樹中繼續尋找 } } }
有了查找排名,如果需要知道第幾是多少,也是可以求的:
int findx(int x){ int now=root;//當前節點 while(1){ if(son[now][0]!=0&&siz[son[now][0]]>=x){//如果左子樹的數量大於x,就是說第x個是在左子樹上(前提是有左子樹) now=son[now][0];//在左子樹上接着搜索 } else{//第x個在以當前節點為頂點的樹中 int less=(son[now][0]!=0?siz[son[now][0]]:0)+cnt[now];//左子樹的數量(可能沒有)+當前節點的值的數量 if(x<=less)//由於之前判斷過是否在左子樹上,並且在之后的運算中排除了所有左子樹,x卻不在右子樹上,那么只可能是當前點的值 return key[now]; x-=less;//在右子樹中還有多少值比它小,排除左子樹 now=son[now][1];//繼續搜索 } } }
接下來是前驅,就是指比它小的第一個點(就是比它自己的值小而且它的前驅與它自己的值的差的絕對值最小)。在樹中表現為它的左子樹中最右邊的那個點,這樣才滿足比它自身的值小並且最接近它自身的值:
int query_pre(int x){
splay(x);//首先旋轉到根節點方便查找 int now=son[root][0];//定位左子樹 while(son[now][1]!=0)now=son[now][1];//循環查找左子樹中最右邊的點 return now;//最底部跳出循環,找到答案 }
有前驅當然也有后繼,類似的,后繼是指比它大的第一個點(比它自己的值大而且它的后繼與它自己的值的差的絕對值最小)。在樹中表現為它右子樹最左邊的那個點:(原理一模一樣)
int query_next(int x){ splay(x); int now=son[root][1]; while(son[now][0]!=0)now=son[now][0]; return now; }
Last but not the least:刪除。這里的刪除是刪除一次值,就是說受cnt的影響,不一定是刪一個節點。這個挺復雜的,比如說刪除一個值v,首先我們用之前的find(v)將值等於v的點旋轉到根上,之后要分5種情況:
- 這個值出現了不止一次:直接減少次數,更新數量即可;
- 整棵樹只剩下它一個值 孤苦伶仃 :將樹清空;
- 這個點沒有左子樹:直接將右子樹的頂點提出來,取代它的父節點;
- 這個點沒有右子樹:直接將左子樹的頂點提出來,取代它的父節點;
- 這個點左右子樹都有:先把前驅為作新樹的根(叫做新根)(后繼也完全沒有問題,只是之后的處理要想一想是左子樹還是右子樹),將它旋轉到根,這個時候在將原根的右兒子接到新根的左兒子上,更新即可。
這里先給出清空:(就是把所有關於這個點的東西歸零)
void clear(int x){ son[x][0]=son[x][1]=fa[x]=siz[x]=key[MX]=cnt[x]=0; }
接下來就可以開始刪除了:
void del(int v){ find(v); if(cnt[root]>1){//第一種情況 cnt[root]--; update(root); return; } if(son[root][0]==0&&son[root][1]==0){//第二種情況 clear(root); root=0;//將樹清空 return; } if(son[root][0]==0){//第三種情況 int old=root; root=son[root][1]; fa[root]=0;//新根的父節點更新 clear(old); return; } if(son[root][1]==0){//第四種情況 int old=root; root=son[root][0]; fa[root]=0; clear(old); return; } int newroot=query_per(root),oldroot=root; splay(newroot);//將新根轉上來 fa[son[oldroot][1]]=newroot; son[root][1]=son[oldroot][1];//繼承右子樹 clear(oldroot);//舊根歸零 update(root);//更新新根 }