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的更多相关文章

  1. 在平衡树的海洋中畅游(一)——Treap

    记得有一天翔哥毒奶我们: 当你们已经在平衡树的海洋中畅游时,我还在线段树的泥沼中挣扎. 我觉得其实像我这种对平衡树一无所知的蒟蒻也要开一开数据结构了. 然后花了一天啃了下最简单的平衡树Treap,感觉 ...

  2. 在平衡树的海洋中畅游(三)——Splay

    Preface 由于我怕学习了Splay之后不直接写blog第二天就忘了,所以强行加了一波优先级. 论谁是天下最秀平衡树,我Splay第一个不服.维护平衡只靠旋转. 一言不合转死你 由于平衡树我也介绍 ...

  3. 在平衡树的海洋中畅游(二)——Scapegoat Tree

    在平衡树的广阔天地中,以Treap,Splay等为代表的通过旋转来维护平衡的文艺平衡树占了觉大部分. 然而,今天我们要讲的Scapegoat Tree(替罪羊树)就是一个特立独行的平衡树,它通过暴力重 ...

  4. 简析平衡树(四)——FHQ Treap

    前言 好久没码过平衡树了! 这次在闪指导的指导下学会了\(FHQ\ Treap\),一方面是因为听说它可以可持久化,另一方面则是因为听说它是真的好写. 简介 \(FHQ\ Treap\),又称作非旋\ ...

  5. 【fhq Treap】bzoj1500(听说此题多码上几遍就能不惧任何平衡树题)

    1500: [NOI2005]维修数列 Time Limit: 10 Sec  Memory Limit: 64 MBSubmit: 15112  Solved: 4996[Submit][Statu ...

  6. 【BZOJ5020】【THUWC2017】在美妙的数学王国中畅游(Link-Cut Tree,组合数学)

    [BZOJ5020][THUWC2017]在美妙的数学王国中畅游(Link-Cut Tree,组合数学) 题解 Description 数字和数学规律主宰着这个世界. 机器的运转, 生命的消长, 宇宙 ...

  7. [BZOJ5020][THUWC2017]在美妙的数学王国中畅游(LCT)

    5020: [THUWC 2017]在美妙的数学王国中畅游 Time Limit: 80 Sec  Memory Limit: 512 MBSec  Special JudgeSubmit: 323  ...

  8. 【BZOJ5020】[THUWC 2017]在美妙的数学王国中畅游 泰勒展开+LCT

    [BZOJ5020][THUWC 2017]在美妙的数学王国中畅游 Description 数字和数学规律主宰着这个世界. 机器的运转, 生命的消长, 宇宙的进程, 这些神秘而又美妙的过程无不可以用数 ...

  9. 平衡树合集(Treap,Splay,替罪羊,FHQ Treap)

    今天翻了翻其他大佬的博客,发现自己有些...颓废... 有必要洗心革面,好好学习 序:正常的BST有可能退化,成为链,大大降低效率,所以有很多方法来保持左右size的平衡,本文将简单介绍Treap,S ...

随机推荐

  1. .Net Core(三)MVC Core

    MVC Core的改动感觉挺大的,需要的功能大多从Nuget安装,还内置了IOC,支持SelfHost方式运行等等. 一.项目结构的变化创建的新MVC项目的结构发生了变化,比如:静态文件需要统一放置到 ...

  2. Hook技术--Activity的启动过程的拦截

    1.寻找Hook点的原则 Android中主要是依靠分析系统源码类来做到的,首先我们得找到被Hook的对象,我称之为Hook点:什么样的对象比较好Hook呢?自然是容易找到的对象.什么样的对象容易找到 ...

  3. 【Java入门提高篇】Day34 Java容器类详解(十五)WeakHashMap详解

    源码详解系列均基于JDK8进行解析 说明 在Java容器详解系列文章的最后,介绍一个相对特殊的成员:WeakHashMap,从名字可以看出它是一个 Map.它的使用上跟HashMap并没有什么区别,所 ...

  4. weblogic系列漏洞整理 -- 3. weblogic 后台提权

    目录 三. weblogic 后台提权 0. 思路分析 1. 利用过程 2. 提示和技巧 一.weblogic安装 http://www.cnblogs.com/0x4D75/p/8916428.ht ...

  5. JMeter—前置处理器(九)

    参考<全栈性能测试修炼宝典JMeter实战>第六章 JMeter 元件详解中第四节前置处理器前置处理器用来处理请求前的一些准备工作,比如参数设置.环境变变量设置等 一.BeanShell ...

  6. SQL Server 2005详细安装过程及配置

    说明:个人感觉SQL Server 2005是目前所有的SQL Server版本当中最好用的一个版本了,原因就是这个版本比起其它版本来说要安装简单,操作简便,只可惜这个版本只能在Windows7或者低 ...

  7. EF 排序扩展

    public static class LinqOrderEx { private static IOrderedQueryable<T> OrderingHelper<T>( ...

  8. 机器学习算法_knn(福利)

    这两天翻了一下机器学习实战这本书,算法是不错,只是代码不够友好,作者是个搞算法的,这点从代码上就能看出来.可是有些地方使用numpy搞数组,搞矩阵,总是感觉怪怪的,一个是需要使用三方包numpy,虽然 ...

  9. VMware安装CentOS6

    1. 搭建虚拟化环境常见故障讲解 2. 安装CentOS Linux系统 ……………… PS:运维老鸟教你安装centos6.5如何选择安装包 3. 远程连接LInux ip配置 注意:不用做任何修改 ...

  10. 【Java多线程】AtomicLong和LongAdder

    AtomicLong简要介绍 AtomicLong是作用是对长整形进行原子操作,显而易见,在java1.8中新加入了一个新的原子类LongAdder,该类也可以保证Long类型操作的原子性,相对于At ...