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,1−22]
时, α \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

3万+

被折叠的 条评论
为什么被折叠?



