Splay樹
這是一篇宏偉的巨篇
首先介紹BST,也就是所有平衡樹的開始,他的China名字是二叉查找樹.
BST性質簡介
給定一棵二叉樹,每一個節點有一個權值,命名為 ** 關鍵碼 **,至於為什么叫這個名字,我也不知道.
BST性質也就是,對於樹中任何一個節點,都滿足一下性質.
- 這個節點的關鍵碼不小於它的左子樹上,任意一個節點的關鍵碼
- 這個節點的關鍵碼不大於它的右子樹上,任意一個節點的關鍵碼
然后我們就可以發現這棵樹的中序遍歷,就是一個關鍵碼單調遞增的節點序列,說的直白點,就是一個排好序的數組.
這就是偉大的BST性質,一個引起無數OIer憎恨的性質.(手動滑稽)
Splay的背景
什么是Splay,它就是一種可以旋轉的平衡樹.它可以解決BST樹這棵樹一個極端情況,也就是退化情況.
如下圖所示.
我們發現上面這個圖是一條鏈,這種恐怖的數據讓時間從\(O(log(n))\)退化到\(O(n)\).
於是各種各樣的科學家們,就開始了思考人生,開始了喪心病狂地創造出了各種平衡樹方法,然后一個異常有名的科學家Tarjan開始了它喪心病狂的Tarjan算法系列.
Splay思路
這是一棵特殊的BST樹,或者說平衡樹基本都是改變樹結構樣式,但是卻不改變最后得出的排列序列.
來一個yyb大佬的美圖

這張圖片大致意思如下所示:正方形部分表示一棵子樹,然后圓形表示節點.(當然了其實你也可以都看作成為節點,可能更好理解吧,看個人看法)
對於這樣一棵樹,我們可以做一些特殊的操作,來讓它變換樹的形態結構,但是最后的答案卻是正確的.平衡樹的精髓就是這個,就是改變樹的形態結構,但是不改變最后的中序遍歷,也就是答案數組.
重點來了,以下為splay精華所在之處,一定要全神貫注地看,並且手中拿着筆和草稿紙,一步步跟着我一起做,那么我保證你看一遍就可以懂.相信自己一定可以明白的,不要因為文字太多而放棄.
現在我們的現在目標只有一個x節點往上面爬,爬到它原本的父親節點y,然后讓y節點下降
首先思考BST性質,那就是右子樹上的點,統統都比父親節點大對不對,現在我們的x節點是父親節點y的左節點,也就是比y節點小. 那么我們為了不改變答案順序,我們可以讓y節點成為x節點的右兒子,也就是y節點仍然大於我們的x節點
這么做當然是沒有問題的,那么我們現在又有一個問題了,x節點的右子樹原來是有子樹B的,那么如果說現在y節點以及它的右子樹(沒有左子樹,因為曾經x節點是它的左子樹),放到了x節點的右子樹上,那么豈不是多了什么嗎?
我們知道 x節點的右子樹必然是大於x節點的,然后y節點必然是大於x節點的右子樹和x節點的,因為x節點和它的右子樹都是在y節點的左子樹,都比它小
既然如此的話,我們為什么不把x節點原來的右子樹,放在節點y的左子樹上面呢?這樣的話,我們就巧妙地避開了沖突,達成了x節點上移,y節點下移.
移動后的圖片
這就是一種情況,但是我們不能夠局限一種情況,我們要找到通解.以下為通解
若節點x為y節點的位置z.( z為0,則為左節點,1則為右節點 )
- 那么y節點就放到x節點的z^1的位置.(也就是,x節點為y節點的右子樹,那么y節點就放到左子樹,x節點為y節點左子樹,那么y節點就放到右子樹位置,這樣就完美滿足條件)
- 如果說x節點的z^1的位置上,已經有節點,或者一棵子樹,那么我們就將原來x節點z^1位置上的子樹,放到y節點的位置z上面.(自己可以畫圖理解一下)
- 移動完畢.
yyb大佬的代碼,個人認為最精簡的代碼&最適合理解的代碼.主要是因為注釋多,不要打我臉
t是樹上節點的結構體,ch數組表示左右兒子,ch[0]是左兒子,ch[1]是右兒子,ff是父節點
void update(int x)
{
t[x].size=t[t[x].ch[0]].size+t[t[x].ch[1]].size+t[x].cnt;//左子樹+右子樹+本身多少個,cnt記錄重復個數.
}
void rotate(int x)//X是要旋轉的節點
{
int y=t[x].ff;//X的父親
int z=t[y].ff;//X的祖父
int k=t[y].ch[1]==x;//X是Y的哪一個兒子 0是左兒子 1是右兒子
t[z].ch[t[z].ch[1]==y]=x;//Z的原來的Y的位置變為X
t[x].ff=z;//X的父親變成Z
t[y].ch[k]=t[x].ch[k^1];//X的與X原來在Y的相對的那個兒子變成Y的兒子
t[t[x].ch[k^1]].ff=y;//更新父節點
t[x].ch[k^1]=y;//X的 與X原來相對位置的兒子變成 Y
t[y].ff=x;//更新父節點
update(y);update(x);//yyb大佬忘記寫了,這里是上傳修改.
}
這里面用到了t[y].ch[1]==x
t[y].ch[1]
是y的右兒子,如果x是右兒子,那么這個式子是1,否則是0,也正好對應着左右兒子,同樣的k^1,表示相對的兒子,左兒子0^1=1 右兒子1^1=0.其實上面我的文字們已經講述清楚了,不過yyb大佬的代碼,寫得很好看這是真心話!!!
如果你已經讀到了這里,那么恭喜你,現在的你成功完成了splay大部分了,但是你發現這條鏈表結構還是會卡死你,是不是感覺被耍了,不要氣惱&氣餒,因為你只需要再來一個splay函數就好了.你已經完成了85%,接下來的很容易的.
如果說x,y,z這三個節點共線,也就是x和它的父親節點y和它的祖先節點在同一條線段上的話,那么我們就需要再來一些特殊處理了,其實就是一些很容易的操作.
下面就是三點共線的一張圖片
這張圖片里面的最長鏈是Z->Y->X->A
如果說我們一直都是x旋轉的話,那么就會得到下面這張圖片.
而一直旋轉x的最長鏈是X->Z->y->B
我們發現旋轉和不旋轉似乎並沒有任何區別,實際上也沒有區別,也就是這些旋轉操作后的樹和不旋轉的樹,其實是沒有多大區別的.
算法失敗了,不過不用害怕,其實我們還有辦法.
- 如果當前處於共線狀態的話,那么先旋轉y,再旋轉x.這樣可以強行讓他們不處於共線狀態,然后平衡這棵樹.
- 如果當前不是共線狀態的話,那么只要旋轉x即可.
當你看懂這個以后恭喜你,你已經成功學會了splay的雙旋操作了,然后你就可以看yyb大佬的代碼了.
splay操作
void splay(int x,int goal)//將x旋轉為goal的兒子,如果goal是0則旋轉到根
{
while(t[x].ff!=goal)//一直旋轉到x成為goal的兒子
{
int y=t[x].ff,z=t[y].ff;//父節點祖父節點
if(z!=goal)//如果Y不是根節點,則分為上面兩類來旋轉
(t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);//判斷共線還是不共線
//這就是之前對於x和y是哪個兒子的討論
rotate(x);//無論怎么樣最后的一個操作都是旋轉x
}
if(goal==0)root=x;//如果goal是0,則將根節點更新為x
}
查找find操作
從根節點開始,左側都比他小,右側都比他大,
所以只需要相應的往左/右遞歸
如果當前位置的val已經是要查找的數
那么直接把他Splay到根節點,方便接下來的操作
類似於二分查找,
所以時間復雜度O(logn)
inline void find(int x)//查找x的位置,並將其旋轉到根節點
{
int u=root;
if(!u)return;//樹空
while(t[u].ch[x>t[u].val]&&x!=t[u].val)//當存在兒子並且當前位置的值不等於x
u=t[u].ch[x>t[u].val];//跳轉到兒子,查找x的父節點
splay(u,0);//把當前位置旋轉到根節點
}
Insert操作
往Splay中插入一個數
類似於Find操作,只是如果是已經存在的數,就可以直接在查找到的節點的進行計數
如果不存在,在遞歸的查找過程中,會找到他的父節點的位置,
然后就會發現底下沒有啦。。。
所以這個時候新建一個節點就可以了
inline void insert(int x)//插入x
{
int u=root,ff=0;//當前位置u,u的父節點ff
while(u&&t[u].val!=x)//當u存在並且沒有移動到當前的值
{
ff=u;//向下u的兒子,父節點變為u
u=t[u].ch[x>t[u].val];//大於當前位置則向右找,否則向左找
}
if(u)//存在這個值的位置
t[u].cnt++;//增加一個數
else//不存在這個數字,要新建一個節點來存放
{
u=++tot;//新節點的位置
if(ff)//如果父節點非根
t[ff].ch[x>t[ff].val]=u;
t[u].ch[0]=t[u].ch[1]=0;//不存在兒子
t[tot].ff=ff;//父節點
t[tot].val=x;//值
t[tot].cnt=1;//數量
t[tot].size=1;//大小
}
splay(u,0);//把當前位置移到根,保證結構的平衡
}
前驅/后繼操作Next
首先就要執行Find操作
把要查找的數弄到根節點
然后,以前驅為例
先確定前驅比他小,所以在左子樹上
然后他的前驅是左子樹中最大的值
所以一直跳右結點,直到沒有為止
找后繼反過來就行了
inline int Next(int x,int f)//查找x的前驅(0)或者后繼(1)
{
find(x);
int u=root;//根節點,此時x的父節點(存在的話)就是根節點
if(t[u].val>x&&f)return u;//如果當前節點的值大於x並且要查找的是后繼
if(t[u].val<x&&!f)return u;//如果當前節點的值小於x並且要查找的是前驅
u=t[u].ch[f];//查找后繼的話在右兒子上找,前驅在左兒子上找
while(t[u].ch[f^1])u=t[u].ch[f^1];//要反着跳轉,否則會越來越大(越來越小)
return u;//返回位置
}
刪除操作
現在就很簡單啦
首先找到這個數的前驅,把他Splay到根節點
然后找到這個數后繼,把他旋轉到前驅的底下
比前驅大的數是后繼,在右子樹
比后繼小的且比前驅大的有且僅有當前數
在后繼的左子樹上面,
因此直接把當前根節點的右兒子的左兒子刪掉就可以啦
inline void Delete(int x)//刪除x
{
int last=Next(x,0);//查找x的前驅
int next=Next(x,1);//查找x的后繼
splay(last,0);splay(next,last);
//將前驅旋轉到根節點,后繼旋轉到根節點下面
//很明顯,此時后繼是前驅的右兒子,x是后繼的左兒子,並且x是葉子節點
int del=t[next].ch[0];//后繼的左兒子
if(t[del].cnt>1)//如果超過一個
{
t[del].cnt--;//直接減少一個
splay(del,0);//旋轉
}
else
t[next].ch[0]=0;//這個節點直接丟掉(不存在了)
}
第K大
從當前根節點開始,檢查左子樹大小
因為所有比當前位置小的數都在左側
如果左側的數的個數多余K,則證明第K大在左子樹中
否則,向右子樹找,找K-左子樹大小-當前位置的數的個數
記住特判K恰好在當前位置
inline int kth(int x)//查找排名為x的數
{
int u=root;//當前根節點
if(t[u].size<x)//如果當前樹上沒有這么多數
return 0;//不存在
while(1)
{
int y=t[u].ch[0];//左兒子
if(x>t[y].size+t[u].cnt)
//如果排名比左兒子的大小和當前節點的數量要大
{
x-=t[y].size+t[u].cnt;//數量減少
u=t[u].ch[1];//那么當前排名的數一定在右兒子上找
}
else//否則的話在當前節點或者左兒子上查找
if(t[y].size>=x)//左兒子的節點數足夠
u=y;//在左兒子上繼續找
else//否則就是在當前根節點上
return t[u].val;
}
}
以下為我自己編寫的代碼,其實就是yyb大佬的代碼,只不過修改了一丟丟而已了.
#include <bits/stdc++.h>
using namespace std;
const int N=201000;
struct splay_tree
{
int ff,cnt,ch[2],val,size;
} t[N];
int root,tot;
void update(int x)
{
t[x].size=t[t[x].ch[0]].size+t[t[x].ch[1]].size+t[x].cnt;
}
void rotate(int x)
{
int y=t[x].ff;
int z=t[y].ff;
int k=(t[y].ch[1]==x);
t[z].ch[(t[z].ch[1]==y)]=x;
t[x].ff=z;
t[y].ch[k]=t[x].ch[k^1];
t[t[x].ch[k^1]].ff=y;
t[x].ch[k^1]=y;
t[y].ff=x;
update(y);update(x);
}
void splay(int x,int s)
{
while(t[x].ff!=s)
{
int y=t[x].ff,z=t[y].ff;
if (z!=s)
(t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);
rotate(x);
}
if (s==0)
root=x;
}
void find(int x)
{
int u=root;
if (!u)
return ;
while(t[u].ch[x>t[u].val] && x!=t[u].val)
u=t[u].ch[x>t[u].val];
splay(u,0);
}
void insert(int x)
{
int u=root,ff=0;
while(u && t[u].val!=x)
{
ff=u;
u=t[u].ch[x>t[u].val];
}
if (u)
t[u].cnt++;
else
{
u=++tot;
if (ff)
t[ff].ch[x>t[ff].val]=u;
t[u].ch[0]=t[u].ch[1]=0;
t[tot].ff=ff;
t[tot].val=x;
t[tot].cnt=1;
t[tot].size=1;
}
splay(u,0);
}
int Next(int x,int f)
{
find(x);
int u=root;
if (t[u].val>x && f)
return u;
if (t[u].val<x && !f)
return u;
u=t[u].ch[f];
while(t[u].ch[f^1])
u=t[u].ch[f^1];
return u;
}
void Delete(int x)
{
int last=Next(x,0);
int Net=Next(x,1);
splay(last,0);
splay(Net,last);
int del=t[Net].ch[0];
if (t[del].cnt>1)
{
t[del].cnt--;
splay(del,0);
}
else
t[Net].ch[0]=0;
}
int kth(int x)
{
int u=root;
while(t[u].size<x)
return 0;
while(1)
{
int y=t[u].ch[0];
if (x>t[y].size+t[u].cnt)
{
x-=t[y].size+t[u].cnt;
u=t[u].ch[1];
}
else if (t[y].size>=x)
u=y;
else
return t[u].val;
}
}
int main()
{
int n;
scanf("%d",&n);
insert(1e9);
insert(-1e9);
while(n--)
{
int opt,x;
scanf("%d%d",&opt,&x);
if (opt==1)
insert(x);
if (opt==2)
Delete(x);
if (opt==3)
{
find(x);
printf("%d\n",t[t[root].ch[0]].size);
}
if (opt==4)
printf("%d\n",kth(x+1));
if (opt==5)
printf("%d\n",t[Next(x,0)].val);
if (opt==6)
printf("%d\n",t[Next(x,1)].val);
}
return 0;
}
/*
插入數值x。
刪除數值x(若有多個相同的數,應只刪除一個)。
查詢數值x的排名(若有多個相同的數,應輸出最小的排名)。
查詢排名為x的數值。
求數值x的前驅(前驅定義為小於x的最大的數)。
求數值x的后繼(后繼定義為大於x的最小的數)。
*/
作者:秦淮岸燈火闌珊
鏈接:https://www.acwing.com/activity/content/code/content/24072/
來源:AcWing
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
感謝yyb大佬
注:我學會splay就是在yyb大佬的blog下成功的,然后成功地將它詳細寫了一遍,這道題目除了代碼和一些圖片,其他都是我寫的,所以來說也算得上原汁原味的原著吧.