[洛谷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;
}

总结

这个算法还是很好背板子理解的,重点是明白新建节点操作的原因和好处
完。

posted @ 2020-04-04 14:01  ajthreac  阅读(522)  评论(0编辑  收藏  举报