可持久数据结构主要指的是我们可以查询历史版本的情况并支持插入,利用使用之前历史版本的数据结构来减少对空间的消耗(能够对历史进行修改的是函数式)。
在这里只讲下比较常用的可持久化线段树和trie。
对于线段树我们记录每个节点的左右儿子,如果空间允许的话我们也可以记录每个数代表的区间,对于打标签操作我们则需要新建两个节点表示新的历史,比较常用的是用可持久化线段树来维护权值,然后维护不同区间的权值分布情况,比较经典的例子就是无修改的区间K大值,我们以这个问题为例子来讲解可持久化线段树的操作。
首先如果我们只询问区间[1,n]的k大值我们可以先将各个点的权值离散化,然后建立一颗权值树表示权值的分布状况,那么我们对于每个区间只需要判断左儿子的节点数是否大于k,然后二分的去找就好了,那么对于区间的查询我们则需要建立这个区间的权值树,那么我们先建一颗空树,表示什么都没有插入的时候的区间的情况,然后再按照数列的顺序不断的插入这个值,因为权值树满足加减的性质(即区间[l,r]中x值的个数为区间[1,r]中x的个数减去区间[1,l-1]中x的个数),所以我们可以处理出所有区间为[1,i]的时候的权值树,但是空间会消耗很大。那么我们考虑一次插入,这一次插入改变的值只为包含x的区间的这logn个节点,那么剩下的节点我们可以从上一个历史版本继承过来。
建空树比较容易,设t[x].son[0/1]表示x节点的左右儿子,t[x].left,right表示x节点所表示的左右区间,t[x].cnt表示这个节点表示区间的权值个数。那么我们可以得到
void build(int &x,int l,int r) { if (!x) x=tot++; t[x].left=l; t[x].right=r; if (t[x].left==t[x].right) return ; int mid=t[x].left+t[x].right>>1; build(t[x].son[0],l,mid); build(t[x].son[1],mid+1,r); }
下面就是插入操作,表示我们按一个值为key的元素新建一颗线段树并继承历史版本为rot的线段树
void insert(int &x,int rot,int key) { if (!x) x=tot++; t[x].left=t[rot].left; t[x].right=t[rot].right; if (t[x].left==t[x].right) { t[x].cnt=t[rot].cnt+1; return ; } int mid=t[x].left+t[x].right>>1; if (key>mid) { t[x].son[0]=t[rot].son[0]; insert(t[x].son[1],t[rot].son[1],key); } else { t[x].son[1]=t[rot].son[1]; insert(t[x].son[0],t[rot].son[0],key); } t[x].cnt=t[rot].cnt+1; }
因为需要继承历史版本,所以这个节点的所有信息比如left,right都需要继承,cnt则需要被更新。
那么现在我们有了n棵线段树,用rot[i]分别表示区间[1,i]的权值树的根节点(表示权值区间为[1,max_sum]的节点),那么对于询问[l,r]我们就可以用rot[r]-rot[l-1]来确定表示区间为[l,r]的权值树的个点的cnt值。
下面是几道比较简单的权值树的题。
还有一些其他可持久化线段树的用法,比如
下面介绍一下可持久化trie。
可持久化trie中最简单的一类就是存储二进制的二叉trie树,由于xor运算为一个群,所以满足之前加法满足的性质,那么我们可以根据这个性质来维护区间[1,i]的xor值从而维护区间[l,r]的xor值,这样我们就可以处理给定一个数列,询问区间[l,r]中所有数与x的xor值最大值的问题了。
假设需要维护的数的大小在1<<31左右的时候,最开始我们就要建一颗深度为31的满二叉树,这样的空间消耗肯定是不允许的,那么我们可以在开始的时候不建空树,而是直接插入各个节点。