浅谈主席树(可持久化线段树)
主席树(可持久化线段树)
前言:
建议先掌握线段树再来学习此知识点
这几天在中山纪中训练时有做到可持久化字典树的题,听别人说可持久化数据结构的思路都是差不多的,于是我便先上网学了学我看起来认为最简单的可持久化线段树(主席树),学了两个晚上终于学会了一点皮毛 (~ ̄▽ ̄)~,于是我便来总结一下理理思路
博客可能写的一般大佬勿喷……
名字分析:
顾名思义,可持久化的就是可以记录历史版本的意思,通过主席树,我们可以查询到单个节点历史的信息(或者也可以做到静态区间查询),或者对于某个历史版本进行单点修改产生新的版本。(如果想进行其它更难的操作可以使用树套树,可是我太弱了暂时还不会)
至于主席树这个名字嘛,据说时发明这个东西的人的姓名缩写是HJT,跟当时国家主席的姓名缩写一样,于是便有了这个说法……
引导:
从这个名字看来,知道主席树跟线段树肯定是有联系的,那么我们来思考一下这个问题:如果只用普通的线段树怎么记录历史版本?
最直截了当的方法就是每次操作之后再重新建一棵线段树,数据小,题目空间限制大还可以做;如果数据和空间稍微恶心那么一点点,那么恭喜你,你将同时享受到MLE(空间超限)与TLE(时间超限)。
这里我画个图来示意一下线段树单点修改(查询)的过程(区间同理)
假设有一个数列长度为5,当前要修改(查询)第2个数,如下图,红色线代表到的区间:
我们可以发现,其实每次线段树改变的就只有一条链,这很重要,主席树的思想就差不多出来了
对于一次新的修改,我们只需要再次构造一条新的表示这一段区间的链,之后另外节点与前面的共用就行了,我再画一个图示意一下主席树的思想(还是以上面的为例,如下图:)
为了好看一点我把一些新节点的左右儿子反过来画了(不过还是好丑),大家凑合着看一看吧
所以主席树每一次进行操作,时间和增加的空间都是\(log\)的(树套树的话好像是2个\(log\)的,我也不太清楚,毕竟我还没开始学)
例题:
主席树模板题(因为当前我在gmoj找不到主席树的模板题,于是就用了luogu的)
题意大概是讲一开始给你一个长度为n初始数列,为版本0,之后有m个操作,对于每个操作有两种情况:
1. 改变某一个版本的某一个数,形成一个新的版本
2. 对某一个版本进行单点查询并输出,再生成一个完全一样的版本
(也就是说每次的版本号就是操作的号数)
讲解&代码
既然是模板题,我们就直接上代码和讲解吧
首先明白一个事实,因为主席树是要一些节点共用儿子节点的(也就是说一个节点可能有很多父亲),所以很显然是不能用\(now<<1\)或\(now<<1|1\)(也就是堆式结构)来表示节点now的儿子的,而是应该使用动态开点
变量:
struct arr
{
int lson;//左儿子
int rson;//右儿子
int num;//节点值
}tree[40000005];//线段树节点
int n,m,tot,a[1000005]//原始数组;
int root[1000005];//每个版本的线段树根的编号
建树:
对于建树操作,跟线段树很类似,只不过是动态开点
void build(int &now,int l,int r)//这里的now是加了取址符的,所以这个传进去的变量也会随之改变
{
now=++tot;//动态开点
if (l==r) tree[now].num=a[l];//叶子节点赋值
else
{
int mid=l+r>>1;
build(tree[now].lson,l,mid);
build(tree[now].rson,mid+1,r);//注意不能用now<<1和now<<1|1(也就是堆式结构)
}
}
修改
对于修改操作,我们只是修改一条链,所以只用查找要修改的点进行新建节点,每次递归下来一个区间,就复制一份原线段树中当前节点的值,其它操作就跟线段树差不多一样了
void change(int last,int &now,int l,int r,int k,int x)//last表示上一个版本的线段树
{
now=++tot;
tree[now]=tree[last];//复制一份上一个版本的对应节点
if (l==r) tree[now].num=x;
else
{
int mid=l+r>>1;
if (k<=mid) change(tree[last].lson,tree[now].lson,l,mid,k,x);
else change(tree[last].rson,tree[now].rson,mid+1,r,k,x);//判断要修改的点是在左区间还是右区间
}
}
查询
这基本就跟线段树一模一样了
int find(int now,int l,int r,int k)
{
if (l==r) return tree[now].num;
else
{
int mid=l+r>>1;
if (k<=mid) return find(tree[now].lson,l,mid,k);
else return find(tree[now].rson,mid+1,r,k);
}
}
总代码
#include<bits/stdc++.h>
using namespace std;
struct arr
{
int lson;//左儿子
int rson;//右儿子
int num;//节点值
}tree[40000005];//线段树节点
int n,m,tot,a[1000005]//原始数组;
int root[1000005];//每个版本的线段树根的编号
void build(int &now,int l,int r)//这里的now是加了取址符的,所以这个传进去的变量也会随之改变
{
now=++tot;//动态开点
if (l==r) tree[now].num=a[l];//叶子节点赋值
else
{
int mid=l+r>>1;
build(tree[now].lson,l,mid);
build(tree[now].rson,mid+1,r);//注意不能用now<<1和now<<1|1(也就是堆式结构)
}
}
void change(int last,int &now,int l,int r,int k,int x)//last表示上一个版本的线段树
{
now=++tot;
tree[now]=tree[last];//复制一份上一个版本的对应节点
if (l==r) tree[now].num=x;
else
{
int mid=l+r>>1;
if (k<=mid) change(tree[last].lson,tree[now].lson,l,mid,k,x);
else change(tree[last].rson,tree[now].rson,mid+1,r,k,x);//判断要修改的点是在左区间还是右区间
}
}
int find(int now,int l,int r,int k)
{
if (l==r) return tree[now].num;
else
{
int mid=l+r>>1;
if (k<=mid) return find(tree[now].lson,l,mid,k);
else return find(tree[now].rson,mid+1,r,k);
}
}
int main()
{
scanf("%d%d",&n,&m);
int i;
for (i=1;i<=n;i++)
scanf("%d",&a[i]);
build(root[0],1,n);//建树存到版本0中
for (i=1;i<=m;i++)
{
int v,cz,loc;
scanf("%d%d%d",&v,&cz,&loc);
if (cz==1)
{
int value;
scanf("%d",&value);
change(root[v],root[i],1,n,loc,value);//修改操作
}
else
{
printf("%d\n",find(root[v],1,n,loc));//查询操作
root[i]=root[v];//生成一个一模一样的新版本(题目要求)
}
}
return 0;
}
结语:
由于本人还没有深入去学主席树(可持久化线段树),所以一些比较难的操作我还不是很会(主要是没有尝试码过) (~ ̄▽ ̄)~,不过我这次写得很用心,如果有问题望各位大佬指出,或者如果大家有问题也可以评论下来,如果是我懂的我会尽力帮助大家解决,谢谢!