可持久化WBLT 学习笔记
什么是 WBLT
WBLT 是一种 Leafy 的平衡二叉树,即序列中的元素都挂在叶子处,而非叶节点则处理子树内叶子的信息合并,线段树就是典型的 Leafy 树。每个非叶节点都有两个儿子,不难发现对于长为 \(n\) 序列,其 WBLT 有 \(2n-1\) 个节点。
WBLT 的树高为 \(O(\log n)\),除了支持平衡树的基本操作外,还支持分裂、合并、可持久化,这和 FHQ_Treap 拥有的功能类似,但 WBLT 常数小,且拥有 Leafy 的性质,这使得我们可以把一个点的信息挂在其父亲上而不影响其他无关节点。
合并操作
我们用合并操作保证 WBLT 的树高,我们设定一个常数 \(\alpha\),并使得一个点两个子树 \(x,y\) 满足 \(\frac {\max\{sz_x,sz_y\}}{sz_x+sz_y}\ge\alpha\),其中 \(\alpha\approx 0.292\)。在下面的过程和代码中,如果 \(sz_x>3\times sz_y\),我们称 \(x\) 过重,不满足平衡。
假如合并 \(x,y\) 且 \(sz_x>sz_y\),有以下递归合并的策略,复杂度为 \(O(\log \frac {sz_x}{sz_y})\)
- 如果 \(x\) 过重,考虑其左右儿子 \(u,v\)。
- 若 \(v+y\) 仍过重,考虑 \(v\) 的左右儿子 \(w,z\),递归将 \(u,w\) 合并、\(z,y\) 合并,并将两者的结果合并。
- 否则将 \(u\) 与 \(v+y\) 合并的结果合并。
- 否则已平衡,设立新点,令新点左右儿子为 \(x,y\)。
实现如下:
pi cut(int x) {pushdown(x); del(x); return {ls[x],rs[x]};
}
bool hev(int x,int y) {return x>3*y;}
int merge(int x,int y) {if(!x||!y) return x|y;if(hev(sz[x],sz[y])) {auto [u,v]=cut(x);if(hev(sz[v]+sz[y],sz[u])) {auto [w,z]=cut(v);return merge(merge(u,w),merge(z,y));}return merge(u,merge(v,y));}if(hev(sz[y],sz[x])) {auto [u,v]=cut(y);if(hev(sz[x]+sz[u],sz[v])) {auto [w,z]=cut(u);return merge(merge(x,w),merge(z,v));}return merge(merge(x,u),v);}return join(x,y);
}
其中 cut 是先将标记下传,再回收节点,因为在 merge 中要找一个点的左右儿子时,可以发现这个点就没用了。而 join 则是新建一个节点并连接它们并上传信息。
注意在 WBLT 中回收节点很重要,因为合并与分裂过程会产生很多垃圾节点,不然可能爆空间。
分裂操作
复杂度为树高,即 \(O(\log n)\)。同样这里使用 cut 用于下传标记并回收节点。
pi split(int x,int y) {if(!x) return {0,0};if(!y) return {0,x};if(sz[x]==y) return {x,0};auto [u,v]=cut(x);if(sz[u]>y) {auto t=split(u,y);return {t.fi,merge(t.se,v)};}auto t=split(v,y-sz[u]);return {merge(u,t.fi),t.se};
}
插入、删除、区间查询
由于 Leafy 树的性质,插入、删除、查询可以不用分裂。
找到要插入和删除的叶子位置,将其与新叶子合并或将其变成空节点,然后自底向上原路合并即可。实现如下:
int insert(int x,int y,int z) {if(!x) return leaf(z);if(sz[x]==1) {int a=nw(),b=leaf(z);ls[a]=x,rs[a]=b;pushup(a);return a;}pushdown(x);if(y<=sz[ls[x]]) return merge(insert(ls[x],y,z),rs[x]);else return merge(ls[x],insert(rs[x],y-sz[ls[x]],z));
}
int erase(int x,int y) {if(sz[x]==1) return 0;pushdown(x);if(y<=sz[ls[x]]) return merge(erase(ls[x],y),rs[x]);else return merge(ls[x],erase(rs[x],y-sz[ls[x]]));
}
查询则类似线段树的查询。
注意递归时要下方标记即可。
可持久化
我们给每个点记录 \(hs_x\) 表示其版本编号,再记录一个全局编号 \(his\),用于防止重复复制节点。那么我们在下放标记时复制节点即可,而在合并过程中新建的节点都看做当前版本的节点。
我们实现一个 refresh 函数,其接受一个引用,用于将旧节点复制成当前版本的节点。
int nw() {int x=(top?was[top--]:++tot); // was 为回收的垃圾节点栈... // 初始化hs[x]=his;return x;
}
void refresh(int &x) {if(hs[x]==his) return; // 已经是新版本节点无需复制int y=nw();... // 将 x 的信息复制到 yhs[y]=his,x=y;
}
void pushdown(int &x) {if(sz[x]==1) return;refresh(x); // 这里一定要 refresh 防止更改旧版本出现未知问题if(...) { // 存在懒标记... // 更新信息 refresh(ls[x]);refresh(rs[x]);... // 下方标记 清空标记}
}
