
筆者一個數據結構的蒟蒻還是奇跡般的搞明白了splay的基本原理以及實現方法,所以寫下這篇隨筆希望能幫到像我當初一臉懵逼的人。
我們從二叉查找樹開始說起:
二叉查找樹是一棵二叉樹,它滿足這樣一個性質:所有小於當前節點的點都在該節點的左子樹上,所有大於當前節點的點都在該節點的右子樹上。對於和當前節點一樣大的點,我們有兩種方法,一種是直接默認它到右子樹上去,但是這樣會造成空間的浪費。我們有一種比較好的操作是設置一個權值數組,如果出現了這種一樣的情況,就直接把這個點的權值+1就可以了。
手繪了一棵二叉查找樹:

那么這棵樹有什么用呢?
我們先來看這樣一道題吧:

很明顯,這個題我們可以直接用最高級的數據結構——數組實現,直接全讀進來排個序什么的就直接OK了。
但是出題人就是想讓這個題變得難一點,他使這個題變成了一邊插入一邊詢問。很明顯,剛才那個方法萎了,現在我們就要引入我們的二叉查找樹了。
顯然,我們可以很輕松的使用二叉查找樹來完成插入這個工作。重要的是完成詢問2和3。
先來看詢問2吧:由於二叉查找樹的性質,我們比較詢問的妹子的好感度與當前節點的好感度,如果少了那就向左查找,多了就向右查找。我們最終總是會找到的。然后這個妹子前面有k個人,那么這個妹子就排名為k+1.
然后是詢問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),而且靈活多變,可以處理多類問題,至於常數大這個缺點,用各種玄學方式優化一下吧。。。
