就像十二省联考 的出题人所说 “无论省选对于你意味着什么,都希望你能拥有光明的未来”
所以我就开始学可持久化的一系列数据结构了……从不带修改的可持久化线段树学起~
· 引入
- 什么是可持久化?
一个具有可持久化性质的数据结构能够保存自己的历史版本;
也就是说它每一次修改不会修改原来的结构(旧版本),而是创建新的结构(新版本)。当然这样在内存、时间上都会有一个比较大的开销(对于时间往往是常数开销),所以我们对一些数据结构做可持久化时进行了一些优化。
- 线段树的可持久化
考虑最简单的实现可持久化的方法(当然这样内存开销会非常大)—— 每次修改时将旧版本整个 copy 到新版本上,再在新版本上做修改。
我们发现由于只修改一次,也就是新版本上只有从根出发的一条路径上的节点被修改了,而其他大多数节点都与就版本相同。于是可以想到这些节点可以沿用旧版本,而不新建节点。这就是可持久化线段树 实现的原理。
· 可持久化
- 前置
权值线段树:和普通线段树结构上相同,但是它的节点表示的是一个值域 $[l,r]$,该节点的大小为 $a_i\in[l,r]$ 的 $i$ 的个数;
通俗一点就是说对于某个节点,它的左右端点为 $l,r$ ,那么它的值就是 大于等于 $l$ 小于等于 $r$ 的数的个数。离散化;
- 可持久化的实现
简单的说就是 只有要修改的节点才新建,其余保留旧版本 。
举个例子来说明:建立一棵根节点为区间 $[1,4]$ 的权值线段树,最开始所有节点的大小都为 $0$:
然后插入一个 $1$ ,步骤如下:
- 从根节点出发,由于 $1\in [1,4]$ ,所以根节点要修改,于是建立一个新版本的根节点;
- 发现 $1\in[1,2]$ ,但 $1\not\in[3,4]$ ,所以 创建新版本的节点$[1,2]$ ,节点$[3,4]$ 不新建;
- 从新版本的 节点$[1,2]$ 继续插入;
- 直到插入到叶子节点 $[1,1]$ ,插入完成。
最后就变成了下面这样:
- C++实现
1 | //rt是当前位置的新版本节点;prt是当前位置的旧版本节点 |
· 可持久化线段树的用途
比较常见的就是在 $O(\log n)$ 内静态(无修改)查询一个数列中某一段数的第 $k$ 大了。(##POJ - 2104##)
给出 $n$ 个数 $a_1…a_n$ 以及 $m$ 个询问;
询问为:求 $a_l…a_r$ 的第 $k$ 大
我们可以把可持久化线段树的“第$k$个版本”看成只插入了 $a_1…a_k$ 的权值线段树。
定义两棵权值线段树“相减”为对应节点的权值相减。根据前缀和的思想,插入了 $a_l…a_r$ 的权值线段树就是 版本$r$ - 版本 $(l-1)$ 。
假设 版本$r$ 和 版本$(l-1)$ 的表示区间 $[Lef,Rig]$ 的节点分别为 rt,prt
,且已经确定“第$k$大”在 $[Lef,Rig]$ 中。定义 $Mid=\lfloor\frac{Lef+Rig}2\rfloor$ ,则 $[Lef,Mid]$ 的数量为 rt
的左儿子大小 减去 prt
的左儿子大小,记这个数量为 totlef
。如果 $k\leq totlef$ ,则说明 $[Lef,Rig]$的第$k$大 是 $[Lef,Mid]$的第$k$大;否则 $[Lef,Rig]$的第$k$大 是 $[Mid+1,Rig]$的第$(k-totlef)$大。
Tab. 需要离散化,而且记得把数组开大一点
· 源代码
(打了包,直接可以用,题目是 POJ-2104)
1 | /*Lucky_Glass*/ |
The End
Thanks for reading!
Email: lucky_glass@foxmail.com ,欢迎提问~