1. 程式人生 > >Splay 總結及模板

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);
}

嗯,就這麼多吧。

一些練習的題目: