动态开点与可持久化线段树
1. 动态开点线段树
在讲可持久化线段树之前,先让我们来了解一下动态开点线段树。
常规的线段树大多都是只能够维护一个不算特别长
比如说:我们需要在一个长度为
那么,我们一开始只创建一个根节点。接下来的所有操作的原则都是:除非需要用到一个节点,否则我们就不实际创建它。
比如下面这个例子:在一个长度为8的数组上,首先尝试修改1,2,8,然后询问[2,7]。
其中空缺的节点就自动视为0。这样就只需要存储必要的节点就好了。
下面再来看看区间修改时,线段树需要如何处理:
不同之处在于:有的节点可能会有PushDown的操作,导致影响到一个不存在的节点。
当然,这也不会有很大的问题,有两个解决方案:
在PushDown的时候,如果没有左or右孩子,直接创立一个新的左or右孩子。
使用标记永久化的技巧,让节点不再进行pushdown。这种方法消耗空间会小很多。
查询的时候同理:如果遇到空节点,就意味着这个节点的答案是0。
复杂度分析:
首先,如果区间长度为n,则单次操作的复杂度总是一样的,是
不同的是空间复杂度:由于每次操作都有可能访问全新的一系列节点,因此空间复杂度也是
特别小心区间修改的线段树:首先一个询问的标记本身就会被分解为
2. 可持久化线段树
顾名思义,就是可以存储历史信息的线段树。
比如我们对数组进行了n次修改,然后突然希望回到某个第i次版本。然后又基于这个版本进行一些新的修改等,就是可持久化线段树需要解决的问题。
要点在哪里呢?实际上关键在于:我们不再修改每个老节点的信息,而是类似于动态开点线段树一样,要改一个节点的时候就创建一个新的节点!
这样,老的线段树(蓝色)并不会被影响。而查询新的版本的时候,只需要从新的根进入(橙色10号点)就可以访问新的版本的线段树(10,11,12)
当然,很明显可以看到:新的线段树的节点会与之前的线段树节点有所重合!因为它们是一样的,我们不必整个复制。
那下次又修改到蓝色节点怎么办?会不会影响之前的版本?:答案当然是不会,注意我们的关键在于要改一个节点的时候就创建一个新的节点!所以老节点永远不会被改变,只会有新节点加入。
一些简单的应用:
给你一个数组,多次询问,每次查询某个区间的第k小。
解答:
这个题就可以用可持久化线段树做:建立一个以离散化后的值域为下标的线段树,然后考虑扫描线:从左到右扫描每个数,每次遇到一个数就在对应的下标+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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效