關於Splay的學習感受
【關於Splay】
之前記得五月份聽過一次外省金牌選手講過一次,然後七月份又講過一次,但本人腦子比較笨,當時完全聽得一臉懵逼啊,練了兩個月確實不一樣,現在談一下學習Splay的一些感受。
首先欲知Splay為何物,不得不先講講它的祖宗:二叉查詢樹,即BST(Binary Search Tree),關於二叉查詢樹,它不是一顆空樹就是擁有以下幾個性質的二叉樹:
1.樹中每個結點被賦予了一個權值;(下面假設不同結點的權值互不相同。)
2.若左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;
3.若右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;
4.左、右子樹也分別為二叉查詢樹;
下圖就是一個合法的BST(蜜汁畫風【捂臉】):
通常二叉查詢樹支援如下操作:查詢一個元素x在集合中是否存在;插入或刪除一個元素,這些都可以通過遞迴實現。
但這也有個小問題,每個操作的複雜度都取決於樹的高度,那麼當這棵樹退化成一條鏈的時候,複雜度就退化成O(n)的了,由於二叉查詢樹的形態不一,所以平衡樹就出現了,它的每種操作最壞情況、均攤、期望的時間複雜度就是O(log n)的。而常用平衡樹有兩種:Splay和Treap,這裡介紹Splay,下一篇文章介紹Treap。
(廢話一堆然後轉入正題)
Splay,即伸展樹,是二叉查詢樹的一種改進,它與二叉查詢樹性質相似,與其不同的就是它可以自我調整。
關於Splay操作:伸展操作 Splay(x, S)是在保持伸展樹有序性的前提下,通過一系列旋轉將伸展樹 S 中的元素 x調整至樹的根部。在調整的過程中,要分以下三種情況分別處理:這個東西常數不能靠譜的可持久化,所以比起Treap而言,Splay的優勢似乎僅在於求LCT。
1.節點x的父節點y是根節點:如果x是左兒子,那麼就右旋一下,如果是右兒子,那麼就左旋一下,一次操作後,x就變成根節點了,完成,下面是示意圖:
2.節點x的父節點y不是根節點,y的父節點為z,且x,y均為父節點的左兒子或右兒子,那麼就右旋或左旋兩次,分別交換y,z和x,y,下面是示意圖(靈魂畫師再次上線):
3.節點x的父節點y不是根節點,y的父節點為z,x,y一個是左兒子,一個是右兒子,這時如果x是左兒子就先右旋再左旋,如果是右兒子就先左旋再右旋,注意兩次都是轉x,示意圖:
所以由上圖可以感性理解一下,經旋轉的平衡樹確實比之前要平衡很多,注意每次操作後都要進行伸展操作,然後這樣下來各操作複雜度均攤為O(log n)(因為不會證所以我就直接跳了)。
Splay支援以下操作:
1.Find(x,S):判斷元素x是否在伸展樹S表示的有序集中。首先,訪問根節點,如果x比根節點權值小則訪問左兒子;如果x比根節點權值大則訪問右兒子;如果權值相等,則說明x在樹中;如果訪問到空節點,則x不在樹中。如果x在樹中,則再執行Splay(x,S)調整伸展樹。
2.Insert(x,S):將元素x插入伸展樹S表示的有序集中。首先,訪問根節點,如果x比根節點權值小則訪問左兒子;如果x比根節點權值大則訪問右兒子;如果訪問到空節點t,則把x插入該節點,然後執行Splay(t,S)。
3.Merge(S1,S2):將兩個伸展樹S1與S2合併成為一個伸展樹。其中S1的所有元素都小於S2的所有元素。首先,我們找到伸展樹S1中最大的一個元素x,再通過Splay(x,S1)將x調整到伸展樹S1的根。然後再將S2作為x節點的右子樹。這樣,就得到了新的伸展樹S。如圖所示:
4.Delete(x,S):把節點x從伸展樹表示的有序集中刪除。首先,執行Splay(x,S)將x旋轉至根節點。然後取出x的左子樹S1和右子樹S2,執行Merge(S1,S2)把兩棵子樹合併成S。如圖所示:
5.Split(x,S):以x為界,將伸展樹S分離為兩棵伸展樹S1和S2,其中S1中所有元素都小於x,S2中的所有元素都大於x。首先執行Find(x,S),將元素x調整為伸展樹的根節點,則x的左子樹就是S1,而右子樹為S2。圖示同上。
除了以上操作,Splay還支援求最大值、最小值、前驅後繼,同時,Splay還能進行區間操作,對於待操作區間[l,r],我們將l-1通過Splay旋至根節點處,再將r+1旋至根節點右兒子處,這時r+1的左兒子就是待操作區間,可以像線段樹那樣打lazy-tag標記,然後具體實現見下方程式碼(主要給Rotate(左旋右旋合併版,只用打一段即可,不必打一個Zig打一個Zag),Splay,build,lazy,find(int k)(尋找第k大數),getnext(求後繼,前驅類似)):
(因為從來不打指標所以只能夠打打陣列)
結構體:
struct Num{ //根據題目不同決定
int val,id;
bool operator<(const Num &a)const{
if(val==a.val)
return id<a.id;
return val<a.val;
}
}So[MAXN/5];
struct Tr{ //平衡樹
int fa,sum;
int val,c[2],lz;
}tr[MAXN];
int tot,root,n;
1.Rotate:
void Rotate(int x,int k)
{
if(tr[x].fa==-1)
return ;
int fa=tr[x].fa,w;
lazy(fa);
lazy(x);
tr[fa].c[!k]=tr[x].c[k];
if(tr[x].c[k]!=-1)
tr[tr[x].c[k]].fa=fa;
tr[x].fa=tr[fa].fa,tr[x].c[k]=fa;
if(tr[fa].fa!=-1)
{
w=tr[tr[fa].fa].c[1]==fa;
tr[tr[fa].fa].c[w]=x;
}
tr[fa].fa=x;
Push(fa);
Push(x);
}
2.Splay
void Splay(int x,int goal)
{
if(x==-1)
return ;
lazy(x);
while(tr[x].fa!=goal)
{
int y=tr[x].fa;
lazy(tr[y].fa);
lazy(y),lazy(x);
bool w=x==tr[y].c[1];
if(tr[y].fa!=goal&&w==(y==tr[tr[y].fa].c[1]))
Rotate(y,!w);
Rotate(x,!w);
}
if(goal==-1)
root=x;
Push(x);
}
3.build
int build(int l,int r,int f) //返回根節點
{
if(r<l)
return -1;
int mid=l+r>>1;
int ro=newtr(mid,f,mid);
data[mid]=ro;
tr[ro].c[0]=build(l,mid-1,ro);
tr[ro].c[1]=build(mid+1,r,ro);
Push(ro);
return ro;
}
4.lazy
void lazy(int id) //此懶標記表示交換左右兒子,即區間反轉
{
if(tr[id].lz)
{
swap(lc,rc);
tr[lc].lz^=1,tr[rc].lz^=1;
tr[id].lz=0;
}
}
5.find
int find(int k)
{
int id=root;
while(id!=-1)
{
lazy(id);
int lsum=(lc==-1)?0:tr[lc].sum;
if(lsum>=k)
{
id=lc;
}
else
if(lsum+1==k)
break;
else
{
k=k-lsum-1;
id=rc;
}
}
return id;
}
6.getnext
int Getnext(int id)
{
lazy(id);
int p=tr[id].c[1];
if(p==-1)
return id;
lazy(p);
while(tr[p].c[0]!=-1)
{
p=tr[p].c[0];
lazy(p);
}
return p;
}
關於Splay,在實現上還有許多細節,如每次操作後的Splay,此處不再贅述。
以上大概是我關於Splay的一些學習總結,以後可能還會填坑【PS:感謝wcr大佬的幫助】