在平衡樹的海洋中暢遊(四)——FHQ Treap
Preface
關於那些比較基礎的平衡樹我想我之前已經介紹的已經挺多了。
但是像Treap,Splay這樣的旋轉平衡樹碼亮太大,而像替罪羊樹這樣的重量平衡樹卻沒有什麼實際意義。
然而類似於SBT,AVL,RBT這些高階的亂搞平衡樹無論時思想還是碼量都讓人難以接受。
而且在許多複雜的問題中需要維護區間,但是Splay的維護區間對於我這個蒟蒻來說實在是學不會。
許多的原因綜合起來,在加上CJJ dalao的偶然安利,我便結識了神奇的FHQ Treap,一眼本命平衡樹的感覺。
所以NOIP結束以後立馬學了一發,不禁為它的神奇思想已經簡單程式碼深深折服。
所以淺淺總結一下,希望有益於人吧。
一些說在前面的話
雖然FHQ Treap和Treap除了名字比較像並沒有什麼特別大的聯絡,但是它們的那種基於隨機值來保證樹的高度的方法還是需要理解的。
因此建議大家先學習下Treap並瞭解一下BST的相關性質再來食用本文。
簡介FHQ Treap
首先為這麼這個東西叫做FHQ Treap,當然是根據OI界的慣例,是由fhq神犇提出的Treap
一般的Treap通過旋轉來維護自身平衡,但是FHQ Treap就十分特立獨行,它提出了一種合併與分裂以維護複雜度的神奇操作。而這種性質也使得它十分靈活
一般平衡樹可以搞的FHQ Treap都可以做,一般線段樹可以搞的FHQ Treap還可以搞,一般普通的平衡樹不可以搞的FHQ Treap也可以搞。
就比如區間問題,除了Splay和FHQ Treap其它的平衡樹都不好做。
而且也有dalao提出了用FHQ Treap維護LCT的方法,雖然比Splay多一個\(\log\),但是真的好寫啊!
而且據dalao說這是唯一可以持久化的平衡樹,讓我真的是跪了。
反正對於我這種一轉就頭暈的人來說還是FHQ Treap合適。寫其它平衡樹都是板子題調幾個小時,而寫FHQ Treap樣例都不測就過了
兩個絕對核心操作
可以說FHQ Treap所有的操作和變化都凝聚於這兩個操作,因此一定要好好理解。
merge
這個操作類似於左偏樹的合併其實程式碼也挺像的,我們合併兩棵Treap,但必須滿足一棵樹中的所有點的權值均小於另一棵樹。
那麼這樣合併的時候權值就沒有什麼根據了,那麼平衡就難以保證。
沒事,注意到FHQ Treap和普通Treap一樣還有保證樹高隨機權值。在merge時我們只需要判斷保持的堆的性質即可(本人預設大根堆)
如果權值較小的隨機權值來的大那麼就把小的右子樹和大的合併(BST的性質)。相反類似。這個直接結合程式碼理解一下即可。
inline void merge(int &now,int x,int y) //將x(權值均小於y)和y合併
{
if (!x||!y) return (void)(now=x+y); if (node[x].dat>node[y].dat)
now=x,merge(rc(now),rc(x),y); else now=y,merge(lc(now),x,lc(y)); pushup(now);
}
split
這是在許多資料結構中都很少出現的分裂操作,其目的是根據某種規則將原來的Treap分成兩棵。
而且分裂的時候也很靈活。可以根據權值來分也可以根據排名來分(視具體情況使用)
這個類似於merge的逆過程,不過要注意分裂的時候要根據選擇的標準決定分裂的方向。
這裡直接給出程式碼(話說真是精短簡潔)。
按權值的劃分法:
inline void split(int now,int &x,int &y,int val) //分裂now,將所有權值小於等於val的分入x,大於val的分入y
{
if (!now) return (void)(x=y=0); if (node[now].val<=val)
x=now,split(rc(now),rc(x),y,val); else y=now,split(lc(now),x,lc(y),val); pushup(now);
}
按排名的劃分法:
inline void split(int now,int &x,int &y,int rk) //將權值前rk小的分入x,其餘的分入y
{
if (!now) return (void)(x=y=0); if (node[lc(now)].size<rk)
x=now,split(rc(now),rc(x),y,rk-node[lc(now)].size-1);
else y=now,split(lc(now),x,lc(y),rk); pushup(now);
}
結合這兩個神奇的操作我們就可以實現神奇的平衡樹操作以及區間操作了。
FHQ Treap的平衡樹板子
還是看Luogu的板子題吧:P3369 【模板】普通平衡樹,這裡可以具體理解下一些基礎的操作。
insert
注意到FHQ Treap只有在一棵樹的權值小於另一棵樹時才能合併。所以我們考慮先給要插入的樹找到它該去的位置,然後在合併上去。看程式碼吧(真的超好理解的)
inline void insert(int val)
{
int x=0,y=0,z=create(val); split(rt,x,y,val); //這樣x的權值全部小於等於要插入的權值,因此可以合併
merge(x,x,z); merge(rt,x,y); //全部並回去,注意FHQ Treap再分裂原樹時候千萬不要忘記合併回去
}
remove
有了插入操作的經驗我們也很容易得到刪除的方法。但由於在FHQ Treap中重複權值的節點會被重複建立,因此我們找出待刪除節點的值是要合併它的左右子樹,以達到刪除的目的。
inline void remove(int val)
{
int x=0,y=0,z=0; split(rt,x,y,val); split(x,x,z,val-1); //分完了z裡都是權值為val的點了
merge(z,lc(z),rc(z)); merge(x,x,z); merge(rt,x,y); //按順序合併回去
}
get_rk
得到一個數的排名,這個更加簡單。我們把權值小於它的全部分出來然後查詢\(size+1\)即可(注意在本題實現中因為加入了兩個極小極大的虛擬節點,因此沒有加一。
inline void get_rk(int val)
{
int x=0,y=0; split(rt,x,y,val-1);
F.write(node[x].size); merge(rt,x,y);
}
find
然後考慮到查詢排名為多少的數,這個貌似不能像直接分裂權值(因為在這個板子裡沒有寫關於按排名分裂的函式),因此我們像一般的BST一樣直接查詢即可
inline void find(int now,int rk) //查詢排名為rk的數
{
while (node[lc(now)].size+1!=rk)
{
if (node[lc(now)].size+1>rk) now=lc(now);
else rk-=node[lc(now)].size+1,now=rc(now);
}
F.write(node[now].val);
}
不過直接分裂排名的做法當然也是可以的啦,由於我們下面還要多次利用這個函式,因此就用這個了。
get_val
這個直接呼叫find
即可
inline void get_val(int rk)
{
find(rt,rk);
}
get_pre
找前驅的話我們先按權值把樹分裂成兩棵,然後直接在前面那棵樹裡呼叫find
即可
inline void get_pre(int val)
{
int x=0,y=0; split(rt,x,y,val-1);
find(x,node[x].size); merge(rt,x,y);
}
get_nxt
這個和上面一樣,不過注意是在大的一顆裡找排名為\(1\)的。
inline void get_nxt(int val)
{
int x=0,y=0; split(rt,x,y,val);
find(y,1); merge(rt,x,y);
}
最後整個題的CODE上一下吧
#include<cstdio>
#include<cctype>
#define RI register int
using namespace std;
const int N=100005,INF=1e9;
int t,opt,x;
class FileInputOutput
{
private:
#define S 1<<21
#define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,S,stdin),A==B)?EOF:*A++)
#define pc(ch) (Ftop<S?Fout[Ftop++]=ch:(fwrite(Fout,1,S,stdout),Fout[(Ftop=0)++]=ch))
char Fin[S],Fout[S],*A,*B; int Ftop,pt[15];
public:
FileInputOutput() { A=B=Fin; Ftop=0; }
inline void read(int &x)
{
x=0; char ch; int flag=1; while (!isdigit(ch=tc())) flag=ch^'-'?1:-1;
while (x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc())); x*=flag;
}
inline void write(int x)
{
if (!x) return (void)(pc('0'),pc('\n')); if (x<0) pc('-'),x=-x; RI ptop=0;
while (x) pt[++ptop]=x%10,x/=10; while (ptop) pc(pt[ptop--]+48); pc('\n');
}
inline void Fend(void)
{
fwrite(Fout,1,Ftop,stdout);
}
#undef S
#undef tc
#undef pc
}F;
class FHQ_Treap
{
private:
struct treap
{
int ch[2],size,val,dat;
treap(int Lc=0,int Rc=0,int Size=0,int Val=0,int Dat=0)
{
ch[0]=Lc; ch[1]=Rc; size=Size; val=Val; dat=Dat;
}
}node[N]; int tot,rt,seed;
#define lc(x) node[x].ch[0]
#define rc(x) node[x].ch[1]
inline int rand(void)
{
return seed=(int)seed*482711LL%2147483647;
}
inline int create(int val)
{
node[++tot]=treap(0,0,1,val,rand()); return tot;
}
inline void pushup(int now)
{
node[now].size=node[lc(now)].size+node[rc(now)].size+1;
}
inline void merge(int &now,int x,int y)
{
if (!x||!y) return (void)(now=x+y); if (node[x].dat>node[y].dat)
now=x,merge(rc(now),rc(x),y); else now=y,merge(lc(now),x,lc(y)); pushup(now);
}
inline void split(int now,int &x,int &y,int val)
{
if (!now) return (void)(x=y=0); if (node[now].val<=val)
x=now,split(rc(now),rc(x),y,val); else y=now,split(lc(now),x,lc(y),val); pushup(now);
}
inline void find(int now,int rk)
{
while (node[lc(now)].size+1!=rk)
{
if (node[lc(now)].size+1>rk) now=lc(now);
else rk-=node[lc(now)].size+1,now=rc(now);
}
F.write(node[now].val);
}
public:
FHQ_Treap() { seed=233; }
inline void init(void)
{
rt=create(-INF); rc(rt)=create(INF); pushup(rt);
}
inline void insert(int val)
{
int x=0,y=0,z=create(val);
split(rt,x,y,val);
merge(x,x,z);
merge(rt,x,y);
}
inline void remove(int val)
{
int x=0,y=0,z=0; split(rt,x,y,val); split(x,x,z,val-1);
merge(z,lc(z),rc(z)); merge(x,x,z); merge(rt,x,y);
}
inline void get_rk(int val)
{
int x=0,y=0; split(rt,x,y,val-1);
F.write(node[x].size); merge(rt,x,y);
}
inline void get_val(int rk)
{
find(rt,rk);
}
inline void get_pre(int val)
{
int x=0,y=0; split(rt,x,y,val-1);
find(x,node[x].size); merge(rt,x,y);
}
inline void get_nxt(int val)
{
int x=0,y=0; split(rt,x,y,val);
find(y,1); merge(rt,x,y);
}
#undef lc
#undef rc
}T;
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
for (T.init(),F.read(t);t;--t)
{
F.read(opt); F.read(x); switch (opt)
{
case 1:
T.insert(x); break;
case 2:
T.remove(x); break;
case 3:
T.get_rk(x); break;
case 4:
T.get_val(x+1); break;
case 5:
T.get_pre(x); break;
case 6:
T.get_nxt(x); break;
}
}
return F.Fend(),0;
}
FHQ Treap的維護區間板子
還是看Luogu板子題:Luogu P3391 【模板】文藝平衡樹(Splay)
首先我們注意到這時FHQ Treap每一個節點的權值其實應該就是這個數的值了。
根據BST的性質我們只需要最後對樹中序遍歷一遍就可以得出原來的序列了。
那麼考慮區間翻轉,這個顯然打一個標記就可以解決。在操作的適時下傳即可。
因此我們只需要在每次翻轉\((l,r)\)時按排名分裂出\((1,l-1),(l,r),(r+1,n)\),然後給\((l,r)\)打上標記然後再合併起來就好了。
感覺看上去就是非常的直觀,至少對於我來說Splay的鬼畜旋轉不是很好理解。
直接上CODE了
#include<cstdio>
#include<cctype>
#define RI register int
using namespace std;
const int N=100005;
int n,m,l,r;
class FileInputOutput
{
private:
#define S 1<<21
#define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,S,stdin),A==B)?EOF:*A++)
#define pc(ch) (Ftop<S?Fout[Ftop++]=ch:(fwrite(Fout,1,S,stdout),Fout[(Ftop=0)++]=ch))
char Fin[S],Fout[S],*A,*B; int Ftop,pt[15];
public:
FileInputOutput() { A=B=Fin; Ftop=0; }
inline void read(int &x)
{
x=0; char ch; while (!isdigit(ch=tc()));
while (x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));
}
inline void write(int x)
{
if (!x) return (void)(pc('0'),pc(' ')); RI ptop=0;
while (x) pt[++ptop]=x%10,x/=10; while (ptop) pc(pt[ptop--]+48); pc(' ');
}
inline void Fend(void)
{
fwrite(Fout,1,Ftop,stdout);
}
#undef S
#undef tc
#undef pc
}F;
class FHQ_Treap
{
private:
struct treap
{
int ch[2],tag,size,val,dat;
treap(int Lc=0,int Rc=0,int Tag=0,int Size=0,int Val=0,int Dat=0)
{
ch[0]=Lc; ch[1]=Rc; tag=Tag; size=Size; val=Val; dat=Dat;
}
}node[N]; int tot,seed;
#define lc(x) node[x].ch[0]
#define rc(x) node[x].ch[1]
inline int rand(void)
{
return seed=(int)seed*482711LL%2147483647;
}
inline int create(int val)
{
node[++tot]=treap(0,0,0,0,val,rand()); return tot;
}
inline void swap(int &a,int &b)
{
int t=a; a=b; b=t;
}
inline void pushup(int now)
{
node[now].size=node[lc(now)].size+node[rc(now)].size+1;
}
inline void pushdown(int now)
{
if (!node[now].tag) return;
if (lc(now)) node[lc(now)].tag^=1;
if (rc(now)) node[rc(now)].tag^=1;
node[now].tag=0; swap(lc(now),rc(now));
}
inline void merge(int &now,int x,int y)
{
if (!x||!y) return (void)(now=x+y); if (node[x].dat>node[y].dat)
pushdown(x),now=x,merge(rc(now),rc(x),y),pushup(x);
else pushdown(y),now=y,merge(lc(now),x,lc(y)),pushup(y);
}
inline void split(int now,int &x,int &y,int rk)
{
if (!now) return (void)(x=y=0); pushdown(now); if (node[lc(now)].size<rk)
x=now,split(rc(now),rc(x),y,rk-node[lc(now)].size-1);
else y=now,split(lc(now),x,lc(y),rk); pushup(now);
}
public:
FHQ_Treap() { seed=233; }
int rt;
inline void insert(int val)
{
int x=0,y=0,z=create(val); split(rt,x,y,val);
merge(x,x,z); merge(rt,x,y);
}
inline void reverse(int l,int r)
{
int x=0,y=0,z=0; split(rt,x,y,l-1); split(y,y,z,r-l+1);
node[y].tag^=1; merge(y,y,z); merge(rt,x,y);
}
inline void trasvel(int now)
{
if (!now) return; pushdown(now);
trasvel(lc(now)); F.write(node[now].val); trasvel(rc(now));
}
#undef lc
#undef rc
}T;
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
RI i; for (F.read(n),i=1;i<=n;++i) T.insert(i);
for (F.read(m),i=1;i<=m;++i) F.read(l),F.read(r),T.reverse(l,r);
return T.trasvel(T.rt),F.Fend(),0;
}
Postscript
FHQ Treap相比於其他平衡樹無論是思想還是程式碼複雜度都是更好理解(記憶)的。
同時用它解決區間問題的話。。。真的就是天選之樹吧,在一些比較複雜的問題中就可以看出它的妙處。
希望大家都可以理解掌握並愛上FHQ Treap。