因为近期进行了有关 主席树 的专题训练,为了巩固对 “主席树” 的了解,同时方便后期复习,在这里整理了一下有关 “主席树” 的一些要点。
————————————————————
首先,我们来讲讲
什么是 “主席树” ?
主席树 ,又名 “可持久化线段树” ,顾名思义,其本质就是一棵 “进化版” 的线段树,且进化了 “可持久化” 这个功能。
那么,什么是 “可持久化” ?
百度百科
上的解释是 “持久化是将程序数据在持久状态和瞬时状态间转换的机制” 。
换句话说,传统意义上的线段树在修改后就无法复原,只能记录当前状态,不够“持久”,而可持久化线段树在修改后仍然可以访问以前的状态,很“持久”。
可它为什么又叫 “主席树” ?
这是一个比较有意思的问题,有一种说法是:据说发明主席树的人的姓名简写为hjt,与当时的国家主席的姓名简写一样,故叫做“主席树”。
————————————————————
了解了主席树,我们再了解一下
主席树如何 “持久” ?
传统的线段树只能维护一维,并在这一维上进行修改、询问。
就像这样:
而主席树在线段树的基础上多了一维,相当于同时存了多个线段树,就像这样:
或者有点像这样:
但这仍然是暴力开出来的多个线段树,在实际应用中很容易MLE,可真正的主席树是一个实用且好用的算法,所以显然不是这样。
我们注意到,每次更新线段树时,只会更新一个点或者一个区间,也就是说,大部分节点是没有改变的。
只要我们把这些不变的点压成一个点,无论是时间还是内存上都会大大优化。
换句话说,在更新时,只用将修改的节点重开即可,其他节点保留。
所以真正的主席树长得有点像这样:
看不习惯?换一个:
也就是说,主席树通过建点不建树这种折中的办法,不仅节约了时间和空间,还实现了“持久化”。
————————————————————
到这里为止,已经对主席树有了大致的了解。那么
如何实现主席树?
以
模板题
为例。
事先说明,在本题中,主席树起到了前缀和的作用,主席树节点以数值(区间)为下标进行二分,主席树节点权值记录该数值(区间)出现的数量的前缀和。
先解释一下主席树最关键的几个变量:
- rt,即root,每棵主席树的根节点。
- ls,即left son,记录每个节点的左儿子
- rs,即right son,记录每个节点的右儿子
- t,即tree,记录每个节点的权值
- tot,即total,统计节点的总个数,用于建新点
主席树的关键在于建点,所以,建点的部分是必不可少的。建新点时要注意将上一个点的状态继承过来,这样的主席树才是完整的:
void nw(int &p){
++tot;
t[tot]=t[p];
ls[tot]=ls[p];
rs[tot]=rs[p];
p=tot;
}
更新时,大体上与传统的线段树相似,都是从\([l,r]\)区间转移到\([l,mid]\)或\([mid+,r]\)区间。
不同的在于,传统线段树转移时,节点由\(p\)变为\(p<<1\)或\(p<<1|1\),而主席树因为建了许多新点,编号不是吧连续的,所以转移时要变为\(ls[p]\)或\(rs[p]\)。
不要忘记建新点,这才是主席树的精髓!
void change(int l,int r,int x,int &p){
nw(p);++t[p];
if(l==r)return;
int mid=l+r>>1;
if(x<=mid)change(l,mid,x,ls[p]);
else change(mid+1,r,x,rs[p]);
}
询问时,大体上也和传统线段树相似,在本题中,因为询问的区间第k小,所以需要记录\(L,R\)两个节点,通过做差求得区间某数个数,再与k比较判断向左还是向右转移:
注意:询问时不能建新点!
int query(int l,int r,int k,int L,int R){
if(l==r)return l;
int mid=l+r>>1;
int s=t[ls[R]]-t[ls[L]];
if(k<=s)return query(l,mid,k,ls[L],ls[R]);
return query(mid+1,r,k-s,rs[L],rs[R]);
}
综上,完整的主席树成型了:
struct HJT{
int tot,ls[MM],rs[MM],t[MM];
void nw(int &p){
++tot;
t[tot]=t[p];
ls[tot]=ls[p];
rs[tot]=rs[p];
p=tot;
}
void change(int l,int r,int x,int &p){
nw(p);++t[p];
if(l==r)return;
int mid=l+r>>1;
if(x<=mid)change(l,mid,x,ls[p]);
else change(mid+1,r,x,rs[p]);
}
int query(int l,int r,int k,int L,int R){
if(l==r)return l;
int mid=l+r>>1;
int s=t[ls[R]]-t[ls[L]];
if(k<=s)return query(l,mid,k,ls[L],ls[R]);
return query(mid+1,r,k-s,rs[L],rs[R]);
}
}T;
再放入题目中,写得以下代码:
#include<bits/stdc++.h>
#define lb lower_bound
using namespace std;
const int M=2e5+5,MM=M*20;
int n,m,a[M],b[M],cnt,rt[M];
struct HJT{
int tot,ls[MM],rs[MM],t[MM];
void nw(int &p){
++tot;
t[tot]=t[p];
ls[tot]=ls[p];
rs[tot]=rs[p];
p=tot;
}
void change(int l,int r,int x,int &p){
nw(p);++t[p];
if(l==r)return;
int mid=l+r>>1;
if(x<=mid)change(l,mid,x,ls[p]);
else change(mid+1,r,x,rs[p]);
}
int query(int l,int r,int k,int L,int R){
if(l==r)return l;
int mid=l+r>>1;
int s=t[ls[R]]-t[ls[L]];
if(k<=s)return query(l,mid,k,ls[L],ls[R]);
return query(mid+1,r,k-s,rs[L],rs[R]);
}
}T;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i){scanf("%d",&a[i]);b[i]=a[i];}
sort(b+1,b+n+1);cnt=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;++i){
a[i]=lb(b+1,b+cnt+1,a[i])-b;
rt[i]=rt[i-1];
T.change(1,cnt,a[i],rt[i]);
}
for(int l,r,k;m;--m){
scanf("%d%d%d",&l,&r,&k);
printf("%d\n",b[T.query(1,cnt,k,rt[l-1],rt[r])]);
}
return 0;
}
————————————————————
到这里为止,已经掌握了主席树最基本也是最关键的部分。
当然,主席树的应用还有很多,它不仅可以随着数组变化,记录前缀和,还可以随着时间变化,并随时回溯(据说Word、Dev等应用中的返回功能与主席树的原理类似)。
\(\mathcal{By}\quad\mathcal{Most}\ \mathcal{Handsome}\)