[洛谷P3919][题解]可持久化数组&&主席树讲解
终于发现在哪里启用\(Markdown\)和\(\LaTeX\)了…
什么是主席树
主席树的全名是可持久化线段树,从名字就可以看出来,它很持久
是一种可以回退到任意历史版本的神器!
如何实现主席树
主席树的功能看起来很美妙,那我们怎么实现呢?
既然要记录历史版本,那我们把历史版本都存下来不就完了?
呵,天真。MLE等着你
先丢一张图:
看起来很毒瘤对不对,其实我们只要一步步来拆分就好啦~
首先,这是一棵正常的线段树:
众所周知,线段树在修改或查询时要走过一条长度\(n\log n\)的路径
可以发现,只有这条路径上的数被修改了!
所以我们秉承着不铺张浪费的原则,只需要新建被修改的节点,然后其他节点用之前的
我们试着修改一下:
这样,我们的空间就小了很多,减少到了\(O((n+m)\log n)\)!
具体为什么是这么大自己想一想就好咯
那么,再修改一个试试?
代码实现
主席树有很多版本,所以我们需要用一个rt[N]
来记录每个历史版本的根
而且由于结构的特殊性导致主席树不能用<<1
或<<1|1
来存储子节点,我们必须使用结构体存储
struct Node {
int l,r,data;
}tr[N*20];//空间的原因刚才已经说过了
那么新建节点也是必须有的了(此处的新建节点需先复制之前的)
inline int Newnode(int x){
tr[++tot]=tr[x];
return tot;
}
建树操作和普通线段树一样:(ls
,rs
,nmid
是我习惯用的宏定义,明白意思就好)
int Build(int k,int l,int r){
k=++tot;
if(l==r){//到了
tr[k].data=a[l];
return tot;
}
ls=Build(ls,l,nmid);//建左子树
rs=Build(rs,nmid+1,r);//建右子树
return k;
}
修改操作要记得新建节点以便后续回退:
int Modify(int k,int l,int r,int pos,int num){
k=Newnode(k);
if(l==r){
tr[k].data=num;//到了
}else {
if(pos<=nmid)ls=Modify(ls,l,nmid,pos,num);//往左走
else rs=Modify(rs,nmid+1,r,pos,num);//or 往右走
}
return k;
}
查询操作也是大同小异:
int Query(int k,int l,int r,int pos){
if(l==r){
return tr[k].data;//到了
}else {
if(pos<=nmid)return Query(ls,l,nmid,pos);//往左走
else return Query(rs,nmid+1,r,pos);//or 往右走
}
}
最后查询就直接从对应版本的根上查询就好啦(别忘了询问操作也要复制一遍,此时只新建根就好了)
Code:(不带注释)
#include<cstdio>
#include<cstdlib>
#include<cmath>
#include<ctime>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<stack>
#include<queue>
#include<vector>
#include<bitset>
#include<set>
#include<map>
#define LL long long
#define rg register
#define us unsigned
#define eps 1e-6
#define INF 0x3f3f3f3f
#define ls tr[k].l
#define rs tr[k].r
#define tmid ((tr[k].l+tr[k].r)>>1)
#define nmid ((l+r)>>1)
#define Thispoint tr[k].l==tr[k].r
#define pushup tr[k].wei=tr[ls].wei+tr[rs].wei
#define pub push_back
#define lth length
using namespace std;
inline void Read(int &x){
int f=1;
char c=getchar();
x=0;
while(c<'0'||c>'9'){
if(c=='-')f=-1;
c=getchar();
}
while(c>='0'&&c<='9'){
x=(x<<3)+(x<<1)+c-'0';
c=getchar();
}
x*=f;
}
#define N 1000010
int n,m,a[N],rt[N],tot;
struct Node {
int l,r,data;
}tr[N*20];
inline int Newnode(int x){
tr[++tot]=tr[x];
return tot;
}
int Build(int k,int l,int r){
k=++tot;
if(l==r){
tr[k].data=a[l];
return tot;
}
ls=Build(ls,l,nmid);
rs=Build(rs,nmid+1,r);
return k;
}
int Modify(int k,int l,int r,int pos,int num){
k=Newnode(k);
if(l==r){
tr[k].data=num;
}else {
if(pos<=nmid)ls=Modify(ls,l,nmid,pos,num);
else rs=Modify(rs,nmid+1,r,pos,num);
}
return k;
}
int Query(int k,int l,int r,int pos){
if(l==r){
return tr[k].data;
}else {
if(pos<=nmid)return Query(ls,l,nmid,pos);
else return Query(rs,nmid+1,r,pos);
}
}
int main(){
Read(n),Read(m);
for(rg int i=1;i<=n;i++)Read(a[i]);
rt[0]=Build(0,1,n);
for(rg int i=1;i<=m;i++){
int idx,opt,pos,num;
Read(idx),Read(opt),Read(pos);
if(opt==1){
Read(num);
rt[i]=Modify(rt[idx],1,n,pos,num);
}else {
printf("%d\n",Query(rt[idx],1,n,pos));
rt[i]=rt[idx];
}
}
return 0;
}
总结
这个算法还是很好背板子理解的,重点是明白新建节点操作的原因和好处
完。
内容来自_ajthreac_的博客(https://www.cnblogs.com/juruoajh/),未经允许,不得转载。