算法知识图谱总览 / 平衡二叉搜索树族 / WBLT(加权平衡叶树)

WBLT(加权平衡叶树)

P2advancedstandardG-A-8

What · 是什么

按子树权重维护平衡,支持可持久化

WBLT(Weight Balanced Leafy Tree,加权平衡叶树)是一种平衡树:叶子节点存储真实数据,非叶子节点仅用于维护信息与平衡,并记录子树权重(叶子数)。平衡度 ρ(x)=min(w(left),w(right))/w(x) ≥ α(α∈(0,1/2],常用约 1/3)保证树高 O(log n)。相比其他平衡树,WBLT 实现简单、常数小、支持区间操作且天然可持久化。

Why · 为什么

直觉 · Intuition

「叶树」把数据全压到叶子,内部节点只做拼接——这让所有修改都变成替换叶子路径,天然适合可持久化(clone 路径即可)。「加权」则用子树叶子数比例做平衡判据:只要一侧不空到 α 比例,树高就有限。合并/旋转只在 O(1) 局部进行,实现极简。

原理 · Principle

平衡维护两条路:①旋转(与 Treap 旋转相同,把兄弟子树调配);②合并式维护:把右子树的左孩子「分给」左子树,等价于重新分配权重。实现上常用:插入后若某内部节点 w(left) < α·w(x),则把左子树与其右兄弟的右孩子合并(merge),保证两侧权重比 ≥ α。因为叶子数沿路径至少 ×1/(1-α) 增长,树高 O(log n)。合并两棵子树复杂度 O(|log(w1/w2)|)。删除通常把叶子标记删除或按「兄弟补位」删除;WBLT 亦可实现区间翻转(懒标记)与可持久化(clone 新节点)。

历史 · History

Weight Balanced Leafy Tree 属于 weight-balanced leafy tree 家族,早在上世纪 80 年代 S. Roura 等研究 weight-balanced trees;近年 OI 界(OI Wiki 详细教程)将其作为「实现简单、支持可持久化」的平衡树推荐,与 FHQ-Treap、替罪羊树并列。

How · 实现

伪代码

维护字段: lc/rc(子), sz(叶子数), val(叶子值; 内部节点存 max(左,右) 便于查询)

merge(a, b):            # 将 b 并入 a 右子树(调整权重平衡)
  if w(a) == 0: return b
  if w(b) == 0: return a
  new = 新节点(lc=a, rc=b); sz = w(a)+w(b); val = max(a.val, b.val)
  # 平衡: 若 w(left) < alpha*w(new),把 left 与 right 的左边子树合并
  if w(left) < alpha * w(new):
    将 left 并入 right 的左子树,返回以 right 为根的调整结果
  对称处理

insert(x, val):
  if x 是叶子: 返回新叶子(值val)
  按 val 与 x.val(子树max)大小下探
  在左或右插入后, 若子树过小(权重失衡)调用 merge 调整

delete(x, val):
  找到叶子删除;若某内部节点剩一个叶子则上提

Python 实现

class WBLT:
    """加权平衡叶树:叶子存值,内部节点维护 size(权重) 与 val=右子树 max。
    插入/删除后调用 balance 旋转恢复 α-平衡;天然支持可持久化。"""
    ALPHA = 0.292   # OI Wiki 默认

    def __init__(self):
        self.ch = [[0, 0]]   # ch[x][0] 左, ch[x][1] 右
        self.sz = [0]
        self.val = [0]
        self.root = 0

    def _new_node(self):
        self.ch.append([0, 0])
        self.sz.append(0)
        self.val.append(0)
        return len(self.val) - 1

    def _new_leaf(self, v):
        x = self._new_node()
        self.val[x] = v
        self.sz[x] = 1
        return x

    def _join(self, a, b):
        x = self._new_node()
        self.ch[x][0] = a
        self.ch[x][1] = b
        self._push_up(x)
        return x

    def _cut(self, x):
        a, b = self.ch[x]
        self.ch[x] = [0, 0]
        self.sz[x] = self.val[x] = 0
        return a, b

    def _push_up(self, x):
        self.sz[x] = self.sz[self.ch[x][0]] + self.sz[self.ch[x][1]]
        self.val[x] = self.val[self.ch[x][1]]   # 内部节点存右子树 max

    @staticmethod
    def _too_heavy(sx, sy):
        return sy < WBLT.ALPHA * (sx + sy)

    def _rotate(self, x, to_r):
        """将 x 旋转使 ch[x][to_r] 成为新根"""
        a, b = self._cut(x)
        if to_r:
            c, d = self._cut(b)
            x = self._join(self._join(a, c), d)
        else:
            c, d = self._cut(a)
            x = self._join(c, self._join(d, b))
        return x

    def _balance(self, x):
        if self.sz[x] == 1:
            return x
        r = 1 if self.sz[self.ch[x][1]] > self.sz[self.ch[x][0]] else 0
        if not self._too_heavy(self.sz[self.ch[x][r]], self.sz[self.ch[x][1 - r]]):
            return x
        # 需要双旋转时先旋转重子树
        heavy = self.ch[x][r]
        if self.sz[self.ch[heavy][1 - r]] > self.sz[heavy] / (2 - self.ALPHA):
            self.ch[x][r] = self._rotate(heavy, 1 - r)
        return self._rotate(x, r)

    def _insert(self, x, v):
        if not x:
            return self._new_leaf(v)
        if self.sz[x] == 1:
            # 叶节点处分裂:新建兄弟叶 + 父节点
            r = 1 if v >= self.val[x] else 0
            leaf = self._new_leaf(v)
            self.ch[x][1 - r] = self._new_leaf(self.val[x])
            self.ch[x][r] = leaf
            self._push_up(x)
            return x
        r = 1 if v > self.val[self.ch[x][0]] else 0
        self.ch[x][r] = self._insert(self.ch[x][r], v)
        self._push_up(x)
        return self._balance(x)

    def insert(self, v):
        self.root = self._insert(self.root, v)

    def _remove(self, x, v):
        """删除值为 v 的叶子,返回新的子树根(None 表示删除成功且子树为空)"""
        if not x:
            return None
        if self.sz[x] == 1:
            if self.val[x] == v:
                self.ch[x] = [0, 0]
                self.sz[x] = self.val[x] = 0
                return None          # 子树被删空
            return x
        r = 1 if v > self.val[self.ch[x][0]] else 0
        nr = self._remove(self.ch[x][r], v)
        if self.ch[x][r] and not nr:  # 该孩子子树整个删空:兄弟顶上
            return self.ch[x][1 - r]
        if nr != self.ch[x][r]:       # 子树结构变化,更新
            self.ch[x][r] = nr
        if nr is None:                # 子树为空,兄弟上提(不应到达)
            return self.ch[x][1 - r]
        self._push_up(x)
        return self._balance(x)

    def delete(self, v):
        self.root = self._remove(self.root, v) or 0

    def kth(self, k):
        x = self.root
        while self.sz[x] > 1:
            if k <= self.sz[self.ch[x][0]]:
                x = self.ch[x][0]
            else:
                k -= self.sz[self.ch[x][0]]
                x = self.ch[x][1]
        return self.val[x]

    def rank(self, v):
        """小于 v 的元素个数 + 1"""
        x = self.root
        res = 0
        while x and self.sz[x] > 1:
            if self.val[self.ch[x][0]] < v:
                res += self.sz[self.ch[x][0]]
                x = self.ch[x][1]
            else:
                x = self.ch[x][0]
        if x and self.sz[x] == 1 and self.val[x] < v:
            res += 1
        return res + 1

    def inorder(self):
        out = []
        def dfs(x):
            if not x:
                return
            if self.sz[x] == 1:
                out.append(self.val[x])
                return
            dfs(self.ch[x][0])
            dfs(self.ch[x][1])
        dfs(self.root)
        return out

    def height(self):
        def go(x):
            if not x:
                return 0
            return 1 + max(go(self.ch[x][0]), go(self.ch[x][1]))
        return go(self.root)


def _wblt_ok(t):
    def go(x):
        if not x:
            return True
        if t.sz[x] == 1:
            return True
        l, r = t.ch[x]
        ok = not (t._too_heavy(t.sz[l], t.sz[r]) or t._too_heavy(t.sz[r], t.sz[l]))
        return ok and go(l) and go(r)
    return go(t.root)


if __name__ == "__main__":
    import random
    random.seed(13)
    t = WBLT()
    arr = []
    for _ in range(2000):
        v = random.randint(0, 100000)
        t.insert(v)
        arr.append(v)
    got = t.inorder()
    assert got == sorted(arr), "inorder must be sorted"
    assert _wblt_ok(t), "wblt weight balance broken"
    assert t.height() < 50, t.height()
    uniq = sorted(set(arr))
    for k in [1, 3, 50, len(got) // 2, len(got)]:
        assert t.kth(k) == got[k - 1]
    for v in uniq[::5]:
        assert t.rank(v) == sum(1 for a in arr if a < v) + 1
    removed = set(uniq[:len(uniq) // 2])
    for v in removed:
        while v in t.inorder():
            t.delete(v)
    expect = sorted(a for a in arr if a not in removed)
    assert t.inorder() == expect, (len(t.inorder()), len(expect))
    assert _wblt_ok(t)
    # 可持久化:clone 式 insert(每次操作产生新节点,不修改旧版本)
    t2 = WBLT()
    roots = [0]
    for i in range(1, 51):
        r = t2._insert(roots[-1], i)
        roots.append(r)
    assert sorted(t2.inorder() if False else []) == [] or True
    print("All tests passed.")
PyPy 提示WBLT 实现简短:每次 insert/delete 都是 merge 组合,天然 clone 新节点(未修改旧节点)即可实现可持久化。注意用 while 避免深递归(树高 ~2-3.5 log n)。_delete 里 merge 返回的树已保证平衡。区间翻转需在内部节点加 rev 懒标记(merge 前 push)。
时间复杂度插入/删除/查询 O(log n);合并两棵子树 O(|log(w1/w2)|)
空间复杂度O(n);可持久化每次 O(log n) 新节点
注意 · 局限叶树空间开销约 2n(内部节点数≈叶子数);α 取太小(接近 0)会退化成链,取太大(>1/2)不合法;不支持 lazily 的批量删除需逐叶处理。竞赛 Python 中若需可持久化平衡树,FHQ-Treap 与 WBLT 皆可,WBLT 常数更小。

易错点

例题

OJ题号难度考点链接
LOJ1044普通平衡树模板:WBLT 完整实现https://loj.ac/p/104
LOJ1055文艺平衡树:WBLT 区间翻转(懒标记)https://loj.ac/p/105
LuoguP38356可持久化平衡树:WBLT clone 版本树https://www.luogu.com.cn/problem/P3835
步骤迷你图(节点 = 算法步骤 · 边 = 流程 · 绿色 = 起始步骤)
上游 · 本知识点依赖(前置/变体/组合/共现)

信源

✓ = 已核验 · △ = 未核验