FHQ Treap 学习笔记
FHQ Treap
这是一个很好理解、码量短、应用多变、容易可持久化的平衡树。我认为这是最实用的平衡树。
我们结合例题讲解:【模板】普通平衡树。
主要通过 split 和 merge 来维护。
首先平衡树有三个性质:
-
堆性质,这里我们将维护一个小根堆(事实上维护大根堆也没有关系),即是一棵二叉树,树中每个节点的优先级值都会大于它的左儿子和右儿子。
-
搜索树性质,也就是每个节点的权值大于等于它左儿子的权值(如果有的话),并且小于等于它右儿子的权值(如果有的话)。更进一步,每个节点的权值大于等于它左子树所有节点的最大权值,小于等于它右子树所有节点的最小权值。
-
深度为
左右。
那么我们怎么维护它呢?
我们用一个结构体来维护平衡树每个节点的信息。
struct bst{
int l,r;//左右儿子
int pri,val;//优先级和权值
int sz;//以它为根的子树大小
}t[100010];
当然不同题目不同对待。
然后就是当前树根的编号
首先我们要知道如何新增节点。
inline newnode(int x){//新增一个权值为 x 的节点
t[++cnt]={0,0,rand(),x,1};
//新增节点编号为 cnt,左右儿子为空、优先级随机(保证了平衡树的深度)、权值为 x、子树大小为 1(它自己一个点)
}
然后是维护大小,每个节点的子树大小是它左儿子的大小加上右儿子的大小加一(它自己也是一个点)。
inline update(int x)//更新编号为 x 的节点的子树大小
t[x].sz=t[t[x].l].sz+t[t[x].r].sz+1;
}
接下来是分裂,假设我们要将以节点
那我们就要依据权值的大小,看一下根节点属于
void split(int x,int k,int &l,int &r){
if(!x){l=r=0;return;}//空节点,直接返回两个空节点
if(t[x].val<=k){//x 权值小于等于 k,说明整个左子树都是 l 的
l=x;
split(t[x].r,k,t[x].r,r);
}else{
r=x;
split(t[x].l,k,l,t[x].l);
}
}
还有另外一种分裂,就是按照权值的排名分裂,我们要将以节点
void split(int x,int k,int &l,int &r){
if(!x){l=r=0;return;}
if(t[t[x].l].sz+1<=k){
l=x;
split(t[x].r,k-t[t[x].l].sz-1,t[x].r,r);
}else{
r=x;
split(t[x].l,k,l,t[x].l);
}
}
接下来是更简单的合并,传入两棵平衡树的根
由于
注意如果不满足以上任意两点中的一点就会出错。
我们直接依据优先级的大小,看一下根节点属于
具体来说:
如果
如果
与左偏树很像,但是不用维护左偏性质,不难。
inline int merge(int l,int r){
if(!l||!r)return l+r;
if(t[l].pri<=t[r].pri){
t[l].r=merge(t[l].r,r);
update(l);
return l;
}else{
t[r].l=merge(l,t[r].l);
update(r);
return r;
}
}
接下来是求以
具体来说,求到了一个节点,看一下
-
如果
小于等于 左子树的大小,那么就返回 的左子树的第 小值。 -
如果
刚好等于 左子树的大小加一(指 ),那么就返回 这个位置对应的值。 -
如果
大于 左子树的大小加一,那么就返回 的右子树的第 小值。
inline int kth(int x,int k){
if(t[t[x].l].sz>=k)return kth(t[x].l,k);
else if(t[x].sz==k-1)return b[t[x].val];//因为离散化过
else return kth(t[x].r,k-t[t[x].l].sz-1);
}
然后就是其它正常操作了。
- 要往平衡树里增加一个值为
的元素。
先把
那我们可以将权值
inline void insert(int x){
int l,r;
split(rt,x,l,r);
newnode(x);
rt=merge(merge(l,cnt),r);
}
- 要删掉一个值为
的元素,而且只删掉 1 个。
那我们可以把值为
我们先把平衡树
然后再把平衡树
那我们要在
inline void delet(int x){
int l,r,p;
split(rt,x,l,r);
split(l,x-1,l,p);
rt=merge(merge(l,merge(t[p].l,t[p].r)),r);
}
- 如果要查询数字
的排名,那就是要输出 的数的个数加一。
把平衡树
输出
inline void getrank(int x){
int l,r;
split(rt,x-1,l,r);
cout<<t[l].sz+1<<'\n';
rt=merge(l,r);
}
- 要查询排名为
的数的值。
直接调用
- 求数值
的前驱,那就是要输出小于 的最大数。
把平衡树
输出
inline void getpre(int x){
int l,r;
split(rt,x-1,l,r);
cout<<kth(l,t[l].sz)<<'\n';
rt=merge(l,r);
}
- 求数值
的后继,那就是要输出大于 的最小数。
把平衡树
输出
inline void getback(int x){
int l,r;
split(rt,x,l,r);
cout<<kth(r,1)<<'\n';
rt=merge(l,r);
}
把以上几个综合起来就是代码了。理解之后其实很好写。
事实上,除了 split 都很好理解。由于 split、merge、kth 都是
然后就是 【模板】文艺平衡树。
这个也不难,就是要处理区间翻转,那我们要像线段树一样,给平衡树打上懒标记。
我们可以发现,如果我们搞一个中序遍历,那么平衡树的权值就是单调的。
然后,翻转一个区间
-
翻转
。 -
翻转
。 -
交换
与 。
那么对应在平衡树上,就是找到区间
之所以可以这么操作,是因为这题不用排大小,所以权值不用满足二叉搜索树的性质。
我们只需要用二叉搜索树维护当前的序列,也就是保证它的所有懒标记下传后,中序遍历是当前序列就好了。
那么可以考虑用懒标记,为 1 代表要翻转,为 0 代表不翻转。
由于 FHQ Treap 的父子关系总是变来变去,为了防止下传到错误的节点上,每一次 split 或者 merge 之前都要下传懒标记。
下传的方式是异或,因为翻转两次代表不翻转。
考虑 split 操作,对于每一次操作,我们是要对整个
那我们可以每一次把整棵树都 split 2 次,变成 3 棵,分别代表了区间
我们是要对区间进行 split,那就是依据排名分裂。
然后这道题就做完了。时间复杂度
inline void newnode(int x){t[++cnt]={0,0,x,rand(),1,0};}
inline void update(int x){t[x].sz=t[t[x].l].sz+t[t[x].r].sz+1;}
inline void pushdown(int x){
swap(t[x].l,t[x].r);
if(t[x].l)t[t[x].l].t^=1;
if(t[x].r)t[t[x].r].t^=1;
t[x].t=0;
}
inline int merge(int l,int r){
if(!l||!r)return l+r;
if(t[l].pri<=t[r].pri){
if(t[l].t)pushdown(l);
t[l].r=merge(t[l].r,r);
update(l);
return l;
}else{
if(t[r].t)pushdown(r);
t[r].l=merge(l,t[r].l);
update(r);
return r;
}
}
inline void split(int x,int k,int &l,int &r){
if(!x){l=r=0;return;}
if(t[x].t)pushdown(x);
if(t[t[x].l].sz+1<=k){
l=x;
split(t[x].r,k-t[t[x].l].sz-1,t[x].r,r);
}else{
r=x;
split(t[x].l,k,l,t[x].l);
}
update(x);
}
inline void dfs(int x){
if(t[x].t)pushdown(x);
if(t[x].l)dfs(t[x].l);
write(t[x].val);
putchar(32);
if(t[x].r)dfs(t[x].r);
}
int main(){
srand(time(0));n=read();m=read();
for(int i=1;i<=n;i++){
newnode(i);
rt=merge(rt,cnt);
}
for(int i=1,l,r,p,x,y;i<=m;i++){
x=read();y=read();
split(rt,y,l,r);
split(l,x-1,l,p);
t[p].t^=1;
rt=merge(merge(l,p),r);
}
dfs(rt);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)