平衡树入门

前言

平衡树的分类
有旋:Splay,Treap,AVL Tree
无旋:FHQ Treap,替罪羊树
平衡树双子星:FHQ Treap,Splay

引入

BST,即二叉搜索树,可以快速找到序列中第 k 大的元素。
然而,在构造 BST 的过程中,它可能会退化为一条链。
为防止退化,我们可以参考快排,引入随机数。

FHQ Treap

Treap 是一类具有特殊构造规则的 BST,Treap = Tree + heap。

  1. Treap 本身是一棵 BST,其点权满足 BST 的性质,即中序遍历有序,左子树<节点<右子树。
  2. 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【模板】普通平衡树

洛谷 P3369AC 代码提交记录
此题涉及到的 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;
	}
}
posted @   LittleDrinks  阅读(41)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示