可持久化数据结构小结

(CSP赛前复健,今年最后一次机会了,希望能拿个好成绩)

可持久化数据结构就是总是可以保留每一个历史版本,并且支持操作的数据结构

可持久化数组

题目传送门:Luogu P3919

题目描述

你需要维护这样的一个长度为 \(n\) 的数组,支持如下几种操作

  1. 在某个历史版本上修改某一个位置上的值

  2. 访问某个历史版本上的某一位置的值

此外,每进行一次操作(对于操作2,即为生成一个完全一样的版本,不作任何改动),就会生成一个新的版本。版本编号即为当前操作的编号(从1开始编号,版本0表示初始状态数组)

数据范围\(1 \leq n \leq {10}^6\)

首先考虑暴力,暴力每次生成一个新的数组,那么时间空间肯定全都炸了,不行

怎么优化呢?我们发现每次数组上只会修改一个值,如果能在原本的数组上进行修改并且记录下怎么修改就好了,但是如果直接这么做显然也不太行

我们考虑以原本的数组为叶子节点建立一棵线段树,从根节点向下找就能找到对应的值,现在对于每一次修改,我们希望能只修改与这个节点有关的点

也就是从线段树的根节点到要被修改的位置对应的叶子节点的路径上的所有节点,我们把这些节点复制一遍,将一个儿子更新为新的节点(如下图)

image

这样我们就利用了线段树的性质,只修改了 \(\log\) 个节点(新增加了 \(\log\) 个节点)就满足了需求

现在我们只需要记录新的树根就能做到访问历史版本了,不同的树根的叶子节点就是不同版本的数组

需要注意必须要动态开点,所以要记录左右儿子编号,具体实现细节见代码

C++ Code

#include<bits/stdc++.h>
using namespace std;
namespace cmyIO
{
	inline void read(){}
	inline void read(char *_s){unsigned _cnt=0;char _ch=getchar();while(_ch==' '||_ch=='\n'||_ch=='\r')_ch=getchar();while(_ch!=' '&&_ch!='\n'&&_ch!='\r'&&_ch!=EOF)*(_s+(_cnt++))=_ch,_ch=getchar();}
	template<typename _Tp>inline void read(_Tp &_x){char _c=getchar();_x=0;int _f=1;while(_c<48){if(_c=='-')_f=-1;_c=getchar();}while(_c>47)_x=(_x*10)+(_c^48),_c=getchar();_x*=_f;}
	template<typename _Tp>inline _Tp original_read(){char _c=getchar();_Tp _x=0,_f=1;while(_c<48){if(_c=='-')_f=-1;_c=getchar();}while(_c>47)_x=(_x*10)+(_c^48),_c=getchar();return _x*_f;}
	template<typename Head,typename... Tail>inline void read(Head& _H,Tail&... _T){read(_H),read(_T...);}
	inline void write(){}
	inline void write(char _c){putchar(_c);}
	inline void write(string _s){for(unsigned i=0;i<_s.size();++i)putchar(_s[i]);}
	inline void write(const char *_s){for(unsigned i=0;i<strlen(_s);++i)putchar(_s[i]);}
	template<typename _Tp>inline void mwrite(_Tp _a){if(_a>9)mwrite(_a/10);putchar((_a%10)|48);}
	template<typename _Tp>inline void write(_Tp _a){mwrite(_a<0?(putchar('-'),_a=-_a):_a);}
	template<typename Head,typename... Tail>inline void write(Head _H,Tail... _T){write(_H),write(_T...);}
}using namespace cmyIO;
//上面都是读入优化
const int N=1e6+6;
struct Node//线段树上节点信息
{
	int ls,rs,v;
	Node()=default;
}node[N<<5];
int n,m,cnt;
int rt[N];//每个版本对应的树根
inline int build(int l,int r)//建树
{
	int pos=++cnt;
	if(l==r) return read(node[pos].v),pos;
	int mid=(l+r)>>1;
	if(l<=mid) node[pos].ls=build(l,mid);
	if(r>mid) node[pos].rs=build(mid+1,r);
	return pos;
}
inline int modify(int cur,int l,int r,int x,int v)//修改
{
	int pos=++cnt;
	node[pos]=node[cur];//复制原本的节点
	if(l==r) return node[pos].v=v,pos;
	int mid=(l+r)>>1;
	if(x<=mid) node[pos].ls=modify(node[cur].ls,l,mid,x,v);//修改儿子编号
	else node[pos].rs=modify(node[cur].rs,mid+1,r,x,v);
	return pos;
}
inline int query(int pos,int l,int r,int x)//询问
{
	if(l==r) return node[pos].v;
	int mid=(l+r)>>1;
	if(x<=mid) return query(node[pos].ls,l,mid,x);
	else return query(node[pos].rs,mid+1,r,x);
}
signed main()
{
	read(n,m);
	build(1,n);
	rt[0]=1;
	for(int ver,loc,v,i=1;i<=m;++i)
	{
		read(ver);
		if(original_read<int>()==1)
		{
			read(loc,v);
			rt[i]=modify(rt[ver],1,n,loc,v);
		}
		else
		{
			read(loc);
			rt[i]=rt[ver];
			write(query(rt[ver],1,n,loc),'\n');
		}
	}
	return 0;
}

可持久化并查集

题目传送门:Luogu P3402

题目描述

给定 \(n\) 个集合,第 \(i\) 个集合内初始状态下只有一个数,为 \(i\)

\(m\) 次操作。操作分为 \(3\) 种:

  • 1 a b 合并 \(a,b\) 所在集合;

  • 2 k 回到第 \(k\) 次操作(执行三种操作中的任意一种都记为一次操作)之后的状态;

  • 3 a b 询问 \(a,b\) 是否属于同一集合,如果是则输出 \(1\),否则输出 \(0\)

数据范围\(1\le n\le 10^5\)\(1\le m\le 2\times 10^5\)

有了可持久化数组,我们就能很方便的做到可持久化并查集,因为并查集主要就是对于 father 数组的修改,将这个数组可持久化就做到了可持久化并查集

但问题是如果使用路径压缩并查集会出现问题,那就是每次路径压缩的时候都会修改大量节点的 father 值,可持久化数组的空间虽然每次修改只增加 \(\log\) 个节点,但现在每次对于并查集操作都会导致新增大量节点,这显然是无法接受的

于是乎考虑使用启发式合并,按照子树大小合并(当然如果想要按照深度合并或是按秩合并也可以),优先把子树节点多的作为子树节点少的的父节点

接着只要把 father 数组和 size 数组都可持久化就好了

其实只有数组维护方式有所改变,剩余部分就只是普通的并查集,代码还是比较简单的

细节见代码(使用封装来简化代码并增加了可读性)

C++ Code

#include<bits/stdc++.h>
using namespace std;
namespace cmyIO
{
	inline void read(){}
	inline void read(char *_s){unsigned _cnt=0;char _ch=getchar();while(_ch==' '||_ch=='\n'||_ch=='\r')_ch=getchar();while(_ch!=' '&&_ch!='\n'&&_ch!='\r'&&_ch!=EOF)*(_s+(_cnt++))=_ch,_ch=getchar();}
	template<typename _Tp>inline void read(_Tp &_x){char _c=getchar();_x=0;int _f=1;while(_c<48){if(_c=='-')_f=-1;_c=getchar();}while(_c>47)_x=(_x*10)+(_c^48),_c=getchar();_x*=_f;}
	template<typename _Tp>inline _Tp original_read(){char _c=getchar();_Tp _x=0,_f=1;while(_c<48){if(_c=='-')_f=-1;_c=getchar();}while(_c>47)_x=(_x*10)+(_c^48),_c=getchar();return _x*_f;}
	template<typename Head,typename... Tail>inline void read(Head& _H,Tail&... _T){read(_H),read(_T...);}
	inline void write(){}
	inline void write(char _c){putchar(_c);}
	inline void write(string _s){for(unsigned i=0;i<_s.size();++i)putchar(_s[i]);}
	inline void write(const char *_s){for(unsigned i=0;i<strlen(_s);++i)putchar(_s[i]);}
	template<typename _Tp>inline void mwrite(_Tp _a){if(_a>9)mwrite(_a/10);putchar((_a%10)|48);}
	template<typename _Tp>inline void write(_Tp _a){mwrite(_a<0?(putchar('-'),_a=-_a):_a);}
	template<typename Head,typename... Tail>inline void write(Head _H,Tail... _T){write(_H),write(_T...);}
}using namespace cmyIO;
//上面都是读入优化
const int N=1e5+5;
int n,m,ver;
struct A//封装好的可持久化数组
{
	struct Node//线段树节点信息
	{
		int ls,rs,v;
		Node()=default;
	}node[N<<5];
	int cnt,rt[N],init[N];//rt是版本对应树根,init为可持久化数组初始化的值
	A()=default;
	inline int build(int l,int r)//建树
	{
		int pos=++cnt;
		if(l==r) return node[pos].v=init[l],pos;//赋值为初始值
		int mid=(l+r)>>1;
		if(l<=mid) node[pos].ls=build(l,mid);
		if(r>mid) node[pos].rs=build(mid+1,r);
		return pos;
	}
	inline int modify(int cur,int l,int r,int x,int v)//修改
	{
		int pos=++cnt;
		node[pos]=node[cur];
		if(l==r) return node[pos].v=v,pos;
		int mid=(l+r)>>1;
		if(x<=mid) node[pos].ls=modify(node[cur].ls,l,mid,x,v);
		else node[pos].rs=modify(node[cur].rs,mid+1,r,x,v);
		return pos;
	}
	inline int query(int pos,int l,int r,int x)//查询
	{
		if(l==r) return node[pos].v;
		int mid=(l+r)>>1;
		if(x<=mid) return query(node[pos].ls,l,mid,x);
		else return query(node[pos].rs,mid+1,r,x);
	}
}fa=A(),sz=A();//存放父节点信息和字数大小
inline int getf(int x){return fa.query(fa.rt[ver],1,n,x);}//获取父节点编号
inline int getsz(int x){return sz.query(sz.rt[ver],1,n,x);}//获取字数大小
inline int modifyf(int x,int v){return fa.modify(fa.rt[ver],1,n,x,v);}//修改父节点编号
inline int modifysz(int x,int v){return sz.modify(sz.rt[ver],1,n,x,v);}//修改字数大小
inline int findf(int x)//找最远祖先
{
	int tmp=getf(x);
	while(tmp!=x)
	{
		x=tmp;
		tmp=getf(x);
	}
	return x;
}
inline void merge(int x,int y)//合并
{
	if((x=findf(x))==(y=findf(y)))return;//已经在一棵树上了就不合并了
	int szx=getsz(x),szy=getsz(y);
	if(szx<szy)//按照size合并
	{
		fa.rt[ver]=modifyf(x,y);
		sz.rt[ver]=modifysz(y,szx+szy);
	}
	else
	{
		fa.rt[ver]=modifyf(y,x);
		sz.rt[ver]=modifysz(x,szx+szy);
	}
}
signed main()
{
	read(n,m);
	for(int i=1;i<=n;++i) fa.init[i]=i,sz.init[i]=1;//初始化别忘了
	fa.rt[0]=fa.build(1,n),sz.rt[0]=sz.build(1,n);
	int opt,a,b;
	for(ver=1;ver<=m;++ver)
	{
		fa.rt[ver]=fa.rt[ver-1];
		sz.rt[ver]=sz.rt[ver-1];
		read(opt);
		if(opt==1)
		{
			read(a,b);
			merge(a,b);
		}
		else if(opt==2)
		{
			read(a);
			fa.rt[ver]=fa.rt[a];
			sz.rt[ver]=sz.rt[a];
		}
		else
		{
			read(a,b);
			putchar(findf(a)==findf(b)?'1':'0'),putchar('\n');
		}
	}
	return 0;
}

可持久化权值线段树

又名“主席树”(至于为啥叫这个名字,号还要,这里就不展开说了)

题目传送门:Luogu P3834

题目描述

给定 \(n\) 个整数构成的序列 \(a\),询问 \(m\) 次,每次询问将对于指定的闭区间 \([l, r]\) 查询其区间内的第 \(k\) 小值(保证存在答案)

数据范围\(1 \leq n,m \leq 2\times 10^5\)\(|a_i| \leq 10^9\)

先考虑这个问题的简化版本,多次询问一个序列的全局第 \(k\) 小值

对于全局的多次询问第 \(k\) 小值我们可以利用权值线段树求解,建立一棵权值线段树,每个节点维护对应值域内数字的个数,并在线段树上二分(从根节点往下走,每次看左儿子的信息,如果比 \(k\) 大就往左儿子走,比 \(k\) 小就减去左儿子的信息并往右儿子走,最后走到的节点就是答案)就可以了,这样还支持修改操作

那么怎么扩展到指定区间呢?如果用权值线段树来那我们要建 \(n^2\) 棵树,再怎么也装不下,要想办法优化一下,首先我们只考虑怎么求解 \([1,r]\),这样就可以通过 \([1,r]\) 的权值线段树减去 \([1,l-1]\) 的权值线段树来得到 \([l,r]\) 区间的信息,但是这样仍然需要建立 \(n\) 棵权值线段树,空间复杂度还是无法接受

我们可以发现,对于 \([1,x]\)\([1,x+1]\) 这两棵权值线段树来说,仅仅修改了一个点的权值,只是为了一个点的变化就新建了一整棵线段树显然很浪费,很容易就能想到利用可持久化来把空间节省下来

我们从头开始,每新增一个元素就相当于建立一个新版本,在原本版本的基础上修改,只需要新增 \(\log\) 个节点就可以装下新的信息

下面再考虑怎么询问,每次找两个版本,分别从对应的根节点同时往下跳,用更右边的树的信息减去左边树的信息再不断往下走就好了

需要注意由于值域很大所以需要先离散化,实现细节还是见代码

C++ Code

#include<bits/stdc++.h>
using namespace std;
namespace cmyIO
{
	inline void read(){}
	inline void read(char *_s){unsigned _cnt=0;char _ch=getchar();while(_ch==' '||_ch=='\n'||_ch=='\r')_ch=getchar();while(_ch!=' '&&_ch!='\n'&&_ch!='\r'&&_ch!=EOF)*(_s+(_cnt++))=_ch,_ch=getchar();}
	template<typename _Tp>inline void read(_Tp &_x){char _c=getchar();_x=0;int _f=1;while(_c<48){if(_c=='-')_f=-1;_c=getchar();}while(_c>47)_x=(_x*10)+(_c^48),_c=getchar();_x*=_f;}
	template<typename _Tp>inline _Tp original_read(){char _c=getchar();_Tp _x=0,_f=1;while(_c<48){if(_c=='-')_f=-1;_c=getchar();}while(_c>47)_x=(_x*10)+(_c^48),_c=getchar();return _x*_f;}
	template<typename Head,typename... Tail>inline void read(Head& _H,Tail&... _T){read(_H),read(_T...);}
	inline void write(){}
	inline void write(char _c){putchar(_c);}
	inline void write(string _s){for(unsigned i=0;i<_s.size();++i)putchar(_s[i]);}
	inline void write(const char *_s){for(unsigned i=0;i<strlen(_s);++i)putchar(_s[i]);}
	template<typename _Tp>inline void mwrite(_Tp _a){if(_a>9)mwrite(_a/10);putchar((_a%10)|48);}
	template<typename _Tp>inline void write(_Tp _a){mwrite(_a<0?(putchar('-'),_a=-_a):_a);}
	template<typename Head,typename... Tail>inline void write(Head _H,Tail... _T){write(_H),write(_T...);}
}using namespace cmyIO;
//上面都是读入优化
const int N=2e5+5;
int n,m,q,cnt;
int a[N],b[N],rt[N];
struct Node
{
	int ls,rs,sum;
	Node()=default;
}node[N<<5];
inline int build(int l,int r)//建树
{
	int pos=++cnt;
	node[pos]=Node();//最初的树什么都没有,全为空节点
	if(l==r) return pos;
	int mid=(l+r)>>1;
	if(l<=mid) node[pos].ls=build(l,mid);
	if(r>mid) node[pos].rs=build(mid+1,r);
	return pos;
}
inline int modify(int cur,int l,int r,int x)//修改
{
	int pos=++cnt;
	node[pos]=node[cur];
	++node[pos].sum;//权值线段树,出现次数+1即可
	if(l==r) return pos;
	int mid=(l+r)>>1;
	if(x<=mid) node[pos].ls=modify(node[cur].ls,l,mid,x);
	else node[pos].rs=modify(node[cur].rs,mid+1,r,x);
	return pos;
}
inline int query(int cur1,int cur2,int l,int r,int k)//询问
{
	if(l==r) return l;
	int mid=(l+r)>>1;
	int x=node[node[cur2].ls].sum-node[node[cur1].ls].sum;//排名与差分比较
	if(x>=k) return query(node[cur1].ls,node[cur2].ls,l,mid,k);//记得同时往下跳
	else return query(node[cur1].rs,node[cur2].rs,mid+1,r,k-x);
}
inline int getls(int x){return lower_bound(b+1,b+m+1,x)-b;}
inline int getori(int x){return b[x];}
signed main()
{	
	read(n,q);
	for(int i=1;i<=n;++i) read(a[i]),b[i]=a[i];
	sort(b+1,b+n+1);
	m=unique(b+1,b+n+1)-b-1;//离散化
	rt[0]=build(1,m);
	for(int i=1;i<=n;++i) rt[i]=modify(rt[i-1],1,m,getls(a[i]));
	int l,r,k;
	while(q--)
	{
		read(l,r,k);
		write(getori(query(rt[l-1],rt[r],1,m,k)),'\n');
	}
	return 0;
}

补充

这样我们会发现无法支持修改操作,怎么办呢?注意到瓶颈主要在于如果要修改一个节点,那么我们需要修改这个节点被添加之后的所有权值线段树,复杂度就不可接受了,考虑先差分,问题就转化为了单点修改和区间求和,利用更方便维护前缀和的数据结构——树状数组(线段树空间太大了)就搞定了,这就是树套树(树状数组套主席树)

似乎有点跑题,不过问题不大,感兴趣可以去做做看这道题,题目传送门:Luogu P2617

更多可持久化数据结构

可持久化字典树(Trie),可持久化平衡树(基于 FHQ-Treap)……

暂时就写这么多吧,其实都大同小异,最近太忙了,后面大概会更新?(咕)


博客园传送门

知乎传送门

posted @ 2022-10-04 23:39  人形魔芋  阅读(46)  评论(1编辑  收藏  举报