可持久化数据结构
可持久化数据结构是指在每次修改后都保存了修改前版本的数据结构。在可持久化数据结构中,我们可以很方便的追溯到某次修改前的版本,以此来完成某些操作。简单来说,对于次插入操作,我们就存储个版本,第个版本对应了前次操作后的数据结构。当然,不可能真的存个完全独立的版本,空间会直接爆炸(~ ̄▽ ̄)~。可以注意到,对于Trie和线段树等数据结构来说,每次插入只改变了一条链上的节点,因此对于每次插入操作,我们只需要新建这条链上的节点,而其他的节点可以直接白嫖上一个版本的。
常用的可持久化数据结构主要有可持久化的Trie和可持久化线段树。
可持久化Trie
首先,既然有个版本,当然就对应了个根节点,从不同的根节点开始遍历就可以得到不同版本的数据结构。下面简单演示一下可持久化Trie的插入过程。
最开始是一个空的Trie,插入一个字符串no(黑色节点表示根节点)
然后插入一个字符串game,由于这整个字符串都属于要改变的节点,所以都要新建,同时,为了从新建的根节点能够访问到以前插入的字符串,新的根节点也要有边连向n
继续再插入一个字符串no,但是no之前已经插入过了,如果是一般的Trie,那么插入之后不会有变化,但是对于可持久化Trie来说,不论之前有没有,新插入的字符串都需要再开新的节点存储
最后插入字符串life
从上面的演示可以看出,从对应版本的根节点开始遍历,就可以得到这个版本的对应的Trie,而不会遍历到在这之后插入的节点。
具体的代码如下:
1 | // 实际占据空间为所有字符串长度和(每次插入都要创建新节点)加上字符串数量(每次都要创建一个根节点) |
接下来是一道例题:https://www.luogu.com.cn/problem/P4735
首先看到区间异或和,自然想到前缀和做法,而01Trie则可以找出最大的异或和,那么如何限制与呢?我们可以使用可持久化Trie,由于当从第个根节点开始遍历时只会遍历到至的数字,这样就限制了。接下来就是限制,注意到对于每次插入可持久化Trie都会创建新的节点,所以对于每一个数字中的每一位,在01Trie中其实都有唯一一个节点与其对应,后面的节点都只是在引用这个节点,而一般的Trie由于公共前缀只存储一个,所以没有这样的一一对应关系。因此,我们可以在每个节点上再存储一个信息,即这个节点是作为第几个数字被插入的,这样在遍历寻找时,只需要判断一下是否大于等于,就能保证遍历的范围限定在之后。
1 |
|
可持久化线段树
在单点修改的线段树中,每次修改都只改变了这一条路径上的个节点,所以线段树自然也有可持久化版本。其操作也与可持久化Trie类似,改变的节点创建新的节点,不改变的直接继承自上一个版本。下面用区间最值的线段树简单演示一下修改过程。
初始状态
将1修改为6,那么对于1到根节点上的所有节点,都需要新建一个节点
虽然有点扭曲,但还是可以看出来第二个线段树的样子的。由于过程基本和可持久化Trie一样主要是图变难画了,在此只演示一次修改。
可持久化线段树的插入操作代码如下,由于可持久化线段树内存占用较大,所以可以通过不存储和,而是直接将和作为递归参数传递,同时由于后面新建立的线段树不再是完全二叉树,不能直接线性存储,只能动态开点:
1 | // lc:左儿子,rc:右儿子 |
主席树
既然都提到了可持久化线段树,那就不得不提一下大名鼎鼎的主席树。
主席树的本质其实就是可持久化权值线段树,用来解决区间第大之类的问题。首先什么是权值线段树?权值线段树其实就是一个维护数字出现次数的线段树,比如叶子节点的区间为,值为,就表示出现了次,当然实际使用时一般需要离散化,因为权值线段树不关心数字的具体大小。有了权值线段树,我们就可以找出整个数组的第大,具体来说,每次递归时先判断左边区间里的数字数量是否大于,是则说明第大的数在右边,那么就往右儿子递归,同时减去左边出现的数字数量,否则往左儿子递归。
这是全局的第大,那么如何限制区间呢。可以使用可持久化线段树,这样从第个根节点开始遍历就可以得到前个数字的第大,这样就限制了,对于,我们可以使用前缀和的思想来作差,从个根节点开始遍历,两者相减就可以得到至这段区间中数字出现的次数。
具体可以看看模板题https://www.luogu.com.cn/problem/P3834
1 |
|
暑假集训开始了,愉快的假期生活结束了>︿<