可持久化数据结构

什么是可持久化

保留每个版本,其中只有最新版本可以修改的称为部分可持久化,所有版本都可以修改的称为完全可持久化。

可持久化线段树

可持久化数组

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 的贡献值。有几点需要注意:

  1. 在 push_up 也应考虑 tag 给的贡献。
  2. 当前节点的 tag 已经对当前节点处理好了贡献,tag 指向子节点。
  3. 在查询时要注意加上 tag 的贡献。

有了标记永久化,我们就可以实现区间修改的可持久化线段树,时间复杂度为 \(O(n \log n)\),但需要注意此时的空间复杂度变成了 \(2n-1+4n \times (\lceil \log n\rceil+1)\),大约开 \(2^7 \times N\) 的空间足够。

SP11470 TTM - To the moon

#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)\).

Luogu P3402 可持久化并查集

下面给出的是维护 siz 数组的 code.

需要注意的几点:

  1. 注意计算空间:因为每次 merge 的时候既要修改 fa 又要修改 siz,因此空间大约是 \(2n-1+2m (\lceil \log n\rceil+1)\),即大约要开 \(2^6 \times M\).
  2. 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

posted @ 2024-12-13 21:45  ldh081122  阅读(244)  评论(0)    收藏  举报