线段树的可持久化

线段树进阶

可持久化

能够保留每一个历史版本的数据结构。

那么可持久化线段树就是能保留历史版本的线段树。

原谅我之前一直叫它可持续化线段树

可持久化线段树

一般来说,可持久化线段树本质其实是可持久化数组,即支持单点修改、单点查询。

因为要保留历史版本,那么如果对于每次的的修改和查询均新生成一棵线段树的话。

一棵线段树需要 \(4\times n\) 的空间, \(m\) 次操作代表了 \(m\) 棵线段树,空间复杂度 \(O(4mn)\) ……

炸空间怎么办?想想之前值域 \(10^9\) 的权值线段树。

没错,动态开点。

当对一个叶节点进行修改之后,你会发现整棵树上修改的点会形成一条链,那我们就将这条链维护出来,也就是下图的红色节点(图片来自 OI wiki )。

建树

很常规的动态开点线段树。

void build(int &i,int l,int r)
{
	i=++cnt;
	if(l==r)
	{
		T[i].val=a[l];
		return;
	}
	int mid=(l+r)>>1;
	build(T[i].lc,l,mid);
	build(T[i].rc,mid+1,r);
}

修改

在修改的过程中,我们先新建一个节点,赋给它对应的历史节点的信息(包括左右子节点和权值)。

还有,每新建一棵线段树,都要将其根节点记录下来,作为历史版本的查找依据。

void change(int &i,int last,int l,int r,int pos,int k)
{
	i=++cnt;T[i]=T[last];
	if(l==r)
	{
		T[i].val=k;
		return;
	}
	int mid=(l+r)>>1;
	if(pos<=mid)change(T[i].lc,T[last].lc,l,mid,pos,k);
	else change(T[i].rc,T[last].rc,mid+1,r,pos,k);
}

查询

查询和普通动态开点线段树基本一样。

int ask(int i,int l,int r,int pos)
{
	if(l==r)return T[i].val;
	int mid=(l+r)>>1;
	if(pos<=mid)return ask(T[i].lc,l,mid,pos);
	else return ask(T[i].rc,mid+1,r,pos);
}

主函数中:

build(rot[0],1,n);
int var=re(),op=re(),pos=re();
if(op==1)change(rot[i],rot[var],1,n,pos,re());
else rot[i]=rot[var],wr(ask(rot[i],1,n,pos)),putchar('\n');

还记得那首诗嘛?

一年 OI 一场空,不开 long long 见祖宗。

坑点

  • 空间

    线段树对的空间的需求极大,典型的空间换时间,所以数组千万不要开太大,可持续化线段树开到原数组大小的 二十三倍 就好了,否则就会无情的 Runtime Error.Memory Limit Exceeded.

    所以线段树中就不要储存所代表区间的左右端点了,将端点作为递归的参数传下去,否则会 MLE。

    update in 2022.09.14 空间好像放开了,之前是 500MB,现在看是 1GB。

  • 关于区间修改

    一般来说,可持久化线段树不支持区间修改,因为懒标在下传时会导致之前版本的节点发生改变,然后空间复杂度爆炸。

Code

const int inf=1e6+7;
int n,m,a[inf];
struct Seg_Tree{
	int lc,rc,val;
}T[inf*23];
int rot[inf],cnt;
void build(int &i,int l,int r)
{
	i=++cnt;
	if(l==r)
	{
		T[i].val=a[l];
		return;
	}
	int mid=(l+r)>>1;
	build(T[i].lc,l,mid);
	build(T[i].rc,mid+1,r);
}
void change(int &i,int last,int l,int r,int pos,int k)
{
	i=++cnt;T[i]=T[last];
	if(l==r)
	{
		T[i].val=k;
		return;
	}
	int mid=(l+r)>>1;
	if(pos<=mid)change(T[i].lc,T[last].lc,l,mid,pos,k);
	else change(T[i].rc,T[last].rc,mid+1,r,pos,k);
}
int ask(int i,int l,int r,int pos)
{
	if(l==r)return T[i].val;
	int mid=(l+r)>>1;
	if(pos<=mid)return ask(T[i].lc,l,mid,pos);
	else return ask(T[i].rc,mid+1,r,pos);
}
int main()
{
	n=re();m=re();
	for(int i=1;i<=n;i++)
		a[i]=re();
	build(rot[0],1,n);
	for(int i=1;i<=m;i++)
	{
		int var=re(),op=re(),pos=re();
		if(op==1)change(rot[i],rot[var],1,n,pos,re());
		else rot[i]=rot[var],wr(ask(rot[i],1,n,pos)),putchar('\n');
	}
	return 0;
}

主席树

线段树能可持久化,权值线段树也能。

主席树(Hjt 树),全名 可持久化权值线段树

引入

给定 n 个整数构成的序列 a,将对于指定的闭区间 [l,r] 查询其区间内的第 k 小值。

好像就是把区间从 \(1\sim n\) 扩展到了 \(l\sim r\)

于是我们就从权值线段树扩展到了可持续化权值线段树。

思路

发明者的原话是:“对于原序列的每一个前缀 \([1\sim i]\) 建出一棵线段树维护值域上每个数出现的次数,则其树是可减的。”

其实就是前缀和。

权值线段树储存的是值域上数的个数。对于版本 \(R\) 的权值线段树减去版本 \(L-1\) 的权值线段树,就得到了维护 \([L,R]\) 的权值线段树。

Code

const int inf=2e5+7;
int n,m,num,a[inf],bok[inf];
struct Seg_Tree{
	int lc,rc;
	int sum;
}T[inf<<5];
int rot[inf],cnt;
void insert(int &i,int j,int l,int r,int k)
{
	i=++cnt;T[i]=T[j];
	if(l==r){T[i].sum++;return;}
	int mid=(l+r)>>1;
	if(k<=mid)insert(T[i].lc,T[j].lc,l,mid,k);
	else insert(T[i].rc,T[j].rc,mid+1,r,k);
	T[i].sum=T[T[i].lc].sum+T[T[i].rc].sum;
}
int ask(int i,int j,int l,int r,int k)
{
	if(l==r)return l;
	int mid=(l+r)>>1,sum=T[T[j].lc].sum-T[T[i].lc].sum;
	if(k<=sum)return ask(T[i].lc,T[j].lc,l,mid,k);
	return ask(T[i].rc,T[j].rc,mid+1,r,k-sum);
}
int main()
{
	n=re();m=re();
	for(int i=1;i<=n;i++)
		bok[i]=a[i]=re();
	sort(bok+1,bok+n+1);
	num=unique(bok+1,bok+n+1)-bok-1;
	for(int i=1;i<=n;i++)
		a[i]=lower_bound(bok+1,bok+num+1,a[i])-bok;
	for(int i=1;i<=n;i++)
		insert(rot[i],rot[i-1],1,num,a[i]);
	for(int i=1;i<=m;i++)
	{
		int l=re(),r=re(),k=re();
		wr(bok[ask(rot[l-1],rot[r],1,num,k)]),putchar('\n');
	}
	return 0;
}

拓展

对于带修改的主席树,一般用 树状数组套主席树

练习

P3939
P2633

可持久化并查集

前置知识

并查集按秩合并

如果只学过并查集的路径压缩,那么上边的文章对你来说很重要。

因为可持久化并查集并不能路径压缩,它需要按秩合并。

思路

回忆一下,并查集我们需要维护什么?

联通块的根 fa,联通树的树高 dep

而本质上,这两个都是数组,而且只涉及单点修改,单点查询。

那么就可以用可持久化线段树(可持久化数组)维护。

代码对比

这是一个普通的按秩合并并查集:

const int inf=1e4+7;
int n,m;
int fa[inf],dep[inf];
int find(int x)
{
	if(fa[x]==x)return x;
	return find(fa[x]);
}
void merge(int x,int y)
{
	x=find(x),y=find(y);
	if(x==y)return;
	if(dep[x]==dep[y])
		fa[x]=y,dep[y]++;
	else
	{
		if(dep[x]>dep[y])swap(x,y);
		fa[x]=y;
	}
}
int main()
{
	n=re();m=re();
	for(int i=1;i<=n;i++)
		fa[i]=i;
	for(int i=1;i<=m;i++)
	{
		int op=re(),u=re(),v=re();
		if(op==1)merge(u,v);
		else
		{
			int r1=find(u),r2=find(v);
			puts((r1^r2)?"N":"Y");
		}
	}
	return 0;
}

将其可持久化就是这样:

const int inf=2e5+7;
int n,m;
struct Seg_Tree{
	int lc,rc,val;
}T[inf<<6];
int fat[inf],dep[inf],cnt;
void build(int &i,int l,int r)
{
	i=++cnt;
	if(l==r)
	{
		T[i].val=l;
		return;
	}
	int mid=(l+r)>>1;
	build(T[i].lc,l,mid);
	build(T[i].rc,mid+1,r);
}
void change(int &i,int last,int l,int r,int pos,int k)
{
	i=++cnt;T[i]=T[last];
	if(l==r)
	{
		T[i].val=k;
		return;
	}
	int mid=(l+r)>>1;
	if(pos<=mid)change(T[i].lc,T[last].lc,l,mid,pos,k);
	else change(T[i].rc,T[last].rc,mid+1,r,pos,k);
}
int ask(int i,int l,int r,int pos)
{
	if(l==r)return T[i].val;
	int mid=(l+r)>>1;
	if(pos<=mid)return ask(T[i].lc,l,mid,pos);
	else return ask(T[i].rc,mid+1,r,pos);
}
int find(int i,int x)
{
	int fax=ask(fat[i],1,n,x);
	if(fax==x)return x;
	return find(i,fax);
}
void merge(int i,int x,int y)
{
	x=find(i-1,x),y=find(i-1,y);
	if(x==y)
	{
		fat[i]=fat[i-1],dep[i]=dep[i-1];
		return;
	}
	int depx=ask(dep[i-1],1,n,x),depy=ask(dep[i-1],1,n,y);
	if(depx==depy)
	{
		change(fat[i],fat[i-1],1,n,x,y);
		change(dep[i],dep[i-1],1,n,y,depy+1);
	}
	else
	{
		if(depx>depy)swap(x,y);
		change(fat[i],fat[i-1],1,n,x,y);
		dep[i]=dep[i-1];
	}
}
int main()
{
	n=re();m=re();
	build(fat[0],1,n);
	for(int i=1;i<=m;i++)
	{
		int op=re();
		if(op==1)
		{
			int x=re(),y=re();
			merge(i,x,y);
		}
		if(op==2)
		{
			int k=re();
			fat[i]=fat[k],dep[i]=dep[k];
		}
		if(op==3)
		{
			int x=re(),y=re();
			int fax=find(i-1,x),fay=find(i-1,y);
			wr(fax==fay),putchar('\n');
			fat[i]=fat[i-1],dep[i]=dep[i-1];
		}
	}
	return 0;
}

值得注意的是,在合并和查询的时候,都应该以上一个版本为准(因为当前版本什么都没有)。

STL 中的可持久化

STL 真是 C 党福利!!!

rope,是 STL 扩展提供的一种可持久化平衡树。

注意啊,是 STL 扩展,和 pb_ds 一个爹

头文件 #include<ext/rope>,名字空间 __gnu_cxx

变量定义 rope<int> *h[1000007];

可持久化 h[i]=new rope<int>(*h[j]);,时间复杂度 \(O(1)\)

函数调用:

  • h[i]->push_back(val)h[i] 的末尾加入 val
  • h[i]->at(k) 访问第 k 个元素。
  • h[i]->replace(pos,val) 将位置为 pos 的元素换成 val
  • h[i]->size() 返回 h[i] 的元素个数。
  • h[i]->insert(pos,val)pos 位置插入 val
  • h[i]->erase(pos,k)pos 位置向后删除 k 个元素。
  • h[i]->substr(pos,k)pos 位置开始提取 k 个元素。
  • h[i]->copy(pos,k,q) 将从 pos 位置向后 x 个元素拷贝到 q 中。

和 vector 差不多。

如果维护文艺平衡树(即区间翻转),就维护一正一反两个 rope,区间翻转就把两个 rope 的对应区间 swap 一下就行了。

但是!!

STL 的通病:常数巨大。

并且,rope 空间复杂度极高!!

实现 P3919:

#include<cstdio>
#include<ext/rope>
using namespace __gnu_cxx;
int re()
{
	int s=0,f=1;char ch=getchar();
	while(ch>'9'||ch<'0')
	{
		if(ch=='-')f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
		s=s*10+ch-48,ch=getchar();
	return s*f;
}
void wr(int s)
{
	if(s<0)putchar('-'),s=-s;
	if(s>9)wr(s/10);
	putchar(s%10+48);
}
const int inf=1e6+7;
int n,m;
rope<int>*a[inf];
int main()
{
	n=re();m=re();a[0]=new rope<int>(0);
	for(int i=1;i<=n;i++)
		a[0]->push_back(re());
	for(int i=1;i<=m;i++)
	{
		int var=re();
		a[i]=new rope<int>(*a[var]);
		int op=re(),x=re();
		if(op==1)a[i]->replace(x,re());
		else wr(a[i]->at(x)),putchar('\n');
	}
	return 0;
}

实际得分:80 分

时间空间双重爆炸。

不过这个代码量是真的友好,不会可持久化的可以用来骗分。

代码长度:691B | 用时:6.51s | 内存:1.00GB

优化建图

posted @ 2022-09-07 16:42  Zvelig1205  阅读(257)  评论(0编辑  收藏  举报