浅谈FHQ-Treap
确实 FHQ-Treap 不知道比隔壁 Splay 好多少,码量少,常数小。
前置知识
C++
BST
Head
原理&代码实现
FHQ Treap
不是通过旋转来保持平衡的,而是通过分裂和合并。 FHQ Treap
会按二叉搜索树一样根据键值排序结点,并且随机赋给每个结点一个优先级,按照二叉堆的顺序排序结点(这里用大根堆)。Treap
通过旋转,使平衡树同时满足这两个性质,从而达到平衡。而 FHQ Treap
通过调用合并函数时使平衡树满足堆序,实现原理与 Treap
不同。
新建一个节点也就是赋值与初始化各项信息,当然需要返回节点编号作为描述这个节点的信息
int new(int val){
tr[++ind].val=val;//权值
tr[ind].rnd=rand();//优先级
tr[ind].siz=1;//子树大小
return ind;
}
关于为什么优先级随机的话树高期望为 的证明先咕咕咕。
最基本的上传合并左儿子和右儿子的大小(当然也可以维护权值信息):
void pushup(int x){
int ls=tr[x].ls;
int rs=tr[x].rs;
tr[x].siz=tr[ls].siz+tr[rs].siz+1;//当然要算上自己所以+1
}
Part 1:Split(分裂)
分为两种,各有各的功能:
- 按权值分裂:分成两颗子树,一颗子树的值全部 另一颗树全部 。
- 按大小分裂,使得一颗子树大小为 另一颗为剩下的。
按权值大小分裂:
例如按权值 分裂:
可以发现所以比 值小的树都在以 为根的树上。
那我们该怎么写捏?假设 是分裂出来的两颗树的根,我们当前到了点 则如果 则它应该在 树上,否则就在 树上。如果假设成立,则仍需要向右子树去寻找是否有节点 使得 即在 的右子树里是否有比当前值大,但权值仍比 小的节点。
void split_val(int x,int val,int &a,int &b){
if(!x){a=b=0;return;}
if(tr[x].val<=val){
split_val(tr[x].rs,val,tr[x].rs,b);
a=x;
}else{
split_val(tr[x].ls,val,a,tr[x].ls);
b=x;
}pushup(x);
}
按大小分裂:
和按权值分裂相似,只是在 时,也就是递归右子树时需要将 这是显然的。
void split(int root, int sze, int &x, int &y) {
if (root == 0) {
x = y = 0;
return;
}
if (Tree[Tree[root].Left].Size + 1 <= sze) {
x = root;
split(Tree[root].Right, sze - Tree[Tree[root].Left].sze - 1, Tree[root].Right, y);
} else {
y = root;
split(Tree[root].Left, sze, x, Tree[root].Left);
}
pushup(root);
}
Part 2:merge(合并)
如图
在合并时,需要满足 两颗树中 的最大值小于 的最小值。
这时只需要按优先级合并就好啦,优先级大的在上面,小的在下面并不会影响 BST
的性质。
void merge(int x,int y,int &a){
if(!x || !y){a=x+y;return;}
if(tr[x].rnd<tr[y].rnd){
merge(tr[x].rs,y,tr[x].rs);
a=x;pushup(x);
}else{
merge(x,tr[y].ls,tr[y].ls);
a=y;pushup(y);
}
}
各种操作
平衡树的基本操作是很简单的。
但由这些操作衍生出的操作就可多了去了。
插入:
假设插入的值 ,把树按权值 分裂成两棵树,最后新建节点合并起来就好。
void insert(int i){
split_val(root,i-1,a,b);
merge(a,new(i),a);
merge(a,b,root);
}
删除:
假设删除值 则把树按 分裂成 两颗,再把树 按 分成 两颗树,此时树 上的所有权值都为 如果只删一个点那么直接将树 的左右儿子合并然后赋值给 ,如果删除所有是这个值的点则直接合并 就好啦。
void remove(T key) {
int x, y, z;
split(Root, key, x, z);
split(x, key - 1, x, y);
if (y) { // 如果删除所有,就直接去掉这个if语句块,并且下面的只合并x, z
y = merge(Tree[y].Left, Tree[y].Right);
}
Root = merge(merge(x, y), z);
}
指定权值查排名:
假设查询的值为 则按 分裂成 最后查询 树的大小然后 +1 就好啦。
int rank(T key) {
int x, y, ans;
split(Root, key - 1, x, y);
ans = Tree[x].Size + 1;
Root = merge(x, y);
return ans;
}
查询指定排名的值:
写法一:
从根节点开始,用左子树的 确定答案,三种情况。
- 在左子树。
- 在右子树。
- 找到答案。
int root = Root;
while (true) {
if (Tree[Tree[root].Left].Size + 1 == r) {
break;
} else if (Tree[Tree[root].Left].Size + 1 > r) {
root = Tree[root].Left;
} else {
r -= Tree[Tree[root].Left].Size + 1;
root = Tree[root].Right;
}
}
return Tree[root].Key;
写法二:
按排名分裂成三棵树,去中间那颗的值。
// 这里的split是按大小分裂
T at(int r) {
int x, y, z;
split(Root, r - 1, x, y);
split(y, 1, y, z);
T ans = Tree[y].Key;
Root = merge(merge(x, y), z);
return ans;
}
很明显,写法一更快,写法二码量小。
前驱指小于当前数的最大值
后继指大于当前数的最小值
查询前驱:
按 分裂,在 那棵树上一直往右儿子走,走到叶子结点就是前驱。
int x, y, root;
T ans;
split(Root, key - 1, x, y);
root = x;
while (Tree[root].Right) root = Tree[root].Right;
ans = Tree[root].Key;
Root = merge(x, y);
return ans;
查询后继:
后继同理。
询问一个数是否存在:
按 分裂成三棵树,看中间那颗树大小是否为 0。
例题:
文艺平衡树
对于区间翻转先咕咕咕。
参考文献:
本文作者:Mu_leaf
本文链接:https://www.cnblogs.com/muleaf/p/17940777
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端