FHQ Treap
FHQ Treap
简介
FHQ Treap是一棵利用分裂和合并实现插入,删除,查询操作的平衡树。
先提一嘴:所有平衡树的基础操作都能通过合并与分裂实现,所以先实现这两个看似与平衡树无关的操作是必要的,它们的重要性就像其它平衡树的 rotate
。
分裂
我们需要将一棵 BST 分成两棵,一棵中的每个节点的权值小于等于数字 ,另一棵中每个节点的权值都要比数字 大。
为了方便,我们不妨把节点权值小的那棵树叫做小树,另一棵叫做大树。
怎么实现呢?
我们想,当我们遇到一个节点时,它的权值可能有两种情况:
- 小于等于 。
- 大于 。
如果我们遇到的节点小于等于 ,根据 BST 性质,它的左子树中的所有节点,权值一定都小于等于 。
所以呢?
统统丢进我们的小树里就好啦~
那如果大于呢?
把它的右子树统统丢进我们的大树里就好啦~
然后呢?
递归一直丢,丢到叶子节点就好啦~
怎么丢?
我也不知道啦~
咳咳,我们可以先整理一下刚才的成果,无非就下面一句话:
小进小,递归右;大进大,递归左。
意思就是:
如果当前节点的权值是小过 的,进小树,然后递归右子树,反之同理。
那么:
如果当前节点权值小过 ,我们的小树当前的左子树就是当前节点的左子树,差右子树没接,同时我们往右递归。
所以接下来,我们再要接小树时,应该接到当前节点的右儿子位置上。
同理,如果当前节点权值大过 ,我们接下来如果要接大树时,就要接在当前节点的左儿子位置上?
知道这个有什么用呢?
我们可以粗略考虑像下面这样完成分裂。
( now
表示当前节点, x
表示接小树该接的地方, y
表示接大树该接的地方, ls
指代左儿子, rs
指代右儿子, val[]
为记录每个节点权值的数组 )
void split(int now,int k,int &x,int &y)
{
if (val[now]<=k) x=now,split(rs[now],k,rs[now],y);
else y=now,split(ls[now],k,x,ls[now]);
}
这里的 x=now
, y=now
,就是前面所说的:
小进小;大进大。
后面的就是:
递归左;递归右。
不过我们为了完成接下来的操作,我们还要记录每个节点的子树,这需要我们在分裂合并时更新。
inline void update(int x) { siz[x]=siz[ls[x]]+siz[rs[x]]+1; }
我们还要为递归加上边界,所以最后的分裂代码是这样的:
void split(int now,int k,int &x,int &y)
{
if (!now) x=y=0;
else
{
if (val[now]<=k) x=now,split(rs[now],k,rs[now],y);
else y=now,split(ls[now],k,x,ls[now]);
update(now);
}
}
合并
顾名思义,合并就是将两棵 BST 合并为 一棵 BST 。特别的是,因为我们在 split
操作中严格规定了哪棵树中的节点更大,所以在合并时我们也可以利用这一点,传递变量时保护好这个性质,这样我们合并时就只用比较 rd
值的大小了。
rd
是什么?
因为 FHQ Treap 是一棵 Treap ,所以它维持平衡的原理是让每个节点带上一个随机值,在维护权值 BST 性质的同时维护随机值的堆性质,形成一棵笛卡尔树,因为堆键值是随机的,所以期望高度为 。
像是下面这样:
void merge(int x,int y)
{
if (rd[x]<rd[y]) rs[x]=merge(rs[x],y);
else ls[y]=merge(x,ls[y]);
}
上面这段代码已经能够概括合并的思路了,但是有细节上的问题:
- 我们在合并时改变了 BST 的结构,所以需要重新维护
siz
的大小。 - 这样的合并是成功了,但是我们也丢失了新树的根,会为下一次的分裂操作带来麻烦。
- 我们没有设置递归边界。
所以我们对其进行修改:
int merge(int x,int y)
{
if (!x||!y) return x+y;
if (rd[x]<rd[y])
{
rs[x]=merge(rs[x],y);
update(x);
return x;
}
else
{
ls[y]=merge(x,ls[y]);
update(y);
return y;
}
}
其中的 if (!x||!y) return x+y;
表示若二者之一为 ,返回另一者,即递归边界。
第 大数
这个操作其实与后面的 num()
目的相同,即求 BST 中第 大的数,但因为可以用于求前驱后继,所以先单独拎出来写。
kth()
的原理非常简单,这得益于我们之前费力维护的 siz
数组,它将在这个环节大显身手。
如果当前节点的 siz
大过 ,向左子树递归;
如果当前节点的 siz
等于 ,那答案就是当前节点;
否则当前节点的 siz
一定小于 ,将 减去左子树的 siz
,再减去 ( 即当前节点 ),向右子树递归。
代码如下:
inline int kth(int now,int k)
{
while (1)
if (k<=siz[ls[now]]) now=ls[now];
else if (k==siz[ls[now]]+1) return now;
else k-=siz[ls[now]]+1,now=rs[now];
}
插入
终于进入到平衡树所需要完成的操作了,接下来的方便一定会让你感到之前的努力没有白费。
因为我们已经能够将一棵 BST 分裂,所以插入一个点时,直接新建这个点,让原来的树沿着这个点的权值裂开,再将左右子树与这个点合并即可。
我们不妨顺手写一个新建节点的函数,这是我们最后的准备了:
inline int create(int x)
{
siz[++tot]=1;
val[tot]=x;
rd[tot]=rand();
return tot;
}
那么插入操作的代码如下:
inline void ins(int k)
{
split(root,k,x,y);
root=merge(merge(x,create(k)),y);
}
删除
有了分裂操作,删除也是易如反掌。
我们直接将需要删除的节点的两边裂开,把中间的节点 ”取走“ ,再把裂出来的两棵树合并回来就好。
代码如下:
inline void del(int k)
{
split(root,k,x,z);
split(x,k-1,x,y);
y=merge(ls[y],rs[y]);
root=merge(merge(x,y),z);
}
查询排名
因为我们处理了 siz
数组,所以一个数的排名就等于比它小的节点构成的树的大小,我们同样可以用 split()
完成这项工作。
代码如下:
inline void rank(int k)
{
split(root,k-1,x,y);
printf("%d\n",siz[x]+1);
root=merge(x,y);
}
查询第 大数
这不就是我们刚才写的 kth()
嘛,摆上摆上。
代码如下:
inline void num(int k) { printf("%d\n",val[kth(root,k)]); }
前驱
求前驱,相当于求比这个节点权值小的节点构成的树( 小树 )中的权值最大节点,说起来有点绕,上码。
代码如下:
inline void last(int k)
{
split(root,k-1,x,y);
printf("%d\n",val[kth(x,siz[x])]);
root=merge(x,y);
}
后继
与求前驱类似,求后继就是求比这个节点权值大的节点构成的树( 大树 )中的权值最小节点。
代码如下:
inline void next(int k)
{
split(root,k,x,y);
printf("%d\n",val[kth(y,1)]);
root=merge(x,y);
}
Code
#include <stdlib.h>
#include <cstdio>
#include <ctime>
using namespace std;
#define MAXN (int)(1e5+233)
int n,opt,in;
int tot,root,x,y,z;
int ls[MAXN],rs[MAXN],val[MAXN],rd[MAXN],siz[MAXN];
inline int read()
{
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9') {if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9') {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return f*x;
}
inline void update(int x) { siz[x]=siz[ls[x]]+siz[rs[x]]+1; }
inline int create(int x)
{
siz[++tot]=1;
val[tot]=x;
rd[tot]=rand();
return tot;
}
void split(int now,int k,int &x,int &y)
{
if (!now) x=y=0;
else
{
if (val[now]<=k) x=now,split(rs[now],k,rs[now],y);
else y=now,split(ls[now],k,x,ls[now]);
update(now);
}
}
int merge(int x,int y)
{
if (!x||!y) return x+y;
if (rd[x]<rd[y])
{
rs[x]=merge(rs[x],y);
update(x);
return x;
}
else
{
ls[y]=merge(x,ls[y]);
update(y);
return y;
}
}
inline int kth(int now,int k)
{
while (1)
if (k<=siz[ls[now]]) now=ls[now];
else if (k==siz[ls[now]]+1) return now;
else k-=siz[ls[now]]+1,now=rs[now];
}
inline void ins(int k)
{
split(root,k,x,y);
root=merge(merge(x,create(k)),y);
}
inline void del(int k)
{
split(root,k,x,z);
split(x,k-1,x,y);
y=merge(ls[y],rs[y]);
root=merge(merge(x,y),z);
}
inline void rank(int k)
{
split(root,k-1,x,y);
printf("%d\n",siz[x]+1);
root=merge(x,y);
}
inline void num(int k) { printf("%d\n",val[kth(root,k)]); }
inline void last(int k)
{
split(root,k-1,x,y);
printf("%d\n",val[kth(x,siz[x])]);
root=merge(x,y);
}
inline void next(int k)
{
split(root,k,x,y);
printf("%d\n",val[kth(y,1)]);
root=merge(x,y);
}
int main()
{
srand(time(NULL));
n=read();
for (int i=1;i<=n;i++)
{
opt=read(),in=read();
if (opt==1) ins(in);
else if (opt==2) del(in);
else if (opt==3) rank(in);
else if (opt==4) num(in);
else if (opt==5) last(in);
else next(in);
}
return 0;
}
区间翻转
平衡树还能实现一种操作,就是维护一个有序序列,支持对一个区间内的元素进行区间翻转。
因为我们维护的是节点的顺序,而不是节点的大小排序,所以我们在分裂时并不用考虑节点的权值大小,而是考虑节点的排名。
相对应,原来的分裂方式我们称为权值分裂,而现在的分裂方式我们称为排名分裂。
void split(int now,int k,int &x,int &y)
{
if (!now) { x=y=0; return; }
if (tag[now]) pushdown(now);
if (siz[ls[now]]<k) x=now,split(rs[now],k-siz[ls[now]]-1,rs[now],y);
else y=now,split(ls[now],k,x,ls[now]);
update(now);
}
如上,我们在分裂时比较的关键字由节点权值变为节点排名。因为要支持翻转,所以我们要同时维护一个 lazy_tag 。
那么翻转操作也就很简单了。无非就是把要改的那棵子树拉出来,根打上 tag
,再塞回去。( 好恶心口区 )
inline void reverse(int l,int r)
{
split(root,l-1,x,y);
split(y,r-l+1,y,z);
tag[y]^=1;
root=merge(x,merge(y,z));
}
#include <stdlib.h>
#include <cstdio>
#include <ctime>
using namespace std;
#define MAXN (int)(1e5+233)
int n,m,l,r;
int tot,root,x,y,z;
int ls[MAXN],rs[MAXN],val[MAXN],rd[MAXN],siz[MAXN];
bool tag[MAXN];
inline int read()
{
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9') {if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9') {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return f*x;
}
inline void swap(int &x,int &y) { int t=x;x=y,y=t; }
inline void update(int x) { siz[x]=siz[ls[x]]+siz[rs[x]]+1; }
void pushdown(int x)
{
swap(ls[x],rs[x]);
if (ls[x]) tag[ls[x]]^=1;
if (rs[x]) tag[rs[x]]^=1;
tag[x]=0;
}
inline int create(int x)
{
siz[++tot]=1;
val[tot]=x;
rd[tot]=rand();
return tot;
}
void split(int now,int k,int &x,int &y)
{
if (!now) { x=y=0; return; }
if (tag[now]) pushdown(now);
if (siz[ls[now]]<k) x=now,split(rs[now],k-siz[ls[now]]-1,rs[now],y);
else y=now,split(ls[now],k,x,ls[now]);
update(now);
}
int merge(int x,int y)
{
if (!x||!y) return x+y;
if (rd[x]<rd[y])
{
if (tag[x]) pushdown(x);
rs[x]=merge(rs[x],y);
update(x);
return x;
}
else
{
if (tag[y]) pushdown(y);
ls[y]=merge(x,ls[y]);
update(y);
return y;
}
}
inline void ins(int k)
{
split(root,k,x,y);
root=merge(merge(x,create(k)),y);
}
inline void reverse(int l,int r)
{
split(root,l-1,x,y);
split(y,r-l+1,y,z);
tag[y]^=1;
root=merge(x,merge(y,z));
}
void dfs(int now)
{
if (!now) return;
if (tag[now]) pushdown(now);
dfs(ls[now]);
printf("%d ",val[now]);
dfs(rs[now]);
}
int main()
{
srand(time(NULL));
n=read(),m=read();
for (int i=1;i<=n;i++) ins(i);
for (int i=1;i<=m;i++)
{
l=read(),r=read();
reverse(l,r);
}
dfs(root);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效