终于发现在哪里启用\(Markdown\)和\(\LaTeX\)了…
什么是主席树
主席树的全名是可持久化线段树,从名字就可以看出来,它很持久
是一种可以回退到任意历史版本的神器!
如何实现主席树
主席树的功能看起来很美妙,那我们怎么实现呢?
既然要记录历史版本,那我们把历史版本都存下来不就完了?
呵,天真。MLE等着你
先丢一张图:
看起来很毒瘤对不对,其实我们只要一步步来拆分就好啦~
首先,这是一棵正常的线段树:
众所周知,线段树在修改或查询时要走过一条长度\(n\log n\)的路径
可以发现,只有这条路径上的数被修改了!
所以我们秉承着不铺张浪费的原则,只需要新建被修改的节点,然后其他节点用之前的
我们试着修改一下:
这样,我们的空间就小了很多,减少到了\(O((n+m)\log n)\)!
具体为什么是这么大自己想一想就好咯
那么,再修改一个试试?
代码实现
主席树有很多版本,所以我们需要用一个rt[N]
来记录每个历史版本的根
而且由于结构的特殊性导致主席树不能用<<1
或<<1|1
来存储子节点,我们必须使用结构体存储
struct Node {
int l,r,data;
}tr[N*20];//空间的原因刚才已经说过了
那么新建节点也是必须有的了(此处的新建节点需先复制之前的)
inline int Newnode(int x){
tr[++tot]=tr[x];
return tot;
}
建树操作和普通线段树一样:(ls
,rs
,nmid
是我习惯用的宏定义,明白意思就好)
int Build(int k,int l,int r){
k=++tot;
if(l==r){//到了
tr[k].data=a[l];
return tot;
}
ls=Build(ls,l,nmid);//建左子树
rs=Build(rs,nmid+1,r);//建右子树
return k;
}
修改操作要记得新建节点以便后续回退:
int Modify(int k,int l,int r,int pos,int num){
k=Newnode(k);
if(l==r){
tr[k].data=num;//到了
}else {
if(pos<=nmid)ls=Modify(ls,l,nmid,pos,num);//往左走
else rs=Modify(rs,nmid+1,r,pos,num);//or 往右走
}
return k;
}
查询操作也是大同小异:
int Query(int k,int l,int r,int pos){
if(l==r){
return tr[k].data;//到了
}else {
if(pos<=nmid)return Query(ls,l,nmid,pos);//往左走
else return Query(rs,nmid+1,r,pos);//or 往右走
}
}
最后查询就直接从对应版本的根上查询就好啦(别忘了询问操作也要复制一遍,此时只新建根就好了)
Code:(不带注释)
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
总结
这个算法还是很好背板子理解的,重点是明白新建节点操作的原因和好处
完。