Archipelago

Lec 6整数优先队列:van Emde Boas 树与基数堆

讲义完成更新于 2026-09-17
后续

当前课程暂无后续已收录内容。

Lec 6 整数优先队列:van Emde Boas 树与基数堆(Integer Priority Queues: van Emde Boas Trees & Radix Heaps)

TL;DR

  • 全域开方分治实现双重对数界:van Emde Boas 树将大小为 UU 的全域划分为 U\sqrt{U} 个大小为 U\sqrt{U} 的子簇,通过顶层摘要与递归寻址,将搜索与动态维护时间降至严格的 O(log⁡log⁡U)O(\log\log U)。
  • 极值缓存确保单次递归执行:将子树最小值与最大值显式缓存在节点中,且最小值不递归向下存储;对空树的插入与提取退化为 O(1)O(1),从而在结构上保证每次操作最多只触发一次非平凡的递归调用。
  • 基数堆(Radix Heaps)在单调场景下的轻量优化:利用边权上界 CC 建立 O(log⁡C)O(\log C) 个按二进制位跨度划分的动态桶,配合单调性势能摊还,实现减键 O(1)O(1)、提取最小值 O(log⁡C)O(\log C),达到 O(m+nlog⁡C)O(m + n\log C) 的最短路性能。

整数优先队列与超对数加速

在标准比较模型中,信息论给出了二叉搜索树与优先队列的天然下界:任何基于两两比较的动态查找结构,单次操作至少需要 Ω(log⁡n)\Omega(\log n) 时间。

然而在计算机的字 RAM 模型(Word RAM)中,键值是长度为 ww 比特的非负整数,全域规模 U=2wU = 2^w。计算机硬件的算术逻辑单元(ALU)不仅支持比较,还支持在 O(1)O(1) 时间内执行按位与、位移、加减法与数组基地址变址寻址。

这种计算能力的释放催生了两类突破性结构:

  1. van Emde Boas 树(1977):面向全域 UU 上的全功能动态优先队列与前驱/后继查找,将复杂度推进至 O(log⁡log⁡U)O(\log\log U);
  2. 基数堆(Radix Heaps,Ahuja et al. 1990):针对 Dijkstra 单调优先队列场景,以极其紧凑的二进制动态桶结构实现 O(m+nlog⁡C)O(m + n \log C) 最短路径。

van Emde Boas 树的核心架构

全域递归分解:高半字与低半字

设全域大小为 U=22kU = 2^{2^k}(若非二次幂可通过补齐比特位向上取整)。任何属于 [0,U−1][0, U - 1] 的整数 xx 在二进制下均由高半字和低半字构成:

high(x)=⌊x/U⌋,low(x)=x mod U\mathrm{high}(x) = \lfloor x / \sqrt{U} \rfloor, \quad \mathrm{low}(x) = x \bmod \sqrt{U}
  • high(x)\mathrm{high}(x) 决定了 xx 属于哪一个规模为 U\sqrt{U} 的子簇(Cluster);
  • low(x)\mathrm{low}(x) 决定了 xx 在该子簇内部的具体位置;
  • 复合重构公式为:index(h,l)=h⋅U+l\mathrm{index}(h, l) = h \cdot \sqrt{U} + l。

节点组成与极值缓存设计

一个规模为 UU 的 van Emde Boas 树节点 VV 包含以下组件:

  1. min 与 max 缓存:
    • 存储当前以 VV 为根的结构中所包含的最小键值与最大键值;
    • 核心约束(Crucial Invariant):V.min 中存储的元素绝不向下递归存储在任何子簇中!
    • 如果 VV 只包含一个元素,则该元素仅记录在 V.min 中,其 V.max 亦等于该元素,而下属的所有子簇全部为空。
  2. 子簇数组 cluster[0 ... \sqrt{U}-1]:
    • 包含 U\sqrt{U} 个指针,每个指针指向一个规模为 U\sqrt{U} 的子 vEB 树。
  3. 摘要结构 summary:
    • 是一棵独立的、规模为 U\sqrt{U} 的 vEB 树;
    • 记录所有子簇的状态:当且仅当子簇 cluster[i] 包含至少一个元素(除去全局 V.min 外)时,下标 ii 才存在于 V.summary 中。

单递归定理与操作算法

vEB 树之所以能达到 O(log⁡log⁡U)O(\log\log U),根本在于其递归关系式:

T(U)=T(U)+O(1)T(U) = T(\sqrt{U}) + O(1)

令全域位宽为 b=log⁡Ub = \log U,则 T(b)=T(b/2)+O(1)  ⟹  T(b)=O(log⁡b)=O(log⁡log⁡U)T(b) = T(b/2) + O(1) \implies T(b) = O(\log b) = O(\log\log U)。 实现该递推式的铁律是:任何公共操作在每一层分解中,绝不能产生两次以上规模为 U\sqrt{U} 的递归调用。

1. 查找前驱与后继(Successor)

要在树 VV 中查找严格大于 xx 的最小元素 Successor(V, x):

  1. 边界检查:若 VV 为空或 x≥V.max⁡x \ge V.\max,返回不存在;若 x<V.min⁡x < V.\min,直接返回 V.min⁡V.\min(耗时 O(1)O(1))。
  2. 簇内查找:
    • 分解 h=high(x)h = \mathrm{high}(x),l=low(x)l = \mathrm{low}(x);
    • 检查 xx 所在子簇的极大值:若 l<V.cluster[h].max⁡l < V.\mathrm{cluster}[h].\max,说明后继必在该子簇内部,递归调用 Successor(V.cluster[h], l);
  3. 跨簇查找:
    • 若 l≥V.cluster[h].max⁡l \ge V.\mathrm{cluster}[h].\max,说明后继位于后续的某一个非空子簇中;
    • 在摘要树中查询下一个非空子簇号:succ_h=Successor(V.summary,h)succ\_h = \mathrm{Successor}(V.\mathrm{summary}, h);
    • 后继元素就是该非空簇中的最小值:l′=V.cluster[succ_h].min⁡l' = V.\mathrm{cluster}[succ\_h].\min;
    • 返回合成键值 index(succ_h,l′)\mathrm{index}(succ\_h, l')。

在上述逻辑中,分支 2 与分支 3 是严格互斥的,单次查询至多发生一次 U\sqrt{U} 规模的递归,总运行时间严格为 O(log⁡log⁡U)O(\log\log U)。

2. 插入元素(Insert)

向树 VV 中插入元素 xx:

  1. 空树插入:若当前 VV 为空(即 V.min == null),令 V.min⁡=V.max⁡=xV.\min = V.\max = x,直接返回(耗时 O(1)O(1))。
  2. 极小值交换:若 x<V.min⁡x < V.\min,将 xx 与 V.min⁡V.\min 互换。此时原先的 V.min⁡V.\min 成为待插入到子簇中的新元素。
  3. 分解与递归插入:
    • 设待插入元素为 xx,分解为 h=high(x)h = \mathrm{high}(x) 与 l=low(x)l = \mathrm{low}(x);
    • 分支 A(目标子簇为空):
      • 若 V.cluster[h].min⁡==nullV.\mathrm{cluster}[h].\min == \mathrm{null},说明该子簇之前为空;
      • 必须将子簇号 hh 插入到摘要树中:Insert(V.summary, h);
      • 随后将 ll 插入目标子簇:Insert(V.cluster[h], l)。
      • 关键洞察:向空子树插入元素只需更新其 min 和 max,在步骤 1 立即以 O(1)O(1) 结束!因此这里虽然有两次形式上的函数调用,但只有对 summary 的调用是真正的非平凡递归!
    • 分支 B(目标子簇非空):
      • 直接递归调用 Insert(V.cluster[h], l),而无需触碰 V.summary。递归调用同样只有一次!
  4. 更新极大值:若 x>V.max⁡x > V.\max,更新 V.max⁡=xV.\max = x。

3. 提取最小值与删除元素(Delete)

删除操作同样巧妙利用极值缓存避免多重递归:

  • 若删除的是 V.min,需要从非空子簇中找出第二小的元素提升为新的 V.min;
  • 通过 h=V.summary.min⁡h = V.\mathrm{summary}.\min 得到首个非空子簇;
  • 取得新最小值 x′=index(h,V.cluster[h].min⁡)x' = \mathrm{index}(h, V.\mathrm{cluster}[h].\min),将其填入 V.min,随后从该子簇中递归删除 V.cluster[h].min⁡V.\mathrm{cluster}[h].\min;
  • 若删除导致该子簇变空,则递归从 summary 中剔除 hh。由于子簇变空时其大小为 1,底层删除也是常数时间完成。
  • 总删除时间严格维持为 O(log⁡log⁡U)O(\log\log U)。

空间压缩:从 O(U)O(U) 到 O(n)O(n)

朴素 vEB 树预先分配所有子簇指针,递归空间递推为 S(U)=(U+1)S(U)+O(U)S(U) = (\sqrt{U} + 1)S(\sqrt{U}) + O(\sqrt{U}),解得空间为 O(U)O(U)。当字长 w=64w=64 时,U=264U = 2^{64},物理上无法预分配。

优化方案: 利用动态完美哈希或哈希表按需分配子簇(仅当子簇非空时才建立映射)。这种被称为 x-fast trie 与 y-fast trie(Willard 1983)的衍生结构,在期望 O(n)O(n) 或最坏 O(n)O(n) 线性空间下完整保留了 O(log⁡log⁡U)O(\log\log U) 的查询时间。

基数堆(Radix Heaps)与单调单源最短路

在单源最短路径(Dijkstra)问题中,边权上限为 CC。vEB 树虽然能给出 O(m+nlog⁡log⁡C)O(m + n\log\log C) 的极快时间,但指针层级较多且依赖动态分配。Ahuja、Mehlhorn、Orlin 与 Tarjan(1990)提出了基于二进制位动态重分配的基数堆(Radix Heaps)。

结构设计与二进制动态区间

设图的最大边权为 CC,定义 B=2+⌈log⁡2(C+1)⌉B = 2 + \lceil \log_2(C + 1) \rceil。基数堆维护 BB 个桶 b[0],b[1],…,b[B−1]b[0], b[1], \dots, b[B-1]。

每个桶 ii 关联一个闭区间 [ui,vi][u_i, v_i],负责接纳键值落在该区间内的所有节点。桶的区间大小随着当前堆顶提取的最小值单调动态调整:

  • 桶 0:始终只容纳唯一确定的单个键值(当前全局最小值);
  • 桶 1:容纳下一个连续的单个键值;
  • 桶 ii(i≥2i \ge 2):区间长度为 2i−12^{i-1},覆盖从 uiu_i 到 ui+2i−1−1u_i + 2^{i-1} - 1 的键值范围。

操作流程与桶的动态重分发(Rebucketing)

  1. Insert(x) / Decrease-Key(x):
    • 根据新键值直接通过位运算(异或与最高有效位查找 clz\mathrm{clz})定位所在桶;
    • 直接插入对应桶的双向链表,耗时 O(1)O(1)。
  2. Extract-Min():
    • 若桶 0 非空,直接弹出其中的节点(O(1)O(1));
    • 若桶 0 为空,寻找最小的非空桶 kk;
    • 线性扫描桶 kk 找出其中的最小键值 dmin⁡d_{\min};
    • 动态重分发(Rebucketing):
      • 将当前基准基数更新为 dmin⁡d_{\min};
      • 重新划分桶 0,1,…,k−10, 1, \dots, k-1 的区间界限(以 dmin⁡d_{\min} 为起点重新展开指数区间);
      • 将桶 kk 中的所有剩余元素依次取出,根据其新差值重新分发落入到更低编号的桶中!

势能分析与复杂度

为什么重分发不会拖垮时间?关键依然是单调性:

  • 在 Dijkstra 推进过程中,全局最小值单调上升;
  • 每次某个元素从桶 kk 中被倒出重新投递时,它必然落入编号严格小于 kk 的更低桶中!
  • 每个元素从初次进入堆到最终被弹出,最多只能从高编号桶向下移动到低编号桶。由于总桶数仅为 B=O(log⁡C)B = O(\log C),任何单个元素在整个算法生命周期内的总下移次数至多为 O(log⁡C)O(\log C) 次!

将重分发下推代价均摊到元素自身:

  • Decrease-Key:均摊代价 O(1)O(1);
  • Extract-Min:均摊代价 O(log⁡C)O(\log C);
  • 单源最短路径总运行时间为:
O(m+nlog⁡C)O(m + n \log C)

基数堆无需复杂的指针树递归,仅使用一维数组加双向链表,在工程实践与理论分析中均展现出极致的简洁与高效。

优先队列技术横向综合对比

数据结构最小键查询提取最小值减小键值插入操作空间复杂度最短路总时间
二叉堆 (Binary Heap)O(1)O(1)O(log⁡n)O(\log n)O(log⁡n)O(\log n)O(log⁡n)O(\log n)O(n)O(n)O(mlog⁡n)O(m \log n)
Fibonacci 堆O(1)O(1)O(log⁡n)∗O(\log n)^*O(1)∗O(1)^*O(1)O(1)O(n)O(n)O(m+nlog⁡n)O(m + n \log n)
Dial 算法O(1)O(1)O(C)∗O(C)^*O(1)O(1)O(1)O(1)O(n+C)O(n + C)O(m+nC)O(m + nC)
Denardo-Fox 惰性字典树O(1)O(1)O(C1/k)∗O(C^{1/k})^*O(1)∗O(1)^*O(k)∗O(k)^*O(n+kC1/k)O(n + k C^{1/k})O(m+nlog⁡Clog⁡log⁡C)O(m + n\frac{\log C}{\log\log C})
基数堆 (Radix Heap)O(1)O(1)O(log⁡C)∗O(\log C)^*O(1)∗O(1)^*O(1)∗O(1)^*O(n+log⁡C)O(n + \log C)O(m+nlog⁡C)O(m + n \log C)
van Emde Boas 树O(1)O(1)O(log⁡log⁡U)O(\log\log U)O(log⁡log⁡U)O(\log\log U)O(log⁡log⁡U)O(\log\log U)O(U)→O(n)O(U) \to O(n)O(m+nlog⁡log⁡C)O(m + n \log\log C)

我的理解

我的理解

vEB 树与基数堆是对“位”(Bit)信息的两种极高维利用: 传统数据结构将数字视为黑盒原子,只能通过大于小于进行探针式试探;而整数算法将数字视为二进制的几何坐标。

van Emde Boas 树的非凡在于将维度折半——从 ww 位空间降解为 2w=2w/2\sqrt{2^w} = 2^{w/2} 的递归笛卡尔积,并借助“极值不递归存储”这一神来之笔,硬生生把两次分支砍成了一次有效调用,让时间的增长速度比对数还要慢一个层级(log⁡w\log w)。

基数堆则是在单调性约束下的实用主义巅峰:它不强求在全域上处处开花,而是用对数个指数扩张的二进制桶充当“差值过滤器”。全局最小值的单向推移赋予了元素下落的单向势能,使得复杂的基数排序转化为平滑的单调均摊流动。