可持久线段树学习总结

可持久化线段树

预备知识:线段树


看了许多博客,思路有点乱,写一篇博客总结。
首先来一道例题:
【题面】
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大在离散化之后的位置,然后还原即可。
对于单点修改的操作还是与上面大体是一样的,在已有的主席树的基础上连上我们目前不会修改的点,新增我们修改到的点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值