平衡树入门
前言
平衡树的分类
有旋:Splay,Treap,AVL Tree
无旋:FHQ Treap,替罪羊树
平衡树双子星:FHQ Treap,Splay
引入
BST,即二叉搜索树,可以快速找到序列中第
然而,在构造 BST 的过程中,它可能会退化为一条链。
为防止退化,我们可以参考快排,引入随机数。
FHQ Treap
Treap 是一类具有特殊构造规则的 BST,Treap = Tree + heap。
- Treap 本身是一棵 BST,其点权满足 BST 的性质,即中序遍历有序,左子树<节点<右子树。
- Treap 中节点的层数根据随机索引决定,其索引满足堆的性质,即父节点大于子节点(大根堆)。
操作 1:按照 value 进行 split
按照一个给定的 value
把原来的 Treap 分为两棵子树,子树 x
所有点权都小于等于 value
,子树 y 所有点权都大于 value
。
void split(int pos, int val, int &x, int &y)
{
if (!pos) {
x = y = 0;
return;
}
if (t[pos].val <= val) {
x = pos;
split(t[pos].rson, val, t[pos].rson, y);
} else {
y = pos;
split(t[pos].lson, val, x, t[pos].lson);
}
push_up(pos);
}
操作 2:merge
将子树 x
与子树 y
合并,该操作与 split()
对应,即两棵子树各自是一个 Treap。
不妨假设子树 x
的根节点权值,小于子树 y
的根节点权值。
现在要将两棵 Treap 合并,无论谁作为根节点,BST 的性质都是可以满足的。
所以,我们需要根据随机索引决定谁作为合并之后的根。
假设以 y
作为根,那么接下来需要把子树 x
合并到子树 y
的左儿子,这无疑和现有的左儿子产生冲突。
我们依然通过考虑BST 的性质和堆的性质来决定谁作为新的左儿子,而谁又将被合并。
不难发现,此时问题与【子树 x
和子树 y
合并】同构,于是我们可以递归处理。
我们可以用这样的代码来描述上述过程:
if (t[x].idx < t[y].idx) { // y作为根
t[y].lson = merge( // 谁将作为新的左儿子,此问题与原问题同构
x, t[y].lson
);
}
于是我们就使用递归的方式,成功地合并了两个 Treap,具体代码如下:
int merge(int x, int y)
{
if (!x || !y) { return x+y; } // 此时x或y至少有一个为0,返回另一个
if (t[x].idx > t[y].idx) { // 以x为根
t[x].rson = merge(t[x].rson, y);
push_up(x);
return x;
} else { // 以y为根
t[y].lson = merge(x, t[y].lson);
push_up(y);
return y;
}
}
洛谷 P3369【模板】普通平衡树
洛谷 P3369,AC 代码提交记录
此题涉及到的 6 种操作,均可以采用若干次【按照 value
进行 split()
】和【 merge()
】的组合实现。
void ins(int val)
{
t[++cnt] = { val, rand(), 0, 0, 1 };
split(root, val, x, y);
root = merge(merge(x, cnt), y);
}
void del(int val)
{
split(root, val, x, y);
split(x, val-1, x, z);
z = merge(t[z].lson, t[z].rson);
root = merge(merge(x,z), y);
}
int get_rank(int val)
{
split(root, val-1, x, y);
int rk = t[x].sz + 1;
root = merge(x, y);
return rk;
}
int get_val(int rk)
{
int pos = root;
while (pos) {
if (rk <= t[t[pos].lson].sz) {
pos = t[pos].lson;
} else if (rk == t[t[pos].lson].sz + 1) {
return t[pos].val;
} else {
rk -= t[t[pos].lson].sz + 1;
pos = t[pos].rson;
}
}
return 0;
}
int get_pre(int val)
{
split(root, val-1, x, y);
int pos = x;
while (t[pos].rson) { pos = t[pos].rson; }
int pre = t[pos].val;
root = merge(x, y);
return pre;
}
int get_nxt(int val)
{
split(root, val, x, y);
int pos = y;
while (t[pos].lson) { pos = t[pos].lson; }
int nxt = t[pos].val;
root = merge(x, y);
return nxt;
}
文艺平衡树
P3391 【模板】文艺平衡树,AC代码提交记录
和普通平衡树不同的是,这题的 split()
操作是按照 size
,即子树大小进行
void split(int pos, int sz, int &x, int &y)
{
if (!pos) { x=y=0; return; }
spread_down(pos);
if ( t[t[pos].lson].sz < sz ) {
x = pos;
split(t[pos].rson, sz-t[t[pos].lson].sz-1, t[pos].rson, y);
} else {
y = pos;
split(t[pos].lson, sz, x, t[pos].lson);
}
push_up(pos)
对单次修改而言,只需要【对于其每个子节点,都交换左右儿子】即可。
这一步可以通过打上【懒标记】维护。
struct node {
int lson, rson, sz, val, idx, tag;
} t[MAXN];
void spread_down(int pos)
{
if (t[pos].tag) {
swap(t[pos].lson, t[pos].rson);
t[t[pos].lson].tag ^= 1;
t[t[pos].rson].tag ^= 1;
t[pos].tag = 0;
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现