Archipelago

Lec 3持久化数据结构与平面点定位

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

Lec 3 持久化数据结构与平面点定位(Persistent Data Structures & Planar Point Location)

对应核心文献:

  • Sarnak, N., & Tarjan, R. E. (1986). Planar point location using persistent search trees. Communications of the ACM.
  • Driscoll, J. R., Sarnak, N., Sleator, D. D., & Tarjan, R. E. (1989). Making data structures persistent. Journal of Computer and System Sciences.

TL;DR

  • 时空转换解决几何点定位:利用平面扫描线将静态二维平面点定位转化为一维有序集合的动态演变过程,借助持久化搜索树实现 O(n)O(n) 空间与 O(log⁡n)O(\log n) 最优查询时间。
  • 持久化的四大范式:区分了短暂型(Ephemeral)、部分持久化(Partial Persistence,线性版本链)、完全持久化(Full Persistence,分支版本树)与汇合持久化(Confluent Persistence,DAG 图)。
  • 节点复制法实现 O(1)O(1) 均摊开销:通过为有限入度节点设置固定数量的修改槽(Modification Fields),结合势能法摊还分析,消除朴素路径复制带来的 O(log⁡n)O(\log n) 空间膨胀,达到每次更新均摊 O(1)O(1) 空间与 O(1)O(1) 额外时间。

平面点定位问题与扫描线范式

问题定义与经典瓶颈

平面点定位(Planar Point Location)是计算几何的基础问题:给定平面上的一个连通平面直线图(PSLG),包含 nn 条线段,将其划分为若干互不相交的多边形面域。给定任意查询点 q=(x,y)q = (x, y),需要快速确定 qq 落在哪个多边形区域内。

评价该算法的三个维度包括:预处理时间、存储空间与单次查询时间。

Dobkin 与 Lipton(1976)提出了垂直条带法(Vertical Slabs):

  • 通过所有线段端点作垂直线,将平面切分为至多 2n2n 个垂直条带(Slabs);
  • 在每个条带内,线段互不相交,沿 yy 轴天然具备严格的全序关系;
  • 查询时:先用二分查找定位点 qq 所在的垂直条带(耗时 O(log⁡n)O(\log n));再在该条带内部沿着 yy 轴二分查找位于 qq 正下方的第一条线段(耗时 O(log⁡n)O(\log n))。

该方法的致命缺陷在于空间膨胀:一条长线段可能跨越大量条带,导致最坏情况下每个条带都要存储 O(n)O(n) 条线段,总空间复杂度高达 O(n2)O(n^2)。

扫描线与时空对偶

Cole 与 Sarnak-Tarjan 提出了关键观察:相邻条带之间的差异极小。 当一条垂直扫描线从左向右连续移动时:

  • 只有在遇到线段的左端点时,才有一条新线段被插入到 yy 轴有序集合中;
  • 只有在遇到线段的右端点时,才有一条旧线段被从 yy 轴有序集合中删除;
  • 其余任何时刻,线段的相对上下次序完全保持不变。

如果将水平方向的 xx 坐标视为时间轴 tt,点定位问题便等价于: 维护一个随时间不断更新的动态一维有序集合,支持历史版本时间旅行查询 Access(y, t)。 平面上线段总数为 nn,对应产生 m=2nm = 2n 次更新(nn 次插入与 nn 次删除)。如果能设计出每次更新只占用 O(1)O(1) 空间、且支持 O(log⁡n)O(\log n) 历史版本检索的数据结构,平面点定位问题就能以 O(n)O(n) 空间和 O(log⁡n)O(\log n) 查询时间得到终极解决!

持久化数据结构的概念模型

持久化数据结构(Persistent Data Structures)旨在支持对历史状态的访问与变更。根据支持版本操作的自由度,理论上划分为四种模式:

  1. 短暂型(Ephemeral):传统数据结构。任何更新操作都就地修改(In-place mutation),旧版本立即销毁,不可再读写。
  2. 部分持久化(Partial Persistence):
    • 所有的版本构成一条线性的历史时间链 V0,V1,…,VmV_0, V_1, \dots, V_m;
    • 允许对所有历史版本进行只读查询(Read any version);
    • 更新操作只能作用于最新版本,产生新的最新版本 Vm+1V_{m+1}。
  3. 完全持久化(Full Persistence):
    • 允许在任意历史版本上执行修改操作;
    • 版本演化构成一棵版本树(Version Tree),支持从分支节点分叉出平行历史。
  4. 汇合持久化(Confluent Persistence):
    • 允许将两个或多个不同的历史版本合并(如两个集合的 Union),版本演化构成有向无环图(DAG)。

平面点定位问题仅需要部分持久化能力,因为扫描线是沿 xx 轴单向推进的。

经典持久化技术演进

为了将短暂型二叉平衡搜索树改造为持久化结构,计算机科学家经历了三个阶段的探索:

1. 胖节点法(Fat Node Method)

  • 机制:不创建新节点,每个逻辑节点膨胀为一个“胖节点”。节点内的每个指针字段和数据字段被改写为一个动态增长的数组或链表,存储三元组 (version, field_name, value)。
  • 更新代价:在当前版本修改指针时,只需在对应字段追加一条记录,耗时 O(1)O(1),每次修改空间开销 O(1)O(1)。
  • 查询代价的退化:当查询版本 tt 时,每次沿着指针跳转都需要在胖节点内部二分查找版本不大于 tt 的最新值。这使得单次指针跳转耗时从 O(1)O(1) 恶化为 O(log⁡m)O(\log m),导致整体查询时间由 O(log⁡n)O(\log n) 放大为 O(log⁡nlog⁡m)O(\log n \log m)。

2. 路径复制法(Path Copying Method)

  • 机制:保持节点内部字段为定长纯指针。当修改树中某个节点 vv 时,直接复制生成新节点 v′v'。由于其父节点的指针必须指向新节点 v′v',其父节点也必须被复制,这一连锁反应沿树根方向一直传递到根节点。
  • 版本索引:为每个版本 VtV_t 维护一个独立的根指针 roott\mathrm{root}_t。
  • 查询代价:查询版本 tt 时从 roott\mathrm{root}_t 出发,指针均为普通定长跳转,查询时间完美保持为最坏 O(log⁡n)O(\log n)。
  • 空间代价的膨胀:每次插入或删除需要复制整条从叶到根的路径,消耗 O(log⁡n)O(\log n) 个新节点。对于 mm 次更新,总空间消耗为 O(mlog⁡n)O(m \log n)。对于平面点定位,空间依然达不到预期的 O(n)O(n)。

Driscoll-Tarjan 节点复制法(Limited Node Copying)

Driscoll, Sarnak, Sleator 与 Tarjan(1989)提出了著名的有限修改槽节点复制法,成功将路径复制与胖节点法熔铸于一体,在入度有界(p=O(1)p = O(1))的指针机模型下实现了每次更新均摊 O(1)O(1) 空间与 O(log⁡n)O(\log n) 最坏查询时间。

节点结构设计

对于二叉树(最大入度 p=1p = 1),每个持久化节点包含:

  1. 标准字段:包含数据域 key、左孩子指针 left、右孩子指针 right 及其创建时刻的初始版本号;
  2. 有限修改槽(Mods Log):分配固定数量 kk 个额外的修改槽位(对于二叉树通常取 k=2k = 2 或 k=2pk = 2p)。每个槽位记录:
    • version:修改发生时的版本号;
    • field:被修改的字段标识(如 left 或 right);
    • value:修改后的新指针值。
  3. 反向指针(In-pointers):指向所有引用该节点的父节点(二叉树中入度为 1)。

遍历与查询算法

当在版本 tt 查询节点 uu 的左孩子时:

  1. 检查节点 uu 的修改槽中所有版本号 ≤t\le t 且修改目标为 left 的记录;
  2. 若存在,取其中版本号最大的那条记录的 value;
  3. 若不存在,则直接读取标准字段 left。 由于修改槽容量 kk 为常数(k=O(1)k = O(1)),单次指针解析只需 O(1)O(1) 时间!因此查询时间严格保持为二叉搜索树的深度 O(log⁡n)O(\log n)。

更新与级联节点复制算法

当在当前最新版本修改节点 uu 的指针时:

  1. 槽位未满:若 uu 尚有空闲修改槽,直接将新指针、当前版本号和字段名写入修改槽中,操作在 O(1)O(1) 时间内结束,无需分配新节点!
  2. 槽位已满(Node Copying):
    • 分配一个全新节点 u′u';
    • 将 uu 截至当前最新版本的所有字段的最终有效值复制到 u′u' 的标准字段中;
    • u′u' 的修改槽全部重置为空;
    • 更新 uu 的父节点中指向 uu 的指针,使其重定向到新节点 u′u'。
    • 递归连锁:父节点更新指针时同样遵循上述规则:若父节点修改槽未满则写入槽中;若父节点修改槽亦满,则同样对父节点执行复制并向上递归。

势能法均摊空间分析

令势能函数为整棵数据结构中所有已用修改槽的总数:

Φ=∑v∈T(节点 v 中已被占用的修改槽数量)\Phi = \sum_{v \in T} (\text{节点 } v \text{ 中已被占用的修改槽数量})

由于修改槽数非负,恒有 Φ≥0\Phi \ge 0。

假设某次更新操作触发了连续 cc 次节点复制:

  • 实际复制代价与分配空间为 cc 个节点;
  • 被复制的 cc 个节点中,每个原本都处于修改槽全满状态(各占用 kk 个槽),复制后生成的新节点修改槽均为空,因此槽占用量减少了 k⋅ck \cdot c;
  • 连锁更新向上终止时,在最后一个未满的祖先节点中消耗了 11 个修改槽(或者创建了一个全新的根节点);
  • 势能净变化:
ΔΦ≤−k⋅c+1\Delta \Phi \le -k \cdot c + 1

均摊空间与时间开销满足:

c^=c+ΔΦ≤c−k⋅c+1=1−(k−1)c\hat{c} = c + \Delta \Phi \le c - k \cdot c + 1 = 1 - (k - 1)c

只要取修改槽容量 k≥2k \ge 2(通常取 k=2pk = 2p),便有 k−1≥1k - 1 \ge 1,从而:

c^≤1\hat{c} \le 1

这以极其简洁的方式证明了:不论单次更新引起多么冗长的级联节点复制,每次指针修改均摊消耗的空间与时间恒为严格的 O(1)O(1)!

部分持久化红黑树与点定位总结

在红黑树的动态更新(插入与删除)中:

  • 每次插入或删除最多引起 O(1)O(1) 次指针旋转;
  • 虽然可能引起 O(log⁡n)O(\log n) 次节点颜色(Red-Black)的重新着色,但通过将颜色信息独立压缩或记录于修改槽中,指针实际变更次数严格保持为均摊 O(1)O(1);
  • 结合 Driscoll-Tarjan 节点复制法,对包含 nn 条线段的平面划分建立部分持久化红黑树,处理 2n2n 次端点事件的总空间严格为 O(n)O(n),预处理耗时 O(nlog⁡n)O(n \log n),单次点定位查询时间为最坏情况下的 O(log⁡n)O(\log n)。

这一成果彻底攻克了平面点定位的理论最优界,并奠定了现代时序数据结构与不可变数据结构(如 Git 版本快照与函数式持久化树)的理论根基。

我的理解

我的理解

空间持久化的本质是“增量日志”与“快照压缩”的动态平衡: 朴素路径复制是极端的“快照派”,每次变化都克隆一条完整路径,导致空间过度膨胀;胖节点法是极端的“日志派”,原地无限追加变更记录,导致版本查找效率被不断摊薄。

Driscoll-Tarjan 的有限修改槽机制则达到了中庸的艺术:给每个节点配发定量的“就地修改额度”(修改槽)。额度内用日志冲抵复制,额度用尽时以一次性快照重置额度。势能分析揭示出,只要额度大于节点入度(k≥2pk \ge 2p),快照带来的释放势能就足以永久买单级联复制的开销。这种在局部引入微小常数缓冲区从而在全局消除渐近复杂度的思想,是高级数据结构设计中最具普适性的优雅模式。