docs/ds/persistent-balanced.md
OI 常用的可持久化平衡树 一般就是 可持久化无旋转 Treap 所以推荐首先学习 无旋转 Treap.
对于非旋转 Treap,可通过 Merge 和 Split 操作过程中复制路径上经过的节点(一般在 Split 操作中复制,确保不影响以前的版本)就可完成可持久化.
对于旋转 Treap,在复制路径上经过的节点同时,还需复制受旋转影响的节点(若其已为这次操作中复制的节点,则无需再复制),对于一次旋转一般只影响两个节点,那么不会增加其时间复杂度.
上述方法一般被称为 path copying.
「一切可支持操作都可以通过 Merge Split Newnode Build 完成」,而 Build 操作只用于建造无需理会,Newnode(新建节点)就是用来可持久化的工具.
我们来观察一下 Merge 和 Split,我们会发现它们都是由上而下的操作!
因此我们完全可以 参考线段树的可持久化操作 对它进行可持久化.
可持久化 是对 数据结构 的一种操作,即保留历史信息,使得在后面可以调用之前的历史版本.
对于 可持久化线段树 来说,每一次新建历史版本就是把 沿途的修改路径 复制出来
那么对可持久化 Treap(目前国内 OI 常用的版本)来说:
在复制一个节点 $X_{a}$($X$ 节点的第 $a$ 个版本)的新版本 $X_{a+1}$($X$ 节点的第 $a+1$ 个版本)以后:
需要的东西:
一个 struct 数组 存 每个节点 的信息(一般叫做 tree 数组);(当然写 指针版 平衡树的大佬就可以考虑不用这个数组了)
一个 根节点数组,存每个版本的树根,每次查询版本信息时就从 根数组存的节点 开始;
split() 分裂 从树中分裂出两棵树
merge() 合并 把两棵树按照随机权值合并
newNode() 新建一个节点
build() 建树
对于 分裂操作,每次分裂路径时 新建节点 指向分出来的路径,用 std::pair 存新分裂出来的两棵树的根.
split(x,k) 返回一个 std::pair;
表示把 $_x$ 为根的树的前 $k$ 个元素放在 一棵树 中,剩下的节点构成在另一棵树中,返回这两棵树的根(first 是第一棵树的根,second 是第二棵树的).
static std::pair<int, int> _split(int _x, int k) {
if (_x == 0)
return std::make_pair(0, 0);
else {
int _vs = ++_cnt; // 新建节点(可持久化的精髓)
_trp[_vs] = _trp[_x];
std::pair<int, int> _y;
if (_trp[_vs].key <= k) {
_y = _split(_trp[_vs].leaf[1], k);
_trp[_vs].leaf[1] = _y.first;
_y.first = _vs;
} else {
_y = _split(_trp[_vs].leaf[0], k);
_trp[_vs].leaf[0] = _y.second;
_y.second = _vs;
}
_trp[_vs]._update();
return _y;
}
}
merge(x,y) 返回 merge 出的树的根.
同样递归实现.如果 x 的随机权值>y 的随机权值,则 merge(x_{rc},y),否则 merge(x,y_{lc}).
static int _merge(int _x, int _y) {
if (_x == 0 || _y == 0)
return _x ^ _y;
else {
if (_trp[_x].fix < _trp[_y].fix) {
_trp[_x].leaf[1] = _merge(_trp[_x].leaf[1], _y);
_trp[_x]._update();
return _x;
} else {
_trp[_y].leaf[0] = _merge(_x, _trp[_y].leaf[0]);
_trp[_y]._update();
return _y;
}
}
}
可持久化 WBLT 由 WBLT 改动而来,所以首先学习 WBLT.
使用 路径复制 的方法,将一次操作中 修改过 的节点复制下来,不能影响之前的节点.
为了处理懒标记,我们这样考虑:在一棵持久化的 WBLT 上,一个点可能有多个父亲,但是儿子数量只能是 $0$ 或 $2$ 个.pushdown 的下放懒标记的操作,只会影响它的儿子,我们对一个点进行 pushdown,是没有影响的;反而是它的儿子,它的儿子可能不止它一个父亲,将它的标记下放到儿子,可能导致在别的父亲的版本上,多了一个不属于那个版本的懒标记,这就错了;除非它的儿子只有它一个父亲.所以我们应该在 pushdown 的时候,复制一遍儿子,把懒标记打到新的儿子上.
在进行路径复制的时候,我们可以定义一个 refresh 函数,它接受一个节点 $p$ 的引用,表示把节点 $p$ 复制一下,产生一个新的节点,重新赋值给 $p$.使用 refresh 函数的原则是,如果它将要被修改,或者它拥有的儿子即将发生变动(而不是它的儿子的信息将要被修改),那么就 refresh 它,否则不需要.
对于静态的查询,除了 pushdown 之外都不用 refresh.如果保证什么操作都做路径复制,那么 pushdown 和 refresh 的顺序是无所谓的.
这里有一个优化.观察到 pushdown 的时候要复制两个节点,可以写标记永久化,但是刚才说了,如果它的儿子只有它一个父亲,可以不用复制.针对这一个性质,可以进行优化,以减少复制多余的节点.
考虑记录每个节点有多少个父亲(认为每个版本的根都有一个父亲),记为 $use$.每次 refresh 的时候,如果 $use\leq 1$ 则不需要重新复制节点,否则新建节点,并且 $use$ 自减 $1$,表示父亲带着这个儿子跑了,这样父亲就可以随意修改新的节点而不影响其它版本.另外每次复制节点的时候,如果节点有儿子,那么两个儿子的 $use$ 自增 $1$;合并两个子树时,返回的节点对两个儿子也有一个父亲的 $use$;删除节点时,两个子节点都丢失一个父亲:这样能优化一些时空.
??? note "完整代码(可持久化文艺平衡树)"
cpp --8<-- "docs/ds/code/persistent-balanced/persistent-wblt.cpp"
???+ note "洛谷 P3835【模版】可持久化平衡树" 你需要实现一个数据结构,要求提供如下操作(最开始时数据结构内无数据):
1. 插入 $x$ 数;
2. 删除 $x$ 数(若有多个相同的数,应只删除一个,如果没有请忽略该操作);
3. 查询 $x$ 数的排名(排名定义为比当前数小的数的个数 + 1);
4. 查询排名为 $x$ 的数;
5. 求 $x$ 的前驱(前驱定义为小于 $x$,且最大的数,如不存在输出 $-2\,147\,483\,647$);
6. 求 $x$ 的后继(后继定义为大于 $x$,且最小的数,如不存在输出 $2\,147\,483\,647$).
以上操作均基于某一个历史版本,同时生成一个新的版本(操作 3, 4, 5, 6 即保持原版本无变化).而每个版本的编号则为操作的序号.特别地,最初的版本编号为 0.
就是 普通平衡树 一题的可持久化版,操作和该题类似.
只是使用了可持久化的 merge 和 split 操作.