大体思路
可持久化,就是按照时间(例如T个时间点),每一个时间点建立一个数据结构。暴力做肯定会炸,所以通过指向历史节点的方法来节省空间,时间,从而实现访问历史情形。
至于具体是如何指向历史节点的,请直接看下面的例子
1.可持久化线段树(又名主席树/函数式线段树)
(这个详讲,后面的数据结构都差不多同理,毕竟我懒)
举个版题:POJ 2104
题意就是给出一个数组(10w),和一些询问(5k),每次询问给出一个区间[a,b](数组下标)和k,表示求[a,b]中第k大的数。
这道题有很多种解法,这里只讨论可持久化线段树做法
我们可以先读入数据,离散化,按照数组下标划分时间版本(即输入数组第一个数后版本为1,输入第二个数后版本为2),然后将线段树维护的值sum定义为:在这个版本上的所有数,在[l,r]区间内的有多少个。这样的话询问数组下标[a,b]中第k大时,就可以将版本b和版本a-1的同一区间的维护值相减,来得到[a,b]的实际sum,然后在树里按往常的方法搜,即,如果当前节点左儿子(两个版本相减)的sum大于等于当前k,就往左找,否则往右找,到叶子时,这个节点的[l,r],l或者r(l=r)就是要找的数。
接下来就是可持久化的关键:指向上一版本节点
当我们读入第i个数时(记为a),加入,那么线段树中受影响的节点数只有log(n)个,它跟上一个版本不一样节点的也只有log(n)个那么仅新建受影响的节点,sum=上一个版本sum+1,就可以了,这些节点的左右儿子,除了指向新建的节点的,都指向上一个版本的节点。
PS:建议将未加入任何数的版本build出来作为0版本
举个例子:
形象的说,只有从上往下的一条线上的节点改了,数量就是线段树深度log(n)
附上爆丑无比的 AC代码:
#include<cstdio>
#include<algorithm>
#define MAXN 100006
using namespace std;
int b[MAXN],aa,bb,a[MAXN],root[MAXN];
int n,m,cnt,bn,k;
struct node
{
int ls,rs;
int sum;
}I[MAXN*20];
int place(long long x)
{
int l=1,r=bn;
while(l<r)
{
int mid=(l+r)/2;
if(b[mid]>x) r=mid-1;
if(b[mid]==x) break;
if(b[mid]<x) l=mid+1;
}
return (l+r)/2;
}
void build(int now,int l,int r)
{
if(l==r) return;
int mid=(l+r)/2;
I[now].ls=++cnt;
build(I[now].ls,l,mid);
I[now].rs=++cnt;
build(I[now].rs,mid+1,r);
}
void insert(int now,int last,int x,int l,int r)
{
int mid=(l+r)/2;
if(l==r) return;
if(x>mid)
{
I[now].ls=I[last].ls;
I[now].rs=++cnt;
I[cnt].sum=I[I[last].rs].sum+1;
insert(cnt,I[last].rs,x,mid+1,r);
}
else
{
I[now].rs=I[last].rs;
I[now].ls=++cnt;
I[cnt].sum=I[I[last].ls].sum+1;
insert(cnt,I[last].ls,x,l,mid);
}
}
int find(int now,int last,int k,int l,int r)
{
int mid=(l+r)/2;
if(l==r) return l;
if(I[I[now].ls].sum-I[I[last].ls].sum<k) return find(I[now].rs,I[last].rs,k-I[I[now].ls].sum+I[I[last].ls].sum,mid+1,r);
else return find(I[now].ls,I[last].ls,k,l,mid);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
b[i]=a[i];
}
sort(b+1,b+1+n);
bn=unique(b+1,b+1+n)-b-1;
root[0]=++cnt;
build(root[0],1,bn);
for(int i=1;i<=n;i++)
{
root[i]=++cnt;
I[root[i]].sum=I[root[i-1]].sum+1;
insert(root[i],root[i-1],place(a[i]),1,bn);
}
for(int t=1;t<=m;t++)
{
scanf("%d%d%d",&aa,&bb,&k);
printf("%d\n",b[find(root[bb],root[aa-1],k,1,bn)]);
}
}
2.可持久化数组
建立线段树,除了叶子结点以外的其它所有节点,都只起记录左右儿子的作用,叶子结点记录数组下标对应的值,同样用之前的可持久化线段树的方式,指向历史节点就可以了。
3.可持久化并查集
并查集本体就是一个fa数组,只要通过可持久化线段树实现可持久化数组,再把并查集操作稍微改一下就可以了。
4.可持久化Tri(字典树)
同上
5.可持久化Treap
先说Treap:由于随机建树期望深度是log1.39(n)的,所以直接随机建树,通过给每一个数赋随机权值,加数时除了保证平衡树性质,同时保证子树任意值权值大于(或小于)当前节点权值即可。合并操作时,也要如此做。
然后可持久化就可以了(滑稽)
PS:这几个版我写的都奇丑,就懒得粘上来了,喵。