Splay 總結及模板
<目錄>
0.NewNode 操作——————————–(新建一個節點)
1.Splay、Rotate 操作 ————————(Splay 旋轉<核心>)
2.GetPre、GetNext 操作 ——————–(獲得前驅後繼)
3.GetMin、GetMax 操作 ——————– (獲得最小值,最大值)
4.Insert 操作 ————————————(插入節點,以二叉排序樹原則)
5.Delete 操作 ———————————– (刪除節點)
6.Find_Kth 操作 ——————————— (獲取第K大點)
7.Insert_Kth 操作 —————————— (將節點插入序列第K個位置)
8.PushUp操作 ——————————— (維護區間資訊)
9.Merge 操作 ———————————— (將兩個子樹合併)
10.CoutAns 操作 ———————————-(輸出整個序列)
11.GetSequence 操作 ———————— (獲得一段序列)
12.Insert_Sequence 操作 ——————— (將序列插入現序列的K位置)
13.PushDown 操作 —————————–(維護’線段樹’Lazy下放標記)
14.Delete_UpK 操作 —————————–(刪除 大於或小於K的所有數)
15.Same 操作 ————————————–(當操作涉及重複元素)
16.極簡程式碼
快速連結:
前言:
本文中,ll為define long long ,x為splay節點,tarfa為目標父親位置,
ls為左兒子,rs為右兒子,fa為父親,sz為子樹大小,root為當前樹的根。
splay的精華在於每次Insert等後都進行Splay旋轉,這樣就保證了樹的高度永遠為logN級別。
0.NewNode 操作(新建一個節點)
直接上程式碼:
IL void NewNode(RG int Value){ //新建一個權值為Value的結點
fa[++cnt] = ls[cnt] = rs[cnt] = 0;
blg[cnt] = cnt; val[cnt] = Value; sum [cnt] = 1; return;
}
此函式中的各個變數用處會在下文中有所展現。
1.Splay、Rotate 操作 (Splay 旋轉<核心>)
沒什麼好說的。
Left_Rotate:
IL void Left_Rotate(RG ll x){
RG ll y = fa[x],z = fa[y];
fa[ls[x]] = y; rs[y] = ls[x];
fa[x] = z; if(z){if(ls[z]==y)ls[z]=x; else rs[z]=x;}
fa[y] = x; ls[x] = y; Update(y); Update(x);
}
Right_Rotate:
IL void Right_Rotate(RG ll x){
RG ll y = fa[x],z = fa[y];
fa[rs[x]] = y; ls[y] = rs[x];
fa[x] = z; if(z){if(ls[z]==y)ls[z]=x; else rs[z]=x;}
fa[y] = x; rs[x] = y; Update(y); Update(x);
}
Splay:
IL void Splay(RG ll x,RG ll tarfa){ //把x splay到tarfa的兒子
while(fa[x] != tarfa){
RG ll y = fa[x],z = fa[y];
if(z == tarfa){
if(ls[y] == x)Right_Rotate(x);
else if(rs[y] == x)Left_Rotate(x);
}
else if(z != tarfa){
if(ls[z] == y && rs[y] == x){Left_Rotate(x); Right_Rotate(x);}
else if(rs[z] == y && ls[y] == x){Right_Rotate(x); Left_Rotate(x);}
else if(ls[z] == y && ls[y] == x){Right_Rotate(x); Right_Rotate(x);}
else if(rs[z] == y && rs[y] == x){Left_Rotate(x); Left_Rotate(x);}
}
if(!tarfa)root = x;
}return;
}
2.GetPre、GetNext 操作 (獲得前驅後繼)
以獲取node節點的前驅,後繼為例。
GetPre(獲取前驅):
IL ll Get_pre(RG ll x){
Splay(x,0); x = ls[x];
while(rs[x])x = rs[x]; return x;
}
Pre_node = Get_pre(ls[node]); //即左子樹中最大的。
GetNext(獲取後繼):
IL ll Get_next(RG ll x){
Splay(x,0); x = rs[x];
while(ls[x])x = ls[x]; return x;
}
Next_node = Get_Next(rs[node]); //即右子樹中最小的。
3.GetMin、GetMax 操作 (獲得最小值,最大值)
即序列的最左端點與最右端點,類似GetPre,GetNext。
GetMin:
IL void GetMin(RG ll x){while(ls[x])x = ls[x]; return x;}
Min = GetMin(root);
GetMax:
IL void GetMax(RG ll x){while(rs[x])x = rs[x]; return x;}
Max = GetMax(root);
4.Insert 操作 (插入節點,以二叉排序樹原則)
與二叉排序樹一樣,以按照val從大到小為排序原則為例。
void Insert(RG ll rt,RG ll nw){
if(val[rt]>val[nw] && !ls[rt]){ls[rt]=nw; fa[nw]=rt; return;}
else if(val[rt]>val[nw] && ls[rt])Insert(ls[rt],nw);
else if(val[rt]<val[nw] && !rs[rt]){rs[rt]=nw; fa[nw]=rt; return;}
else if(val[rt]<val[nw] && rs[rt])Insert(rs[rt],nw);
}
//把x節點插入: Insert(root,x);
5.Delete 操作 (刪除節點)
以刪除x節點為例子。
IL void Delete(RG ll x){
Splay(x,0);
if(!ls[x] && !rs[x]){root = 0; return;}
if(!ls[x] && rs[x]){fa[rs[x]] = 0; root = rs[x]; return;}
if(!rs[x] && ls[x]){fa[ls[x]] = 0; root = ls[x]; return;}
RG ll Rc = GetMin(rs[x]); Splay(Rc,0);
ls[Rc] = ls[x]; fa[ls[x]] = Rc; fa[Rc] = 0;
root = Rc; Splay(Rc); return;
}
解釋一下:
首先預先找到x節點。 然後Splay x 到根節點。
這時候分4中情況討論
<1> (!ls[x] && !rs[x]):樹中已經沒有節點,root = 0;
<2> (!ls[x] && rs[x]): 讓右兒子成為新的根即可。
<3> (!rs[x] && ls[x]):讓左兒子成為新的根即可。
<4> (ls[x] && rs[x]):這種情況比較麻煩,見下面詳解:
{
我們擬定把左子樹接到右子樹上面去。
先找到右子樹的Min,Rc = GetMin(rs[x]);
然後把Rc Splay到根部去。
那麼此時Rc即為ls[x]的後繼。
此時讓Rc成為新的根,root = Rc,fa[Rc] = 0;
連線一下即可:ls[Rc] = ls[x],fa[ls[x]] = Rc;
}
6.Find_Kth 操作 (獲取第K大點)
也類似二叉排序樹:
ll Find(RG ll rt,RG ll K){
if(sz[ls[rt]] == K-1)return rt;
else if(sz[ls[rt]] >= K && ls[rt])return Find(ls[rt],K);
else return Find(rs[rt],K-sz[ls[rt]]-1); //注意要減1(自己)
}
Kth = Find(root,K);
7.Insert_Kth 操作 (將節點插入序列第K個位置)
注意一下return判斷的方式即可。 以把x節點插入到第K個位置為例。
void Ins_Kth(RG ll rt,RG ll nw,RG ll K){
if(K == 1 && !ls[rt]){ls[rt]=nw; fa[nw]=rt; return;}
if(K == 2+sz[ls[rt]] && !rs[rt]){rs[rt]=nw; fa[nw]=rt; return;}
if(K<=1+sz[ls[rt]])Ins_Kth(ls[rt],nw,K);
else Ins_Kth(rs[rt],nw,K-1-sz[ls[rt]]); //注意減1(自己)
}
8.PushUp操作 (維護區間資訊)
類似線段樹的PushUp,以維護子樹大小為例:
IL void PushUp(RG ll x){
sz[x] = 1;
if(ls[x])sz[x]+=sz[ls[x]];
if(rs[x])sz[x]+=sz[rs[x]]; return;
}
這裡歸納一下需要PushUp的地方(當然條件為相關函式存在)。
<1>.Rotate<以右旋為例>:
IL void Right_Rotate(RG ll x){
RG ll y = fa[x],z = fa[y];
......
PushUp(y); PushUp(x); return; //請注意PushUp的順序
}
<2>.Insert:
void Insert(RG ll rt,RG ll nw){
//cout<<rt<<" "<<" "<<nw<<endl;
if(val[rt]>val[nw] && !ls[rt]){... Update(rt); return;}
else if(val[rt]>val[nw] && ls[rt]){... Update(rt); return;}
else if(val[rt]<val[nw] && !rs[rt]){... Update(rt); return;}
else if(val[rt]<val[nw] && rs[rt]){... Update(rt); return;}
}
//把x節點插入: Insert(root,x);
<3>Insert_Kth 、Insert_Sequence:與Insert類似
<4>Delete:
IL void Delete(RG ll x){
Splay(x,0);
if(!ls[x] && !rs[x]){root = 0; return;}
if(!ls[x] && rs[x]){fa[rs[x]] = 0; root = rs[x]; return;}
if(!rs[x] && ls[x]){fa[ls[x]] = 0; root = ls[x]; return;}
......
root = Rc; PushUp(Rc); Splay(Rc); return;
}
9.Merge 操作 (將兩個子樹合併)
用並查集維護是否需要合併,如果需要那麼暴力啟發式合併即可。
每個節點最多合併複雜度為logN<執行一次Insert操作>,所以總複雜度為N*logN是正確的。
以將 x1所在樹 合併到 x2所在樹為例。
其中bzj[i]為並查集陣列,FindRoot為並查集操作。
void Merge_Visit(RG ll rt,RG ll x2){
Insert(x2,rt); //將rt節點插入到 x2所在樹中
if(ls[rt])Merge_Visit(ls[rt],x2);
if(rs[rt])Merge_Visit(rs[rt],x2);
}
void Merge_Start(RG ll x1,RG ll x2){
Splay(x1,0); Splay(x2,0);
if(sz[x1] > sz[x2])swap(x1,x2); //x1 ----> x2,x1儘量小。
Merge_Visit(x1,x2);
return;
}
IL void Merge(){
RG ll x = gi(),y = gi();
RG ll f1 = FindRoot(x),f2 = FindRoot(y);
if(f1 != f2){ Merge_Start(x,y); bzj[f1]=f2; }
return;
}
10.CoutAns 操作 (輸出整個序列)
理解Splay原理後很容易知道,當前序列即為樹的中序遍歷。所以中序遍歷一遍即可。
void CoutAns(RG ll rt){
if(ls[rt])CoutAns(ls[rt]);
printf("%lld ",rt);
if(rs[rt])CoutAns(rs[rt]);
}
//CoutAns(root);
*、
11.GetSequence 操作(獲得一段序列操作)
以提取[ a , b ]為例。
一篇部落格:http://blog.51cto.com/sbp810050504/1029553
注意一下上面那個部落格中,把b+1提到a-1的兒子說的太複雜了,直接Splay(b+1,a-1)即可。
那麼這裡還是來概括一下具體步驟。
<1>Splay(a-1 , 0);
<2>Splay(b+1 , a-1 ) ;
<3>所求區間即為ls[ b+1 ]所在子樹。
<4>可以以ls[b+1]為根,把一個區間當做一個點,進行移動,刪除等操作。
這裡有一個問題,a == 1 與 b == N 時會有鬼( T_T )。
處理方案有兩個(這裡Ans 表示提取的區間樹的根節點):
【1】討論思想(弱智思路無解釋):
IL void GetSequence(){
RG ll a = gi() , b = gi();
RG ll L = Find(root,a) , R = Find(root,b);
RG ll Min = GetMin(),Max = GetMax();
RG ll Ro = Lo = GetPre(L),GetNext(R);
if(L== Min && R == Max){Ans = root; return;}
if(L == Min){Splay(Ro,0); Ans = ls[root]; return;}
if(R == Max){Splay(Lo,0); Ans = rs[root]; return;}
else {Splay(Lo,0); Splay(Ro,Lo); Ans = ls[Ro]; return;}
}
這裡極其不推薦這種方法,這種方法會導致常數超級巨大,建議使用下面那種方法。
【2】虛點思想:
建立兩個虛點,N+1表示最右端的那個點,N+2表示最左端那個點。
注意一下根據排序原則把val[N+1] = INF,val[N+2] = -INF確保其位置不變 。
對於PushUp等操作,把它當做正常點做就行了。
那麼注意需要改變的是:
<1>Find(root,K)
應該為 Find(root,K+1)
<2>CoutAns(root)
時,如果rt == (N+1 || N+2) 則不輸出。
IL void GetSequence(){
RG ll a = gi() , b = gi();
RG ll L = Find(root,a) , R = Find(root,b+2);
// Find(root,a-1) ⇒ Find(root,a) ; Find(root,b+1) ⇒ Find(root,b+2) ;
Splay(L,0);Splay(R,L);
Ans = ls[R] return;
}
void CoutAns(RG ll rt){
if(ls[rt])CoutAns(ls[rt]);
if(rt != N+1 && rt != N+2)printf("%lld ",rt);
if(rs[rt])CoutAns(rs[rt]); return;
}
int main(){
......
val[N+1] = INF Insert(root,N+1);
val[N+2] = -INF; Insert(root,N+2);
......
}
12.Insert_Sequence 操作 (將序列插入現序列的K位置)
以把序列插入原來序列的K位置為例。
通過GetSequence , 得到 待插入序列 的根Rot;
<1>Rot = GetMin(Rot);
<2>Insert_Kth( root , Rot , K );
程式碼略,其實就是兩個步驟合起來。
13.PushDown 操作 (維護’線段樹’Lazy下放標記)
與線段樹類似,但是一定要注意該PushDown的位置一定要PushDown。
這裡以維護區間翻轉問題為例,dt[i]為lazy標記,主動修改lazy的 視題目而定函式 略。
【0】PushDown函式:
IL void PushDown(RG ll x){
RG ll rson = rs[x],lson = ls[x];
dt[rson] ^= 1; dt[lson] ^= 1; dt[x] = 0;
ls[x] = rson; rs[x] = lson; return;
}
//
那麼需要PushDown的部分有:
【1】Splay :
IL void Splay(RG ll x,RG ll tarfa){
RG ll tt = x,cnt = 0;
while(tt != tarfa){tmp[++cnt] = tt; tt = fa[tt];}
while(cnt--)if(dt[tmp[cnt]])PushDown(tmp[cnt]);
while(fa[x] != tarfa){
......
}return;
}
具體來說,在splay的while之前,先:
<1>把splay向上翻轉路徑上的點都摳出來。
<2>對於這條路徑,從上往下先把lazy標記都放了。
這樣在Rotate時,就排除了lazy標記的干擾。
.
【2】Find:
ll Find(RG ll rt,RG ll K){
if(dt[rt])PushDown(rt);
......
}
【3】CoutAns:
void CoutAns(RG ll rt){
if(dt[rt])PushDown(rt);
......
}
【4】(!) GetMin、GetMax;
在跳轉之前先要PushDown!! (最易錯的地方); 以GetMax為例:
IL ll GetMax(){
RG ll t = root;
while(1){
if(dt[t])PushDown(t);
if(rs[t])t=rs[t]; else break;
} return t;
}
注:、GetPre、GetNext操作由於需要進行Splay,所以不需要。
【5】Insert,Insert_Kth,Insert_Sequence:
以Insert函式為例:
void Insert(RG ll rt,RG ll nw){
if(dt[rt])PushDown(rt);
......
}
//把x節點插入: Insert(root,x);
【6】視題目而定,反正能PushDown一下就PushDown一下。
14.Delete_UpK 操作 (刪除大於或小於K的所有數)
以刪除小於K的所有數為例:
IL void Delete_DnK(){
RG ll Mix = K-1; if(!root)return; //記得減1,確保 ==K 不會被剪出去
fa[++cnt] = ls[cnt] = rs[cnt] = 0; val[cnt] = Mix;
Insert(root,cnt);
pre = GetPre(cnt);
if(!pre){root = 0; return;}
Splay(pre); rs[pre] = 0;
}
具體來說,步驟為:
<1> 向樹中插入一個大小為K-1的節點
<2> 將這個節點Splay到根。
<3>找到這個點的前驅pre,Splay pre到根節點
<4>此時pre右側的所有點即為小於等於K-1的點,直接剪掉右子樹(包含新增點)即可。
15.Same 操作 (當操作涉及重複元素)
重複元素時,我們只需對於每一個點記錄一個sum值,表示這個點的對應點數。
但是注意要標記每個點的對映關係,初始blg[i]=i,用處下文講;
具體來說,
<1>Insert類操作:
void Insert(RG ll rt,RG ll nw){
if(val[rt] == val[nw]){sum[rt]++; blg[nw] = rt;return;}
.....
}
<2>Find類操作:
ll Find(RG ll rt,RG ll K){
if(sz[ls[rt]]+1 <= K && K <= sz[ls[rt]]+sum[rt])return rt;
if(sz[ls[rt]] >= K)return Find(ls[rt],K);
else return Find(rs[rt],K-sum[rt]-sz[ls[rt]]);
}
然後是blg的作用:當你插入一個點後要操作時,請注意對應關係。
下面給一個例子:
Insert(root,x); Splay(x);
這個是會出問題的。因為如果有重複點,那麼你只是把一個點Splay了。
正確做法是這樣:
Insert(root,x); ll t = (blg[x]==x)?(x:blg[x]); Splay(t);
16.極簡程式碼:
IL void Rot(int x){
RG int y = fa[x],z = fa[y],c = Son(x);
if(z)ch[z][Son(y)] = x; fa[x] = z;
ch[y][c] = ch[x][!c]; fa[ch[y][c]] = y;
ch[x][!c] = y; fa[y] = x; PushUp(y);
}
IL void Splay(int x){
//PushDown:
RG int top = 0; S[++top] = x;
for(RG int i = x; fa[i]; i = fa[i])S[++top] = fa[i];
while(top)PushDown(S[top--]);
//Splay:
for(RG int y = fa[x]; fa[x]; Rot(x) , y = fa[x])
if(z) Son(x) ^ Son(y) ? Rot(x) : Rot(y);
PushUp(x);
}
嗯,就這麼多吧。