简析平衡树(三)——浅谈Splay
前言
原本以為\(Treap\)已經很難了,學習了\(Splay\),我才知道,沒有最難,只有更難。(強烈建議先去學一學\(Treap\)再來看這篇博客)
簡介
\(Splay\)是平衡樹中的一種,除了平衡樹所共有的作用之外,它還可以維護區間翻轉,這也是它能成為\(LCT\)輔助樹的原因(不過\(LCT\)并不是這篇博客所探討的內容)。
因此,這篇博客將分為三個部分,第一個部分講講\(Splay\)與其他平衡樹的不同之處,另外兩個部分則分別借助兩道模板題,來講講\(Splay\)兩方面的作用。
\(Splay\)與其他平衡樹的不同之處
\(splay\)這個單詞在百度翻譯上的解釋是張開。而在這里,則是指伸展(其實伸展與張開差不多)。
\(Splay\)最重要的操作自然就是伸展操作了。
而伸展操作,則是建立于類似于\(Treap\)的\(Rotate\)旋轉操作上的。
\(Rotate\)操作在簡析平衡樹(二)中已經介紹過了,這里就直接貼代碼了:
inline void Rotate(int x,int &k)//好吧,兩個Rotate操作還是有點區別的,因為Splay還要記錄每個節點的父親,這里的Rotate是要將x節點向上旋轉一位,而k則是要旋轉到的目標位置
{int fa=node[x].Father,grandpa=node[fa].Father,d=Which(x);//我們用fa記錄當前節點的父親,grandpa記錄當前節點的祖父,d記錄當前節點是父親的哪一個兒子if(fa^k) node[grandpa].Son[Which(fa)]=x;//如果當前節點的父親不是目標位置,那么更新當前節點祖父的兒子為當前節點else k=x;//否則說明已到達目標位置node[x].Father=grandpa,node[fa].Son[d]=node[x].Son[d^1],node[node[x].Son[d^1]].Father=fa,node[x].Son[d^1]=fa,node[fa].Father=x,PushUp(fa),PushUp(x);//這個與Treap中的操作差不多,先將當前節點的父親更新為當前節點的祖父,并將當前節點原先父親這一位的兒子更新為當前節點與這個方向相反的兒子,并將當前節點原先的父親更新為當前節點的兒子,最后記得更新節點信息
} 而\(Splay\)的主要功能,就是通過不斷地\(Rotate\),使某一節點旋轉至目標位置。
我們可以對\(Splay\)每次的旋轉操作進行分類討論:
第一種情況,\(fa\)即為目標位置,此時只要上旋\(x\)即可到達目標位置。
第二種情況,\(x\),\(fa\),\(grandpa\)在同一直線上,且\(fa\)不為目標位置。此時,我們需要將\(fa\)上旋,同時帶動\(x\)上旋,然后再上旋一遍x。
第三種情況,\(x\),\(fa\),\(grandpa\)不在同一直線上,且\(fa\)不為目標位置。此時,我們直接將\(x\)上旋,將\(x\)的父親更新為它的祖父\(grandpa\),然后再上旋一遍\(x\)即可。
具體代碼實現如下:
inline void Splay(int x,int &k)//將x一直旋轉到目標位置k
{for(int fa=node[x].Father;x^k;fa=node[x].Father)//重復循環,直至x為k{if(fa^k) Rotate(Which(fa)^Which(x)?x:fa,k);//如果fa不是目標位置,若x,fa,grandpa在同一直線上,我們需要將fa上旋,否則,直接將x上旋Rotate(x,k);//歸納可以得出,不管什么情況下都要將x上旋}
} 講完了\(Splay\)與其他平衡樹的不同之處,后面的就很簡單了。
模板1:【洛谷3369】【模板】普通平衡樹
這應該是我第三次用到這個模板了,每學一種平衡樹,都要過一次這個模板。
借助這個模板,我們主要研究的是\(Splay\)作為平衡樹的必有功能:\(BST\)的功能。
不得不說,在插入和查詢這兩方面,\(Splay\)和\(BST\)還是十分像的,唯一區別是,每次插入/查詢結束后,都要用\(Splay\)操作將當前操作的點旋轉到根。
為什么要這么做呢?
因為這樣就使得\(Splay\)的形狀在不停地變化,即使在某一時刻成了一條鏈,下一刻一定又會變成其他形狀,就很難被卡掉了。
插入/查詢代碼如下:
inline void Insert(int &x,int val,int fa)//插入一個數
{if(!x)//如果當前節點為空 {node[x=++tot].Val=val,node[x].Cnt=node[x].Size=1,node[x].Father=fa,node[x].Son[0]=node[x].Son[1]=0,Splay(x);//將元素插入這個節點,并將其旋轉到根return;}if(node[x].Val==val) ++node[x].Cnt,PushUp(x),PushUp(node[x].Father),Splay(x);//如果當前元素等于插入元素,就將當前元素的出現次數加1,并將當前節點旋轉到根else if(node[x].Val>val) Insert(node[x].Son[0],val,x);//否則,如果當前元素大于插入元素,就將插入元素插入至當前元素的左子樹(遵循BST的基本性質嘛)else Insert(node[x].Son[1],val,x);//否則,插入當前元素的右子樹
} inline int get_rank(int val)//詢問值為val的數的排名
{int x=rt,rk=0;while(x)//只要當前節點不是空節點{if(node[x].Val==val) {rk+=node[node[x].Son[0]].Size,Splay(x);return rk+1;}//若當前元素與查詢元素值相等,那么將它旋轉到根節點,并返回答案else if(node[x].Val>val) x=node[x].Son[0];//否則,如果當前元素大于插入元素,就訪問當前元素的左子樹else rk+=node[node[x].Son[0]].Size+node[x].Cnt,x=node[x].Son[1];//否則,更新排名,并訪問右子樹}
} inline int get_val(int rk)//詢問排名為rk的數的值
{int x=rt;while(x){if(node[node[x].Son[0]].Size>=rk) x=node[x].Son[0];//如果當前節點左子樹大小大于等于rk,說明答案在左子樹else if(node[node[x].Son[0]].Size+node[x].Cnt>=rk) {Splay(x);return node[x].Val;}//否則,如果當前節點左子樹大小加上當前節點存在個數大于等于rk,說明當前元素就是答案,那么將它旋轉到根節點,并返回答案else rk-=node[node[x].Son[0]].Size+node[x].Cnt,x=node[x].Son[1];//否則,更新排名,并訪問右子樹}
} 插入/查詢操作還挺基礎的,而刪除操作就略顯麻煩了。
首先,需要查詢一下要刪除元素的排名,目的是將這個元素旋到根。
然后分類討論即可。
具體代碼如下:
inline void Delete(int x)//刪除一個值為x的元素
{get_rank(x);//先通過查詢,將它旋轉到根if(--node[rt].Cnt) return;//如果當前節點存在個數大于1,則將其存在個數減少1即可if(!node[rt].Son[0]&&!node[rt].Son[1]) rt=0;//如果當前元素沒有兒子,說明這棵Splay只有一個節點,將rt修改為0即可else if(!node[rt].Son[1]) rt=node[rt].Son[0],node[rt].Father=0,PushUp(rt);//如果當前元素沒有右兒子,那么直接將根修改為當前元素的左兒子即可else if(!node[rt].Son[0]) rt=node[rt].Son[1],node[rt].Father=0,PushUp(rt);//類似的,如果當前元素沒有左兒子,那么直接將根修改為當前元素的右兒子即可else//否則,說明當前元素既有左兒子又有右兒子,那么就把當前元素的前驅作為新的根 {int pre=node[rt].Son[0],k=rt;//用k來存儲現在的根,用pre來求當前元素的前驅while(node[pre].Son[1]) pre=node[pre].Son[1];//由于當前元素是根節點,所以左子樹中最右邊的一個節點就是當前元素的前驅Splay(pre),node[node[k].Son[1]].Father=rt,node[rt].Son[1]=node[k].Son[1],PushUp(rt);//將這個前驅旋轉至根,將原來的根的右兒子的父親改為現在的根,再將現在的根的右兒子改為原來的根的右兒子,最后更新根節點的信息}
} 最后,照常貼一份代碼:
#include<bits/stdc++.h>
#define max(x,y) ((x)>(y)?(x):(y))
#define min(x,y) ((x)<(y)?(x):(y))
#define LL long long
#define swap(x,y) (x^=y,y^=x,x^=y)
#define tc() (A==B&&(B=(A=ff)+fread(ff,1,100000,stdin),A==B)?EOF:*A++)
#define pc(ch) (pp_<100000?pp[pp_++]=(ch):(fwrite(pp,1,100000,stdout),pp[(pp_=0)++]=(ch)))
#define N 100000
int pp_=0;char ff[100000],*A=ff,*B=ff,pp[100000];
using namespace std;
int n,rt=0,tot=0;
struct splay
{int Son[2],Cnt,Val,Size,Father;
}node[N+5];
inline void read(int &x)
{x=0;int f=1;char ch;while(!isdigit(ch=tc())) f=ch^'-'?1:-1;while(x=(x<<3)+(x<<1)+ch-'0',isdigit(ch=tc()));x*=f;
}
inline void write(int x)
{if(x<0) pc('-'),x=-x;if(x>9) write(x/10);pc(x%10+'0');
}
inline void PushUp(int x)
{node[x].Size=node[node[x].Son[0]].Size+node[node[x].Son[1]].Size+node[x].Cnt;
}
inline int Which(int x)
{return node[node[x].Father].Son[1]==x;
}
inline void Rotate(int x)
{int fa=node[x].Father,grandpa=node[fa].Father,d=Which(x),dd=Which(fa);node[fa].Son[d]=node[x].Son[d^1],node,node[node[fa].Son[d]].Father=fa,node[x].Son[d^1]=fa,node[fa].Father=x,node[x].Father=grandpa;if(grandpa) node[grandpa].Son[dd]=x;PushUp(x),PushUp(fa);
}
inline void Splay(int x)
{for(int fa=node[x].Father;fa=node[x].Father;Rotate(x))if(node[fa].Father) Rotate(Which(fa)==Which(x)?fa:x);rt=x;
}
inline void Insert(int &x,int val,int fa)
{if(!x) {node[x=++tot].Val=val,node[x].Cnt=node[x].Size=1,node[x].Father=fa,node[x].Son[0]=node[x].Son[1]=0,Splay(x);return;}if(node[x].Val==val) ++node[x].Cnt,PushUp(x),PushUp(node[x].Father),Splay(x);else if(node[x].Val>val) Insert(node[x].Son[0],val,x);else Insert(node[x].Son[1],val,x);
}
inline int get_rank(int val)
{int x=rt,rk=0;while(x){if(node[x].Val==val) {rk+=node[node[x].Son[0]].Size,Splay(x);return rk+1;}else if(node[x].Val>val) x=node[x].Son[0];else rk+=node[node[x].Son[0]].Size+node[x].Cnt,x=node[x].Son[1];}
}
inline int get_val(int rk)
{int x=rt;while(x){if(node[node[x].Son[0]].Size>=rk) x=node[x].Son[0];else if(node[node[x].Son[0]].Size+node[x].Cnt>=rk) {Splay(x);return node[x].Val;}else rk-=node[node[x].Son[0]].Size+node[x].Cnt,x=node[x].Son[1];}
}
inline int get_pre(int val)
{int x=rt,pre;while(x){if(node[x].Val<val) pre=node[x].Val,x=node[x].Son[1];else x=node[x].Son[0];}return pre;
}
inline int get_nxt(int val)
{int x=rt,nxt;while(x){if(node[x].Val>val) nxt=node[x].Val,x=node[x].Son[0];else x=node[x].Son[1];}return nxt;
}
inline void Delete(int x)
{get_rank(x);if(--node[rt].Cnt) return;if(!node[rt].Son[0]&&!node[rt].Son[1]) rt=0;else if(!node[rt].Son[1]) rt=node[rt].Son[0],node[rt].Father=0,PushUp(rt);else if(!node[rt].Son[0]) rt=node[rt].Son[1],node[rt].Father=0,PushUp(rt);else {int pre=node[rt].Son[0],k=rt;while(node[pre].Son[1]) pre=node[pre].Son[1];Splay(pre),node[node[k].Son[1]].Father=rt,node[rt].Son[1]=node[k].Son[1],PushUp(rt);}
}
int main()
{register int i;for(read(n),i=1;i<=n;++i){int op,x;read(op),read(x);switch(op){case 1:Insert(rt,x,0);break;case 2:Delete(x);break;case 3:write(get_rank(x)),pc('\n');break;case 4:write(get_val(x)),pc('\n');break;case 5:write(get_pre(x)),pc('\n');break;case 6:write(get_nxt(x)),pc('\n');break;}}return fwrite(pp,1,pp_,stdout),0;
} 模板2:【洛谷3391】【模板】文藝平衡樹
這個模板主要體現了\(Splay\)的\(維護區間翻轉\)的功能,而這也是\(Treap\)和替罪羊樹做不到的。
初始化時,我們用中序遍歷到的順序為\(2\sim n+1\)的節點分別表示序列中第\(1\sim n\)個元素,并同時在一頭一尾加入兩個節點,防止越界。
然后,如果要翻轉某個區間\([l,r]\),我們就找到這個區間的前后兩個元素\(l-1\)和\(r+1\),由于中序遍歷到的順序為\(i+1\)的節點表示序列中第\(i\)個元素,也就是說我們要找到中序遍歷到的順序為\(l\)和\(r+2\)的節點。
找到這兩個節點后,我們將順序為\(l\)的節點旋轉至根,并將順序為\(r+2\)的節點旋轉至根的右兒子。
我們可以發現,由于\(BST\)的性質,根的右兒子的左子樹中每個節點被中序遍歷到的順序一定大于根節點且小于根節點的右子樹,也就是說大于\(l\)且小于\(r+2\),在\([l+1,r+1]\)的范圍內,而它們所表示的序列中的元素就是第\(l\sim r\)個元素,因此,我們只需要給根的右兒子的左兒子打一個標記表示它被翻轉了即可。
注意,在每次操作到一個節點時,必須先下推翻轉標記,才能繼續操作。
而下推操作很簡單,只要交換左右兒子即可。
代碼如下:
#include<bits/stdc++.h>
#define max(x,y) ((x)>(y)?(x):(y))
#define min(x,y) ((x)<(y)?(x):(y))
#define LL long long
#define swap(x,y) (x^=y,y^=x,x^=y)
#define tc() (A==B&&(B=(A=ff)+fread(ff,1,100000,stdin),A==B)?EOF:*A++)
#define pc(ch) (pp_<100000?pp[pp_++]=(ch):(fwrite(pp,1,100000,stdout),pp[(pp_=0)++]=(ch)))
#define N 100000
int pp_=0;char ff[100000],*A=ff,*B=ff,pp[100000];
using namespace std;
int n,m,rt;
struct splay
{int Son[2],Size,Father,flag;
}node[N+5];
inline void read(int &x)
{x=0;int f=1;char ch;while(!isdigit(ch=tc())) f=ch^'-'?1:-1;while(x=(x<<3)+(x<<1)+ch-'0',isdigit(ch=tc()));x*=f;
}
inline void write(int x)
{if(x<0) pc('-'),x=-x;if(x>9) write(x/10);pc(x%10+'0');
}
inline void PushUp(int x)
{node[x].Size=node[node[x].Son[0]].Size+node[node[x].Son[1]].Size+1;
}
inline void PushDown(int x)//下推翻轉標記
{if(node[x].flag) swap(node[x].Son[0],node[x].Son[1]),node[node[x].Son[0]].flag^=1,node[node[x].Son[1]].flag^=1,node[x].flag=0;//如果當前節點有翻轉標記,那么交換其左右兒子,更新其左右兒子的翻轉標記,然后清空當前節點的翻轉標記
}
inline void Build(int l,int r,int &x)//一個建樹的過程,是不是很像線段樹?
{node[x=l+r>>1].Size=1;//先記錄當前節點的編號和子樹大小if(l<x) Build(l,x-1,node[x].Son[0]),node[node[x].Son[0]].Father=x;//如果當前節點左邊還有元素,那么就繼續對其左兒子建樹if(x<r) Build(x+1,r,node[x].Son[1]),node[node[x].Son[1]].Father=x;//如果當前節點右邊還有元素,那么就繼續對其右兒子建樹PushUp(x);//更新節點信息
}
inline int Which(int x)//判斷當前節點是父親的哪一個兒子
{return node[node[x].Father].Son[1]==x;
}
inline void Rotate(int x,int &k)//旋轉操作
{int fa=node[x].Father,grandpa=node[fa].Father,d=Which(x);if(fa^k) node[grandpa].Son[Which(fa)]=x;else k=x;node[x].Father=grandpa,node[fa].Son[d]=node[x].Son[d^1],node[node[x].Son[d^1]].Father=fa,node[x].Son[d^1]=fa,node[fa].Father=x,PushUp(fa),PushUp(x);
}
inline void Splay(int x,int &k)//不斷將一個元素旋轉至目標位置
{for(int fa=node[x].Father;x^k;fa=node[x].Father){if(fa^k) Rotate(Which(fa)^Which(x)?x:fa,k);Rotate(x,k);}
}
inline int get_val(int pos)//求出中序遍歷到的順序為pos的節點的值
{int x=rt;while(x){PushDown(x);//先下推標記,然后再操作if(node[node[x].Son[0]].Size==pos) return x;//如果當前節點中序遍歷到的順序等于pos,就返回當前節點的值if(node[node[x].Son[0]].Size>pos) x=node[x].Son[0];//如果當前節點左子樹被中序遍歷到的順序大于pos,就訪問當前節點的左子樹else pos-=node[node[x].Son[0]].Size+1,x=node[x].Son[1];//否則,更新pos,訪問右子樹}
}
inline void rever(int x,int y)//翻轉一個區間,具體操作見上面的解析
{int l=get_val(x-1),r=get_val(y+1);Splay(l,rt),Splay(r,node[rt].Son[1]),node[node[node[rt].Son[1]].Son[0]].flag^=1;
}
int main()
{register int i;int x,y;for(read(n),Build(1,n+2,rt),read(m);m;--m) read(x),read(y),rever(x,y);for(i=1;i<=n;++i) write(get_val(i)-1),pc(' ');//由于我們用中序遍歷到的順序為2~n+1的節點來表示序列中第1~n個元素,所以輸出時將答案減1return fwrite(pp,1,pp_,stdout),0;
}
轉載于:https://www.cnblogs.com/chenxiaoran666/p/Splay.html
總結
以上是生活随笔為你收集整理的简析平衡树(三)——浅谈Splay的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 小初号,小一号,四号,小四号,五号分别对
- 下一篇: os