可持久化线段树学习笔记
可持久化线段树就是可以询问历史版本状态的线段树。
我们考虑暴力怎样实现询问线段树的历史状态,我们每次在对线段树进行操作的时候,每次都把位修改前的版本存一下,然后要找历史版本的时候直接找,这样时间复杂度和空间复杂都会爆炸。
我们可以发现,每次对线段树进行操作最多才会修改 (log_2(size)) 个节点,其中 (size) 为线段树的大小,所以我们每次把线段树全部复制一遍是不必要的,我们只需要把修改时经过的节点新建出来,然后将其他的节点连接了原来的线段树上就可以了。
(like this)
严格的来说这个东西不是一棵树,但是他如果从父亲节点往儿子节点走就是一棵树,所以对于任意一颗这样的树它还满足线段树的性质,所以我们不用纠结这个是不是一棵树。
然后每次修改都进行一下这样的操作,要查询历史版本的时候只需要找到历史版本的根节点,然后直接当成线段树来用就可以了。
所以我们可以发现,可持久化线段树的时间复杂度和线段树一样,建树是 (Theta (size)),单次查询是 (Theta (log_2size)) , (size) 维护的数据范围,空间复杂度是 (Theta (log_2n)) ,(n) 是操作次数。
inline void insert(int &rt,int l,int r,int x)
{
t[++cnt]=t[rt];
rt=cnt;
if (l==r)
{
t[rt].sum++;
return ;
}
int mid=(l+r)>>1;
if (x<=mid) insert(t[rt].ls,l,mid,x);
else insert(t[rt].rs,mid+1,r,x);
push_up(rt);
return ;
}
与普通线段树不同的地方就是对 (rt) 加了取地址,表示对传入的值的原值进行操作,还有下面的这两句
t[++cnt]=t[rt]; //将原节点的信息复制给新建的节点的信息,不要丢失另一个不修改儿子的信息
rt=cnt; //把树根转化为新建节点,再加上取地址,这样对于父亲节点来说,它的修改的儿子就是新建的节点
这道题目咋一看和可持久化线段树没有任何关系,但是这确实是可持久化线段树的日常操作。
我们考虑假设给你 (l-1) 和 (r) 这个状态下的权值线段树,这题肯定就很好做了,只需要两颗线段树减一下就可以了,然后我们发现这就是维护一颗可持久化线段树,然后在查询历史信息。
#include<bits/stdc++.h>
const int N=2e5+100;
using std::sort;
int n,m,bsize;
int a[N],b[N],rt[N];
struct tree{
int cnt;
struct Tree{
int sum,ls,rs;
}t[N*20];
inline void clear() {cnt=0;return ;}
inline void push_up(int rt)
{
t[rt].sum=t[t[rt].ls].sum+t[t[rt].rs].sum;
return ;
}
inline void insert(int &rt,int l,int r,int x)
{
t[++cnt]=t[rt];
rt=cnt;
if (l==r)
{
t[rt].sum++;
return ;
}
int mid=(l+r)>>1;
if (x<=mid) insert(t[rt].ls,l,mid,x);
else insert(t[rt].rs,mid+1,r,x);
push_up(rt);
return ;
}
inline int query(int rt1,int rt2,int l,int r,int k)
{
if (l==r) return l;
int sz=t[t[rt2].ls].sum-t[t[rt1].ls].sum,mid=(l+r)>>1;
if (k<=sz) return query(t[rt1].ls,t[rt2].ls,l,mid,k);
else return query(t[rt1].rs,t[rt2].rs,mid+1,r,k-sz);
}
}t;
inline int midfind(int x)
{
int l=1,r=bsize;
while (l<=r)
{
int mid=(l+r)>>1;
if (b[mid]==x) return mid;
if (b[mid]<x) l=mid+1;
else r=mid-1;
}
return 0;
}
inline int read()
{
int ans=0,p=1;
char ch=getchar();
while (ch<'0'||ch>'9') {if (ch=='-') p=-1;ch=getchar();}
while (ch>='0'&&ch<='9') {ans=ans*10-'0'+ch;ch=getchar();}
return ans*p;
}
inline void Pre()
{
sort(b+1,b+n+1);
for (int i=1;i<=n;i++)
{
int k=i;
b[++bsize]=b[i];
while (b[k]==b[k+1]) k++;
i=k;
}
return ;
}
int main()
{
n=read();m=read();
for (int i=1;i<=n;i++)
a[i]=b[i]=read();
Pre();
t.clear();
for (int i=1;i<=n;i++)
{
int now=midfind(a[i]);
rt[i]=rt[i-1];
t.insert(rt[i],1,bsize,now);
}
while (m--)
{
int l,r,k;
l=read();r=read();k=read();
printf("%d
",b[t.query(rt[l-1],rt[r],1,bsize,k)]);
}
return 0;
}
这道题目才比较像可持久化应该做的东西,对历史状态进行修改,查询历史状态,不过需要注意的是每次查询都要新建一个历史状态。
#include<bits/stdc++.h>
const int N=1e6+100;
int n,m,No;
int a[N],root[N];
struct Tree{
int cnt;
struct tree{
int ls,rs,sum;
}t[N*20];
inline void clear() {cnt=1;return ;}
inline int query(int rt,int l,int r,int pos)
{
if (l==r) return t[rt].sum;
int mid=(l+r)>>1;
if (pos<=mid) return query(t[rt].ls,l,mid,pos);
else return query(t[rt].rs,mid+1,r,pos);
}
inline void update(int &rt,int l,int r,int pos,int k)
{
t[++cnt]=t[rt];
rt=cnt;
if (l==r)
{
t[rt].sum=k;
return ;
}
int mid=(l+r)>>1;
if (pos<=mid) update(t[rt].ls,l,mid,pos,k);
else update(t[rt].rs,mid+1,r,pos,k);
return ;
}
inline void build(int rt,int l,int r)
{
if (l==r)
{
t[rt].sum=a[l];
return ;
}
int mid=(l+r)>>1;
t[rt].ls=++cnt;
build(t[rt].ls,l,mid);
t[rt].rs=++cnt;
build(t[rt].rs,mid+1,r);
return ;
}
}T;
inline int read()
{
int ans=0,p=1;
char ch=getchar();
while (ch<'0'||ch>'9') {if (ch=='-') p=-1;ch=getchar();}
while (ch>='0'&&ch<='9') {ans=ans*10-'0'+ch;ch=getchar();}
return ans*p;
}
int main()
{
n=read();m=read();
for (int i=1;i<=n;i++)
a[i]=read();
T.clear();
T.build(1,1,n);
root[0]=1;
for (int i=1;i<=m;i++)
{
int vi,opt,pos,k;
scanf("%d%d",&vi,&opt);
if (opt==1)
{
scanf("%d%d",&pos,&k);
int nrt=root[vi];
T.update(nrt,1,n,pos,k);
root[++No]=nrt;
}
if (opt==2)
{
scanf("%d",&pos);
root[++No]=root[vi];
printf("%d
",T.query(root[vi],1,n,pos));
}
}
return 0;
}
这道题的第一个思路就是对于序列的从前到后建主席树,然后统计 ([l,r]) 每个数出现多少次,之后二分一下就可以了。
但是这种思路是错误的,因为有一些出现不止一次的数会对二分造成影响,如果强制为把出现多次的数赋值为 (1) 的话,这样做减法又不对。
其实正确的做法是,每次记录对于枚举到当前第 (i) 个数,记录一下前面每一个数最后出现的位置,包括自己。
这样每次查找的时候只需要在 (R) 这个位置建的主席树里找一个最小值小于 (L) 的最短前缀就可以了。
因为这个数在前面出现的位置小于 (L) ,所以肯定合法,因为是最小前缀,所以满足是最小的合法自然数。
如果当前前缀的最小值大于 (L) ,所以需要增长前缀长度,因为经过预处理,所以往后肯定有合法答案,如果小于 (L) 则尽量缩小前缀的长度。
#include<bits/stdc++.h>
using std::min;
using std::sort;
const int N=2e5+100;
int n,m,num,size;
int a[N],b[N<<1],root[N];
struct Tree{
int cnt;
struct tree{
int ls,rs,minn;
}t[N*20];
inline void clear() {cnt=0;return ;}
inline void push_up(int rt)
{
t[rt].minn=min(t[t[rt].ls].minn,t[t[rt].rs].minn);
return ;
}
inline void update(int &rt,int l,int r,int pos,int k)
{
t[++cnt]=t[rt];
rt=cnt;
if (l==r)
{
t[rt].minn=k;
return ;
}
int mid=(l+r)>>1;
if (pos<=mid) update(t[rt].ls,l,mid,pos,k);
else update(t[rt].rs,mid+1,r,pos,k);
push_up(rt);
return ;
}
inline int query(int rt,int l,int r,int nl,int nr)
{
if (l==nl&&r==nr) return t[rt].minn;
int mid=(l+r)>>1;
if (nr<=mid) return query(t[rt].ls,l,mid,nl,nr);
else if (nl>mid) return query(t[rt].rs,mid+1,r,nl,nr);
else return min(query(t[rt].ls,l,mid,nl,mid),query(t[rt].rs,mid+1,r,mid+1,nr));
}
}T;
inline int solve(int val,int rt)
{
int l=1,r=num,ans=num;
while (l<=r)
{
int mid=(l+r)>>1;
if (T.query(rt,1,num,1,mid)>=val) l=mid+1;
else
{
r=mid-1;
ans=mid;
}
}
return b[ans];
}
inline int midfind(int x)
{
int l=1,r=num;
while (l<=r)
{
int mid=(l+r)>>1;
if (b[mid]==x) return mid;
if (b[mid]<x) l=mid+1;
else r=mid-1;
}
return 0;
}
inline void Pre()
{
sort(b+1,b+size+1);
for (int i=1;i<=size;i++)
{
int k=i;
b[++num]=b[i];
while (b[k]==b[k+1]&&k+1<=size) k++;
i=k;
}
return ;
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++)
{
scanf("%d",a+i);
b[++size]=a[i];
b[++size]=a[i]+1;
}
Pre();
T.clear();
for (int i=1;i<=n;i++)
{
root[i]=root[i-1];
T.update(root[i],1,num,midfind(a[i]),i);
}
for (int i=1;i<=m;i++)
{
int l,r;
scanf("%d%d",&l,&r);
printf("%d
",solve(l,root[r]));
}
return 0;
}
然后就会发现套的二分完全没有必要,可以直接在线段树上进行操作。
只需要把二分答案的代码换成下面的代码就可以了。
inline int midquery(int rt,int l,int r,int L)
{
if (l==r) return l;
int mid=(l+r)>>1;
if (t[t[rt].ls].minn<L) return midquery(t[rt].ls,l,mid,L);
else return midquery(t[rt].rs,mid+1,r,L);
}