[OI] 可持久化数据结构
学了一年 OI 才看懂这句话:
是以什么为底的?
其实没什么区别
因为我们自动忽略常数,因此
可持久化数组
可持久化数组是基于可持久化线段树的一种数据结构. 它可以支持如下操作:
- 单点修改
- 查询历史版本
- 在历史版本上修改
考虑到,为了实现这个功能,我们可以给每个历史版本都复制一份副本,但是这样的复杂度是
考虑优化空间复杂度:现在把整个数组放在线段树上,令数组为线段树的全部叶节点,因为我们每次修改节点最多只会带来
比较水,直接放代码了
namespace HIST_Stree{
const int N=1000000;
int root[N];
#define mid(l,r) mid=((l)+(r))/2
struct tree{
int tol,tor;
int w;
}t[30*N];
#define tol t[id].tol
#define tor t[id].tor
int cnt=0;
int newnode(){
t[++cnt]={};
return cnt;
}
int clone(int node){
t[++cnt]=t[node];
return cnt;
}
void build(int &id,int l,int r){
id=newnode();
if(l==r){
t[id].w=a[l];
return;
}
int mid(l,r);
build(tol,l,mid);
build(tor,mid+1,r);
}
void update(int &id,int l,int r,int pos,int val){
id=clone(id);
if(l==r){
t[id].w=val;
return;
}
int mid(l,r);
if(pos<=mid) update(tol,l,mid,pos,val);
else update(tor,mid+1,r,pos,val);
}
int ask(int id,int l,int r,int pos){
if(l==r){
return t[id].w;
}
else{
int mid(l,r);
if(pos<=mid) return ask(tol,l,mid,pos);
else return ask(tor,mid+1,r,pos);
}
}
}
if(op==1){
scanf("%d",&val);
update(root[i]=root[t],1,n,pos,val);
}
else{
printf("%d\n",ask(root[t],1,n,pos));
root[i]=root[t];
}
可持久化线段树
可持久化线段树最经典的应用即为求区间第
先不考虑可持久化,考虑开一颗权值线段树,那么我们按照下述方法即可求出区间
先将原数组
为了方便统计答案,我们规定:插入节点时,途径节点的值加一
比如下列数组:
a: 1 5 2 6 3 7 4
排序后为
b: 1 2 3 4 5 6 7
首先插入
其次插入
以此类推
最终会变成这样:
可以发现:因为我们开的是权值线段树,因此总是满足左区间小于右区间,因此要求
那么对于求区间
实际实现的时候并不需要真的减出一颗树来,可以通过作差来做.
namespace HIST_Stree{
#define mid(x,y) mid=((x)+(y))/2
const int N=200001;
int root[N],cnt=0;
struct tree{
int tol,tor;
int sum;
}t[N*32];
#define tol(id) t[id].tol
#define tor(id) t[id].tor
int newnode(){
t[++cnt]={};
return cnt;
}
int clone(int node){
t[++cnt]=t[node];
return cnt;
}
void build(int &id,int l,int r){
id=newnode();
if(l==r){
return;
}
int mid(l,r);
build(tol(id),l,mid);
build(tor(id),mid+1,r);
}
void change(int &id,int l,int r,int pos){
id=clone(id);
t[id].sum++;
if(l==r) return;
int mid(l,r);
if(pos<=mid) change(tol(id),l,mid,pos);
else change(tor(id),mid+1,r,pos);
}
int ask(int x,int y,int l,int r,int k){
if(l==r) return l;
int mid(l,r);
if(t[t[y].tol].sum-t[t[x].tol].sum>=k){
return ask(t[x].tol,t[y].tol,l,mid,k);
}
else{
return ask(t[x].tor,t[y].tor,mid+1,r,k-(t[t[y].tol].sum-t[t[x].tol].sum));
}
}
}
using namespace HIST_Stree;
sort(b+1,b+n+1);
int l=unique(b+1,b+n+1)-b-1;
build(root[0],1,l);
for(int i=1;i<=n;++i){
change(root[i]=root[i-1],1,l,lower_bound(b+1,b+l+1,a[i])-b);
}
while(m--){
int L,R,K;
scanf("%d %d %d",&L,&R,&K);
printf("%d\n",b[ask(root[L-1],root[R],1,l,K)]);
}
可持久化线段树小结
根据上述问题的解法可以发现,可持久化值域线段树因为其出色的可加减性,因而经常被当做前缀和用.
什么时候才会让可持久化线段树充当前缀和的角色?通常是那些需要对不同版本求前缀和的问题
但实际上并没有哪个题会容易让你看出来怎么构建历史版本(或者根本看不出来可以构建历史版本),这才是可持久化线段树题最难的地方.
可持久化线段树并不全是值域线段树,但是其他的并不常见,多用来构成其他可持久化数据结构. 如可持久化数组,可持久化并查集等.
在维护前缀和时,通常我们需要维护一个求答案的式子,比如:
推知答案
注意到这个答案式里面有两个参数,那么只需要传两个历史版本
多维函数也是一样,比如 P2633,它的答案式为
这些题都是类似的,实际情况实际处理即可.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!