【学习笔记】FHQ-Treap
本文由洛谷博客搬迁而来,实际写作时间为 2022/9/26。
前置知识:二叉搜索树与二叉堆。
1. 简介
Treap,即 Tree+Heap,它的每个结点上存储着一个索引
至于为什么索引是随机的,其实很简单:我们插入的每个数的索引都要满足二叉堆的性质,而用随机数就会出现插入后不知道跑到哪里去了的情况,相当于做到了插入次序随机。
你问我二叉搜索树和二叉堆的性质是啥?自己补前置知识去。
而 Fhq-Treap (防火墙?),就是由范浩强大佬发明的无需旋转操作即可实现的 Treap,也称无旋 Treap,有着代码短、好理解、易于初学者学习等诸多优点。除了 LCT 必须写 Splay 以外(Fhq-Treap 会 TLE),其他平衡树能干的它基本上都能干。
2. 基本操作
Fhq-Treap 的核心操作其实只有两个:分裂与合并。
首先个人习惯写一个结构体来存储每个结点的信息:
struct node{
int ls,rs; //左右儿子结点的编号
int val,key,siz;
}tree[maxn];
然后是一个新建结点的函数,并返回该结点的编号:
mt19937 rnd(233);
inline int newnode(int val){
tree[++cnt_node].val=val;
tree[cnt_node].key=rnd();
tree[cnt_node].siz=1;
return cnt_node;
}
基础的信息上传合并:
void pushup(int now){
tree[now].siz=tree[ls(now)].siz+tree[rs(now)].siz+1;
}
同时为了写起来方便:
#define ls(k) tree[k].ls
#define rs(k) tree[k].rs
然后就可以往下看了。
分裂(split)#
提前声明下,分裂操作分为两种:按值分裂和按大小分裂。一般如果把 fhq-Treap 当一棵普通的平衡树的话,都是使用前者的,本文所讲的也是按值分裂。
分裂操作会将整棵树分裂为两棵树
至于为什么这样分裂,你稍微看下模板题的操作应该就明白了。
至于代码,通过递归的方式来实现,详见注释:
void split(int now,int val,int &x,int &y){ //将树按val分裂成两棵树,分别以x和y为根
if(!now){ //分到底了,返回
x=y=0;
return;
}
if(tree[now].val<=val){ //如果当前的值小于给定的,那么根据二叉搜索树的性质,给定的值一定在右子树中
x=now; //先确定其中一个根
split(rs(now),val,rs(now),y); //然后再去右子树分裂,这个自己稍微想一下,不难理解
}
else{ //反之亦然,给定值一定在左子树中
y=now;
split(ls(now),val,x,ls(now));
}
pushup(now); //儿子都变了,更新信息
}
合并(merge)#
合并操作会将两棵树
代码仍然还是通过递归的方式来实现,如下所示:
int merge(int x,int y){ //将x和y合并为一棵树,并将合并后的根返回
if(!x||!y) return x^y; //这个其实就是x+y,也就是x和y中不为0的那一个,这种情况表示其中有棵树为空
if(tree[x].key>tree[y].key){ //忘记题的一点,这里默认大根堆
rs(x)=merge(rs(x),y); //注意还要满足二叉搜索树的性质
pushup(x); //更新信息
return x;
}
else{ //反之亦然
ls(y)=merge(x,ls(y)); //为满足二叉搜索树
pushup(y); //更新信息
return y;
}
}
到此为止,Fhq-Treap 最核心的部分你已经学完了。
3. 其它操作
有了以上两个核心操作后,我们应该怎么实现其他平衡树的操作呢?相信各位读者看懂了上面后口胡出来都没问题。
接下来让我们挨个分析。
插入#
假如我们要插入的值为
代码只需两行即可搞定:
inline void insert(int val){
split(root,val,x,y);
root=merge(merge(x,newnode(val)),y);
}
删除#
假如我们要删除的值为
代码比插入长个两行:
inline void erase(int val){
split(root,val,x,y);
split(x,val-1,x,z);
z=merge(ls(z),rs(z));
root=merge(merge(x,z),y);
}
查询给定值的排名#
设要查询的值为
别忘了合并回去哈。
inline int get_rank(int val){
split(root,val-1,x,y);
int ret=tree[x].siz+1;
root=merge(x,y);
return ret;
}
查询给定排名的值#
这个也很简单,根据二叉搜索树的性质即可:若当前的左子树大小大于给定排名,答案就在左子树中,否则就去右子树。
注意去右子树的话,要将查询的排名减去左子树的大小,再包括自己占的一个。
写法上递归与非递归皆可。
递归写法:
int get_val(int now,int k){
if(tree[ls(now)].siz+1==k) return tree[now].val;
else if(tree[ls(now).siz]>=k) return get_val(ls(now),k);
else return get_val(rs(now),k-tree[ls(now)].siz-1);
}
非递归写法:
int get_val(int rnk){
int now=root;
while(now){
if(tree[ls(now)].siz+1==rnk) break;
else if(tree[ls(now)].siz>=rnk) now=ls(now);
else rnk-=tree[ls(now)].siz+1,now=rs(now);
}
return tree[now].val;
}
查询前驱#
设要查询的值为
int get_pre(int val){
split(root,val-1,x,y);
int now=x,ret;
while(rs(now)) now=rs(now);
ret=tree[now].val;root=merge(x,y);
return ret;
}
查询后继#
设要查询的值为
int get_nxt(int val){
split(root,val,x,y);
int now=y,ret;
while(ls(now)) now=ls(now);
ret=tree[now].val;root=merge(x,y);
return ret;
}
好的,至此平衡树模板的所有操作我们都已经用分裂和合并这两个基础操作实现出来了!
4. 代码#
#include<bits/stdc++.h>
#define int long long
#define ls(k) tree[k].ls
#define rs(k) tree[k].rs
using namespace std;
const int maxn=1e5+5;
const int inf=0x7fffffff;
int read(){
int ans=0,flag=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')flag=-1;ch=getchar();}
while(isdigit(ch))ans=(ans<<3)+(ans<<1)+(ch^48),ch=getchar();
return ans*flag;
}
struct node{
int ls,rs;
int val,key,siz;
}tree[maxn];
int cnt_node,root,x,y,z;
mt19937 rnd(233);
int newnode(int val){
tree[++cnt_node].val=val;
tree[cnt_node].key=rnd();
tree[cnt_node].siz=1;
return cnt_node;
}
void pushup(int now){
tree[now].siz=tree[ls(now)].siz+tree[rs(now)].siz+1;
}
void split(int now,int val,int &x,int &y){
if(!now){
x=y=0;
return;
}
if(tree[now].val<=val){
x=now;
split(rs(now),val,rs(now),y);
}
else{
y=now;
split(ls(now),val,x,ls(now));
}
pushup(now);
}
int merge(int x,int y){
if(!x||!y) return x^y;
if(tree[x].key>tree[y].key){
rs(x)=merge(rs(x),y);
pushup(x);
return x;
}
else{
ls(y)=merge(x,ls(y));
pushup(y);
return y;
}
}
inline void insert(int val){
split(root,val,x,y);
root=merge(merge(x,newnode(val)),y);
}
void erase(int val){
split(root,val,x,y);
split(x,val-1,x,z);
z=merge(ls(z),rs(z));
root=merge(merge(x,z),y);
}
int get_rank(int val){
split(root,val-1,x,y);
int ret=tree[x].siz+1;
root=merge(x,y);
return ret;
}
int get_val(int rnk){
int now=root;
while(now){
if(tree[ls(now)].siz+1==rnk) break;
else if(tree[ls(now)].siz>=rnk) now=ls(now);
else rnk-=tree[ls(now)].siz+1,now=rs(now);
}
return tree[now].val;
}
int get_pre(int val){
split(root,val-1,x,y);
int now=x,ret;
while(rs(now)) now=rs(now);
ret=tree[now].val;root=merge(x,y);
return ret;
}
int get_nxt(int val){
split(root,val,x,y);
int now=y,ret;
while(ls(now)) now=ls(now);
ret=tree[now].val;root=merge(x,y);
return ret;
}
signed main(){
int Q=read();
while(Q--){
int opt=read(),x=read();
if(opt==1) insert(x);
else if(opt==2) erase(x);
else if(opt==3) printf("%lld\n",get_rank(x));
else if(opt==4) printf("%lld\n",get_val(x));
else if(opt==5) printf("%lld\n",get_pre(x));
else if(opt==6) printf("%lld\n",get_nxt(x));
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?