可持久化线段树-主席树
可持久化,顾名思义,可以持久维护数据,将所有历史版本全部保存下来,但每次修改操作只增加修改部分,不变的部分依旧不变。
每次增加数据时,只会在线段树上增加一条链,整体复杂度nlogn,空间nlogn,非常优秀。
形象化看下图。
我们增加点时将其与不变部分相连,然后递归新建新数据,下面新节点同理。
可持久化线段树每次只增加新数据,而没有重复存储新数据,大大节省了空间。
可持久化线段树
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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现