题解:【模板】可持久化线段树 1 & 2
可持久化线段树 1
Problem
维护序列在某个历史版本上修改/查询某一个位置上的值。
算法介绍
什么是可持久化线段树呢?首先来考虑怎么暴力解决这个问题:
弱化版:n,m\le 10^3
对于每一次操作,我们每次复制一个版本即可。
这样子时空复杂度是
标准版:n,m\le 10^5
首先我们将这个序列化成一个线段树。(至于为什么后面再讲)可是这样子复制的话还是
我们来观察一次复制:
其实真正修改了的只有 树高 个节点(即
但是这样子就不能简单地使用基本的线段树了,于是就有了“可持久化线段树”。
可持久化线段树的原理就是如上讲的,每次修改只插入一条链。
代码实现
按照刚才的分析,最重要的问题就是无法建二叉树。
似乎在动态开点线段树中,我们也遇到了这个问题,当时我们使用了结构体,来存储左儿子右儿子编号。
struct tree{
int l,r,val;
};
既然这样我们就需要开启新结点:
int add(int node)
{
a[++top]=a[node];
return top;
}
top 的意思是当前节点编号,node 的意思是原来的节点“本来修改的对象”,先复制一遍,再进行复制。
接着考虑建树,首先我们定义一个新数组 tr[i],表示第 build 时返回当前节点编号即可。代码实现如下:
int build(int node,int l=1,int r=n)
{
node=++top; // 其实也是 add(node),但是初始版本不用复制,直接这么写就可以了。
if(l==r)
{
a[node].val=read(); // 本题可以直接输入
return node;
}
int mid=(l+r)/2;
a[node].l=build(a[node].l,l,mid);
a[node].r=build(a[node].r,mid+1,r);
return node;
}
update 和 build 差不多,但是 update 不能 node=++top,而需要 node=add(node):
int update(int w,int val,int node,int l=1,int r=n)
{
node=add(node);
if(l==r) a[node].val=val;
else
{
int mid=(l+r)/2;
if(w<=mid) a[node].l=update(w,val,a[node].l,l,mid);
else a[node].r=update(w,val,a[node].r,mid+1,r);
}
return node;
}
query 实现就非常简单了,和正常的没有区别:
int query(int w,int node,int l=1,int r=n)
{
if(l==r) return a[node].val;
else
{
int mid=(l+r)/2;
if(w<=mid) return query(w,a[node].l,l,mid);
else return query(w,a[node].r,mid+1,r);
}
}
建树的话调用这个代码就可以了:
tr[0]=build(0); // 也可以写作 tr[0]=1;build(0);
修改和查询同理。
那么最后的代码如下:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n;
int read() // 快读
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
struct ZX__tree{
struct tree{
int l,r,val;
};
tree a[32000006];
int tr[32000006];
int top;
int build(int node,int l=1,int r=n)
{
node=++top;
if(l==r)
{
a[node].val=read();
return node;
}
int mid=(l+r)/2;
a[node].l=build(a[node].l,l,mid);
a[node].r=build(a[node].r,mid+1,r);
return node;
}
int add(int &node)
{
top++;
a[top]=a[node];
return top;
}
int update(int w,int val,int node,int l=1,int r=n)
{
node=add(node);
if(l==r) a[node].val=val;
else
{
int mid=(l+r)/2;
if(w<=mid) a[node].l=update(w,val,a[node].l,l,mid);
else a[node].r=update(w,val,a[node].r,mid+1,r);
}
return node;
}
int query(int w,int node,int l=1,int r=n)
{
if(l==r) return a[node].val;
else
{
int mid=(l+r)/2;
if(w<=mid) return query(w,a[node].l,l,mid);
else return query(w,a[node].r,mid+1,r);
}
}
}a;
signed main()
{
int T;
cin>>n>>T;
a.tr[0]=a.build(0);
for(int i=1;i<=T;i++)
{
int v=read(),op=read();
if(op==1)
{
int w=read(),val=read();
a.tr[i]=a.update(w,val,a.tr[v]);
}
else
{
int w=read();
cout<<a.query(w,a.tr[v])<<endl;
a.tr[i]=a.tr[v];
}
}
return 0;
}
正确性证明
主要是对时空复杂度的证明。
对于时间复杂度:很显然的是
对于空间复杂度:因为每次只需要建一条链,空间复杂度也优化到了
所以时空复杂度均为
为什么要将序列变为线段树
- 本题虽然用不到节点合并,但并不排除别的题有(比如下一道就有)
- 虽然用不到节点合并,但是二叉树保证了每次我只需要修改左儿子或者右儿子,所以降低了时间复杂度。
不然就不是可持久化线段树的例题了。