可持久化线段树学习笔记

可持久化线段树学习笔记

可持久化线段树就是可以询问历史版本状态的线段树。

我们考虑暴力怎样实现询问线段树的历史状态,我们每次在对线段树进行操作的时候,每次都把位修改前的版本存一下,然后要找历史版本的时候直接找,这样时间复杂度和空间复杂都会爆炸。

我们可以发现,每次对线段树进行操作最多才会修改 (log_2(size)) 个节点,其中 (size) 为线段树的大小,所以我们每次把线段树全部复制一遍是不必要的,我们只需要把修改时经过的节点新建出来,然后将其他的节点连接了原来的线段树上就可以了。

(like this)

严格的来说这个东西不是一棵树,但是他如果从父亲节点往儿子节点走就是一棵树,所以对于任意一颗这样的树它还满足线段树的性质,所以我们不用纠结这个是不是一棵树。

然后每次修改都进行一下这样的操作,要查询历史版本的时候只需要找到历史版本的根节点,然后直接当成线段树来用就可以了。

所以我们可以发现,可持久化线段树的时间复杂度和线段树一样,建树是 (Theta (size)),单次查询是 (Theta (log_2size))(size) 维护的数据范围,空间复杂度是 (Theta (log_2n))(n) 是操作次数。

inline void insert(int &rt,int l,int r,int x)
{
	t[++cnt]=t[rt];
	rt=cnt;
	if (l==r)
	{
		t[rt].sum++;
		return ;
	}
	int mid=(l+r)>>1;
	if (x<=mid) insert(t[rt].ls,l,mid,x);
	else insert(t[rt].rs,mid+1,r,x);
	push_up(rt);
	return ;
}

与普通线段树不同的地方就是对 (rt) 加了取地址,表示对传入的值的原值进行操作,还有下面的这两句

	t[++cnt]=t[rt]; //将原节点的信息复制给新建的节点的信息,不要丢失另一个不修改儿子的信息
	rt=cnt;         //把树根转化为新建节点,再加上取地址,这样对于父亲节点来说,它的修改的儿子就是新建的节点

P3834 可持久化线段树 1(主席树)

这道题目咋一看和可持久化线段树没有任何关系,但是这确实是可持久化线段树的日常操作。

我们考虑假设给你 (l-1)(r) 这个状态下的权值线段树,这题肯定就很好做了,只需要两颗线段树减一下就可以了,然后我们发现这就是维护一颗可持久化线段树,然后在查询历史信息。

#include<bits/stdc++.h>
const int N=2e5+100;
using std::sort;
int n,m,bsize;
int a[N],b[N],rt[N];
struct tree{
	int cnt;
	struct Tree{
		int sum,ls,rs;
	}t[N*20];
	inline void clear() {cnt=0;return ;}
	inline void push_up(int rt)
	{
		t[rt].sum=t[t[rt].ls].sum+t[t[rt].rs].sum;
		return ;
	}
	inline void insert(int &rt,int l,int r,int x)
	{
		t[++cnt]=t[rt];
		rt=cnt;
		if (l==r)
		{
			t[rt].sum++;
			return ;
		}
		int mid=(l+r)>>1;
		if (x<=mid) insert(t[rt].ls,l,mid,x);
		else insert(t[rt].rs,mid+1,r,x);
		push_up(rt);
		return ;
	}
	inline int query(int rt1,int rt2,int l,int r,int k)
	{
		if (l==r) return l;
		int sz=t[t[rt2].ls].sum-t[t[rt1].ls].sum,mid=(l+r)>>1;
		if (k<=sz) return query(t[rt1].ls,t[rt2].ls,l,mid,k);
		else return query(t[rt1].rs,t[rt2].rs,mid+1,r,k-sz);
	}
}t;
inline int midfind(int x)
{
	int l=1,r=bsize;
	while (l<=r)
	{
		int mid=(l+r)>>1;
		if (b[mid]==x) return mid;
		if (b[mid]<x) l=mid+1;
		else r=mid-1;
	}
	return 0;
}
inline int read()
{
	int ans=0,p=1;
	char ch=getchar();
	while (ch<'0'||ch>'9') {if (ch=='-') p=-1;ch=getchar();}
	while (ch>='0'&&ch<='9') {ans=ans*10-'0'+ch;ch=getchar();}
	return ans*p;
}
inline void Pre()
{
	sort(b+1,b+n+1);
	for (int i=1;i<=n;i++)
	{
		int k=i;
		b[++bsize]=b[i];
		while (b[k]==b[k+1]) k++;
		i=k;
	}
	return ;
}
int main()
{
	n=read();m=read();
	for (int i=1;i<=n;i++)
	a[i]=b[i]=read();
	Pre();
	t.clear();
	for (int i=1;i<=n;i++)
	{
		int now=midfind(a[i]);
		rt[i]=rt[i-1];
		t.insert(rt[i],1,bsize,now);
	}
	while (m--)
	{
		int l,r,k;
		l=read();r=read();k=read();
		printf("%d
",b[t.query(rt[l-1],rt[r],1,bsize,k)]);
	}
	return 0;
}

P3919 【模板】可持久化数组

这道题目才比较像可持久化应该做的东西,对历史状态进行修改,查询历史状态,不过需要注意的是每次查询都要新建一个历史状态。

#include<bits/stdc++.h>
const int N=1e6+100;
int n,m,No;
int a[N],root[N];
struct Tree{
	int cnt;
	struct tree{
		int ls,rs,sum;
	}t[N*20];
	inline void clear() {cnt=1;return ;}
	inline int query(int rt,int l,int r,int pos)
	{
		if (l==r) return t[rt].sum;
		int mid=(l+r)>>1;
		if (pos<=mid) return query(t[rt].ls,l,mid,pos);
		else return query(t[rt].rs,mid+1,r,pos);
	}
	inline void update(int &rt,int l,int r,int pos,int k)
	{
		t[++cnt]=t[rt];
		rt=cnt;
		if (l==r)
		{
			t[rt].sum=k;
			return ;
		}
		int mid=(l+r)>>1;
		if (pos<=mid) update(t[rt].ls,l,mid,pos,k);
		else update(t[rt].rs,mid+1,r,pos,k);
		return ;
	}
	inline void build(int rt,int l,int r)
	{
		if (l==r)
		{
			t[rt].sum=a[l];
			return ;
		}
		int mid=(l+r)>>1;
		t[rt].ls=++cnt;
		build(t[rt].ls,l,mid);
		t[rt].rs=++cnt;
		build(t[rt].rs,mid+1,r);
		return ;
	}
}T;
inline int read()
{
	int ans=0,p=1;
	char ch=getchar();
	while (ch<'0'||ch>'9') {if (ch=='-') p=-1;ch=getchar();}
	while (ch>='0'&&ch<='9') {ans=ans*10-'0'+ch;ch=getchar();}
	return ans*p;
}
int main()
{
	n=read();m=read();
	for (int i=1;i<=n;i++)
	a[i]=read();
	T.clear();
	T.build(1,1,n);
	root[0]=1;
	for (int i=1;i<=m;i++)
	{
		int vi,opt,pos,k;
		scanf("%d%d",&vi,&opt);
		if (opt==1)
		{
			scanf("%d%d",&pos,&k);
			int nrt=root[vi];
			T.update(nrt,1,n,pos,k);
			root[++No]=nrt;
		}
		if (opt==2)
		{
			scanf("%d",&pos);
			root[++No]=root[vi];
			printf("%d
",T.query(root[vi],1,n,pos));
		}
	}
	return 0;
}

P4137 Rmq Problem / mex

这道题的第一个思路就是对于序列的从前到后建主席树,然后统计 ([l,r]) 每个数出现多少次,之后二分一下就可以了。

但是这种思路是错误的,因为有一些出现不止一次的数会对二分造成影响,如果强制为把出现多次的数赋值为 (1) 的话,这样做减法又不对。

其实正确的做法是,每次记录对于枚举到当前第 (i) 个数,记录一下前面每一个数最后出现的位置,包括自己。

这样每次查找的时候只需要在 (R) 这个位置建的主席树里找一个最小值小于 (L) 的最短前缀就可以了。

因为这个数在前面出现的位置小于 (L) ,所以肯定合法,因为是最小前缀,所以满足是最小的合法自然数。

如果当前前缀的最小值大于 (L) ,所以需要增长前缀长度,因为经过预处理,所以往后肯定有合法答案,如果小于 (L) 则尽量缩小前缀的长度。

#include<bits/stdc++.h>
using std::min; 
using std::sort;
const int N=2e5+100;
int n,m,num,size;
int a[N],b[N<<1],root[N];
struct Tree{
	int cnt;
	struct tree{
		int ls,rs,minn;
	}t[N*20];
	inline void clear() {cnt=0;return ;}
	inline void push_up(int rt)
	{
		t[rt].minn=min(t[t[rt].ls].minn,t[t[rt].rs].minn);
		return ;
	}
	inline void update(int &rt,int l,int r,int pos,int k)
	{
		t[++cnt]=t[rt];
		rt=cnt;
		if (l==r)
		{
			t[rt].minn=k;
			return ;
		}
		int mid=(l+r)>>1;
		if (pos<=mid) update(t[rt].ls,l,mid,pos,k);
		else update(t[rt].rs,mid+1,r,pos,k);
		push_up(rt);
		return ;
	}
	inline int query(int rt,int l,int r,int nl,int nr)
	{
		if (l==nl&&r==nr) return t[rt].minn;
		int mid=(l+r)>>1;
		if (nr<=mid) return query(t[rt].ls,l,mid,nl,nr);
		else if (nl>mid) return query(t[rt].rs,mid+1,r,nl,nr);
		else return min(query(t[rt].ls,l,mid,nl,mid),query(t[rt].rs,mid+1,r,mid+1,nr));
	}
}T;
inline int solve(int val,int rt)
{
	int l=1,r=num,ans=num;
	while (l<=r)
	{
		int mid=(l+r)>>1;
		if (T.query(rt,1,num,1,mid)>=val) l=mid+1;
		else
		{
			r=mid-1;
			ans=mid;
		}
	}
	return b[ans];
}
inline int midfind(int x)
{
	int l=1,r=num;
	while (l<=r)
	{
		int mid=(l+r)>>1;
		if (b[mid]==x) return mid;
		if (b[mid]<x) l=mid+1;
		else r=mid-1;
	}
	return 0;
}
inline void Pre()
{
	sort(b+1,b+size+1);
	for (int i=1;i<=size;i++)
	{
		int k=i;
		b[++num]=b[i];
		while (b[k]==b[k+1]&&k+1<=size) k++;
		i=k;
	}
	return ;
}
int main()
{
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;i++)
	{
		scanf("%d",a+i);
		b[++size]=a[i];
		b[++size]=a[i]+1;
	}
	Pre();
	T.clear();
	for (int i=1;i<=n;i++)
	{
		root[i]=root[i-1];
		T.update(root[i],1,num,midfind(a[i]),i);
	}
	for (int i=1;i<=m;i++)
	{
		int l,r;
		scanf("%d%d",&l,&r);
		printf("%d
",solve(l,root[r]));
	}
	return 0;
}

然后就会发现套的二分完全没有必要,可以直接在线段树上进行操作。

只需要把二分答案的代码换成下面的代码就可以了。

inline int midquery(int rt,int l,int r,int L)
{
	if (l==r) return l;
	int mid=(l+r)>>1;
	if (t[t[rt].ls].minn<L) return midquery(t[rt].ls,l,mid,L);
	else return midquery(t[rt].rs,mid+1,r,L);
}
原文地址:https://www.cnblogs.com/last-diary/p/11632086.html