可持久化留下的迹象 我们俯身欣赏
——《膜你抄》By Menci&24OI
Micardi最近在学可持久化的东西,可持久化线段树、可持久化并查集、可持久化01Trie......等等等等恶心人的东西QwQ。不过呢,犯其至难,图其至远,如果能静下心来搞定这些乱七八糟的玩意,相信对自己的OI之路也会大有裨益吧QwQ。
言归正传!
1.主席树(可持久化线段树)
属于Micardi的数据结构!
什么是主席树?
主席树,本名可持久化线段树——顾名思义,它是一种基于线段树、并且可持久的一种数据结构。为什么要叫主席树?因为它是由一位名叫黄嘉泰的巨佬发明的......而这位巨佬的名字缩写对应了前国家主席2333333因此得名。主席树还有个名字叫函数式线段树,这我们就不用去管因为什么了因为我也不太懂啊QAQ......
主席树怎么用?
来,让我们步入正题:
Q1:给出序列an,你需要对该序列进行两种操作:1.在某一历史版本基础上修改某个元素值;2.查询某一历史版本里的某一元素值。我们定义初始序列为版本0,每进行一次操作都会生成一个新的版本。序列内元素个数n、操作数m均≤5×106。
单点修改+单点查询,很自然地,我们想到了线段树、树状数组(分块、莫队、splay)......于是最暴力的做法显而易见:每一个版本我们都建立一颗线段树,然后模拟修改和查询。时间复杂度O(mnlogn),显然无法承受——而且m棵线段树,真的不怕赤果果的MLE嘛?
这个时候,我们就要搬出我们的主席树啦!
我们考虑这样一个问题:从算法正确性上考虑,不停地建立线段树——是对的,我们需要做的是优化这种做法可怕的时空复杂度。如何优化呢?我们要从线段树特点上下手啦:
假设已知序列1,2,3,4,5,我们建一颗线段树:
如果我们现在要修改3,会有哪些节点被修改呢?
很显然啦,只有这个叶子节点到树根一条路上的节点会被修改——深入思考一下不难发现,对于每一次单点修改,我们其实只需要修改它->它爸->它爷爷->......->树根这些节点。由于线段树是一颗平衡二叉树,树高差不多就是logn(这么不严谨应该拖出去打死)所以我们每次只需要修改logn个节点即可——完全用不着再重新建一整颗线段树。
可是我们怎么知道要改哪些呢?——画画图也不难发现,由于线段树每一个节点左儿子的sum存储的是该节点对应区间左半边的信息,右儿子存储的是右半边的信息,假设我们要修改的位置是x,那么如果x在左半边(即x<=mid)我们就去修改左子树,保留右子树,反之亦然。这样从根节点递归到叶子结点,我们就可以完成一次修改啦:
#include<bits/stdc++.h> #define mid ((l+r)/2) using namespace std; const int N=400017; int sum[N<<5],L[N<<5],R[N<<5],rt[N<<5],a[N],cnt; int n,m; int build(int l,int r) { int rt=++cnt; if(l==r) { sum[rt]=a[l]; return rt; } L[rt]=build(l,mid);R[rt]=build(mid+1,r); return rt; } int upd(int pre,int l,int r,int x,int v)//把第pre个版本修改成v { int rt=++cnt; L[rt]=L[pre],R[rt]=R[pre],sum[rt]=sum[pre]; if(l==r){sum[rt]=v;return rt;} if(x<=mid)L[rt]=upd(L[pre],l,mid,x,v); else R[rt]=upd(R[pre],mid+1,r,x,v); return rt; } int query(int pre,int l,int r,int x) { if(l==r)return sum[pre]; if(x<=mid)return query(L[pre],l,mid,x); else return query(R[pre],mid+1,r,x); } int main() { scanf("%d%d",&n,&m); for(register int i=1;i<=n;++i)scanf("%d",&a[i]); rt[0]=build(1,n); for(register int i=1;i<=m;++i) { int opt,x,y; scanf("%d%d%d",&x,&opt,&y); if(opt==1) { int z;scanf("%d",&z); rt[i]=upd(rt[x],1,n,y,z); } else { rt[i]=rt[x]; printf("%d\n",query(rt[x],1,n,y)); } } return 0; }