可持久化线段树学习笔记

大概内容:可持久化线段树,可持久化并查集。

待填坑,预计在10.12之前写完。

主席树解决的问题是当一类数据结构要保存历史版本时,普通线段树、平衡树等无法保存历史版本。当前节点更新之后历史版本就被覆盖了。

我们当然可以考虑对于每一次操作建一棵线段树,到时候直接查询就可以了。但这样时空复杂度均是 \(n^2\) 的。
如何优化?

我们考虑这样一个图

假设我们修改了 \(5\) 的值,那么发现,受影响的会是标红的这一条链。

也就是说我们建树的时候不需要建整棵线段树,只需要新建一部分记录这次修改即可。如图。

粉色的时新建出来那部分。即被影响的单独建点,没被影响的直接连边。

然后每次查询的时候找到历史版本对应的那棵树,直接查询即可。

但是这个只支持单点修改,单点查询。区间修改,区间查询要打懒标记,但懒标记要永久化比较麻烦,我还不会。

先放代码吧,单点修改,单点查询。

struct ZXTREE{

	int T[N];
	int ini[N];
	int ls[N<<5];
	int rs[N<<5];
	int val[N<<5];
	int cnt;
	int build(int l,int r)
	{
		int rt=++cnt;
		if(l==r)
		{
			val[rt]=ini[l];
			return rt;
		}
		int mid=(l+r)>>1;
		ls[rt]=build(l,mid);
		rs[rt]=build(mid+1,r);
		return rt; 
	} 
	int change(int ver,int l,int r,int id,int k)
	{
		int rt=++cnt;
		ls[rt]=ls[ver];
		rs[rt]=rs[ver];
		if(l==r)
		{
			val[rt]=k;
			return rt;
		}
		int mid=(l+r)>>1;
		if(id<=mid)
		{
			ls[rt]=change(ls[ver],l,mid,id,k);
		}
		else
		{
			rs[rt]=change(rs[ver],mid+1,r,id,k);
		}
		return rt;
	}
	int query(int rt,int l,int r,int id)
	{
		if(l==r)
		{
			return val[rt];
		}
		int mid=(l+r)>>1;
		if(id<=mid)
		{
			return query(ls[rt],l,mid,id);![](https://img2022.cnblogs.com/blog/2383072/202210/2383072-20221009192349519-1832868958.png)

		}
		else
		{
			return query(rs[rt],mid+1,r,id);
		}
	}
}tree;

还有一个经典问题,区间第 \(k\) 小。

首先考虑整段区间第 \(k\) 小怎么做。引入一个概念,值域线段树。即下标是值域的线段树。

我们先离散化原数组,然后根据值域建一棵线段数,对于每个叶节点,维护一下这个值在原序列的出现次数。对于非叶节点,维护这个值域内的元素个数。

例如以下数组 \(1,2,2,3,3,3,4,4,4,4\) 建出来的值域线段数如下。

其中粉色的表示这个数的出现次数。而紫色的表示这个值域内的元素个数。

下面来不及写了,先复制了,记得自己重写一份。

容易发现,值域线段树可以查 \(1~r\) 的第 \(k\) 大但是无法查询区间的。具体原因是无法确定具体位置。

所以我们考虑可持久化一下,假设原数组有 \(n\) 个数则建立一个有 \(n\) 个根的可持久化值域线段树,对于每一个区间,设当前根为 \(i\) ,维护一个值表示在原数组中 \([1,i]\) 的数的个数。看图理解会好一点。

我们要找 \([1,r]\) 的第 \(k\) 小过程是这样的。

  • 先找到编号为 \(r\) 的那个根。

  • 向下递归,如果左节点的个数 \(x\le k\) 就一定在右子树,否则在左子树,继续往下递归。但是若 \(x\le k\) 向右子树递归时就证明范围已经从 \([1,r]\) 变成了 \([x+1,r]\) 所以问题变成了在 \([x+1,r]\) 中寻找第 \(k-x\) 大的数。

例如我们查找 \([1,4]\) 中第 \(2\) 小的值,图示如下,绿色节点为该值存在的区间位置。

struct ZXTREE{
	int T[N];
	int ini[N];
	int ls[N<<5];
	int rs[N<<5];
	int val[N<<5];
	int sum[N<<5]; 
	int cnt;
	int build(int l,int r)
	{
		int rt=++cnt;
		if(l==r)
		{
			val[rt]=ini[l];
			return rt;
		}
		int mid=(l+r)>>1;
		ls[rt]=build(l,mid);
		rs[rt]=build(mid+1,r);
		return rt; 
	} 
	int change(int ver,int l,int r,int id)
	{
		int rt=++cnt;
		ls[rt]=ls[ver];
		rs[rt]=rs[ver];
		sum[rt]=sum[ver]+1;
		if(l==r)
		{
			// val[rt]=k;
			return rt;
		}
		int mid=(l+r)>>1;
		if(id<=mid)
		{
			ls[rt]=change(ls[ver],l,mid,id);
		}
		else
		{
			rs[rt]=change(rs[ver],mid+1,r,id);
		}
		return rt;
	}
	int query(int u,int v,int l,int r,int id)
	{
		if(l==r)
		{
			return l;
		}
		int mid=(l+r)>>1;
		int x=sum[ls[v]]-sum[ls[u]];
		if(x>=id)
		{
			return query(ls[u],ls[v],l,mid,id);
		}
		else
		{
			return query(rs[u],rs[v],mid+1,r,id-x);
		}
	}
}ans;

可持久化并查集。

可持久化并查集实际上就是用主席树维护并查集的 \(fa_i\) 数组。

不过由于可持久化了,所以路径压缩是不成立的。所以采用按秩合并或启发式合并来实现复杂度。

具体地,建立两棵可持久化线段树,\(fa\)\(sz\) 分别表示每个点的父亲和大小。每次合并的时候查询一下 \(a\)\(b\) 的大小,也就是单点查询。之后把小的合并到大的即可,也就是单点修改。

代码:

int findrt(int x,int ver)
{
	while(fa.query(fa.T[ver],1,n,x)!=x)x=fa.query(fa.T[ver],1,n,x);
	
	return x;
}
void merge(int x,int y,int ver)
{
	int a=findrt(x,ver);
	int b=findrt(y,ver);
	if(a==b) return ;
	int szx=sz.query(sz.T[ver],1,n,a);
	int szy=sz.query(sz.T[ver],1,n,b);
	if(szx<szy)
	{
		fa.T[ver]=fa.change(fa.T[ver],1,n,a,b);
		sz.T[ver]=sz.change(sz.T[ver],1,n,b,szx+szy);
	}
	else
	{
		fa.T[ver]=fa.change(fa.T[ver],1,n,b,a);
		sz.T[ver]=sz.change(sz.T[ver],1,n,a,szx+szy);
	}

}
posted @ 2022-10-09 11:21  zplqwq  阅读(36)  评论(0编辑  收藏  举报