可持久化线段树
预备知识:线段树
看了许多博客,思路有点乱,写一篇博客总结。
首先来一道例题:
【题面】:
Q k l r 查询数列在第k个版本时,区间[l, r]上的最大值
M k p v 把数列在第k个版本时的第p个数修改为v,并产生一个新的数列版本
最开始会给你一个数列,作为第1个版本。
每次M操作会导致产生一个新的版本。
【输入格式】:
第一行两个整数N, Q。N是数列的长度,Q表示询问数
第二行N个整数,是这个数列
之后Q行,每行以0或者1开头,0表示查询操作Q,1表示修改操作M,格式为
0 k l r 查询数列在第k个版本时,区间[l, r]上的最大值 或者
1 k p v 把数列在第k个版本时的第p个数修改为v,并产生一个新的数列版本
【输出格式】:对于每个询问,输出正确答案
【样例数据】:
input
4 5
1 2 3 4
0 1 1 4
1 1 3 5
0 2 1 3
0 2 4 4
0 1 2 4
output
4
5
4
4
【样例解释】:
序列版本1: 1 2 3 4
查询版本1的[1, 4]最大值为4
修改产生版本2: 1 2 5 4
查询版本2的[1, 3]最大值为5
查询版本1的[4, 4]最大值为4
查询版本1的[2, 4]最大值为4
【数据规模】:N <= 10000 Q <= 100000 对于每次询问操作的版本号k保证合法, 区间[l, r]一定满足1 <= l <= r <= N
时间限制:3s
空间限制:256MB
思路总结
如果修改少的话,产生的新版本线段树不多,我们可以直接建树。
但修改一旦多起来,如果每次都记录一个新的数列,空间和时间上都是令人无法承受的。所以我们需要可持久化数据结构。
那什么是是可持久化,如何对线段树进行可持久化?
在较低的时空复杂度下,支持查询统一数据结构的多个版本。
可持久化原理:仔细想想,每次进行修改只会对根到该节点的路径进行修改,说明最多只会有logn个节点受到本次修改的影响,其他节点我们便可以直接进行复制。
对于一棵普通的线段树来说,我们把一个节点的左儿子设成他的左区间边界,右儿子设成右区间边界,然后可以用当前节点2或者2+1来表示左右儿子所代表的节点编号。但是这个对应关系不是必须的,在存储当前区间的信息外,我们可以在每一个节点上额外储存指向左右儿子的指针。
每次产生一棵新的线段树,我们必然会新产生一个根节点。那么对于每一次的单点修改,左右子树一定有一边是不会受到影响的,那么这半边就没有必要重新构建,直接连接到已知线段树的基础上,我们只需要递归地处理出不同的半边即可,递归到叶子节点结束。
详细解释代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1000233;
int cnt,n,a[N],m,root[N];//cnt表示所有线段树图构成一个大图的节点编号(想象为一个图)
int tot=0;//每一个图的根的编号
struct ST
{
int l,r,dat; //此时的l和r表示左右儿子而不是区间端点
#define l(x) tree[x].l
#define r(x) tree[x].r
#define dat(x) tree[x].dat
}tree[N*20];
int build(int l,int r)//起始的线段树
{
int p=++cnt;
if(l==r){dat(p)=a[l];return p;}
int mid=(l+r)>>1;
l(p)=build(l,mid);//更新左儿子 ,得到左儿子的编号
r(p)=build(mid+1,r);//更新右儿子 , 得到右儿子的编号
dat(p)=max(dat(r(p)),dat(l(p)));//由下而上更新
return p;
}
int change(int now,int l,int r,int x,int v)//在now阶段图 大区间 修改位置 修改权值
{
int p=++cnt; tree[p]=tree[now];//先保留全部信息
if(l==r) {dat(p)=v;return p;} //到了叶子节点也就是我们需要修改的位置
int mid=(l+r)>>1;
if(x<=mid) l(p)=change(l(now),l,mid,x,v);//在左子树
if(x>mid) r(p)=change(r(now),mid+1,r,x,v);//在右子树
dat(p)=max(dat(r(p)),dat(l(p)));
return p;
}
int ask(int p,int l,int r,int ql,int qr)//当前节点 大区间 询问区间(这个就和普通线段树差不多,不过要额外传递一个大区间的参量)
{
if(ql<=l&&qr>=r) return dat(p);
int val=-1e5;
int mid=(l+r)>>1;
if(ql<=mid) val=max(val,ask(l(p),l,mid,ql,qr));
if(qr>mid) val=max(val,ask(r(p),mid+1,r,ql,qr));
return val;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
root[++tot]=build(1,n);//建立原图
while(m--)
{
int k,opt,l,r;
scanf("%d%d%d%d",&opt,&k,&l,&r);
if(opt==0)
printf("%d\n",ask(root[k],1,n,l,r)); //在新根节点为root[k]的图中进行查询
else
root[++tot]=change(root[k],1,n,l,r); //在新根节点为root[k]的图的基础上开始修改并构造新的线段树(修改logn个节点,保存大部分的原图)
}
}
可持续化权值线段树–主席树
主席树是家喻户晓的van意儿,和一般的区间线段树不一样,他的节点上存储的是权值在某一个范围的数的数量,我们常用离散化来节省空间并且维护每一个数的相对大小不变。
值得一提的是,主席树满足类似前缀和的性质,在一个区间上,显然若一个主席树为u,记录的是区间[1,r]的信息,一个主席树为v,记录的是区间[1,v]的信息,则很明显若一个节点区间表示[l1,r1],那么两个主席树该区间的信息相减便可以得到[l,r]区间上有关权值在[l1,r1]上的信息。根据这一点,对于一些区间查询第k大,树上路经查询第k大,我们便可以通过如此的性质得到答案。
对于查询第k大的问题,因为我们的节点存储的是在该节点表示的权值区间中的数的数量,那么若左儿子的数的数量lnum>k,则说明第k大在左儿子所表示的区间中,不然就在右儿子中且排行为k-lnum,然后对于每一个儿子我们有可以地去进行处理,最终得到一个叶子节点,该点的区间只表示一个权值,这就是我们最终要的第k大在离散化之后的位置,然后还原即可。
对于单点修改的操作还是与上面大体是一样的,在已有的主席树的基础上连上我们目前不会修改的点,新增我们修改到的点。