可持久化线段树入门(单点更新模板)

一、可持久化线段树的用处

  显然从名字上可以看出来这是一种建立在线段树上的小升级。

 

从一个小问题可以引入:

  当我们使用线段树处理一堆询问时,我们在一棵树上进行更新(如单点更新的操作),如果某一时刻突然让你把手头上的操作停下来,并且询问在你没有进行上一步操作时,树的具体数据。最容易想到的做法就是我退一步回去,把某一单点的值减掉,然后再把树拎出来给大家看。

那么如果再让你还原2步之前的树的样子,3步之前、4步之前……甚至k步之前树的模样又该怎么办呢。其实也很容易办到,就是在一开始操作的时候每一步我都复制一遍原来的树,然后再在新的树上做更新,那么无论询问多少步之前,都可以迅速找到需要的树。

问题就在于每一步我都复制了一遍之前的树,第一时间上不允许,第二在空间上需要的容量已经爆炸掉了。

可持久化线段树就是把这多棵线段树融为了一个,解决了时间与空间上的难题。

 

二、可持久化线段树是什么

总是习惯把一个数据结构画出来,这样才能理解。

首先一个正常的线段树长这样,

我们发现,我们在单点的进行修改时,只会顺着一条枝干向下进行更新,而另外一条枝上的数据我们是不会去修改的。比如我们要修改[ 6 , 10 ]范围上的某点,那么他的另外一侧[ 1 , 5 ]实际上是不会被用到的。回顾之前的问题,如果我们要建立多棵线段树,但是每一个新的树上却只有很少量的信息是和旧树不同的,那么如果我们不把这些旧的信息用起来岂不是很浪费?

所以大大们就想到了(%%%),利用旧树的信息来重建新的树

 

比如现在我需要修改[ 6 , 10 ]范围上的某点,那么我们建立一个新的节点,并且把这个新节点的左儿子接在之前树上的 [ 1 , 5 ] 一侧,欸,我们发现这样的话不就已经完成了左边所有的子树的建立吗,因为1,5一侧不需要更新呀,所以我们不准备给他开新的节点了,直接接上旧的还能用。剩下的只需要操作右边就好啦。

 

保持这种接上旧的还能用的想法,做个几层(假设我们现在需要更新10这个点),我们发现只多出来了几个新节点,大致如下图:

 

仅用一点点的空间就建立了一棵新的树,也就是说,我们现在只需要知道每棵新的树的头节点就可以把新的树拎起来啦。

所以现在的问题就是该如何定义每个节点的编号,显然此时之前定义普通线段树节点的方式(左边*2,右边*2+1)已经不适用了。但是本来这种记录方式就是为了我们写起来方便(指针退散),所以解决起来也很简单,对于每个节点而言,我只需要强行记录下左右节点的编号,那么每个节点的编号无论怎么乱都是被他的父节点记录着的,所以无所谓,随便搞,但是为了方便,采用的是编号由1……n逐渐增大。

 

 

其实想法是很容易理解的,只要想通了,就能发现可持久化的这种思想还能利用在许多不同的数据结构上。

 

三、丑陋的模板

具体的用法隐藏在注释里。

#define LL long long int


const int maxn=100005;
int cnt;
int rt[maxn];

struct Node
{
	int ls, rs, l, r;
	LL sum;
}tr[maxn*33];


void PushUp(int i)
{
	tr[i].sum = tr[tr[i].ls].sum + tr[tr[i].rs].sum;
}

int Build(int l,int r)
{
	int pos = ++cnt;
	if (l == r)
	{
		tr[pos].sum = 0;
		return pos;
	}
	int mid = (l + r) >> 1;
	tr[pos].ls = Build(l,mid);
	tr[pos].rs = Build(++mid,r);
	return pos;
}
//第零号版本的线段树

LL Query(int ed,int l,int r,int ql,int qr)
{                           //ed是 版本号
	if (ql<=l && r<=qr) return tr[ed].sum;
	int mid = (l + r) >> 1;
	if (qr <= mid) return Query(tr[ed].ls,l,mid,ql,qr);
	else if(ql>mid)return Query(tr[ed].rs,mid+1,r,ql,qr);
	else
    {
        return Query(tr[ed].ls,l,mid,ql,mid)+Query(tr[ed].rs,mid+1,r,mid+1,qr);
    }
}
//查询第ed个版本的线段树[ql,qr]上的和

int update(int ed,int l,int r,int p,LL k)
{
	int pos = ++cnt;
    tr[pos].sum=tr[ed].sum;
	if (l == r)
	{
		tr[pos].sum += k;
		return pos;
	}
	tr[pos].ls = tr[ed].ls;
	tr[pos].rs = tr[ed].rs;

	int mid = (l + r) >> 1;
	if (p <= mid) tr[pos].ls = update(tr[ed].ls,l,mid,p,k);
	else tr[pos].rs = update(tr[ed].rs,++mid,r,p,k);
	PushUp(pos);
	return pos;
}
//单点更新p点的值加上k
//返回值为新的版本树的根节点




int main()
{
	rt[0]=Build(1,maxn);//建树的范围可随题意变化
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
    {
        int p;
        LL w;
        scanf("%d %lld",&p,&w);
        rt[i]=update(rt[i-1],1,maxn,p,w);//注意!更新新的版本需要记录新的根
    }
    int Q;
    scanf("%d",&Q);
    while(Q--)
    {
        int b;
        int l,r;
        scanf("%d %d %d",&b,&l,&r);
        printf("%lld\n",Query(rt[b],1,maxn,l,r));//查询操作
		//查询第b个版本的线段树上的信息
    }
}

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值