最近正在学习一种数据结构——可持久化线段树。看了网上的许多博客,弄了几道模板题,思路有点乱了,所以还是来总结整理下吧。
可持久化线段树
首先要了解此数据结构的基础——线段树。百度一下,你就知道!
推荐一下这篇博客,对线段树的基本操作讲得挺详细的。
为了更好地理清思路,我在这里先放个模板题吧。
洛谷题目传送门
题目描述
你需要维护这样的一个长度为\(N\)的数组,支持如下几种操作
- 在某个历史版本上修改某一个位置上的值
- 访问某个历史版本上的某一位置的值
此外,每进行一次操作(对于操作2,即为生成一个完全一样的版本,不作任何改动),就会生成一个新的版本。版本编号即为当前操作的编号(从1开始编号,版本0表示初始状态数组)
输入输出格式
输入格式:
输入的第一行包含两个正整数\(N,M\)分别表示数组的长度和操作的个数。
第二行包含 N N个整数,依次为初始状态下数组各位的值(依次为\(a_i, 1 \leq i \leq N\))。
接下来\(M\)行每行包含3或4个整数,代表两种操作之一(\(i\)为基于的历史版本号):
对于操作1,格式为\(v_i \ 1 \ {loc}_i \ {value}_i v\),即为在版本\(v_i\)的基础上,将\(a_{{loc}_i}\)修改为\({value}_i\)
对于操作2,格式为\(v_i \ 2 \ {loc}_i\),即访问版本\(v_i\)中的\(a_{{loc}_i}\)的值
输出格式:
输出包含若干行,依次为每个操作2的结果。
输入输出样例
输入样例#1:
5 10
59 46 14 87 41
0 2 1
0 1 1 14
0 1 1 57
0 1 1 88
4 2 4
0 2 5
0 2 4
4 2 1
2 2 2
1 1 5 91
输出样例#1:
59
87
41
87
88
46
说明
数据规模:
对于30%的数据:\(1 \leq N, M \leq {10}^3\)
对于50%的数据:\(1 \leq N, M \leq {10}^4\)
对于70%的数据:\(1 \leq N, M \leq {10}^5\)
对于100%的数据:\(1 \leq N, M \leq {10}^6, 1 \leq {loc}_i \leq N, 0 \leq v_i < i, -{10}^9 \leq a_i, {value}_i \leq {10}^9\)
经测试,正常常数的可持久化数组可以通过,请各位放心
数据略微凶残,请注意常数不要过大
另,此题I/O量较大,如果实在TLE请注意I/O优化
询问生成的版本是指你访问的那个版本的复制
样例说明:
一共11个版本,编号从0-10,依次为:
0 : 59 46 14 87 41
1 : 59 46 14 87 41
2 : 14 46 14 87 41
3 : 57 46 14 87 41
4 : 88 46 14 87 41
5 : 88 46 14 87 41
6 : 59 46 14 87 41
7 : 59 46 14 87 41
8 : 88 46 14 87 41
9 : 14 46 14 87 41
10 : 59 46 14 87 91
思路分析
很裸的可持久化线段树板子题。可持久嘛!就是当出现历史版本的时候,能够非常方便地维护一个区间的历史版本。
自然,我们需要建\(N\)棵线段树。最粗暴的想法,对每个新版本都把原版本内容复制一遍,然后修改对应的值。这根本不用想,直接MLE+TLE。那维护历史版本又是怎样实现的呢?
对于本题,每个版本的序列,我们可以建一棵线段树来维护它,所有非叶子节点表示的是一段区间,而叶子节点就表示序列的每一个值了。
举个栗子,样例中初始版本可以长这样——
而版本1只是查询了一下(线段树基本操作,这里不再赘述),然后跟初始版本一模一样。这就没必要复制了嘛!我们设版本\(i\)有一个根节点\(root_i\)(表示整段区间),根节点有左右儿子,那么我们直接让\(root_1\)的左右儿子指向\(root_0\)的左右儿子就好了,根本不用复制整个线段树嘛!
那再来看看修改操作。比如从版本1~2。1和0是一样的,而版本2会长这样——
有没有发现1和2真的很像?其实从前到后只改变了一个节点!那么其他相同的地方,我们可不可以共用一段内存呢?
没错,每次创建一个新的版本时,只要新建\(\log_2 n\)个节点,也就是只保存从新版本的根节点到更新的那一个叶子节点的路径就可以了,不在此路径上的左/右儿子只要接原版本对应区间的对应儿子就可以啦。我们可以保证,从对应版本的根节点一定能访问到对应叶子节点的值。
下面是加入新版本的具体实现代码(我写的是非递归版):
#define R register int
inline void insert(R*t,R u,R l,R r,R k)
//t是当前节点指针,u是原版本对应t的节点,l、r为当前区间,k为修改点的位置
{
R m;
while(l!=r)
{
*t=++P;//为新节点分配空间,P是个外部变量
m=(l+r)>>1;//线段树操作,计算区间中点
if(k<=m)r=m,rc[*t]=rc[u],t=&lc[*t],u=lc[u];
else l=m+1,lc[*t]=lc[u],t=&rc[*t],u=rc[u];
//上面两行很关键。(if一行)如果k在左子树中,那么右子树没有变,直接连到旧版本的对应右子树上,t、u更新为当前左子树继续。(else一行反之亦然)
}
in(val[*t=++P]);//读入新叶子节点的值
}
整个程序的代码如下
#include
#include
#define R register int
const int N=1000009,M=20000009;
int P,rt[N],lc[M],rc[M],val[M];
char I[M<<1],O[M],*fi=I,*fo=O;
bool nega;
inline void in(R&z)
{
while(*fi<'-')++fi;
if(*fi=='-')nega=1,++fi;
z=*fi++&15;
while(*fi>'-')z*=10,z+=*fi++&15;
if(nega)nega=0,z=-z;
}
void oi(R z)
{
if(z>9)oi(z/10);
*fo++=z%10|'0';
}
inline void out(R z)
{
z>0?oi(z):(*fo++='-',oi(-z));*fo++='\n';
}//上面快读快写
void build(R&t,R l,R r)//初始化建树,线段树基本操作
{
R m;
t=++P;
if(l!=r)
{
m=(l+r)>>1;
build(lc[t],l,m);
build(rc[t],m+1,r);
}
else in(val[P]);
}
inline void insert(R*t,R u,R l,R r,R k)//更新,插入一个新路径
{
R m;
while(l!=r)
{
*t=++P;
m=(l+r)>>1;
if(k<=m)r=m,rc[*t]=rc[u],t=&lc[*t],u=lc[u];
else l=m+1,lc[*t]=lc[u],t=&rc[*t],u=rc[u];
}
in(val[*t=++P]);
}
inline int ask(R t,R l,R r,R k)//询问
{
R m;
while(l!=r)
{
m=(l+r)>>1;
if(k<=m)r=m,t=lc[t];
else l=m+1,t=rc[t];
}
return val[t];
}
int main()
{
freopen("ct.in","r",stdin);freopen("ct.out","w",stdout);
fread(I,1,sizeof(I),stdin);
R n,m,i,v,op,loc;
in(n);in(m);
build(rt[0],1,n);
for(i=1;i<=m;++i)
{
in(v);in(op);in(loc);
if(op&1)insert(&rt[i],rt[v],1,n,loc);
else
{
out(ask(rt[v],1,n,loc));
rt[i]=++P;//没错,这里的版本复制其实很简单
lc[P]=lc[rt[v]];
rc[P]=rc[rt[v]];
}
}
fwrite(O,1,fo-O,stdout);
fclose(stdin);fclose(stdout);
return 0;
}