可持久化线段树详解(基础)
可持久化线段树,又名主席树(它的发明者叫黄嘉泰,拼音首字母为hjt,与当时主席的拼音首字母相同)。
主席树简介
什么是可持久化
为什么说可持久化线段树可持久,是因为它不同于普通的线段树,普通的线段树在修改改过后就不能保存原有的数据,而主席树可以保存以往的数据,也就是说,你可以实时回溯查找各个版本的线段树中的内容,而他是基于线段树的。
怎么可持久化
最简单的方法就是每修改一次新建一个线段树(妥妥MLE)
所以说我们需要更省空间的方法,我们先列举一个比较简单的线段树
括号表示数值,未被括起来的部分表示区间,我们注意到,如果修改第二个数,只有一条链会受到影响(2,1-2,1-4)也就是说,我们只要把这一段链存储下来就可以实现回溯(把2位置上的1改成2)
如此便实现了主席树最基本的操作
怎么写主席树
例题:可持久化线段树1
题目大意:你需要维护这样的一个长度为 N 的数组,支持如下两种操作
- 在某个历史版本上修改某一个位置上的值
- 访问某个历史版本上的某一位置的值
也就是我们需要建立一个主席树,并且做到单点修改和单点查询,不过这道题对线段树本身要求很低
注意要点
主席树和普通的线段树有很大的区别:
- 由于并不知道需要建多少个点,需要动态开点,也就是用一个点开一个点。
- 建树的过程中我们并不需要实际进行什么操作
- 一个儿子可能有多个父亲,但一个父亲只有两个儿子
- 树会有多个根
- 儿子的下标需要保存而不会再是简单的乘 \(2(+1)\)
- 函数要开 \(int\)
存树
需要 \(struct\) 保存左右儿子和值
struct{
int l,r,v;
}t[N*50];
建树
与普通线段树建树相似,只不过儿子的下标需要保存( \(num\) 目前已经开点的个数),下标没有规律。
int build(int k,int ul,int ur){
num++;
k=num;//开点
if(ul==ur){
t[k].v=a[ul];
return k;
}
int mid=(ul+ur)>>1;
t[k].l=build(t[k].l,ul,mid);
t[k].r=build(t[k].l,mid+1,ur);
return k;
}
单点更改
对于每一个更改,相当于新增一条链,所以说仍然需要开点,剩下的与线段树区别不大
int update(int k,int ul,int ur,int p,int change){
num++;
t[num]=t[k];
k=num;//开点
if(ul==ur){
t[k].v=change;
}
else{
int mid=(ul+ur)>>1;
if(p<=mid)t[k].l=update(t[k].l,ul,mid,p,change);
else t[k].r=update(t[k].r,mid+1,ur,p,change);
}
return k;
}
单点查询
与线段树单点查询相同
int query(int k,int ul,int ur,int p){
if(ul==ur){
return t[k].v;
}
int mid=(ul+ur)>>1;
if(p<=mid) return query(t[k].l,ul,mid,p);
else return query(t[k].r,mid+1,ur,p);
}
版本保存
主席树可以回溯到各个版本的线段树,而我们如何将一个版本的线段树保存下来呢。
我们可以注意到,无论修改哪一个的点,有一个点一定会改变————根
所以说我们需要把每一个线段树根的坐标保存下来,这样子,版本回溯就相当于回溯到相应版本的根
主函数
\(root\) 即为保存版本的根的下标的数组
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
root[0]=build(0,1,n);
for(int i=1;i<=m;i++){
int vl,type,w;
scanf("%d%d%d",&vl,&type,&w);
if(type==1){
int into;
scanf("%d",&into);
root[i]=update(root[vl],1,n,w,into);
}
else{
printf("%d\n",query(root[vl],1,n,w));
root[i]=root[vl];
}
}
return 0;
}
完整代码
#include<stdio.h>
#include<algorithm>
#define N 1000005
using namespace std;
struct{
int l,r,v;
}t[N*50];
int a[N],root[N];
int num=0;
int build(int k,int ul,int ur){
num++;
k=num;
if(ul==ur){
t[k].v=a[ul];
return k;
}
int mid=(ul+ur)>>1;
t[k].l=build(t[k].l,ul,mid);
t[k].r=build(t[k].l,mid+1,ur);
return k;
}
int update(int k,int ul,int ur,int p,int change){
num++;
t[num]=t[k];
k=num;
if(ul==ur){
t[k].v=change;
}
else{
int mid=(ul+ur)>>1;
if(p<=mid)t[k].l=update(t[k].l,ul,mid,p,change);
else t[k].r=update(t[k].r,mid+1,ur,p,change);
}
return k;
}
int query(int k,int ul,int ur,int p){
if(ul==ur){
return t[k].v;
}
int mid=(ul+ur)>>1;
if(p<=mid) return query(t[k].l,ul,mid,p);
else return query(t[k].r,mid+1,ur,p);
}
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
root[0]=build(0,1,n);
for(int i=1;i<=m;i++){
int vl,type,w;
scanf("%d%d%d",&vl,&type,&w);
if(type==1){
int into;
scanf("%d",&into);
root[i]=update(root[vl],1,n,w,into);
}
else{
printf("%d\n",query(root[vl],1,n,w));
root[i]=root[vl];
}
}
return 0;
}
以上便是最基础的可持续线段树
我们知道可以利用权值线段树求解第K大或第K小值,那我们可否可以使用可持续的权值线段树求解区间K大值呢?
可行!
可持续的权值线段树
前置知识(如果您已熟知,可以跳过)
权值线段树
权值线段树是维护值域的线段树,维护的是值在某个区间内数的个数。
比如说 2,1,4,3的数组
我们就可以有这样一个权值线段树:
也就是说,我们可以知道在这个数组中每一个数出现的次数,单点修改复杂度为 \(O(logn)\) 。
而它更厉害的一个用处就是——求取第K小的数。
在访问根的时候,由于树由左至右具有单调递增性,我们只需要看它的左儿子的值是否比K大即可,如果比K大,则说明K小数在做儿子所维护的区间内,反之则在右儿子所维护的区间内。
但是很明显,我们并不能用它来求解区间K小值,因为它只维护值域,所以说我们不能一一对应原数组的区间,所以说我们需要主席树的辅助来求取区间K小值
离散化
离散化讲解
因为权值线段树维护值域,而对于这样一个数列:5,1,\(1 \times 10^{10}\),5 很明显,我们无法开出如此巨大的数组,是不是用不了权值线段树呢?
我们想到了一个技巧——哈希
也就是说,我们可以用一个较小的数表示较大的数,这样数组就可以开下了,而很明显,我们必须需要保持这几个数的大小关系不变
于是我们就产生了离散化的想法
也就是说我们先将这个序列按照大小顺序排列即 1,5,5,\(1 \times 10^{10}\) ,此时我们在分别将他们按大小顺序替换掉(由于5等于5,所以说两者必须用大小相同的数代替)即 1,2,2,3 这个数列只代表它的大小关系,并不代表真实数值,而我们此时还需要一个哈希数组来存储对应值。
代码实现
介绍两个 \(STL\) 函数
- sort(开头地址,结尾地址)--排序
- unique(开头地址,结尾地址)--去重
- lower_bound(开头地址,结尾地址,值)--二分求解第一个大于等于“值”的数的地址
当然 \(sort\) 和 \(unique\) 也可以用 \(set\) 实现。
代码如下:
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
b[i]=a[i];
}
sort(b+1,b+n+1);
int cnt=unique(b+1,b+1+n)-b-1;
for(int i=1;i<=n;i++){
a[i]=lower_bound(b+1,b+1+cnt,a[i])-b;
}
解释:
- b数组存放原值,a数组存放离散化后的值
- 由于 \(unique\) 函数的特殊性,我们需要先排序
- 由于 \(lower_bound\) 返回的是地址,而我们需要的是它在数组里的排位,于是需要 "-b"
可持久化权值线段树的实现
如上所述,它可以解决求取区间第K小的问题
如题:可持久化线段树2
思路
我们知道,之所以权值线段树不可以实现求解区间第K小的问题是因为他无法保存原数组各个数的排位,而我们是否可以利用可持久化解决这个问题呢?这时候我们会发现因为有了可持久化,权值线段树多了一个东西———版本。
也就是说,我们可以用版本来维护原数组的各个数的顺序,于是乎,我们一个接一个的把原数组里的数按照数组中下标排位插入到空树中,形成了一个又一个版本的权值线段树,这样子,我们就可以用版本来维护原数组中各个数的顺序了。
接下来我们需要考虑怎么求解区间K小值,权值线段树可以求解值域范围内所有数的K小值———那么我们可不可以把这个区间变成值域呢?
可以,只要把其他的数都删除了就可以了。
这个想法看似很疯狂且不切实际,可是我们是有版本的,所以说可以实现。假设我们要求解区间[l,r]中的K小值,那么我们就选取插入l-1的版本和插入r的版本,将它们相减(插入r的版本-插入l-1的版本),此时所有在l之前的数对权值线段树造成的影响全部消失了,只剩下了[l,r]的值域,也就是说,这时,我们就把这个权值线段树的值域变成了[l,r]的值域。
代码实现
建树
首先,我们需要建立一个空树(大部分主席树都是这样的)。
int build(int l,int r){
countf++;
int num=countf;
if(l==r){
return num;
}
t[num].l=build(l,mid(l,r));
t[num].r=build(mid(l,r)+1,r);
return num;
}
更新数值
然后再一一插入数值,建立一个又一个版本的权值线段树:
int update(int k,int l,int r,int x){
int num=++countf;
t[num]=t[k];
t[num].v++;
if(l==r){
return num;
}
if(x<=mid(l,r)) t[num].l=update(t[k].l,l,mid(l,r),x);
else t[num].r=update(t[k].r,mid(l,r)+1,r,x);
return num;
}
主函数部分:
for(int i=1;i<=n;i++){
a[i]=lower_bound(b+1,b+1+cnt,a[i])-b;//离散化
root[i]=update(root[i-1],1,cnt,a[i]);
}
查询
很显然,我们需要保存两个版本,然后需要将数值相减,就可以得到在某一值域内数值的个数(权值线段树求解)
int query(int v1,int v2,int l,int r,int x){
if(l==r) return l;
int sum=t[t[v2].l].v-t[t[v1].l].v;
if(sum>=x) return query(t[v1].l,t[v2].l,l,mid(l,r),x);
else return query(t[v1].r,t[v2].r,mid(l,r)+1,r,x-sum);
}
完整code(包含离散化)
#include<stdio.h>
#include<algorithm>
#define N 200005
using namespace std;
struct {
int l,r,v;
}t[N*20];
int a[N],root[N],b[N];
int countf;
int mid(int l,int r){
return (l+r)>>1;
}
int build(int l,int r){
countf++;
int num=countf;
if(l==r){
return num;
}
t[num].l=build(l,mid(l,r));
t[num].r=build(mid(l,r)+1,r);
return num;
}
int update(int k,int l,int r,int x){
int num=++countf;
t[num]=t[k];
t[num].v++;
if(l==r){
return num;
}
if(x<=mid(l,r)) t[num].l=update(t[k].l,l,mid(l,r),x);
else t[num].r=update(t[k].r,mid(l,r)+1,r,x);
return num;
}
int query(int v1,int v2,int l,int r,int x){
if(l==r) return l;
int sum=t[t[v2].l].v-t[t[v1].l].v;
if(sum>=x) return query(t[v1].l,t[v2].l,l,mid(l,r),x);
else return query(t[v1].r,t[v2].r,mid(l,r)+1,r,x-sum);
}
int main(){
int n,m;
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);
int cnt=unique(b+1,b+1+n)-b-1;
root[0]=build(1,cnt);
for(int i=1;i<=n;i++){
a[i]=lower_bound(b+1,b+1+cnt,a[i])-b;
root[i]=update(root[i-1],1,cnt,a[i]);
}
for(int i=1;i<=m;i++){
int l,r,x;
scanf("%d%d%d",&l,&r,&x);
printf("%d\n",b[query(root[l-1],root[r],1,cnt,x)]);
}
return 0;
}
这里就是基础的主席树介绍的结尾,谢谢大家的阅读,请在评论区批评指正。