FHQ-Treap 学习笔记
平衡树 FHQ-Treap:
一, 一些前言:
虽记吾几时学平衡树,知平衡树甚多,还记正值 Splay 时期,代码二百余行,好不威风!可其难于调也,一调兮为一日,日复一日,未曾调出郁闷的出纳员,而吾早已郁闷。
待三日已过,遂 A 此题,余感概道 Splay 难作,乃学 Treap 矣。
平衡树多种,FHQ-Treap 可谓最强平衡树也,何处此言?待会知之矣。
(好了不扯了,也不想编了。)
二, FHQ-Treap 的概述:
毕竟是最强平衡树,它可以维护下标,也可以维护值域,还能维护区间修改,还能可持久化!这可是隔壁厕所里的 Splay 干不了的事!
但是嘛,没有东西是十全十美的,它总有比隔壁厕所里的 Splay 弱的地方。
是什么呢?
在维护 LCT 上!
隔壁厕所里的 Splay 维护 LCT 只需要 隔壁厕所里的 Splay 写起来好太多了。
Splay:我厕所上完了哥。
啥,你说你不知道 LCT 是个啥?
其实我也没学 LCT。
那你待会可以去看看 LCT 的详解。
like this.
好的我们继续。
相较于有旋 Treap 而言,FHQ-Treap 最典型的特点即是不需要旋转操作,因此 FHQ-Treap 也称无旋 Treap,无旋 Treap 的操作方式使得它天生支持维护序列,可持久化等特性。
FHQ-Treap 仅有两种核心操作,分裂 与 合并,通过这两种神一般的操作,在很多情况下可以比有旋 Treap 方便许多,下面逐一介绍这两种操作。
三, FHQ-Treap 的分裂操作:
分裂 - Split 操作,简单来说,就是把一个大 Treap 分成两个小 Treap,且保证第一个 Treap 的节点在第二个前面。
Split 操作有两种实现方式,都很有用。
- 按值分裂:
何为按值分裂?就是把权值小于
怎么分裂?如图所示:
就是酱紫~
代码实现:
il void split(int p, int v, int &l, int &r){//p 是当前 Treap 的根
//l,r 分别是 FHQ-Treap 分裂后得到的两个 FHQ-Treap 的根,这里用了引用的方式,便于传递值
if (!p){//空树
l = r = 0;
return ;
}
if (t[p].v <= v){
//当前节点的值小于等于 x(即我写的 v),则证明该节点的左子树上所有节点的值也都小于等于 x
l = p;//让该节点成为左树的节点
split(t[l].r, v, t[l].r, r);//继续递归分裂当前节点的右子树
}
else{
//当前节点的值大于 x,则证明该节点的左子树上所有节点的值也都大于 x
r = p;//让该节点成为右左树的节点
split(t[r].l, v, l, t[r].l);//继续递归分裂当前节点的左子树
}
up(p);//一定要记着 pushup,因为 p 点的左右子树大小变化了,即总个数变化了,需要更新,否则会导致节点个数的信息不对
}
- 按排名分裂:
何又为按排名分裂?相信你已经猜到了,就是把前
思路和按值分裂的思路是一样的,就不说了,看看如何实现:
il void split(int p, int v, int &l, int &r){
if (!p){//空树
l = r = 0;
return ;
}
int ln = t[t[p].l].sn;//左子树的节点个数
if (ln < v){//左边不足 k(即我写的 v)个,递归从右边再取出剩余部分,补给第一个 Treap
l = p;
split(t[p].r, v - ln - 1, t[l].r, r);
}
else{//同上,有 k 个就要分到第二个 Treap 里
r = p;
split(t[p].l, v, l, t[r].l);
}
up(p);
}//十分简单
切记,排名分裂也要牢牢掌握。
分裂讲完了,撒花!
四, FHQ-Treap 的合并操作:
我想起了半年以前自学的并查集()
诶,第二个 merge,难度咋样?
u1s1,在我看来,跟第一个 merge 难度差不多,就稍微难那么一些而已,真的。
合并 - Merge 操作,简单来说,就是把一个小 Treap 和另一个小 Treap 合并成一个 大 Treap,且保证前一个 Treap 的遍历顺序都在后一个前面。(说白了就是保持 BST 结构不被破坏)
如上文所说,虽然合并操作看起来很简单,但也不能乱合并,要必须保持 BST 的结构,否则你还叫啥 Treap。
要合并两棵 Treap,当且仅当左 Treap 上的所有节点的权值全都小于等于右 Treap 上节点的最小权值,或两棵树有空树。
这是个重要的性质。
合并时,由于是我们设置的随机值
详见代码:
il int merge(int l, int r){//左右树的根节点
if (!l || !r) return l | r;//有空树,返回非空树 l+r
//如果左树上当前节点的 rnd 值小于右树上当前节点的 rnd 值
if (t[l].rnd < t[r].rnd){
//将 l 变为合并后的新 FHQ-Treap 的根节点。
t[l].r = merge(t[l].r, r);//继续递归确定 l 的右子树该接谁
up(l);//一定记得要上传
return l;//返回新得到的根节点
}
else{//同上
t[r].l = merge(l, t[r].l);
up(r);
return r;
}
}//十分简单
合并也讲完了,撒花撒花!!
五, FHQ-Treap 的其他杂用操作实现
一个个看吧。
:
插入一个权值为
的点。
把 Treap 按照
code:
il void ins(int v){
split(root, v - 1, tl, tr);
//create(v) 是新建一个权值为 v 的节点
root = merge(merge(tl, create(v)), tr);
}
就这么短。
:
删除一个权值为
的节点。
连续两次分裂,先按值分裂出
code:
il void del(int v){
int tmp;
split(root, v - 1, tl, tr);
split(tr, v, tmp, tr);
//两次分裂得到全是 v 的 tmp 树
tmp = merge(t[tmp].l, t[tmp].r);//去掉根,即不合并根
root = merge(tl, merge(tmp, tr));
}
还是很短。
:
查找
在 Treap 中的排名。
先分裂出
code:
il int find_rk(int v){
split(root, v - 1, tl, tr);
int rk = t[tl].sn + 1;
root = merge(tl, tr);
return rk;
}
短到注释都不想打了。
:
查找 Treap 中 排名为
的数。
这里发现用递归搜索还更简洁,所以就用递归啦,思想见 BST。
code:
il int find_val(int p, int k){
int ln = t[t[p].l].sn + 1;
if (ln == k) return t[p].v;
else if (ln < k) return find_val(t[p].r, k - ln);
else return find_val(t[p].l, k);
}
:
求数
的前驱。
中排名最后的那个数,用上
code:
il int pre(int v){
split(root, v - 1, tl, tr);
int tmp = find_val(tl, t[tl].sn);//这里 tl 树的最后一名的排名是它的节点总个数
root = merge(tl, tr);
return tmp;
}
有超过
:
求数
的后继。
中排名第一的那个数,用上
code:
il int nxt(int v){
split(root, v, tl, tr);
int tmp = kth_find(tr, 1);
root = merge(tl, tr);
return tmp;
}
看来没有超过
讲完了,撒花撒花撒花!!!
六, 结语:
这就是 FHQ-Treap 的基本操作啦,区间操作什么的就先放放,还有,建议可以翻到上面去看一下 LCT。
呼,总算写完了。
【Markdown
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具