可持久化线段树

编程达人挑战赛·第6期 10w+人浏览 556人参与

前置知识:权值线段树,动态开点。

引入

题面

一个长度为 n n n 的数组 a 1 , a 2 … a n a_1,a_2\ldots a_n a1,a2an m m m 次操作,每次询问前 x x x 个元素中数 l ∼ r l\sim r lr 的出现次数。

一棵权值线段树在进行了一次修改操作后,会丢失操作之前的信息,如:一棵权值线段树存入了整个数组后,无法快速得到前半个数组中某些元素的出现次数。

暴力方法

很容易想到一个解决的方案:把一棵线段树当做一个元素,对于 n n n 个元素的数组,建立 n n n 棵线段树,分别存 1 ∼ 1 , 1 ∼ 2 … 1 ∼ n 1\sim1,1\sim2\ldots1\sim n 11,121n 的元素数量。

可是一棵线段树开 4 × N 4\times N 4×N 的空间, N N N 棵线段树需要 4 ∗ N 2 4*N^2 4N2 的空间,直接爆炸。

优化

不难转换,对于序列 a a a,一棵保存 1 ∼ x 1\sim x 1x 区间的线段树可以想成一棵存 1 ∼ x − 1 1\sim x-1 1x1 的线段树对节点 a x a_x ax 执行加一操作。也就是说,每一棵线段树,除了被修改的部分,其他的部分都与前一棵线段树相同。

既然有不变的,为什么要新建一棵线段树呢?只需要将变了的点另外开出来,其余的点还用原来线段树上的点就行了。

复杂度

普通线段树单次的时间复杂度是 O ( log ⁡ N ) O(\log N) O(logN),是因为在线段树的 log ⁡ 2 N \log_2 N log2N 从中,每次操作在每一层只操作一次。换句话来说,线段树每一次修改每层只有一个点会被改变,总共改变的点就是线段树层数 log ⁡ 2 N \log_2 N log2N 个。

回到这道题中,一次操作更改 log ⁡ 2 T \log_2 T log2T 个点(T 是值域大小),也就是新增 log ⁡ 2 T \log_2 T log2T 个点,序列长度为 N N N,所以空间复杂度为 N log ⁡ T N\log T NlogT。同时也可以离散化,使空间复杂度降到 N log ⁡ N N\log N NlogN(其实没啥区别,但只是这道题,树套树不离散化等着 MLE 吧!)。

时间复杂度就是线段树正常的时间复杂度。

如图:
在这里插入图片描述

例题 1

题目传送门

按照可持久化线段树的思路打即可,因为是单个时间,单点查询,不会涉及两棵线段树间的交互,所以不多讲了,每次在上一课线段树上改就行了。

值得一提的是,线段树继承上一个的时候,每次都要新建一个根节点。

#include<bits/stdc++.h>
#define int long long
#define lc f[p].l
#define rc f[p].r
#define endl putchar('\n')
using namespace std;
const int N=3e7;
int read(){
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-'0',c=getchar();
	return x*f;
}
void print(int x){
	if(x<0)putchar('-'),x=-x;
	if(x<10){putchar(x+'0');return;}
	print(x/10);
	putchar(x%10+'0');
}
int n,m,k;
struct node{
	int l,r,ans;
}f[N];
int a[N];
int idx;
int rt[N];
void build(int &p,int L,int R){
	p=++idx;
	f[p].ans=a[L];
	if(L==R)return;
	int mid=L+R>>1;
	build(lc,L,mid);
	build(rc,mid+1,R);
}
int update(int &p,int L,int R,int x,int k){
	f[++idx]=f[p];
	p=idx;
	if(L==R){
		f[p].ans=k;
		return p;
	}
	int mid=L+R>>1;
	if(x<=mid)f[p].l=update(lc,L,mid,x,k);
	else f[p].r=update(rc,mid+1,R,x,k);
	return p;
}
int query(int p,int L,int R,int x){
	if(L==R)return f[p].ans;
	int mid=L+R>>1;
	if(x<=mid)return query(lc,L,mid,x);
	else return query(rc,mid+1,R,x);
}
signed main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++)a[i]=read();
	build(rt[0],1,n);
	for(int i=1;i<=m;i++){
		int v=read();
		int op=read(),p=read();
		if(op==1){
			int x=read();
			rt[i]=rt[v];
			update(rt[i],1,n,p,x);
		}
		else{
			rt[i]=++idx;
			f[rt[i]]=f[rt[v]];
			print(query(rt[i],1,n,p)),endl;
		}
		
	}
}

例题 2

前文中,我们找到了保存序列 1 ∼ x 1\sim x 1x 的区间中数出现情况的方法。每个位置存 1 ∼ x 1\sim x 1x 中每个数的出现次数,并且数的出现次数是支持相减的。
在这里插入图片描述
如上图,存有每一段包含起点的区间,并且支持加减,这不就前缀和吗?

这道题询问的是一个区间内数出现的情况,那么将两棵对应的线段树相减即可。

也就是说,将两个点带进线段树,两者相减即区间内的数出现次数。即区间 l ∼ r l\sim r lr 内的数出现次数即 S 1 ∼ r − S 1 ∼ l − 1 S_{1\sim r}-S_{1\sim l-1} S1rS1l1

不过注意一点,最开始建一棵空的树,本题数据范围 a i ≤ 10 9 a_i\le10^9 ai109,直接建肯定不行,得先离散化。

#include<bits/stdc++.h>
#define int long long
#define lc f[p].l
#define rc f[p].r
//#define lowbit(x) x&-x
#define endl putchar('\n')
#define psp putchar(' ')
using namespace std;
const int N=200e5+5;
const int M=1e3+5;
int read(){
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-'0',c=getchar();
	return x*f;
}
void print(int x){
	if(x<0)putchar('-'),x=-x;
	if(x<10){putchar(x+'0');return;}
	print(x/10);
	putchar(x%10+'0');
}
void putstr(string s){
	for(int i=0;i<s.size();i++)putchar(s[i]);
}
int n,m,k;
int T;
int a[N];
int b[N];
int deepseek(int x){
	int l=1,r=m;
	while(l<r){
		int mid=l+r>>1;
		if(b[mid]>=x)r=mid;
		else l=mid+1;
	}
	return r;
}
struct node{
	int l,r,ans;
}f[N];
int idx;
int rt[N];
int r[N];
int add(int x){
	f[++idx]=f[x];
	return idx;
}
void build(int &p,int L,int R){
	p=++idx;
	if(L==R)return;
	int mid=L+R>>1;
	build(lc,L,mid);
	build(rc,mid+1,R);
}
int query(int l,int r,int L,int R,int k){
	if(L==R)return L;
	int mid=L+R>>1;
	if(f[f[r].l].ans-f[f[l].l].ans>=k)return query(f[l].l,f[r].l,L,mid,k);
	return query(f[l].r,f[r].r,mid+1,R,k-(f[f[r].l].ans-f[f[l].l].ans));
}
void update(int &p,int L,int R,int x,int k){
	p=add(p);
	if(L==R){
		f[p].ans+=k;
		return;
	}
	int mid=L+R>>1;
	if(x<=mid)update(lc,L,mid,x,k);
	else update(rc,mid+1,R,x,k);
	f[p].ans=f[lc].ans+f[rc].ans;
}
signed main(){
	n=read(),k=read();
	for(int i=1;i<=n;i++)a[i]=read(),b[i]=a[i];
	sort(b+1,b+1+n);
	b[0]=b[1]-114514;
	for(int i=1;i<=n;i++){
		if(b[i]!=b[i-1])b[++m]=b[i];
	}
	for(int i=1;i<=n;i++)a[i]=deepseek(a[i]);
	build(rt[0],1,m);
	for(int i=1;i<=n;i++){
		rt[i]=rt[i-1];
		f[rt[i]]=f[rt[i-1]];
		update(rt[i],1,m,a[i],1);
	}
	while(k--){
		int l=read(),r=read(),q=read();
		print(b[query(rt[l-1],rt[r],1,m,q)]),endl;
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值