您的位置:首页 > 其它

学习笔记:可持久化线段树

2016-07-07 11:12 204 查看
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]的线段树了。如下图(其实没有蛮多含义,真正的变化在后面):

#include <cstdio>
#include <algorithm>
#define MAXN 100005
using namespace std;

int a[MAXN],b[MAXN],n,tot,root[MAXN],q,l,r,k,link[MAXN],t;

struct Node
{
int ls,rs,size;
};
Node tree[MAXN*20];

struct cmp
{
bool operator () (int i,int j)
{
return (a[i]《a[j]);
}
};
cmp x;

void discretize()
{
sort(link+1,link+n+1,x); // 以link作为中转站,对a进行排序
for (int i=1;i<=n;i++) b[link[i]]=i;
}

void insert(int &now,int l,int r,int x)
{
tree[++tot]=tree[now]; now=tot;
tree[now].size++;
if (l==r) return;
int mid=(l+r)>>1;
if (x<=mid) insert(tree[now].ls,l,mid,x);
else insert(tree[now].rs,mid+1,r,x);
}

int query(int nl,int nr,int l,int r,int k)
{
if (l==r) return l;
int size=tree[tree[nr].ls].size-tree[tree[nl].ls].size,mid=(l+r)>>1;
if (size>=k) return query(tree[nl].ls,tree[nr].ls,l,mid,k);
else return query(tree[nl].rs,tree[nr].rs,mid+1,r,k-size);
}

int main()
{
freopen("HDU2665.in","r",stdin);
freopen("HDU2665.out","w",stdout);
scanf("%d",&t);
for (int j=1;j<=t;j++)
{
root[0]=0; tot=0;
scanf("%d %d",&n,&q);
for (int i=1;i<=n;i++) { scanf("%d",&a[i]); link[i]=i; }
discretize(); // 离散化
for (int i=1;i<=n;i++)
{
root[i]=root[i-1];
insert(root[i],1,n,b[i]);
}
for (int i=1;i<=q;i++)
{
scanf("%d %d %d",&l,&r,&k);
printf("%d\n",a[link[query(root[l-1],root[r],1,n,k)]]);
}
}
return 0;
}


View Code

++++++++++++++++++++++++++++我是华丽丽的分割线++++++++++++++++++++++++++++

【总结】

单纯的可持久化线段树主要适用于一类只包含查询而不包含修改的(广义)区间查询问题,这类问题至少满足下面的 第二项条件: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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: