主席树也是一种数据结构,是线段树的进阶,作用是可以保留历史版本,支持回退,就是回到之前某个未修改的状态。就像我们在写博客时如果误删了重要的内容,就可以用 Ctrl + z
撤回一次操作,也可以用Ctrl + Shift +z
还原我们撤回的操作,这就需要存下每次编辑的操作。
可持久化线段树,顾名思义,是可持久的线段树(好像是废话)。那么问题就在于怎么去可持久化,支持访问历史版本。
最暴力的,直接复制整棵树,再对新的这棵树进行修改。但这样的话,节点数为 \(n\),操作数为 \(m\),时间复杂就是 \(O(nm)\)。
好了,直接 TLE。显然复制这棵树会有非常多的浪费,思考一下,我们只需要复制修改后发生改变的点就好了。
我们要复制的只是蓝色的点其他的保持不变,要在蓝色的点边上复制一个一模一样的点然后修改。如图:
红色的点即为修改后的点,继承原节点的所有信息,包括未修改的儿子。这样我们只需要存下每次修改后的根,就能访问该版本的树。修改的时间复杂度为 \(O(\log{n})\)。
定义节点,存 5 个信息:左右儿子,左右端点,权值。
#define lc a[u].l #define rc a[u].r #define L a[u].ls #define R a[u].rs struct tree { int l,r; int ls,rs; int v; }a[N];
int tot; int New(int u) { a[++tot]=a[u];//结构体直接复制所有信息 return tot; }
和线段树类似,不过节点编号依次增加,但左右儿子不一定是乘二和乘二加一了。
int build(int u,int l,int r) { u=++tot; L=l,R=r;//更新区间端点 if(l==r) { a[u].v=w[l]; return u; } int mid=(l+r)>>1; lc=build(lc,l,mid); rc=build(rc,mid+1,r); return u; }
先新建节点,然后在新建的树中修改,要记得更新修改了的儿子。
int modify(int u,int x,int v) { u=New(u); if(L==R) a[u].v=v; else { int mid=(L+R)>>1; if(x<=mid) lc=modify(lc,x,v); else rc=modify(rc,x,v); } return u; }
和线段树一毛一样的。
int query(int u,int x) { if(L==R) return a[u].v; else { int mid=(L+R)>>1; if(x<=mid) return query(lc,x); else return query(rc,x); } }
每次修改后最终返回的是新的根节点,所以直接用数组存就好了。
P3919 【模板】可持久化线段树 1(可持久化数组)
#include <bits/stdc++.h> using namespace std; typedef long long ll; #define lc a[u].l #define rc a[u].r #define L a[u].ls #define R a[u].rs const int M=3e7+5,N=1e6+5; struct tree { int l,r; int ls,rs; int v; }a[M]; int n,tot,w[N],rt,root[N],m; int New(int u) { a[++tot]=a[u];//结构体直接复制所有信息 return tot; } int build(int u,int l,int r) { u=++tot; L=l,R=r;//更新区间端点 if(l==r) { a[u].v=w[l]; return u; } int mid=(l+r)>>1; lc=build(lc,l,mid); rc=build(rc,mid+1,r); return u; } int modify(int u,int x,int v) { u=New(u); if(L==R) a[u].v=v; else { int mid=(L+R)>>1; if(x<=mid) lc=modify(lc,x,v); else rc=modify(rc,x,v); } return u; } int query(int u,int x) { if(L==R) return a[u].v; else { int mid=(L+R)>>1; if(x<=mid) return query(lc,x); else return query(rc,x); } } int main () { ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>m; for(int i=1;i<=n;i++) cin>>w[i]; root[0]=build(0,1,n); rt=0; for(int i=1,op,x,y;i<=m;i++) { cin>>rt>>op>>x; if(op==1) cin>>y,root[i]=modify(root[rt],x,y); else cout<<query(root[rt],x)<<"\n",root[i]=root[rt]; } return 0; }
要注意节点实际个数。
P3834 【模板】可持久化线段树 2
首先,因为数据跨度太大,所以先离散化。嗯,不会?没事,看 离散化。
然后,基于数值建立线段树,即权值线段树,维护一段值域内的数的个数。权值线段树求整个区间的第 \(k\) 小的数,就是在权值线段树上二分,从根节点开始,如果 \(k\) 比右儿子大,就说明第 \(k\) 小的数在右儿子所表示的值域中。接着,\(k\) 要减去左儿子,再进入右儿子中继续;如果 \(k\) 比左儿子小,就直接进入左儿子。
那么好,子区间 \([l,r]\) 的第 \(k\) 小的数怎么求呢?(区间都是离散化好了的)
首先思考求 \([1,r]\) 的第 \(k\) 小的数,用主席树,依次加入 \([1,r]\) 的值的节点,生成 \(r\) 个版本,从 \(root[r]\) 开始找就可以了。
那么右边有限制,左边也有限制,该怎么办呢?先明确一点,主席树,每次修改后,对于每个根节点对应的每一棵树,结构都是相同的,就是拓扑排序相同。
也就是说,所有节点对应的区间都是一一对应的。在根节点为 \(root[R]\) 的树中值域为 \([l,r]\) 的个数为 \(cnt1\),在根节点为 \(root[L-1]\) 的树中值域为 \([l,r]\) 的个数为 \(cnt2\),类似于前缀和的思想,\([L,R]\) 中 \([l,r]\) 的个数为 \(cnt1-cnt2\)。
这样就可以了,只需要在递归的时候统计个数,找到对应的那个数输出即可。
#include <bits/stdc++.h> using namespace std; #define lc a[u].l #define rc a[u].r #define L a[u].ls #define R a[u].rs const int N=2e5+5,M=3e7+5; struct tree { int l,r; int ls,rs; int cnt; }a[M]; int n,m,tot,root[N],rt,idx; int b[N],c[N],C[N]; int New(int u) { a[++idx]=a[u]; return idx; } void pushup(int u) { a[u].cnt=a[lc].cnt+a[rc].cnt; } int build(int l,int r) { int u=++idx; L=l,R=r; if(l==r) return u; int mid=(l+r)>>1; lc=build(l,mid); rc=build(mid+1,r); return u; } int find(int x) { return lower_bound(c+1,c+tot+1,x)-c; } int modify(int u,int x) { u=New(u); if(L==R) { a[u].cnt++; return u; } int mid=(L+R)>>1; if(x<=mid) lc=modify(lc,x); else rc=modify(rc,x); pushup(u); return u; } int query(int u,int v,int x) { if(L==R) return R; int cnt=a[a[v].l].cnt-a[a[u].l].cnt; if(x<=cnt) return query(a[u].l,a[v].l,x); else return query(a[u].r,a[v].r,x-cnt); } int main () { ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>m; for(int i=1;i<=n;i++) cin>>b[i],c[i]=b[i]; sort(c+1,c+n+1); tot=n;//不用去重 root[0]=build(1,tot); for(int i=1;i<=tot;i++) root[i]=modify(root[i-1],find(b[i])); while(m--) { int l,r,k; cin>>l>>r>>k; cout<<c[query(root[l-1],root[r],k)]<<"\n"; } }
本题离散化不需要去重,因为按值域加点每次只会增加一 QAQ。