可持久化数据结构
毒液哥 Fudan University
概论
本文假设读者对数据结构有一定了解,对此概念不做赘述。
可持久化数据结构是一类数据结构的统称。若我们能在某一时刻访问一数据结构的任何历史版本,则称该数据结构为可持久化数据结构。
若一个数据结构是可持久化的,则可以通过修改该数据结构,在各操作时间复杂度不改变的同时,使其成为一个可持久化数据结构。
由定义可知,可持久化数据结构的任何一个历史版本都要被保留,因此可持久化数据结构的任何操作都不能直接修改之前存在的节点,必须对每个需要修改的节点创建一个拷贝。因此只有当任何时间的任何操作创建节点拷贝的复杂度都严格小于等于其复杂度,这一数据结构才能可持久化。
下文会讨论一些常见的数据结构的一般版本,及其可持久化版本,并分析一些不能可持久化的一般版本。
可持久化线段树
一般线段树
一般来说,线段树有两种实现方法,一种是基于数组的静态开点的线段树,另一种是使用指针储存树形结构的动态开点(也可静态开点)版本。其中前者本质上是对后者的一种优化。在线段树的坐标范围确定的情况下,整颗线段树的节点数量以及每个节点表示的线段其实是确定的。若将该线段树的结构完整建立,则它是一棵节点数不超过 O ( n ) O(n) O(n)的正则二叉树,其中 n n n为坐标范围的长度。因此可以用数组储存这棵二叉树,从而省去了记录左右儿子关系的空间。
因为线段树的每个操作会严格修改 O ( log n ) O(\log\ n) O(log n)个节点,所以线段树可持久化。但由于前者用数组存储了整棵线段树,但拷贝数组需要 O ( n ) O(n) O(n)的时间,因此前一个版本的线段树不可持久化,而后一个版本的线段树可持久化。
可持久化
若 u u u表示一棵满节点线段树的一个节点,则:
用 l e f t ( u ) left(u) left(u)表示 u u u的左儿子; r i g h t ( u ) right(u) right(u)表示 u u u的右儿子; d a t a ( u ) data(u) data(u)表示 u u u上储存的数据; L ( u ) L(u) L(u)表示 u u u(线段)的左端点; R ( u ) R(u) R(u)表示 u u u(线段)的右端点。
考虑一般线段树的单点递归修改过程。
递归函数 F F F的参数 u , x , y u,\ x,\ y u, x, y分别表示线段树的节点、要修改的坐标、以及数据的修改量,没有返回值:
- 判断 u u u是否是叶子节点,若是,直接修改 d a t a ( u ) data(u) data(u)并返回。
- 判断 x x x位于 l e f t ( u ) left(u) left(u)还是 r i g h t ( u ) right(u) right(u). 不妨设是前者。
- 递归调用 F ( l e f t ( u ) , x , v ) F(left(u),\ x,\ v) F(left(u), x, v).
- 由 l e f t ( u ) left(u) left(u)和 r i g h t ( u ) right(u) right(u)更新 d a t a ( u ) data(u) data(u).
我们发现,所有被访问到的节点的信息都会被修改,因此在可持久化版本中,我们应该对所有访问到的节点制作一份副本,并对副本进行修改。同时需要注意,在第三步中,由于我们为修改过的 l e f t ( u ) left(u) left(u)创建了一份副本,我们应该将新节点的左儿子指向修改过后的 l e f t ( u ) left(u) left(u)副本。下面给出可持久化线段树的单点递归修改过程。
递归函数 G G G的参数 u , x , y u,\ x,\ y u, x, y分别表示线段树的节点、要修改的坐标、以及数据的修改量,返回值为修改过后的 u u u的副本(即 v v v):
- 创建 u u u的一个拷贝,设其为 v v v.
- 判断 v v v是否是叶子节点,若是,直接修改 d a t a ( v ) data(v) data(v)并返回 v v v。
- 判断 x x x位于 l e f t ( u ) left(u) left(u)还是 r i g h t ( u ) right(u) right(u). 不妨设是前者。
- 递归调用 G ( l e f t ( u ) , x , v ) G(left(u),\ x,\ v) G(left(u), x, v). 并令 l e f t ( v ) left(v) left(v)更新为其返回值。
- 由 l e f t ( v ) left(v) left(v)和 r i g h t ( v ) right(v) right(v)更新 d a