可持久化线段树学习笔记
可持久化,即对数据修改后仍可查询到其历史版本。
以模板题为例:
单点修改、查询的可持久化。
暴力时空复杂度:O(nm(版本复制)+m(修改查询)),可持久化线段树 时空复杂度只为:O(m log n+n)
题解口胡:
建一个数组hed存各版本的对应的线段树根
对于修改操作:对修改了的部分进行新建,没有修改的部分共用。因修改而新建的部分不只有叶子节点,还应有其到根节点的路径,否则无法维护线段树的结构。
对于查询操作:使hed[当前版本数]=hed[查询版本数],查询就完事了。(当时还建个新根,连原版本的左右儿子。这都是多此一举)
数组大小(本题单点修改):4*n(一棵线段树(因为用了动态开点,实际一棵线段树的空间占用要更少))+m (ceil(log n) +1)(长度为n的区间的线段树最大深度为ceil(log n)+1)
踩过的坑:
调用修改或查询函数里表区间的l,r形参要为1,n,结果直接写成l和r了,没注意意义。
查询函数里面的递归原树上的点要跟着走
模板题AC代码:
#include<iostream> #include<cstdio> using namespace std; const int N=1e6+6; int n,m,hed[N],num[N*24],ls[N*24],rs[N*24],cnt; inline int read() { int x=0; bool f=0; char ch=getchar(); while(!isdigit(ch)) f|=ch=='-',ch=getchar(); while(isdigit(ch)) x=(x<<3)+(x<<1)+(ch^48),ch=getchar(); return f?-x:x; } void build(int t,int l,int r) { if(l==r) { num[t]=read(); return; } ls[t]=++cnt; build(cnt,l,(l+r)>>1); rs[t]=++cnt; build(cnt,((l+r)>>1)+1,r); } void modify(int u,int t,int l,int r,int w,int v) { if(l==r) { num[u]=v; return; } if(w<=(l+r)>>1) { rs[u]=rs[t]; ls[u]=++cnt; modify(cnt,ls[t],l,(l+r)>>1,w,v); } else { ls[u]=ls[t]; rs[u]=++cnt; modify(cnt,rs[t],((l+r)>>1)+1,r,w,v); } } int fin(int t,int l,int r,int w) { if(l==r) return num[t]; if(w<=(l+r)>>1) return fin(ls[t],l,(l+r)>>1,w); else return fin(rs[t],((l+r)>>1)+1,r,w); } int main() { n=read(),m=read(); cnt=1; build(1,1,n); hed[0]=1; int t,ord,w,v; for(int i=1;i<=m;++i) { t=read(),ord=read(); if(ord==1) { w=read(),v=read(); hed[i]=++cnt; modify(cnt,hed[t],1,n,w,v); } else { w=read(); hed[i]=hed[t]; printf("%d\n",fin(hed[t],1,n,w)); } } return 0; }
理解时可以以需求导向(暴力算法又慢又占空间)理解
核心:共用内存,尽可能少开点(只新建修改的节点)。核心也会适用于一些可持久化线段树以后的变式。
一个操作处理完成后,以后这个操作对应的那棵线段树都不会再变了。(方便深入理解,但不可固化思维)
注意点:序列长度为n,操作数为m,单点修改情况下可持久化线段树空间应开:4*n+m (ceil(log n) +1)
应用:1、对数组可持久化简单维护:
单点修改,简单区间修改(配合标记永久化,单次操作 新建节点数小于 4* (ceil(log n) +1)、时间复杂度O(logn))
2、配合主席树