概述
算法模型:可持久化线段树(主席树)
空间复杂度:O((n+m)logn)
时间复杂度:O((n+m)logn)其中
预处理:O(nlogn)
单次修改:O(logn)
单次查询:O(logn)
1.思想
根据题目要求,我们要做的就是保存m个版本的状态,并实现查找与修改操作。
简单想法:真的开数组保存每个版本。当然会MLE(空间复杂度O(n*m))!!!
idea:我们注意到,对于修改操作,当前版本与它的前驱版本相比,只更改了一个节点的值,其他大多数节点的值没有变化。
能不能重复利用,以达到节省空间的目的?
——分治?没错,如果只修改了左半边,那么我们可以使用前驱版本的右半边,反之同理。(参见楼下某大佬的图)
于是,我们就可以用线段树,进行修改操作时,只要当前节点的左(右)儿子没有被修改,我们就可以使用前驱版本的那个节点。
那查找呢?每次保存版本i的根节点,利用线段树的方法查找就好了。
2.模型
build 函数:
int build(int l,int r)
建立以【l,r】为区间的(初始)线段树。
update 函数:
int update(int pre,int l,int r,int &x,int &c)
对第 pre 版本的【l,r】区间递归查找并修改第 x 位置的值为 c 。
query 函数:
void query(int pre,int l,int r,int& x)
对第 pre 版本的【l,r】区间递归查找第 x 位置的值。
3.代码实现
#include<iostream>
#include<cstdio>
#include<algorithm>
#define mid ((l+r)>>1)
using namespace std;
struct chairman{
//参见空间复杂度,要开大数组
int rt[1000001],T[20000001],L[20000001],R[20000001];
int cnt;//尾节点,插入节点用
int build(int l,int r){
int root=++cnt;
if(l==r){
scanf("%d",&T[root]);return root;
//建树与读入合二为一
}
//递归建子区间
L[root]=build(l,mid);R[root]=build(mid+1,r);
return root;
}
int update(int pre,int l,int r,int &x,int &c){
int root=++cnt;
if(l==r){
T[root]=c;return root;//修改
}
L[root]=L[pre];R[root]=R[pre];//先把子节点指向前驱结点以备复用
//递归修改子区间
if(x<=mid)L[root]=update(L[pre],l,mid,x,c);
else R[root]=update(R[pre],mid+1,r,x,c);
return root;
}
void query(int pre,int l,int r,int& x){
//普通的线段树查询
if(l==r){
printf("%d\n",T[pre]);return;
}
if(x<=mid)query(L[pre],l,mid,x);
else query(R[pre],mid+1,r,x);
}
}hsf;
int main(){
hsf.cnt=0;
int n,m;
scanf("%d%d",&n,&m);
hsf.build(1,n);
int v,cd,x,y;
hsf.rt[0]=1;//第零版本
for(int i=1;i<=m;++i){
scanf("%d%d%d",&v,&cd,&x);
if(cd==1){
scanf("%d",&y);
hsf.rt[i]=hsf.update(hsf.rt[v],1,n,x,y);//修改
}
if(cd==2){
hsf.rt[i]=hsf.rt[v];
hsf.query(hsf.rt[v],1,n,x);//查询
}
}
return 0;
}
4.总结
可见,可持久化线段树不仅保持了线段树良好的修改/查询方式,也利用了分治思想,降低了空间复杂度,提高了效率。