【学习笔记】平衡树
介绍
平衡树是一种特殊的二叉树搜树,他能在被修改后,依靠分裂,合并,等操作使得树能始终保持平衡(每一个节点的两棵子树的大小尽量相等),这里主要讲解 FHQtreap。
操作
FHQtreap 也叫无旋 treap,他的每个节点有两个值 , 其中 满足二叉堆的性质,而 满足 BST 的性质。注意与 Splay 不同,对于 相同的点 FHQtreap 会开不同的点进行维护。
- 更新 pushup
这是最基本的操作。每一个节点的大小等于他的两个儿子的大小加 。
void pushup(int u){
t[u].siz = t[t[u].ls].siz + t[t[u].rs].siz + 1;
}
- 创建新节点 add
更新 即可。
int add(int val){
t[++cnt].pri = rand(), t[cnt].siz = 1, t[cnt].val = val;
return cnt;
}
- 分裂 split(按 )
这是平衡树中最重要的操作。
对于分裂完后的两棵树 需要满足 中的点的 , 中的点的
函数中的 分别表示左树,右树中的接口,即新的节点可以接到哪里。
若当前节点的 则他的左儿子一定属于左树,所以往右递归,而左树新的接口就变为了他的右儿子(左儿子没有改变,并且他要满足 BST 的性质)。
若当前节点的 则他的右儿子一定属于右树,所以往左递归,而右树新的接口就变为了他的左儿子(右儿子没有改变,并且他要满足 BST 的性质)。
void split(int u, int x, int &L, int &R){
if(!u) return L = 0, R = 0, void();
if(t[u].val <= x) L = u, split(t[u].rs, x, t[u].rs, R);
else R = u, split(t[u].ls, x, L, t[u].ls);
pushup(u);
}
- 合并 merge
若当前左树顶点的 更小,则要把左树顶点的右儿子,改为将他原先右儿子与右树合并后的新顶点(BST 性质)。
若当前右树顶点的 更小,则要把右树顶点的左儿子,改为将他原先左儿子与左树合并后的新顶点(BST 性质)。
int merge(int L, int R){
if(!L || !R) return L | R;
if(t[L].pri <= t[R].pri) return t[L].rs = merge(t[L].rs, R), pushup(L), L;
else return t[R].ls = merge(L, t[R].ls), pushup(R), R;
}
- 添加 Insert
将树按权值为 (当前点的值)裂开成两棵树,然后进行两次合并。
void Insert(int val){
int x = add(val), L, R;
split(rt, val, L, R);
rt = merge(merge(L, x), R);
}
- 删除 Delete
将树分别按权值为 裂开为三棵树 ,然后再将左树,t[p].ls
,t[p].rs
,右树,合并
void Delete(int x){
int L, P, R;
split(rt, x, L, R);
split(L, x - 1, L, P);
rt = merge(merge(L, merge(t[P].ls, t[P].rs)), R);
}
- 排名 getrank
将树按 为权值进行分裂,答案就是左树的 。
int getrank(int val){
int L, R, ans;
split(rt, val - 1, L, R);
ans = t[L].siz + 1;
rt = merge(L, R);
return ans;
}
- 第 大的树 kth
按左右树的 DFS 即可。
int Kth(int u, int k){
int x = t[t[u].ls].siz + 1;
if(x == k) return t[u].val;
if(k > x) return Kth(t[u].rs, k - x);
else return Kth(t[u].ls, k);
}
- 前驱 pre 后继 suc
先计算排名,然后再调用kth
函数即可。
注意前驱是getrank(x)-1
而后继是getrank(x+1)
。
int pre(int x){
return Kth(rt, getrank(x) - 1);
}
int suc(int x){
return Kth(rt, getrank(x + 1));
}
例题 文艺平衡树
构造一个数列 a,b,c,d,e,假设一棵树的中序遍历就是这个数列,容易发现若将数列中的某个区间翻转,就是将树中的某个子树的左右儿子全变翻转。
由此我们可以构造一棵平衡树,他的每个节点的 就是数列的下标 ,还是随机的。
对于每次区间操作可以将树按 进行分裂,注意不能按 进行分裂,因为翻转后的树的 是不满足 BST 的性质的。
split(rt, r, L, R);
split(L, l - 1, L, P);
由于每次都对全部的儿子进行翻转是很费时间的,所以可以引入一个类似线段树懒标记 lazytag 的东西。在每次操作的时候进行下传即可。
- pushdown
连续两次翻转相当于不翻转。
void pushdown(int u){
swap(t[u].ls, t[u].rs);
t[t[u].ls].lazy ^= 1;
t[t[u].rs].lazy ^= 1;
t[u].lazy = 0;
}
- split
void split(int u, int x, int &L, int &R){
if(!u) return L = 0, R = 0, void();
if(t[u].lazy) pushdown(u);
if(t[t[u].ls].siz + 1 <= x){
L = u, split(t[u].rs, x - t[t[u].ls].siz - 1, t[u].rs, R);
}
else{
R = u, split(t[u].ls, x, L, t[u].ls);
}
pushup(u);
}
- merge
int merge(int L, int R){
if(!L || !R) return L | R;
if(t[L].pri < t[R].pri){
if(t[L].lazy) pushdown(L);
t[L].rs = merge(t[L].rs, R);
return pushup(L), L;
}
else{
if(t[R].lazy) pushdown(R);
t[R].ls = merge(L, t[R].ls);
return pushup(R), R;
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
· Manus的开源复刻OpenManus初探