介绍一个时间复杂度比非旋 Treap 更优,更稳定,不依赖随机数的算法,WBLT。
什么是 WBLT?
$\text{Weight Balanced Leafy Tree}$,是根据 $\text{Weight}$ 平衡的,信息记录在 $\text{Leafy Nodes}$ 上的树。讲人话就是一颗平衡线段树。大概结构就是这样:
graph TD
A("1
w=3,sum=9") --> B("2
w=3,sum=9") --> C["3
w=1,sum=3"]
B --> D["4
w=1,sum=2"]
A --> E["5
w=1,sum=4"]
- 可以看到,其实 $w$ 就代表当前节点子树内有多少个叶子节点。
- 我们发现,WBLT 是满二叉树。
- WBLT 的 $\text{Non Leafy Node}$ 的左右子树都是 WBLT。
- 我们定义一个 WBLT 是 $\text{Balanced}$,当且仅当对于任意一个非叶子节点都有 $\dfrac{\min(w_l, w_r)}{w} \ge \alpha$。原论文指出 $\alpha \in [\dfrac{2}{11}, 1 - \dfrac{\sqrt 2}{2}]$ 可以保证时间复杂度是正确的,显然 $\alpha=\dfrac{1}{4}$ 是一个再好不过的选择了。
它支持什么?
- 区间操作(加、乘、懒标记等)
- 区间查询(和,矩阵乘积等)
- 区间复制
- 可持久化
时间复杂度:$\mathcal O(n\log n)$
空间复杂度:$\mathcal O(n)$
可以看出,几乎就是一个时间复杂度完全正确的,和 FHQ Treap 没什么两样的平衡树,甚至!比 FHQ Treap 还好写!!!
基础函数
-
1
inline bool needBalance(int wl, int wr) { return wr * 3 < wl; }
这个函数就是告诉你 $wl$ 比 $wr$ 重太多了,要
减肥平衡了! -
结构体定义(使用了语法糖):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16enum {L, R};
struct Node {
int ch[2], w, refCnt;
LL sum, add; // tag
Node& operator()(bool c);
inline int& operator[](bool c) { return ch[c]; }
inline void pushup() { if (w^1) sum = (*this)(L).sum + (*this)(R).sum, w = (*this)(L).w + (*this)(R).w; }
inline void pull(LL x) { add += x; sum += x * w; }
Node() : ch{}, w(), refCnt(), sum(), add() {}
Node(int v) : ch{}, w(1), refCnt(1), sum(v), add() {}
Node(int l, int r) : ch{l, r}, w(), refCnt(1), sum(), add() { pushup(); }
} tr[N << 4];
inline Node& Node::operator()(bool c) { return tr[ch[c]]; }这里,如果构造函数传 1 个参数,就是建一个 $\text{Leafy Node}$,否则就是根据给定的 $ls, rs$ 建 $\text{Non Leafy Node}$ 并且
pushup。 -
我们在进行平衡操作的时候,使用
merge来维护平衡。- 如果有两颗树,左边树所有点下标(第一关键字)小于右边的,怎么合并成一颗?
具体的,如果对于 $u$ 不平衡,我们现在假定了左右孩子的左右子树都是平衡的,那我们就可以通过如图所示的方式进行平衡:
flowchart LR subgraph BEF[ ] direction TB A-->B([3])-->C((1)); B-->D([2])-->E((1)); B-->F((1)); A-->G((1)); end subgraph AFT[ ] direction TB a --> b([2]) --> c((1)); b-->d((1)); a --> e([2]) --> f((1)); e --> g((1)); end BEF --> AFT(这里的节点上的数字都代表 $w$)
这只是一种例子,还有很多别的情况都大同小异,其实和 FHQ Treap 的合并没有什么两样。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20inline int merge(int l, int r) {
if (!l || !r) return l | r;
if (needBalance(tr[l].w, tr[r].w)) { // l too heavy
auto [ll, lr] = cut(l);
if (needBalance(tr[lr].w + tr[r].w, tr[ll].w)) {
auto [lrl, lrr] = cut(lr);
return alloc(alloc(ll, lrl), alloc(lrr, r));
}
return alloc(ll, alloc(lr, r));
}
if (needBalance(tr[r].w, tr[l].w)) { // r too heavy
auto [rl, rr] = cut(r);
if (needBalance(tr[l].w + tr[rl].w, tr[rr].w)) {
auto [rll, rlr] = cut(rl);
return alloc(alloc(l, rll), alloc(rlr, rr));
}
return alloc(alloc(l, rl), rr);
}
return alloc(l, r);
}当然也有单双旋的版本:
Single/Double Rotate 严格 $\mathcal O(1)$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32inline bool needDoubleRot(int u, bool x) { return tr[u](x).w * 2 < tr[u](!x).w; }
inline void rotate(int &u, bool x) { // 将 u->x 旋转到 u
auto [l, r] = cut(u);
if (x) { // r heavier
auto [rl, rr] = cut(r);
u = alloc(alloc(l, rl), rr);
} else {
auto [ll, lr] = cut(l);
u = alloc(ll, alloc(lr, r));
}
}
inline void balance(int &u) {
if (tr[u].w == 1) return;
bool x = tr[u](R).w > tr[u](L).w; // 重儿子编号
if (!needBalance(tr[u](x).w, tr[u](!x).w)) return;
if (needDoubleRot(tr[u][x], x)) rotate(tr[u][x], !x); // 先一边倒,然后拉回来
rotate(u, x);
}
inline int merge(int l, int r) {
if (!l || !r) return l | r;
if (needBalance(tr[l].w, tr[r].w)) { // l too heavy
auto [ll, lr] = cut(l);
int u = alloc(ll, alloc(lr, r));
balance(u); return u;
}
if (needBalance(tr[r].w, tr[l].w)) { // r too heavy
auto [rl, rr] = cut(r);
int u = alloc(alloc(l, rl), rr);
balance(u); return u;
}
return alloc(l, r);
} -
如果在区间复制的情景下:
1
2
3
4template<class... Args> inline int alloc(Args... args) {
int u = bintop ? bin[bintop--] : ++top;
tr[u] = Node(args...); return u;
}使用 引用计数 保证空间复杂度线性。
我们定义 $ref$ 表示引用的次数,那么 $ref=0$ 的时候就可以完全删除了。1
2
3
4
5
6inline void recycle(int u) {
if (u && --tr[u].refCnt == 0) {
if (tr[u].w ^ 1) recycle(tr[u][L]), recycle(tr[u][R]);
bin[++bintop] = u;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24inline void check(int &u) {
if (tr[u].refCnt == 1) return;
--tr[u].refCnt;
if (tr[u].w != 1) ++tr[u](L).refCnt, ++tr[u](R).refCnt;
tr[u = alloc(tr[u])].refCnt = 1;
}
inline void pushdown(int &u) {
if (tr[u].w == 1 || !tr[u].add) return;
check(u);
if (tr[u][L]) check(tr[u][L]), tr[u](L).pull(tr[u].add);
if (tr[u][R]) check(tr[u][R]), tr[u](R).pull(tr[u].add);
tr[u].add = 0;
}
inline std::pair<int,int> cut(int& u) {
if (tr[u].w == 1) return {0,0};
pushdown(u);
++tr[u](L).refCnt, ++tr[u](R).refCnt;
std::pair<int,int> ret(tr[u][L], tr[u][R]);
recycle(u);
u = 0; // u 没啦!再用,你想 use-after-free 喵?
return ret;
}这里我们秉持着可持久化原则,如果我被人用了,那我修改节点信息肯定不能直接修改,要另起炉灶。但是我的两个小弟仍然归我,所以他们都又多了一个个师傅。
-
分割平衡树
1
2
3
4
5
6
7
8
9
10
11inline std::pair<int,int> split(int u, int ord) {
if (!ord) return {0, u};
if (ord == tr[u].w) return {u, 0};
auto [l, r] = cut(u);
if (ord <= tr[l].w) {
auto [ll, lr] = split(l, ord);
return {ll, merge(lr, r)};
}
auto [rl, rr] = split(r, ord - tr[l].w);
return {merge(l, rl), rr};
}(将 $u$ 这颗树的前 $ord$ 个节点分割出来,返回前 $ord$ 个节点的树的根指针 和 其余节点的树的根指针)
实用函数
-
建树
1
2
3
4
5int build(int l, int r) {
if (l == r) return alloc(a[l]);
int mid = l + r >> 1;
return alloc(build(l, mid), build(mid + 1, r));
}(将 $a[l] \dots a[r]$ 建成一颗树,返回根节点指针)
-
区间加
1
2
3
4
5
6
7
inline void plus(int L, int R, int d) {
fetch(L, R);
tr[u].pull(d);
root = merge(merge(l, u), r);
} -
区间复制
1
2
3
4
5
6
7
8
9
10inline void copy(int dL, int dR, int sL, int sR) {
int src; { // Copy src
fetch(sL, sR);
++tr[src = u].refCnt;
root = merge(merge(l, u), r);
}
fetch(dL, dR);
root = merge(merge(l, src), r);
recycle(u);
} -
提取原数组 $\mathcal O(n)$
1
2
3
4
5
6
7
8inline void extract(std::vector<int> &vec, int &u = root) {
if (tr[u].w == 1) vec.push_back(tr[u].sum);
else {
pushdown(u);
extract(vec, tr[u][L]);
extract(vec, tr[u][R]);
}
}