线段树的可持久化
线段树进阶
可持久化
能够保留每一个历史版本的数据结构。
那么可持久化线段树就是能保留历史版本的线段树。
原谅我之前一直叫它可持续化线段树 。
可持久化线段树
一般来说,可持久化线段树本质其实是可持久化数组,即支持单点修改、单点查询。
因为要保留历史版本,那么如果对于每次的的修改和查询均新生成一棵线段树的话。
一棵线段树需要 的空间, 次操作代表了 棵线段树,空间复杂度 ……
炸空间怎么办?想想之前值域 的权值线段树。
没错,动态开点。
当对一个叶节点进行修改之后,你会发现整棵树上修改的点会形成一条链,那我们就将这条链维护出来,也就是下图的红色节点(图片来自 OI wiki )。
建树
很常规的动态开点线段树。
void build(int &i,int l,int r) { i=++cnt; if(l==r) { T[i].val=a[l]; return; } int mid=(l+r)>>1; build(T[i].lc,l,mid); build(T[i].rc,mid+1,r); }
修改
在修改的过程中,我们先新建一个节点,赋给它对应的历史节点的信息(包括左右子节点和权值)。
还有,每新建一棵线段树,都要将其根节点记录下来,作为历史版本的查找依据。
void change(int &i,int last,int l,int r,int pos,int k) { i=++cnt;T[i]=T[last]; if(l==r) { T[i].val=k; return; } int mid=(l+r)>>1; if(pos<=mid)change(T[i].lc,T[last].lc,l,mid,pos,k); else change(T[i].rc,T[last].rc,mid+1,r,pos,k); }
查询
查询和普通动态开点线段树基本一样。
int ask(int i,int l,int r,int pos) { if(l==r)return T[i].val; int mid=(l+r)>>1; if(pos<=mid)return ask(T[i].lc,l,mid,pos); else return ask(T[i].rc,mid+1,r,pos); }
主函数中:
build(rot[0],1,n); int var=re(),op=re(),pos=re(); if(op==1)change(rot[i],rot[var],1,n,pos,re()); else rot[i]=rot[var],wr(ask(rot[i],1,n,pos)),putchar('\n');
还记得那首诗嘛?
一年 OI 一场空,不开 long long 见祖宗。
坑点
-
空间
线段树对的空间的需求极大,典型的空间换时间,所以数组千万不要开太大,可持续化线段树开到原数组大小的 二十三倍 就好了,否则就会无情的
Runtime Error.
和Memory Limit Exceeded.
。所以线段树中就不要储存所代表区间的左右端点了,将端点作为递归的参数传下去,否则会 MLE。
update in 2022.09.14 空间好像放开了,之前是 500MB,现在看是 1GB。
-
关于区间修改
一般来说,可持久化线段树不支持区间修改,因为懒标在下传时会导致之前版本的节点发生改变,然后空间复杂度爆炸。
Code
const int inf=1e6+7; int n,m,a[inf]; struct Seg_Tree{ int lc,rc,val; }T[inf*23]; int rot[inf],cnt; void build(int &i,int l,int r) { i=++cnt; if(l==r) { T[i].val=a[l]; return; } int mid=(l+r)>>1; build(T[i].lc,l,mid); build(T[i].rc,mid+1,r); } void change(int &i,int last,int l,int r,int pos,int k) { i=++cnt;T[i]=T[last]; if(l==r) { T[i].val=k; return; } int mid=(l+r)>>1; if(pos<=mid)change(T[i].lc,T[last].lc,l,mid,pos,k); else change(T[i].rc,T[last].rc,mid+1,r,pos,k); } int ask(int i,int l,int r,int pos) { if(l==r)return T[i].val; int mid=(l+r)>>1; if(pos<=mid)return ask(T[i].lc,l,mid,pos); else return ask(T[i].rc,mid+1,r,pos); } int main() { n=re();m=re(); for(int i=1;i<=n;i++) a[i]=re(); build(rot[0],1,n); for(int i=1;i<=m;i++) { int var=re(),op=re(),pos=re(); if(op==1)change(rot[i],rot[var],1,n,pos,re()); else rot[i]=rot[var],wr(ask(rot[i],1,n,pos)),putchar('\n'); } return 0; }
主席树
线段树能可持久化,权值线段树也能。
主席树(Hjt 树),全名 可持久化权值线段树 。
引入
给定 n 个整数构成的序列 a,将对于指定的闭区间 [l,r] 查询其区间内的第 k 小值。
好像就是把区间从 扩展到了 。
于是我们就从权值线段树扩展到了可持续化权值线段树。
思路
发明者的原话是:“对于原序列的每一个前缀 建出一棵线段树维护值域上每个数出现的次数,则其树是可减的。”
其实就是前缀和。
权值线段树储存的是值域上数的个数。对于版本 的权值线段树减去版本 的权值线段树,就得到了维护 的权值线段树。
Code
const int inf=2e5+7; int n,m,num,a[inf],bok[inf]; struct Seg_Tree{ int lc,rc; int sum; }T[inf<<5]; int rot[inf],cnt; void insert(int &i,int j,int l,int r,int k) { i=++cnt;T[i]=T[j]; if(l==r){T[i].sum++;return;} int mid=(l+r)>>1; if(k<=mid)insert(T[i].lc,T[j].lc,l,mid,k); else insert(T[i].rc,T[j].rc,mid+1,r,k); T[i].sum=T[T[i].lc].sum+T[T[i].rc].sum; } int ask(int i,int j,int l,int r,int k) { if(l==r)return l; int mid=(l+r)>>1,sum=T[T[j].lc].sum-T[T[i].lc].sum; if(k<=sum)return ask(T[i].lc,T[j].lc,l,mid,k); return ask(T[i].rc,T[j].rc,mid+1,r,k-sum); } int main() { n=re();m=re(); for(int i=1;i<=n;i++) bok[i]=a[i]=re(); sort(bok+1,bok+n+1); num=unique(bok+1,bok+n+1)-bok-1; for(int i=1;i<=n;i++) a[i]=lower_bound(bok+1,bok+num+1,a[i])-bok; for(int i=1;i<=n;i++) insert(rot[i],rot[i-1],1,num,a[i]); for(int i=1;i<=m;i++) { int l=re(),r=re(),k=re(); wr(bok[ask(rot[l-1],rot[r],1,num,k)]),putchar('\n'); } return 0; }
拓展
对于带修改的主席树,一般用 树状数组套主席树。
练习
可持久化并查集
前置知识
如果只学过并查集的路径压缩,那么上边的文章对你来说很重要。
因为可持久化并查集并不能路径压缩,它需要按秩合并。
思路
回忆一下,并查集我们需要维护什么?
联通块的根 fa
,联通树的树高 dep
。
而本质上,这两个都是数组,而且只涉及单点修改,单点查询。
那么就可以用可持久化线段树(可持久化数组)维护。
代码对比
这是一个普通的按秩合并并查集:
const int inf=1e4+7; int n,m; int fa[inf],dep[inf]; int find(int x) { if(fa[x]==x)return x; return find(fa[x]); } void merge(int x,int y) { x=find(x),y=find(y); if(x==y)return; if(dep[x]==dep[y]) fa[x]=y,dep[y]++; else { if(dep[x]>dep[y])swap(x,y); fa[x]=y; } } int main() { n=re();m=re(); for(int i=1;i<=n;i++) fa[i]=i; for(int i=1;i<=m;i++) { int op=re(),u=re(),v=re(); if(op==1)merge(u,v); else { int r1=find(u),r2=find(v); puts((r1^r2)?"N":"Y"); } } return 0; }
将其可持久化就是这样:
const int inf=2e5+7; int n,m; struct Seg_Tree{ int lc,rc,val; }T[inf<<6]; int fat[inf],dep[inf],cnt; void build(int &i,int l,int r) { i=++cnt; if(l==r) { T[i].val=l; return; } int mid=(l+r)>>1; build(T[i].lc,l,mid); build(T[i].rc,mid+1,r); } void change(int &i,int last,int l,int r,int pos,int k) { i=++cnt;T[i]=T[last]; if(l==r) { T[i].val=k; return; } int mid=(l+r)>>1; if(pos<=mid)change(T[i].lc,T[last].lc,l,mid,pos,k); else change(T[i].rc,T[last].rc,mid+1,r,pos,k); } int ask(int i,int l,int r,int pos) { if(l==r)return T[i].val; int mid=(l+r)>>1; if(pos<=mid)return ask(T[i].lc,l,mid,pos); else return ask(T[i].rc,mid+1,r,pos); } int find(int i,int x) { int fax=ask(fat[i],1,n,x); if(fax==x)return x; return find(i,fax); } void merge(int i,int x,int y) { x=find(i-1,x),y=find(i-1,y); if(x==y) { fat[i]=fat[i-1],dep[i]=dep[i-1]; return; } int depx=ask(dep[i-1],1,n,x),depy=ask(dep[i-1],1,n,y); if(depx==depy) { change(fat[i],fat[i-1],1,n,x,y); change(dep[i],dep[i-1],1,n,y,depy+1); } else { if(depx>depy)swap(x,y); change(fat[i],fat[i-1],1,n,x,y); dep[i]=dep[i-1]; } } int main() { n=re();m=re(); build(fat[0],1,n); for(int i=1;i<=m;i++) { int op=re(); if(op==1) { int x=re(),y=re(); merge(i,x,y); } if(op==2) { int k=re(); fat[i]=fat[k],dep[i]=dep[k]; } if(op==3) { int x=re(),y=re(); int fax=find(i-1,x),fay=find(i-1,y); wr(fax==fay),putchar('\n'); fat[i]=fat[i-1],dep[i]=dep[i-1]; } } return 0; }
值得注意的是,在合并和查询的时候,都应该以上一个版本为准(因为当前版本什么都没有)。
STL 中的可持久化
STL 真是 C 党福利!!!
rope,是 STL 扩展提供的一种可持久化平衡树。
注意啊,是 STL 扩展,和 pb_ds 一个爹。
头文件 #include<ext/rope>
,名字空间 __gnu_cxx
。
变量定义 rope<int> *h[1000007];
。
可持久化 h[i]=new rope<int>(*h[j]);
,时间复杂度 。
函数调用:
h[i]->push_back(val)
在h[i]
的末尾加入val
。h[i]->at(k)
访问第k
个元素。h[i]->replace(pos,val)
将位置为pos
的元素换成val
。h[i]->size()
返回h[i]
的元素个数。h[i]->insert(pos,val)
在pos
位置插入val
。h[i]->erase(pos,k)
从pos
位置向后删除k
个元素。h[i]->substr(pos,k)
从pos
位置开始提取k
个元素。h[i]->copy(pos,k,q)
将从pos
位置向后x
个元素拷贝到q
中。
和 vector 差不多。
如果维护文艺平衡树(即区间翻转),就维护一正一反两个 rope,区间翻转就把两个 rope 的对应区间 swap 一下就行了。
但是!!
STL 的通病:常数巨大。
并且,rope 空间复杂度极高!!
实现 P3919:
#include<cstdio> #include<ext/rope> using namespace __gnu_cxx; int re() { int s=0,f=1;char ch=getchar(); while(ch>'9'||ch<'0') { if(ch=='-')f=-1; ch=getchar(); } while(ch>='0'&&ch<='9') s=s*10+ch-48,ch=getchar(); return s*f; } void wr(int s) { if(s<0)putchar('-'),s=-s; if(s>9)wr(s/10); putchar(s%10+48); } const int inf=1e6+7; int n,m; rope<int>*a[inf]; int main() { n=re();m=re();a[0]=new rope<int>(0); for(int i=1;i<=n;i++) a[0]->push_back(re()); for(int i=1;i<=m;i++) { int var=re(); a[i]=new rope<int>(*a[var]); int op=re(),x=re(); if(op==1)a[i]->replace(x,re()); else wr(a[i]->at(x)),putchar('\n'); } return 0; }
时间空间双重爆炸。
不过这个代码量是真的友好,不会可持久化的可以用来骗分。
代码长度:691B | 用时:6.51s | 内存:1.00GB
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具