1、前言
线段树,众所周知,在树中的每一个元素中,保存的是线段中的一段,所维护的内容或是最大最小值,或是和等等。可持久化线段树,属于可持久化数据结构中的一种,对于可持久化数据结构这个大知识,我暂时没有去研究,今天只讲其冰山一角。
2、概念
先讲”可持久化“的含义。”可持久化“表示我们当前在处理每个状态,而之前的状态即状态的历史版本全部能够存下来。可持久化线段树,实质上是多颗线段树,最简单的可持久化线段树的题目:求区间第k大。显而易见,求区间最大值的时候我们用普通的线段树就行了,第k大总不能一个个从1数到k吧?可持久化的结构在这个时候就能够帮上大忙了。
我们设区间有n个元素,然后依次进行读入。每读入一个数字,都需要新建一颗线段树(后面会有扩展),这就是能够保存历史状态的线段树了。线段 树中每一个节点维护的是当前已经输入的数的数值位于该区间的个数。有点绕口,没错此时此刻的我也才刚刚懂了——说的直白一点,设目前是第n棵线段树中有一 个节点为[1,4],表示前n个数中数值在1至4的数的个数。Understand?
3、离散化
但是,有一个很重要的问题!题目的空间限 制肯定是有的,假设所输入的数的范围为int,你总不可能开一个大小为int的树吧?而且还要多棵线段树。此时此刻,我们就可以引入一个新知识了——离散 化。看起来很高端,其实很简单,其实你脑补一下map(属于STL)就行了,或者回忆一下高中数学必修一集合那一章,有一个叫映射的东西,和离散化意思差 不多(起码在这道题上的作用是一模一样的),所以不详细阐述,在源代码中会有小小的注释。
好了,目前有一个数列:{2,8,19,6}。假设我们已经离散化结束了,结果为2→1;6→2;8→3;19→4。那么以后我们进行数据的 处理时,1就表示2了,2就表示6了,3就表示8了。。。是不是和映射一个意思?这样的好处在于,我们不需要依赖就弄个[1,2147483647]的线 段树了,若题目规定n<=100000,则最大只需要一棵[1,10000]的线段树了。如下图(其实没有蛮多含义,真正的变化在后面):
【若是建一棵[1,19]的线段树,你想想是多么浪费空间 = =。】
4、历史版本的作用
这么多棵线段树,我们也不可能建立多个结构体来保存。我们可以把所有线段树的节点全部放在tree结构体中,设当前有m个节点,每执行一次插
入操作,新增了x个节点,则存放在tree中的第(m+1)个节点至第(m+x)个节点(当然也有别的编号方式)。同时,我们需要一个root数组,其中
root[i]表示第i棵线段树的根节点的编号。
这样我们就构建完了,来想想——为什么需要历史版本?回到我们一开始的问题,求区间第k大,假设当前询问为求[x,y]的第k大,则我们所需
要用到的线段树为第x+1棵到第y棵。从根节点开始,我们将第y棵树和第x+1棵树一一对应的节点所维护的值进行相减,其所得到的数就是在所询问的
[x,y]中,当前节点表示的子区间的那几个数值在整个区间中出现的次数,用t表示,即t=root[y].[1,mid]-root[x-1].[1,mid]。先判断t是否大于k,如果t大于k,那么说明在区间[x,y]内存在[1,mid]的数的个数大于k,也就是第k大的值在[1,mid]中,否则在[mid+1,r]中。(有点绕,慢慢看 →_→)
5、缩小空间
其实必要的知识已经讲得差不多了,但是我们最后还要面临一个问题——加入一个数,就新建一棵线段树。我们假设有100000个数吧,且有
100000次询问,试想这一大片庞大的线段树森林是要占用多大的内存?一定会MLE的(当然数据小就无所谓)。我们有什么办法缩小空间需求?我们注意
到,每次我们加入一个被离散化后的数x,则从根结点开始向下更新,我们真正相对于前面一棵线段树的差异之处是很少的!设有一颗[1,4]的线段树,若当前
插入值为3,则[1,4]的左儿子[1,2]没有丝毫改动!如果又新建一个,完全是浪费。这样子,我们就有一个方法缩小冗余的空间了——将没有区别的部分
直接指回去!如图所示:
空间是不是小多了!是的。后面的线段树也以此类推。
6、例题
如前面所说,求区间第k大。【HDU 2665】
题目描述
Give you a sequence and ask
you the kth big number of a inteval.
输入格式
The first line is the number
of the test cases.
For each test case, the first
line contain two integer n and m (n, m <= 100000), indicates the
number of integers in the sequence and the number of the
quaere.
The second line contains n
integers, describe the sequence.
Each of following m lines
contains three integers s, t, k.
[s, t] indicates the interval
and k indicates the kth big number in interval [s,
t]
输出格式
For each test case, output m
lines. Each line contains the kth big
number.
输入样例
1
10
1
1 4 2 3 5 6 7 8 9
0
1 3
2
输出样例
2
——————————————————————我是分割线——————————————————————————
1 #include <cstdio> 2 #include <algorithm> 3 #define MAXN 100005 4 using namespace std; 5 6 int a[MAXN],b[MAXN],n,tot,root[MAXN],q,l,r,k,link[MAXN],t; 7 8 struct Node 9 { 10 int ls,rs,size; 11 }; 12 Node tree[MAXN*20]; 13 14 struct cmp 15 { 16 bool operator () (int i,int j) 17 { 18 return (a[i]《a[j]); 19 } 20 }; 21 cmp x; 22 23 void discretize() 24 { 25 sort(link+1,link+n+1,x); // 以link作为中转站,对a进行排序 26 for (int i=1;i<=n;i++) b[link[i]]=i; 27 } 28 29 void insert(int &now,int l,int r,int x) 30 { 31 tree[++tot]=tree[now]; now=tot; 32 tree[now].size++; 33 if (l==r) return; 34 int mid=(l+r)>>1; 35 if (x<=mid) insert(tree[now].ls,l,mid,x); 36 else insert(tree[now].rs,mid+1,r,x); 37 } 38 39 int query(int nl,int nr,int l,int r,int k) 40 { 41 if (l==r) return l; 42 int size=tree[tree[nr].ls].size-tree[tree[nl].ls].size,mid=(l+r)>>1; 43 if (size>=k) return query(tree[nl].ls,tree[nr].ls,l,mid,k); 44 else return query(tree[nl].rs,tree[nr].rs,mid+1,r,k-size); 45 } 46 47 int main() 48 { 49 freopen("HDU2665.in","r",stdin); 50 freopen("HDU2665.out","w",stdout); 51 scanf("%d",&t); 52 for (int j=1;j<=t;j++) 53 { 54 root[0]=0; tot=0; 55 scanf("%d %d",&n,&q); 56 for (int i=1;i<=n;i++) { scanf("%d",&a[i]); link[i]=i; } 57 discretize(); // 离散化 58 for (int i=1;i<=n;i++) 59 { 60 root[i]=root[i-1]; 61 insert(root[i],1,n,b[i]); 62 } 63 for (int i=1;i<=q;i++) 64 { 65 scanf("%d %d %d",&l,&r,&k); 66 printf("%d\n",a[link[query(root[l-1],root[r],1,n,k)]]); 67 } 68 } 69 return 0; 70 }
++++++++++++++++++++++++++++我是华丽丽的分割线++++++++++++++++++++++++++++
【总结】
单纯的可持久化线段树主要适用于一类只包含查询而不包含修改的(广义)区间查询问题,这类问题至少满足下面的 第二项条件:1、整体查询可利用(离散化后的)权值线段树解决,部分区间却无法解决;2、每一个元素的状态可表示为某个前趋元素的修改版本,修改通常是极 少的——具体来说,在线段树中只会修改常数条链。
如果条件1、2都具备,我们通常可以建立具有前缀和性质的可持久化线段树解决。所谓前缀和性质,指的是对每个元素i建立线段树T(i)后,T(i)包含了1-i的信息,而这个信息是可减的。我 们可以利用树的减法将部分区间变为整个区间。具体来说,对于线性表上的查询[l,r],常用的模式是Query(T(l)-T(r-1));对于树上路径 的查询,由于每个节点都有惟一的前趋节点,我们可以把路径视作一种广义的区间,查询[l,r]时,设p=lca(l,r),常用的查询模式是 Query(T(l)+T(r)-T(p)-T(par[p]))(对点)或Query(T(l)+T(r)-2T(p))(对边)。
如果只具备条件2,我们常常不利用前缀和性质(也常常无法利用),而是将区间问题转化为点问题,单独查询某个点上的线段树,并在特定的一棵线段树上获取区间信息。这时,可能需要二分答案。这一类问题的典型例子是BZOJ2653。
上述一类问题的解决过程中,在建树时通常需要利用上一节点的已有版本。
如果题目还要求修改操作,单纯的可持久化线段树通常不能完成。这时,对于满足上述1、2条件的问题,我们会发 现,修改一个版本的线段树会对后续版本造成影响——这与修改前缀和数组中某个值会对后继值造成影响是类似的。因此,我们可以考虑使用树状数组维护前缀和。 如果是树上的问题,我们可以建立DFS序的树状数组来维护前缀和。这样,节点不存在明确的前趋,建树时也就不再需要利用上一节点的已有版本了。
只满足上述条件2且需要修改的题目我还在思考中,不知道有没有这一类的题目(如带修改的bzoj2653)。此外,我还在考虑可持久化线段树更复杂的应用(如区间运算?),因此这个总结未来也许会更新。
参考:http://blog.sina.com.cn/s/blog_6022c4720102w03t.html