#Snow{ position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 99999; background: rgba(255,255,240,0.1); pointer-events: none; }

可持久化线段树-主席树

可持久化,顾名思义,可以持久维护数据,将所有历史版本全部保存下来,但每次修改操作只增加修改部分,不变的部分依旧不变。

每次增加数据时,只会在线段树上增加一条链,整体复杂度nlogn,空间nlogn,非常优秀。

形象化看下图。

我们增加点时将其与不变部分相连,然后递归新建新数据,下面新节点同理。

image

image

image

image

image

可持久化线段树每次只增加新数据,而没有重复存储新数据,大大节省了空间。

可持久化线段树

P3834 【模板】可持久化线段树 2
求第k小数
给定长度为N的整数序列A下标为1∼N。
现在要执行M次操作,其中第i次操作为给出三个整数li,ri,ki求 A[li],A[li+1],…,A[ri] (即 A 的下标区间 [li,ri])中第ki小的数是多少。
N≤105,M≤104,|A[i]|≤109

我们发现a的范围较大,并且我们不关心其具体大小,而只关心相对大小,所以首先要进行离散化,

离散化我们使用vector数组比较方便,但要注意的是vector下标是从0开始的,所以我们离散化后的数值也是从0到n-1的,那我们的权值线段树也要从0开始。

vector<int>nums;
int find(int x) //查找x离散化后的数值
{
	return lower_bound(nums.begin(),nums.end(),x)-nums.begin();
}

	for(int i=1;i<=n;i++)
		cin>>a[i],nums.push_back(a[i]);
	sort(nums.begin(),nums.end()); //排序
	nums.erase(unique(nums.begin(),nums.end()),nums.end()); //去重

我们首先建立一个权值线段树,即在值域范围上建树,维护值域上每个位置上的数出现的次数。
权值线段树的每个区间对应数轴上的区间。

建树

int root[N] //root[]的作用是记录每个版本。
struct tree
{
	int l,r;  //这里和普通线段树不同,l,r指的是左右儿子,而不是左右端点。
	int cnt; //记录出现次数
}t[N*21];   //大小 [n*4+nlogn]

int build(int l,int r)
{
	int p=++idx; //新建节点
	if(l==r) return p;
	int mid=(l+r)>>1;
	t[p].l=build(l,mid),t[p].r=build(mid+1,r); //更新左右儿子
	return p; //返回当前节点
}

插入

int insert(int p,int l,int r,int x)
{
	int q=++idx; //新建节点
	t[q]=t[p]; //继承之前数据,并插入新数据
	if(l==r)
	{
		t[q].cnt++; //更新值域上这个数出现的次数
		return q; //返回节点编号
	}
	int mid=(l+r)>>1;
	if(x<=mid) t[q].l=insert(t[p].l,l,mid,x); //如果在左半边,递归插入并返回左儿子编号
	else t[q].r=insert(t[p].r,mid+1,r,x);//如果在右边,递归插入并返回右儿子编号
	t[q].cnt=t[t[q].l].cnt+t[t[q].r].cnt; //更新节点范围内数出现的总次数
	return q; //返回节点编号
}

查询
我们要查找区间内第k小的数,并且我们的树是值域范围内的树,那我们用差分的方法,用第r个版本和第l-1个版本内数值出现的次数相减,如何理解?我们第r个版本维护的是值域1r内所有数出现的次数,第l-1同理是维护的1l-1所有数出现的次数,那两者做差就是L~R所有数出现的次数了,然后再找第k个数就OK了,具体看代码理解。

int query(int p,int q,int l,int r,int k)
{
	if(l==r) return r; //返回第k小的数
	int cnt=t[t[q].l].cnt-t[t[p].l].cnt; //左半边数的个数
	int mid=(l+r)>>1;
	if(k<=cnt) return query(t[p].l,t[q].l,l,mid,k); //如果k小于等于左边数的个数,那显然再左边找,反之在右边。
	else return query(t[p].r,t[q].r,mid+1,r,k-cnt); //我们要减去左边数的个数,去找右边第k-cnt个数。
}

完整代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
const int M=1e4+10;
vector<int>nums;
int n,m;
int a[N];
int root[N];//记录每个版本
int idx;

struct tree
{
	int l,r;
	int cnt;
}t[N*21];


int find(int x)
{
	return lower_bound(nums.begin(),nums.end(),x)-nums.begin();
}

int build(int l,int r)
{
	int p=++idx;
	if(l==r) return p;
	int mid=(l+r)>>1;
	t[p].l=build(l,mid),t[p].r=build(mid+1,r);
	return p;
}

int insert(int p,int l,int r,int x)
{
	int q=++idx;
	t[q]=t[p];
	if(l==r)
	{
		t[q].cnt++;
		return q;
	}
	int mid=(l+r)>>1;
	if(x<=mid) t[q].l=insert(t[p].l,l,mid,x);
	else t[q].r=insert(t[p].r,mid+1,r,x);
	t[q].cnt=t[t[q].l].cnt+t[t[q].r].cnt;
	return q;
}

int query(int p,int q,int l,int r,int k)
{
	if(l==r) return r;
	int cnt=t[t[q].l].cnt-t[t[p].l].cnt;
	int mid=(l+r)>>1;
	if(k<=cnt) return query(t[p].l,t[q].l,l,mid,k);
	else return query(t[p].r,t[q].r,mid+1,r,k-cnt);
}

int main(){
	
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>a[i],nums.push_back(a[i]);
	sort(nums.begin(),nums.end());
	nums.erase(unique(nums.begin(),nums.end()),nums.end());
	
	root[0]=build(0,nums.size()-1);
	
	for(int i=1;i<=n;i++)
	{
		root[i]=insert(root[i-1],0,nums.size()-1,find(a[i]));
	}
	
	while(m--)
	{
		int l,r,k;
		cin>>l>>r>>k;
		printf("%d\n",nums[query(root[l-1],root[r],0,nums.size()-1,k)]);
	}
	
	return 0;
}

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

题目描述
如题,你需要维护这样的一个长度为 N 的数组,支持如下几种操作
在某个历史版本上修改某一个位置上的值
访问某个历史版本上的某一位置的值
此外,每进行一次操作(对于操作2,即为生成一个完全一样的版本,不作任何改动),就会生成一个新的版本。版本编号即为当前操作的编号(从1开始编号,版本0表示初始状态数组)

输入的第一行包含两个正整数 N,M, 分别表示数组的长度和操作的个数。
第二行包含NN个整数,依次为初始状态下数组各位的值
对于操作1,格式为vi 1 loci valuei 即为在版本vi的基础上将aloci修改为 valuei
对于操作2,格式为vi 2 loci 即访问版本vi aloci的值

我们插入的数在线段树上的位置x对应数组ai下标i,即在1n位置上插入原来a1an.
修改同样在给出的位置x修改。题目要求在历史版本上进行修改,所以我们需要在每次修改时都要用root[]记录当前修改后的版本。

#include<bits/stdc++.h>
using namespace std;
const int N=1e7+10;
int n,m;
int a[N];

struct tree
{
	int l,r;
	int val;//每个位置上的值
}t[N*4];

int root[N];
int idx;

int build(int l,int r)
{
	int p=++idx;
	if(l==r)
	{
		t[p].val=a[l];
		return p;
	}
	int mid=(l+r)>>1;
	t[p].l=build(l,mid),t[p].r=build(mid+1,r);
	return p;
}

int insert(int p,int l,int r,int x,int val)
{
	int q=++idx;
	t[q]=t[p];
	if(l==r)
	{
		t[q].val=val;
		return q;
	}
	int mid=(l+r)>>1;
	if(x<=mid) t[q].l=insert(t[p].l,l,mid,x,val);
	else t[q].r=insert(t[p].r,mid+1,r,x,val);
	return q;
}

int query(int p,int l,int r,int x)
{
	if(l==r) return t[p].val;
	int mid=(l+r)>>1;
	if(x<=mid) return query(t[p].l,l,mid,x);
	else return query(t[p].r,mid+1,r,x);
}

int main(){
	
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	root[0]=build(1,n);
	
	for(int i=1;i<=m;i++)
	{
		int pre,x,d,op;
		scanf("%d%d%d",&pre,&op,&x);
		if(op==1)
		{
			scanf("%d",&d);
			root[i]=insert(root[pre],1,n,x,d);
		}
		else
		{
			printf("%d\n",query(root[pre],1,n,x));
			root[i]=root[pre];
		}
	}
	
	return 0;
}

P1972 [SDOI2009] HH的项链
题目描述
HH 有一串由各种漂亮的贝壳组成的项链。HH 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。HH 不断地收集新的贝壳,因此,他的项链变得越来越长。
有一天,他突然提出了一个问题:某一段贝壳中,包含了多少种不同的贝壳?这个问题很难回答…… 因为项链实在是太长了。于是,他只好求助睿智的你,来解决这个问题。
输入格式
一行一个正整数 nn,表示项链长度。
第二行 nn 个正整数 ai,表示项链中第 i 个贝壳的种类。
第三行一个整数 m,表示 HH 询问的个数。
接下来 m 行,每行两个整数 l,r,表示询问的区间。
输出格式
输出m行,每行一个整数依次表示询问对应的答案。

【数据范围】

1<=n,m,ai<=1e6

这到困扰我们多年的题终于可以解决了。
我们建立一个维护区间不同数的个数的树,对于每一次插入或者说每一个新版本,我们维护1-now不同数的个数,那如果有一个数出现两次怎么办呢?我们可以记录一下每个数上一次出现的地方,对于当前版本,我们只用统计它一次的贡献,那我们就可以在它上一次出现的位置删去它即可。
如何求答案呢?这次我们不再需要差分,对于询问L,R,我们只需要知道第R个版本区间L-R不同数的个数,因为第R个版本维护的是a1~ar不同数的个数。
我们维护的是区间不同数的个数,即区间和,所以类似普通线段树的维护方式。


#include<bits/stdc++.h>
using namespace std;
const int N=4e7+10;
int n,m;
int a[N];


int read()
{
	char ch=getchar();int x=0;
	while(!isdigit(ch)){ch=getchar();} 
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x;
}

unordered_map<int,int>mp;

struct tree
{
	int l,r;
	int val; //不同数的个数
}t[N];

int idx;
int root[N];

void pushup(int p)
{
	t[p].val=t[t[p].l].val+t[t[p].r].val;
}

int insert(int p,int l,int r,int x,int val)
{
	int q=++idx;
	t[q]=t[p];
	if(l==r)
	{
		t[q].val+=val;
		return q;
	}
	int mid=(l+r)>>1;
	if(x<=mid) t[q].l=insert(t[p].l,l,mid,x,val);
	else t[q].r=insert(t[p].r,mid+1,r,x,val);
	pushup(q);
	return q;
}

int query(int p,int l,int r,int L,int R)
{
	if(L<=l&&R>=r) return t[p].val;
	int mid=(l+r)>>1;
	int res=0;
	if(L<=mid) res+=query(t[p].l,l,mid,L,R);
	if(R>mid) res+=query(t[p].r,mid+1,r,L,R);
	return res;
}

int main(){
	n=read();
	for(int i=1;i<=n;i++)
	{
		int x;
		x=read();
		root[i]=insert(root[i-1],1,n,i,1); 
		if(mp[x]) root[i]=insert(root[i],1,n,mp[x],-1);
		mp[x]=i;
	}
	m=read();
	for(int i=1;i<=m;i++)
	{
		int l,r;
		l=read();r=read();
		printf("%d\n",query(root[r],1,n,l,n));
	}
	
	return 0;
}
posted @   繁花孤城  阅读(271)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示