在平衡树的海洋中畅游(四)——FHQ Treap

2022-12-05,,

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。

在平衡树的海洋中畅游(四)——FHQ Treap的相关教程结束。

《在平衡树的海洋中畅游(四)——FHQ Treap.doc》

下载本文的Word格式文档,以方便收藏与打印。