筆者一個數據結構的蒟蒻仍是奇蹟般的搞明白了splay的基本原理以及實現方法,因此寫下這篇隨筆但願能幫到像我當初一臉懵逼的人。ios
咱們從二叉查找樹開始提及:數組
二叉查找樹是一棵二叉樹,它知足這樣一個性質:全部小於當前節點的點都在該節點的左子樹上,全部大於當前節點的點都在該節點的右子樹上。對於和當前節點同樣大的點,咱們有兩種方法,一種是直接默認它到右子樹上去,可是這樣會形成空間的浪費。咱們有一種比較好的操做是設置一個權值數組,若是出現了這種同樣的狀況,就直接把這個點的權值+1就能夠了。數據結構
手繪了一棵二叉查找樹:函數
那麼這棵樹有什麼用呢?工具
咱們先來看這樣一道題吧:優化
很明顯,這個題咱們能夠直接用最高級的數據結構——數組實現,直接全讀進來排個序什麼的就直接OK了。ui
可是出題人就是想讓這個題變得難一點,他使這個題變成了一邊插入一邊詢問。很明顯,剛纔那個方法萎了,如今咱們就要引入咱們的二叉查找樹了。spa
顯然,咱們能夠很輕鬆的使用二叉查找樹來完成插入這個工做。重要的是完成詢問2和3。3d
先來看詢問2吧:因爲二叉查找樹的性質,咱們比較詢問的妹子的好感度與當前節點的好感度,若是少了那就向左查找,多了就向右查找。咱們最終老是會找到的。而後這個妹子前面有k我的,那麼這個妹子就排名爲k+1.code
而後是詢問3:咱們比較每一個節點的k值和當前的k值,依然按照二叉查找樹的性質比較大小就能夠了。
這樣咱們就可指望O(nlogn)出解來八卦掉Refun大神。
可是題是死的,人是活的,出題人是毒瘤的,每種這樣的題目總會有這樣的數據:給出的插入徹底有序,結果咱們的兩個查找一會兒全成了n的複雜度。
就想是這樣一棵樹:
你看這棵長壞了的樹。那如何解決這種問題呢?天然是使這棵樹平衡起來,具體實現有treap,splay等等。
如今咱們就引入今天的正題——splay
splay
首先聲明一些變量:
和一些操做:
求當前節點是左?右?兒子:
inline int get(int x) { return ch[f[x]][1]==x; }
清零操做:
inline void clear(int x){ ch[x][0]=ch[x][1]=size[x]=f[x]=key[x]=cnt[x]=0; }
更新size值的操做:
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]]; } }
而後就是splay的關鍵操做了,旋轉。
有人可能有疑問了,這旋轉有個P用,看上去啥都沒改變啊。然而實際上,這旋轉就是成功把x向上提了一個位置,而咱們的目標就是像這樣一步步把一個節點向上提到他的一個祖先下面,或者就這麼變成了根。
那這個右旋應該怎麼樣實現呢?咱們分三步來解釋:
一:咱們先看看x有沒有右子樹,若是有的話,讓它成爲y的左子樹,同時讓它認y作爹。
二:咱們看看,這個時候x就沒有右子樹了,咱們就讓y認x作爹,而後讓y做爲x的右子樹。
三:咱們再看看,y有沒有爹,若是有的話,假定這個爹叫z,那麼讓x認z作爹,而且要與y的左右子樹的性質一致。
貼一段代碼,看看應該挺好理解的:
至於左旋和右旋很像,不過代碼筆者仍是碼了的:
至於實際操做的時候,咱們天然不能夠把這倆玩意分開,實現起來很複雜,因此用ch數組的兩維表明左右兒子,經過一個綜合函數來實現這兩個函數。而且在旋轉完了以後要緊接着update維護一下。
這樣咱們最基礎的旋轉就已經搞定了,接下來咱們要實現splay的關鍵操做,splay。
splay的目的在於把一個節點一直轉到一個給定的節點底下,而後,通常人們都直接旋轉到根。
能夠用一個簡短的代碼歸納一下
至於怎麼旋轉,咱們要分狀況討論:
若是x,y,z三個點在同一個直線上的話,那麼就要先旋轉y,不然咱們就先旋轉x。若是不這麼作的話,就會形成樹的失衡。
那麼咱們能夠先看一下繁雜的代碼,不過好理解是真的:
很明顯的是,這個代碼很長,不過看上去應該仍是比較清楚的,下面提供一種簡潔不少的版本:
對於直接旋轉到根的狀況來講,這兩個代碼是徹底等價的。
而後就是依題目而定的具體操做了,這裏咱們以各大OJ上都有的一道普通平衡樹的模板題來示例。
首先看一下他須要讓咱們進行的操做
那咱們就一步步的看這些操做都怎麼實現吧:
1.插入一個數:都還記着筆者剛剛開始說二叉查找樹的時候就已經說過了插入是一個很簡單的工做了吧。。
(1):首先對於root==0時,明顯樹是空的,進行一些特殊操做直接退出來就好了。
(2):對於root!=0時的狀況,若是在向下尋找的時候咱們尋找到了一個和它同樣大的點,咱們就能夠直接把它的權值加1,而後update維護下它和它的爹,再splay一下。
若是咱們直接找到了最底下,那沒什麼好說的了,把樹的大小+1,因爲它是最底下的節點,不必update本身,直接維護一下父節點,splay一下就行。
代碼老是有的,筆者就是這麼的善解人意:
刪除一個數比較麻煩一會再說;
2.查找一個數的排名
這裏的操做就和二叉查找樹愈來愈像了。
(1):若是當前節點的數值比咱們如今的小,那麼不用進行其餘的任何操做,咱們直接繼續向左子樹查找就能夠了。
(2):若是當前節點的數值比咱們如今的大,那麼咱們就把返回值加上左子樹以及根的大小,而後向右子樹查找。
還有一個,找着了以後要splay一下。。
3.查詢一個排名的數
(1):首先一上來先看看正找着的這個點有沒有左子樹,若是有的話,而且它的大小比x大,那麼就向左查找,不然向右。
(2):向右查找的時候,注意把節點的大小和右子樹的大小都記錄下來,以便判斷是否要繼續向右子樹查找。
3:求x前驅和後繼
這個操做比較容易的吧,不過得想對。
對於這兩個操做,咱們直接先插進去x,而後求出它在樹上的前驅和後繼,天然也就是它的前驅和後繼,而後把它刪掉就能夠了。
而後咱們發現,在插入這個x的時候咱們把它旋轉到了根節點的位置上,因此前驅就是它左子樹最右的節點,就是先向左找一下,而後一直找到沒有右兒子了爲止,同理後繼就是它右子樹最左的節點。(不知道爲何建議向上翻翻找着二叉查找樹的定義仔細閱讀)。
至於怎麼找,不想說了,實在不明白的就看代碼明白吧。。
5:刪除操做
這個操做仍是比較麻煩的,注意的地方也教前面的操做多一點。
(1):爲了方便接下來的操做,先把x旋轉到根節點,隨你怎麼轉過去。
(2):而後分狀況討論,如今x已是根節點,若是它的權值不爲1,那就好辦,-1以後返回就好了。
(3):然而確定有不少是1的,怎麼辦?若是x一個孩子都沒有,把x刪了就行,反正樹上就它一個節點。
(4):若是x只有任何一個兒子,那麼把x刪了,直接讓兒子當爹就行。
(5):若是有兩個兒子的話,首先咱們要先選一個根,天然是x的前驅或後繼,這裏咱們選擇前驅,而後把前驅旋轉到根節點,而後再把x原來的右子樹當作它的右子樹,update維護一下就行。
這樣一來,這個題就這麼結束了。
其實splay整個操做都是基於二叉查找樹的,咱們的rotate操做很明顯是符合二叉查找樹性質的。
看上去完了?
沒有,咱們還要說一個點.
用splay實現區間翻轉
其實,要操做起來有不少種能夠用splay實現的方法了,這裏介紹一種看上去正常實現起來比較容易的。
咱們根據二叉查找樹的性質,能夠看出假如咱們要在Splay中修改區間的話,能夠先查找siz值爲l與r+2的兩個節點,將一個旋轉到根,另外一個旋轉到根的右兒子上,則要修改的區間就是根的右孩子的左子樹,直接打標記便可。
爲何這麼旋轉就能夠?先上圖:
理解一下,紅圈裏的兩個點就是咱們要旋轉的點,第二個圖中藍圈裏的就是要翻轉的區間,而且這樣翻轉完了以後它仍然與開始那個圖的中序遍歷相同。綠色的點就是咱們要翻轉的點。。爲何是這些點。。。由於要翻轉的必定是比l的下標大比r+2下標小的點。
至於代碼能夠這麼實現,不是一個很麻煩的事。
還有一個要說的是,咱們作這個題創建平衡樹的時候,是按照數組下標建樹,而不是按照大小建樹。因此頗有必要放一下代碼強調一下。
眼神好的人應該能看出來這份代碼和下面的有些區別,事實上,這個代碼可以一開始的時候創建出一個完美平衡樹(雖然不久以後它就不那麼完美了),理論上可以快一點吧。而下面的代碼一開始頗有可能建出來,額。。。一條鏈,不過很快也會splay掉了。
放上題目的徹底代碼了。
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cmath> 5 #include<algorithm> 6 #include<queue> 7 #define re register 8 #define maxn 1000007 9 #define ll long long 10 #define ls rt<<1 11 #define rs rt<<1|1 12 #define inf 1000000007 13 using namespace std; 14 int ch[100001][2],f[maxn],cnt[maxn],key[maxn],size[maxn],mark[maxn],root,sz,data[maxn]; 15 inline int pushdown(int x) 16 { 17 if(x&&mark[x]){ 18 mark[ch[x][0]]^=1; 19 mark[ch[x][1]]^=1; 20 swap(ch[x][0],ch[x][1]); 21 mark[x]=0; 22 } 23 } 24 inline void clear(int x) 25 { 26 ch[x][0]=ch[x][1]=f[x]=cnt[x]=key[x]=size[x]=0; 27 } 28 inline int get(int x) 29 { 30 return ch[f[x]][1]==x; 31 } 32 inline void update(int x) 33 { 34 size[x]=size[ch[x][1]]+size[ch[x][0]]+1; 35 } 36 inline void rotate(int x) 37 { 38 int y=f[x],z=f[y]; 39 int kind=get(x); 40 pushdown(y);pushdown(x); 41 ch[y][kind]=ch[x][kind^1];f[ch[y][kind]]=y; 42 ch[x][kind^1]=y; 43 f[y]=x; f[x]=z; 44 if(z){ 45 ch[z][ch[z][1]==y]=x; 46 } 47 update(y);update(x); 48 } 49 inline void splay(int x,int tar){ 50 for(re int fa;(fa=f[x])!=tar;rotate(x)) 51 if(f[fa]!=tar){ 52 rotate(get(x)==get(fa)?fa:x); 53 } 54 if(!tar) root=x; 55 } 56 inline int build(int fa,int l,int r) 57 { 58 if(l>r) return 0; 59 int mid=l+r>>1; 60 int now=++sz; 61 key[now]=data[mid],f[now]=fa,mark[now]=0; 62 ch[now][0]=build(now,l,mid-1); 63 ch[now][1]=build(now,mid+1,r); 64 update(now); 65 return now; 66 } 67 inline int findx(int k) 68 { 69 int now=root; 70 while(1) 71 { 72 pushdown(now); 73 if(k<=size[ch[now][0]]) 74 now=ch[now][0]; 75 else{ 76 k-=size[ch[now][0]]+1; 77 if(!k) return now; 78 now=ch[now][1]; 79 } 80 } 81 } 82 inline void print(int now) 83 { 84 pushdown(now); 85 if(ch[now][0]) print(ch[now][0]); 86 if(key[now]!=-inf && key[now]!=inf) 87 printf("%d ",key[now]); 88 if(ch[now][1]) print(ch[now][1]); 89 } 90 int main() 91 { 92 int n,m,x,y; 93 cin>>n>>m; 94 for(re int i=1;i<=n;i++) 95 { 96 data[i+1]=i; 97 } 98 data[1]=-inf;data[n+2]=inf; 99 root=build(0,1,n+2); 100 for(re int i=1;i<=m;i++) 101 { 102 cin>>x>>y; 103 int x1=findx(x),y1=findx(y+2); 104 splay(x1,0); 105 splay(y1,x1); 106 mark[ch[ch[root][1]][0]]^=1; 107 } 108 print(root); 109 }
其實splay更多的是一種輔助的工具,理解了以後代碼難度略小於treap(由於筆者如今還沒搞懂treap),並且靈活多變,能夠處理多類問題,至於常數大這個缺點,用各類玄學方式優化一下吧。。。