WBLT 学习笔记

WBLT 作为一个非玄学意义上的平衡树,支持区间操作,且可持久化。

WBLT 全称 Weight Balanced Laefy Tree,是 WBT 和 LT 的结合体。尽管我也不知道那两个是什么。

Weight Balanced Tree 的每个结点储存这个结点下子树的大小,并且通过保持左右子树的大小关系在一定范围来保证树高。

Leafy Tree 维护的原始信息仅存储在树的 叶子节点 上,而非叶子节点仅用于维护子节点信息和维持数据结构的形态。我们熟知的线段树就是一种 Leafy Tree。

基本结构及平衡维护

要实现 WBLT,每个结点需要记录如下信息:

  • ls[x]rs[x]:左右儿子。
  • sz[x]val[x]:以 x x x 为根的子树大小,和结点 x x x 的键值。

因为只有叶子节点实际存储键值,所以其他节点处存储的信息是由它们的子节点合并得到的,以方便后续查询。

辅助函数

WBLT 需要的辅助函数如下:

  • new_node() 新建结点点;
  • del_node(x) 删除结点 x x x
  • new_leaf(v) 新建以 v v v 的键值的叶子结点;
  • join(x, y) 连接以 x , y x, y x,y 为根的子树;
  • cut(x) x x x 的子树拆分成两个子节点,并删除 x x x

另外,WBLT 需要十分依赖于拆分和连接子树,需要使用垃圾回收无用结点。

int new_node(){// 使用垃圾回收新建结点 
	int x = tp ? pool[tp--] : ++id;
	sz[x] = val[x] = ls[x] = rs[x] = 0;
	return x;
}

void del_node(int &x){// 删除一个结点并垃圾回收 
	pool[++tp] = x;
	x = 0;
}

int new_leaf(int v){// 增加一个键值为 v 的叶子节点 
	int x = new_node();
	val[x] = v, sz[x] = 1;
	return x;// 并返回新节点编号 
}

int join(int x, int y){// 合并 x, y 子树并返回根 
	int z = new_node();// 新节点的根
	ls[z] = x, rs[z] = y;
	push_up(z);
	return z;
}

pii cut(int &x){// 将 x 的子树的两个儿子分裂并删除 x 
	int y = ls[x], z = rs[x];
	del_node(x);
	return {y, z};
}

平衡的概念

定义结点 x x x 的平衡度为( s z ( x ) sz(x) sz(x) 表示 x x x 的子树包括 x x x 的大小, l s x , r s x ls_x, rs_x lsx,rsx 分别表示 x x x 的左儿子和右儿子):

ρ ( x ) = min ⁡ { s z ( l s x ) , s z ( r s x ) } s z ( x ) \rho(x) = \frac{\min\{sz(ls_x), sz(rs_x)\}}{sz(x)} ρ(x)=sz(x)min{sz(lsx),sz(rsx)}

于是,我们再应用一个常量 α ∈ ( 0 , 1 2 ] \alpha \in (0, \frac{1}{2}] α(0,21]。如果结点 x x x 的平衡度 ρ ( x ) ≥ α \rho(x) \ge \alpha ρ(x)α,那么结点 x x x 就是 α \alpha α 平衡的。

直观理解一下:如果 s z ( l s x ) sz(ls_x) sz(lsx) s z ( r s x ) sz(rs_x) sz(rsx) 的大小相当的话, s z ( l s x ) s z ( r s x ) \frac{sz(ls_x)}{sz(rs_x)} sz(rsx)sz(lsx) 的值就不会太大。如果 s z ( l s x ) sz(ls_x) sz(lsx) 略小于 s z ( r s x ) sz(rs_x) sz(rsx) 的话,我们规定它们的比值 s z ( l s x ) s z ( r s x ) \frac{sz(ls_x)}{sz(rs_x)} sz(rsx)sz(lsx) 不能超过 α \alpha α,这样这棵树就是 α \alpha α 平衡的。

这样的话,类似于线段树的,这棵树的数高几乎在 log ⁡ n \log n logn 级别的。

神奇吧?

容易发现,WBLT 的旋转操作没什么用,但是有人证明了当

α ∈ ( 2 11 , 1 − 2 2 ] \alpha \in (\frac{2}{11}, 1 - \frac{\sqrt 2}{2}] α(112,122 ]

时, α \alpha α 是优的,也就是说 WBLT 是比较平衡的。

通过合并维护平衡

一般地,我们设 x x x 的左右儿子分别是 u , v u, v u,v,那么满足:

ρ ( x ) ( s z ( u ) + s z ( v ) ) = min ⁡ { s z ( u ) , s z ( v ) } \rho(x) (sz(u) + sz(v)) = \min\{sz(u), sz(v)\} ρ(x)(sz(u)+sz(v))=min{sz(u),sz(v)}

一般令 s z ( u ) > s z ( v ) sz(u) > sz(v) sz(u)>sz(v),所以:

α ( s z ( u ) + s z ( v ) ) ≤ s z ( v ) \alpha(sz(u) + sz(v)) \le sz(v) α(sz(u)+sz(v))sz(v)

现在我们要合并 x , y x, y x,y

  • 如果 y y y 为空,那么返回 y y y
  • 如果 x , y x, y x,y 已经平衡,即 α ( s z ( x ) + s z ( y ) ) ≤ s z ( y ) \alpha(sz(x) + sz(y)) \le sz(y) α(sz(x)+sz(y))sz(y),直接连接 x , y x, y x,y(如图 1);
  • 如果 y y y 过轻,就需要用 x x x 的右儿子 w w w 来帮忙稍微调和一下局面,也就是说,如果 z z z w + y w + y w+y 能平衡的话,先合并 w , y w, y w,y,再合并 z z z w + y w + y w+y(如图 2)。
  • 如果在合并 z , w + y z, w+y z,w+y 的时候发现 z z z 过轻,那么就先合并 z , u z, u z,u 再合并 v , y v, y v,y,最后合并 z + u , v + y z + u, v + y z+u,v+y(如图 3)。

![[Pasted image 20250814092500.png]]

请注意,在此期间我们并不需要注意结点的开销,因为信息都在叶子节点,所以中间删掉的 x x x w w w 之类的结点都不重要。

上述的操作是可以递归实现的。

int merge(int x, int y){
	if(!x || !y)	return x | y;// 如果有一个点为空,返回另一个点
	pii p;
	if(hvy(sz[x], sz[y])){// 如果左子树过重 
		p = cut(x);
		int a = p.first, p = x.second;// 将 x 的左右儿子分离出来 
		if(hvy(sz[b] + sz[y], sz[a])){// 如果此时 b + y 又更重了 
			p = cut(b);
			int c = p.first, d = p.second;// 将 b 的左右儿子分离
			return merge(merge(a, c), merge(d, y)); 
		}
		else	return merge(a, merge(b, y));// 直接合并 
	}
	else if(hvy(sz[y], sz[x])){// 右儿子过重同理 
		p = cut(y);
		int a = p.first, b = p.second;// 分离 y 的左右儿子
		if(hvy(sz[x] + sz[a], sz[b])){
			p = cut(a);
			int c = p.first, d = p.second;
			return merge(merge(x, c), merge(d, b));
		}
		else	return merge(merge(x, a), b);
	}
	else	return join(x, y);// 如果平衡直接合并 
}

void balance(int &x){// 维护结点 x 的平衡 
	if(sz[x] == 1)	return;
	if(hvy(sz[ls[x]], sz[rs[x]]) || hvy(sz[rs[x]], sz[ls[x]])){
		pii p = cut(x);
		int a = p.first, b = p.second;
		x = merge(a, b);
	}
}

至此,我们终于完成了 WBLT 的辅助函数。

WBLT 的基础操作

建树

类似于线段树的建树操作。

int build(int l, int r){
	if(l == r)	return new_leaf(l);
	int mid = (l + r) >> 1;
	return join(build(l, m), build(m + 1, r));
}
插入,删除

插入时,找到第一个大于等于 v v v 的叶子 x x x,然后将 x x x 增加两个儿子,其中左儿子代表新增加的 v v v,右儿子代表原来叶子 x x x 所代表的值。

而删除时,找到结点的兄弟一起删除并用父亲代替兄弟结点原本的值。

void insert(int &x, int v){
	if(!x)	x = new_leaf(v);// 新增一个权值为 v 的结点
	else if(sz[x] == 1){// 到了叶子节点 
		if(v >= val[x]){
			rs[x] = new_leaf(v);
			ls[x] = new_leaf(val[v]);
		}
		else{
			rs[x] = new_leaf(val[v]);
			ls[x] = new_leaf(v);
		}
		push_up(x);
	}
	else{// 没到叶子节点就递归,比较好理解 
		if(v > val[ls[x]])	insert(rs[x], v);
		else	insert(ls[x], v);
		push_up(x);
		balance(x);
	}
}

bool remove(int &x, int v){// 删除权值为 v 的叶子结点 
	if(!x)	return false;
	if(sz[x] == 1){
		if(val[x] == v){
			del_node(x);
			return true;
		}
		return false;
	}
	else{
		bool flg;
		if(v > val[ls[x]])// 乱删 
			flg = remove(rs[x], v);
		else	flg = remove(ls[x], v);
		if((!ls[x]) || (!rs[x])){// 万一删没了就更新一下儿子 
			if(!ls[x])	x = rs[x];
			else	x = ls[x];
		}
		else{
			push_up(x);
			balance(x);
		}
		return flg;
	}
}
查找 v v v 的排名
int cnt_less(int x, int v){// 寻找 x 子树中 < v 的数量个数 
	if(!x)	return 0;
	int res = 0;
	while(sz[x] > 1){
		if(val[ls[x]] < v)	res += sz[ls[x]], x = rs[x];
		else	x = ls[x];
	}
	return res + (val[x] < v ? 1 : 0);
}
查询第 k k k
int find_kth(int x, int k){
	while(sz[x] > 1){
		if(sz[ls[x]] >= k)	x = ls[x];
		else	k -= sz[ls[x]], x = rs[x];
	}
	return val[x];
}
查询前驱后继
int find_pre(int v){
	return find_kth(rt, Rank(rt, v) - 1);
}

int find_nxt(int v){
	return find_kth(rt, Rank(rt, v + 1));
}

分裂

pii split(int x, int k){// 将左半部分的前 k 个分裂出来 
	if(!x)	return {0, 0};
	if(!k)	return {0, x};
	if(k == sz[x])	return {x, 0};
	pii p = cut(x);
	int a = p.first, b = p.second;
	if(k <= sz[a]){
		pii p = split(a, k);
		int l = p.first, r = p.second;
		return {l, merge(r, b)};
	}
	else{
		pii p = split(b, k - sz[a]);
		int l = p.first, r = p.second;
		return {merge(a, l), r};
	}
}

【模板】普通平衡树

呜呜呜终于写完了。

#include <bits/stdc++.h>
#define int long long
#define pb push_back
#define pii pair<int, int>
#define low_bit(x) ((x)&-(x))
#define min(a,b) ((a)<(b)?(a):(b))
#define max(a,b) ((a)>(b)?(a):(b))
using namespace std;
using ll = long long;
const int N = 4e5 + 5;

int sz[N], val[N], ls[N], rs[N], id;
int pool[N], tp, rt;
double alp = 0.25;

void push_up(int x){
	sz[x] = sz[ls[x]] + sz[rs[x]];
//	val[x] = val[rs[x]];
	val[x] = max(val[ls[x]], val[rs[x]]);
}

int new_node(){// 使用垃圾回收新建结点 
	int x = tp ? pool[tp--] : ++id;
	sz[x] = val[x] = ls[x] = rs[x] = 0;
	return x;
}

void del_node(int &x){// 删除一个结点并垃圾回收 
	pool[++tp] = x;
	x = 0;
}

int new_leaf(int v){// 增加一个键值为 v 的叶子节点 
	int x = new_node();
	val[x] = v, sz[x] = 1;
	return x;// 并返回新节点编号 
}

int join(int x, int y){// 合并 x, y 子树并返回根 
	int z = new_node();// 新节点的根
	ls[z] = x, rs[z] = y;
	push_up(z);
	return z;
}

pii cut(int &x){// 将 x 的子树的两个儿子分裂并删除 x 
	int y = ls[x], z = rs[x];
	del_node(x);
	return {y, z};
}

bool hvy(int sx, int sy){
	return sy < alp * (sx + sy);
}

int merge(int x, int y){
	if(!x || !y)	return x | y;// 如果有一个点为空,返回另一个点
	pii p;
	if(hvy(sz[x], sz[y])){// 如果左子树过重 
		p = cut(x);
		int a = p.first, b = p.second;// 将 x 的左右儿子分离出来 
		if(hvy(sz[b] + sz[y], sz[a])){// 如果此时 b + y 又更重了 
			p = cut(b);
			int c = p.first, d = p.second;// 将 b 的左右儿子分离
			return merge(merge(a, c), merge(d, y)); 
		}
		else	return merge(a, merge(b, y));// 直接合并 
	}
	else if(hvy(sz[y], sz[x])){// 右儿子过重同理 
		p = cut(y);
		int a = p.first, b = p.second;// 分离 y 的左右儿子
		if(hvy(sz[x] + sz[a], sz[b])){
			p = cut(a);
			int c = p.first, d = p.second;
			return merge(merge(x, c), merge(d, b));
		}
		else	return merge(merge(x, a), b);
	}
	else	return join(x, y);// 如果平衡直接合并 
}

void balance(int &x){// 维护结点 x 的平衡 
	if(sz[x] == 1)	return;
	if(hvy(sz[ls[x]], sz[rs[x]]) || hvy(sz[rs[x]], sz[ls[x]])){
		pii p = cut(x);
		int a = p.first, b = p.second;
		x = merge(a, b);
	}
}

//int build(int l, int r){
//	if(l == r)	return new_leaf(l);
//	int mid = (l + r) >> 1;
//	return join(build(l, mid), build(mid + 1, r));
//}

void insert(int &x, int v){
	if(!x)	x = new_leaf(v);// 新增一个权值为 v 的结点
	else if(sz[x] == 1){// 到了叶子节点 
		if(v >= val[x]){
			rs[x] = new_leaf(v);
			ls[x] = new_leaf(val[x]);
		}
		else{
			rs[x] = new_leaf(val[x]);
			ls[x] = new_leaf(v);
		}
		push_up(x);
	}
	else{// 没到叶子节点就递归,比较好理解 
		if(v > val[ls[x]])	insert(rs[x], v);
		else	insert(ls[x], v);
		push_up(x);
		balance(x);
	}
}

bool remove(int &x, int v){// 删除权值为 v 的叶子结点 
	if(!x)	return false;
	if(sz[x] == 1){
		if(val[x] == v){
			del_node(x);
			return true;
		}
		return false;
	}
	else{
		bool flg;
		if(v > val[ls[x]])// 乱删 
			flg = remove(rs[x], v);
		else	flg = remove(ls[x], v);
		if((!ls[x]) || (!rs[x])){// 万一删没了就更新一下儿子 
			if(!ls[x])	x = rs[x];
			else	x = ls[x];
		}
		else{
			push_up(x);
			balance(x);
		}
		return flg;
	}
}

int Rank(int x, int v){// 寻找 x 子树中 < v 的数量个数 
	if(!x)	return 0;
	int res = 0;
	while(sz[x] > 1){
//		cout<<val[ls[x]]<<" val[x]\n";
		if(val[ls[x]] < v)	res += sz[ls[x]], x = rs[x];
		else	x = ls[x];
//		cout<<res<<" res\n";
	}
	return res + (val[x] < v);
}

int find_kth(int x, int k){// 查询第 k 大 
	while(sz[x] > 1){
		if(sz[ls[x]] >= k)	x = ls[x];
		else	k -= sz[ls[x]], x = rs[x];
	}
	return val[x];
}

int find_pre(int v){
	return find_kth(rt, Rank(rt, v));
}

int find_nxt(int v){
	return find_kth(rt, Rank(rt, v + 1) + 1);
}

//pii split(int x, int k){// 将左半部分的前 k 个分裂出来 
//	if(!x)	return {0, 0};
//	if(!k)	return {0, x};
//	if(k == sz[x])	return {x, 0};
//	pii p = cut(x);
//	int a = p.first, b = p.second;
//	if(k <= sz[a]){
//		pii p = split(a, k);
//		int l = p.first, r = p.second;
//		return {l, merge(r, b)};
//	}
//	else{
//		pii p = split(b, k - sz[a]);
//		int l = p.first, r = p.second;
//		return {merge(a, l), r};
//	}
//}

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	int n;	cin>>n;
	while(n--){
		int op, x;	cin>>op>>x;
		if(op == 1)	insert(rt, x);
		if(op == 2)	remove(rt, x);
		if(op == 3)	cout<<Rank(rt, x) + 1<<'\n';
		if(op == 4)	cout<<find_kth(rt, x)<<'\n'; 
		if(op == 5)	cout<<find_pre(x)<<'\n';
		if(op == 6)	cout<<find_nxt(x)<<'\n';
	}
	return 0;
}

参考文章 - OI-wiki

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值