可持久化数据结构
什么是可持久化
保留每个版本,其中只有最新版本可以修改的称为部分可持久化,所有版本都可以修改的称为完全可持久化。
可持久化线段树
可持久化数组
Luogu P3919 【模板】可持久化线段树 1(可持久化数组)
每次修改/访问都基于之前的某个版本,保存每个版本。
可持久化数组本质是多根线段树,每棵线段树代表一个版本,但有些版本具有重合部分,此时可以共用节点。由于这个特性,因此需要动态开点。其余部分与线段树无异。
时间复杂度为 \(O(m \log n)\),空间复杂度约为 \(2n-1+m \times (\lceil \log n\rceil+1)\).
其中 \(2n-1\) 是初始线段树由于动态开点的特性所得的节点个数,每次修改/访问只会新建出一条链的节点,因此为 \(\lceil \log n\rceil+1\) 单次,不过一般会开 \(2^5 \times N\).
#include <cstdio>
const int N=1e6+10;
const int M=1e6+10;
int n,m;
int a[N],rt[N]; // rt 用于记录每个版本的根
struct Per_Seg {
struct node {
int l,r,ls,rs;
int val;
}tr[N<<5];int idx=0;
#define ls (tr[p].ls)
#define rs (tr[p].rs)
#define L (tr[p].l)
#define R (tr[p].r)
int clone(int p) { // copy 一个节点 from 节点 p
tr[++idx]=tr[p];return idx;
}
int build(int l,int r) {
int p=++idx;L=l;R=r;
if(l==r) {tr[p].val=a[l];return p;}
int mid=l+r>>1;
ls=build(l,mid);rs=build(mid+1,r);
return p;
}
int modify(int lst,int pos,int val) {
int p=clone(lst);
if(L==R) {tr[p].val=val;return p;}
int mid=L+R>>1;
if(pos<=mid) ls=modify(ls,pos,val);
else rs=modify(rs,pos,val);
return p;
}
int query(int p,int pos) {
if(L==R) return tr[p].val;
int mid=L+R>>1;
if(pos<=mid) return query(ls,pos);
else return query(rs,pos);
}
#undef ls
#undef rs
#undef L
#undef R
}T;
int main() {
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
rt[0]=T.build(1,n);
for(int i=1,v=0,loc=0,type=0,val=0;i<=m;i++) {
scanf("%d%d%d",&v,&type,&loc);
if(type==1) {
scanf("%d",&val);
rt[i]=T.modify(rt[v],loc,val);
} else {
rt[i]=rt[v];
printf("%d\n",T.query(rt[v],loc));
}
}
return 0;
}
可持久化权值线段树(主席树)
最经典最入门的问题——静态区间 kth Luogu P3834 【模板】可持久化线段树 2
首先离散化,把值域缩小到 \(O(n)\) 级别。然后类比前缀和,按照下标顺序依次建立一棵权值线段树,第 \(i\) 棵线段树的节点存储从 \(1 \sim i\) 位于 \([l_i,r_i]\) 的值的出现个数。
对于每次询问,类比差分同时在线段树上二分即可。
时空复杂度同可持久化数组。
#include <cstdio>
#include <algorithm>
const int N=2e5+10;
const int M=2e5+10;
int n,m,tot;
int a[N],lsh[N],rt[N];
struct Per_Seg {
struct node {
int l,r,ls,rs;
int val;
}tr[N<<5];int idx=0;
#define ls(p) (tr[p].ls)
#define rs(p) (tr[p].rs)
#define L(p) (tr[p].l)
#define R(p) (tr[p].r)
int clone(int p) {
tr[++idx]=tr[p];return idx;
}
int init(int l,int r) {
int p=++idx;L(p)=l;R(p)=r;
if(l==r) return p;
int mid=l+r>>1;
ls(p)=init(l,mid);rs(p)=init(mid+1,r);
return p;
}
void modify(int p,int q,int val) {
++tr[q].val;
if(L(q)==R(q)) return;
int mid=L(q)+R(q)>>1;
if(val<=mid) {
ls(q)=clone(ls(p));
modify(ls(p),ls(q),val);
} else {
rs(q)=clone(rs(p));
modify(rs(p),rs(q),val);
}
}
void build() {
rt[0]=init(1,tot);
for(int i=1;i<=n;i++) {
rt[i]=clone(rt[i-1]);
modify(rt[i-1],rt[i],a[i]);
}
}
int query(int p,int q,int k) {
if(L(q)==R(q)) return L(q);
int res=tr[ls(q)].val-tr[ls(p)].val;
if(k<=res) return query(ls(p),ls(q),k);
else return query(rs(p),rs(q),k-res);
}
#undef ls
#undef rs
#undef L
#undef R
}T;
int main() {
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) lsh[i]=a[i];
std::sort(lsh+1,lsh+1+n);
tot=std::unique(lsh+1,lsh+1+n)-lsh-1;
for(int i=1;i<=n;i++) a[i]=std::lower_bound(lsh+1,lsh+1+tot,a[i])-lsh;
T.build();
for(int i=1,l=0,r=0,k=0;i<=m;i++) {
scanf("%d%d%d",&l,&r,&k);
printf("%d\n",lsh[T.query(rt[l-1],rt[r],k)]);
}
return 0;
}
而对于该题的整体二分做法,可见 离线算法 的整体二分-算法流程部分。
标记永久化
这是可持久化里的一个常用 trick,在 zkw 线段树中也有使用。
对于需要区间修改的可持久化线段树,显然不打 tag 时间复杂度无法承受,但是打 tag 又有可能导致把 tag 打到了别的版本上。因此就有了标记永久化。
标记永久化是指不把 tag push_down 而是在查询时加上该节点 tag 的贡献值。有几点需要注意:
- 在 push_up 也应考虑 tag 给的贡献。
- 当前节点的 tag 已经对当前节点处理好了贡献,tag 指向子节点。
- 在查询时要注意加上 tag 的贡献。
有了标记永久化,我们就可以实现区间修改的可持久化线段树,时间复杂度为 \(O(n \log n)\),但需要注意此时的空间复杂度变成了 \(2n-1+4n \times (\lceil \log n\rceil+1)\),大约开 \(2^7 \times N\) 的空间足够。
#include <cstdio>
#include <algorithm>
typedef long long ll;
const int N=1e5+10;
const int M=1e5+10;
int n,m,now,v;
int a[N],rt[M];
struct Pre_Seg {
struct node {
int ls,rs,l,r;
ll sum,tag;
}tr[N<<7];int idx=0;
#define ls (tr[p].ls)
#define rs (tr[p].rs)
#define L (tr[p].l)
#define R (tr[p].r)
int clone(int p) {
tr[++idx]=tr[p];return idx;
}
void push_up(int p) { // 注意 tag 带来的贡献
tr[p].sum=tr[ls].sum+tr[rs].sum+1ll*(R-L+1)*tr[p].tag;
}
int build(int l,int r) {
int p=++idx;L=l;R=r;
if(l==r) {tr[p].sum=a[l];return p;}
int mid=l+r>>1;
ls=build(l,mid);
rs=build(mid+1,r);
push_up(p); // 记得合并
return p;
}
int modify(int lst,int l,int r,int d) {
int p=clone(lst);
if(l<=L&&R<=r) { // 该节点已经处理完贡献,标记指向子节点
tr[p].sum+=(R-L+1)*d;
tr[p].tag+=d;return p;
}
int mid=L+R>>1;
if(l<=mid) ls=modify(ls,l,r,d);
if(mid<r) rs=modify(rs,l,r,d);
push_up(p);return p; // 记得合并
}
ll query(int p,int l,int r) {
if(l<=L&&R<=r) return tr[p].sum;
int mid=L+R>>1;ll res=(std::min(r,R)-std::max(l,L)+1)*tr[p].tag; // 查询时携带贡献
if(l<=mid) res+=query(ls,l,r);
if(mid<r) res+=query(rs,l,r);
return res;
}
#undef ls
#undef rs
#undef L
#undef R
}T;
int main() {
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
rt[0]=T.build(1,n);
for(int i=1;i<=m;i++) {
char op;scanf(" %c",&op);
if(op=='C') {
int l=0,r=0,d=0;
scanf("%d%d%d",&l,&r,&d);
++now;rt[now]=T.modify(rt[now-1],l,r,d);
} else if(op=='Q') {
int l=0,r=0;
scanf("%d%d",&l,&r);
printf("%lld\n",T.query(rt[now],l,r));
} else if(op=='H') {
int l=0,r=0,t=0;
scanf("%d%d%d",&l,&r,&t);
printf("%lld\n",T.query(rt[t],l,r));
} else scanf("%d",&now);
}
return 0;
}
而如果是区间赋值,则在标记额外维护时间戳,在查询时取时间戳最大的一次即可。
可持久化并查集 & 带撤销并查集
首先因为要支持可持久化,因此不可以路径压缩,只能按秩合并。
只要每次可以回到该版本的 fa 和 siz/dep 就可以实现可持久化。因此用可持久化数组维护 fa 和 siz/dep 数组即可。
时间复杂度 \(O(n \log^2 n)\).
下面给出的是维护 siz 数组的 code.
需要注意的几点:
- 注意计算空间:因为每次 merge 的时候既要修改 fa 又要修改 siz,因此空间大约是 \(2n-1+2m (\lceil \log n\rceil+1)\),即大约要开 \(2^6 \times M\).
- merge 的时候最后返回的 root 需注意一下,应该是两次 modify 后共同的 root,而不是第一次,其中第二次 modify 的根应该是第一次 modify 所得到的根。
#include <cstdio>
#include <algorithm>
const int N=2e5+10;
const int M=2e5+10;
int n,m;
int rt[M];
struct Pre_Set {
struct node {
int l,r,ls,rs;
int fa,siz;
}tr[M<<6];int idx=0; // 注意空间
#define L (tr[p].l)
#define R (tr[p].r)
#define ls (tr[p].ls)
#define rs (tr[p].rs)
int clone(int p) {
tr[++idx]=tr[p];return idx;
}
int build(int l,int r) {
int p=++idx;L=l;R=r;
if(l==r) {tr[p].fa=l;tr[p].siz=1;return p;}
int mid=l+r>>1;
ls=build(l,mid);rs=build(mid+1,r);
return p;
}
int modify(int lst,int pos,int val,bool f) {
int p=clone(lst);
if(L==R) {
f?tr[p].fa=val:tr[p].siz=val;
return p;
}
int mid=L+R>>1;
if(pos<=mid) ls=modify(ls,pos,val,f);
else rs=modify(rs,pos,val,f);
return p;
}
int query(int p,int pos,bool f) {
if(L==R) return f?tr[p].fa:tr[p].siz;
int mid=L+R>>1;
if(pos<=mid) return query(ls,pos,f);
else return query(rs,pos,f);
}
int f(int x,int v) {
int fx=query(rt[v],x,true);
return fx==x?x:f(fx,v);
}
bool check(int x,int y,int v) {
int fx=f(x,v),fy=f(y,v);
return fx==fy;
}
int merge(int x,int y,int v) {
int fx=f(x,v),fy=f(y,v);
if(fx==fy) return rt[v];
int sx=query(rt[v],fx,false);
int sy=query(rt[v],fy,false);
if(sx<sy) {std::swap(fx,fy);std::swap(sx,sy);}
int root=modify(rt[v],fy,fx,true); // 注意 merge 后返回的 root
root=modify(root,fx,sx+sy,false);
return root;
}
#undef L
#undef R
#undef ls
#undef rs
}S;
int main() {
scanf("%d%d",&n,&m);
rt[0]=S.build(1,n);
for(int i=1,op=0,a=0,b=0,k=0;i<=m;i++) {
scanf("%d",&op);
if(op==1) {
scanf("%d%d",&a,&b);
rt[i]=S.merge(a,b,i-1);
} else if(op==2) {
scanf("%d",&k);rt[i]=rt[k];
} else {
scanf("%d%d",&a,&b);
rt[i]=rt[i-1];
printf("%d\n",S.check(a,b,i));
}
}
return 0;
}
而如果只需要撤销,那么可以用栈维护操作序列,然后在撤销的时候不断 pop 逆操作即可。
参考文章
[1] 可持久化数据结构 https://www.cnblogs.com/alex-wei/p/DS.html

浙公网安备 33010602011771号