动态开点与可持久化线段树

 


1. 动态开点线段树

在讲可持久化线段树之前,先让我们来了解一下动态开点线段树。

常规的线段树大多都是只能够维护一个不算特别长106以内的数组,但是对于109级别的数组却不能很好的维护(因为空间开不下),所以,我们有了动态开点线段树的想法:节点只有在有需要的时候才被创建。

比如说:我们需要在一个长度为n109的数组上实现:1. 区间求和。2. 单点修改。初始数组均为0。

那么,我们一开始只创建一个根节点。接下来的所有操作的原则都是:除非需要用到一个节点,否则我们就不实际创建它。

比如下面这个例子:在一个长度为8的数组上,首先尝试修改1,2,8,然后询问[2,7]。

其中空缺的节点就自动视为0。这样就只需要存储必要的节点就好了。

下面再来看看区间修改时,线段树需要如何处理:

不同之处在于:有的节点可能会有PushDown的操作,导致影响到一个不存在的节点。

当然,这也不会有很大的问题,有两个解决方案:

PushDown的时候,如果没有左or右孩子,直接创立一个新的左or右孩子。

使用标记永久化的技巧,让节点不再进行pushdown。这种方法消耗空间会小很多。

查询的时候同理:如果遇到空节点,就意味着这个节点的答案是0。

复杂度分析:

首先,如果区间长度为n,则单次操作的复杂度总是一样的,是O(logn),那么m次操作复杂度就是O(mlogn)

不同的是空间复杂度:由于每次操作都有可能访问全新的一系列节点,因此空间复杂度也是O(mlogn),不再是原本线段树的O(n)

特别小心区间修改的线段树:首先一个询问的标记本身就会被分解为4logn个,同时还存在标记下放。则空间复杂度往往很高。尽量使用标记永久化 & 想想其他办法(比如能否离散化修改与询问?)

2. 可持久化线段树

顾名思义,就是可以存储历史信息的线段树。

比如我们对数组进行了n次修改,然后突然希望回到某个第i次版本。然后又基于这个版本进行一些新的修改等,就是可持久化线段树需要解决的问题。

要点在哪里呢?实际上关键在于:我们不再修改每个老节点的信息,而是类似于动态开点线段树一样,要改一个节点的时候就创建一个新的节点!

这样,老的线段树(蓝色)并不会被影响。而查询新的版本的时候,只需要从新的根进入(橙色10号点)就可以访问新的版本的线段树(10,11,12)

当然,很明显可以看到:新的线段树的节点会与之前的线段树节点有所重合!因为它们是一样的,我们不必整个复制。

那下次又修改到蓝色节点怎么办?会不会影响之前的版本?:答案当然是不会,注意我们的关键在于要改一个节点的时候就创建一个新的节点!所以老节点永远不会被改变,只会有新节点加入。

一些简单的应用:

给你一个数组,多次询问,每次查询某个区间的第k小。n105

解答:

这个题就可以用可持久化线段树做:建立一个以离散化后的值域为下标的线段树,然后考虑扫描线:从左到右扫描每个数,每次遇到一个数就在对应的下标+1

这样,我们可以很方便的回答一个询问:在[1,r]这个区间内,有多少个数小于k:我们获取线段树的第r个版本,然后线段树上做一个区间求和就好。

当然,通过简单的差分,我们就可以获得[l,r]这个区间内的答案。再配合线段树上的二分就可以解决这道题了(每次走到一个线段树节点,看左子树的和是否大于当前排名,大于则说明答案在左子树内,否则在右子树内)。

P3919 【模板】可持久化线段树 1(可持久化数组)

代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
int rt[maxn],a[maxn],n,m,v,op,pos,x;
struct Tree{
	int ls[30*maxn],rs[30*maxn],sum[30*maxn],cnt=0;
	void build(int &now,int l,int r){
		now=++cnt;
		if(l==r){
			sum[now]=a[l];
			return ;
		}
		int mid=(l+r)/2;
		build(ls[now],l,mid);
		build(rs[now],mid+1,r);
		return ;
	}
	void update(int &now,int pre,int l,int r,int c,int x){
		now=++cnt;
		ls[now]=ls[pre],rs[now]=rs[pre],sum[now]=sum[pre];
		if(l>r)return ;
		if(l==r&&r==c){
			sum[now]=x;
			return ;
		}
		int mid=(l+r)/2;
		if(c<=mid)update(ls[now],ls[pre],l,mid,c,x);
		else      update(rs[now],rs[pre],mid+1,r,c,x);
		return ;
	}
	int check(int pre,int l,int r,int c){
		if(l>r)return 0;
		if(l==r&&r==c){
			return sum[pre];
		}
		int mid=(l+r)/2;
		if(c<=mid)return check(ls[pre],l,mid,c);
		else      return check(rs[pre],mid+1,r,c);
	}
}T;
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	T.build(rt[0],1,n);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&v,&op,&pos);
		if(op==1){
			scanf("%d",&x);
			T.update(rt[i],rt[v],1,n,pos,x);
		}
		if(op==2){
			printf("%d\n",T.check(rt[v],1,n,pos));
			rt[i]=rt[v];
		}
	}
	
	return 0;
}

P3834 【模板】可持久化线段树 2

代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=2*1e5+5;
map<int,int>mp;
int n,m,a[maxn],cnt=0;
int rt[maxn],d[maxn],x,y,k;
struct Tree{
	int ls[30*maxn],rs[30*maxn],sum[30*maxn];
	void update(int &now,int pre,int l,int r,int c){
		if(l>r)return ;
		now=++cnt;
		ls[now]=ls[pre],rs[now]=rs[pre],sum[now]=sum[pre];
		if(l==r&&r==c){
			sum[now]++;
			return ;
		}
		int mid=(l+r)/2;
		if(c<=mid)update(ls[now],ls[pre],l,mid,c);
		else      update(rs[now],rs[pre],mid+1,r,c);
		sum[now]=sum[ls[now]]+sum[rs[now]];
		return ;
	}	
	int solve(int now1,int now2,int l,int r,int k){
		if(l>r)return 0;
		if(l==r&&k<=sum[now2]-sum[now1])return l;
		int mid=(l+r)/2;
//		cout<<l<<" "<<r<<" "<<mid<<" "<<sum[ls[now1]]<<" "<<sum[ls[now2]]<<" "<<k<<" "<<endl;
		if(sum[ls[now2]]-sum[ls[now1]]>=k)return solve(ls[now1],ls[now2],l,mid,k);
		else return solve(rs[now1],rs[now2],mid+1,r,k-(sum[ls[now2]]-sum[ls[now1]]));
	}
}T;
int main(){
//	freopen("1.txt","r",stdin);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		mp[a[i]]=0;
	}
	for(map<int,int>::iterator it=mp.begin();it!=mp.end();it++)it->second=++cnt;
	for(int i=1;i<=n;i++){
		d[mp[a[i]]]=a[i];
//		cout<<mp[a[i]]<<" ";
	}
//	cout<<endl;
//	for(int i=1;i<=n;i++)cout<<d[i]<<" ";
//	cout<<endl;
	for(int i=1;i<=n;i++)
		T.update(rt[i],rt[i-1],1,n,mp[a[i]]);
	while(m--){
		scanf("%d%d%d",&x,&y,&k);
		printf("%d\n",d[T.solve(rt[x-1],rt[y],1,n,k)]);
	}
	return 0;
}

相关资料

相关资料

posted @   Ayaka_T  阅读(197)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示