线段树区间查改(懒标记+代码细节)

  就如同我上次写链式前向星一样,这次我又一次在模拟赛中打算混点分。经过我缜密的思考基于暴力的猜测,我认为带懒操作的线段树至少可以混70分!(大雾弥漫)。于是我兴冲冲的开始敲代码,然后……线段树就打挂了……比赛结束后我痛定思痛,决定要好好复习一下线段树,然后经过我一下午的折腾,我终于……陷在bug的泥沼中出不来了……总之,我一直折腾到了晚上,才把代码完善(至少能跑了),那么今天就来讲讲关于线段树的懒操作的一些东西吧

-----------------------------------------------分割线---------------------------------------------------------

一,懒操作

  关于线段树最基础的内容,我在去年还是前年抑或是大前年的时候就有提及(线段树初步:建树、单点查改、区间查询)这里就不作详细介绍了

  在之前那篇文章里面我提到了线段树的单点查改,而假如我们用同样的方法进行区间修改,不用计算就知道这个复杂度简直难以想象,但是我们在用这种笨拙的方式进行区间修改时可以发现一些有趣的规律

  举个例子,a[15]={0,1,2,3,4,5,6,7,8,9,10};我们要将a[3]~a[6]集体加上5,考虑一个一个进行单点修改,修改路径依次如下(从左往右为单点修改a[3]~a[6])

  然后我们就发现了这样一个有趣的事,在修改a[4],a[5]的时候,他们的路径大致相同,只是在第5个节点处分叉分别去处理4和5。

  这让我们不禁想到一个优化,我们能不能在区间查改递归到第五个节点时给这个节点上个标记,表示该节点范围内的所有元素都要被修改,然后等到我们需要查询第五个节点的子节点的时候,再把这个标记下放,这样就可以省去修改时对节点10 11的递归了

  没错!这个优化就叫懒操作,只要暂时用不到就不去处理,真的很懒()

  懒操作是线段树的精髓,没有懒操作的线段树只是个花架子,有了懒操作之后,线段树就有了高效的区间查询修改的能力

  那么我们该如何实现懒操作呢

  重点就在于懒标记

二、懒标记

    先来看看以前说过的单点查改线段树的是怎么存的

1 const int mm=100005;
2 struct tree{
3     int l,r;  //左边界和右边界(left,right)
4     int sum;  //存的是l,r这个区间的和
5 }a[mm*4];

  现在我们给每个节点加上一个lazy标签,表示这个点的懒标记,lazy=1就表示该节点范围内的所有元素都应增加1,lazy=2就表示该节点范围内的所有元素都应增加2,以此类推。如果说lazy=0,那么就说明该节点没有被打上懒标记

  每个节点如下

struct node{
	long long l,r,lazy,sum;
}tree[mm*4];

  然后我们来考虑这样一个事情,就是什么时候应该打懒标记,什么时候应当下放懒标记

  让我们回到修改a[4]a[5]的这张图

  显然按照我们刚刚的思路,第五个节点是应当被打上懒标签的,也就是说如果该节点的范围在区间修改的范围以内,就可以给这个节点打上懒标签回溯了

  那假如现在要修改a[5]应当怎么做呢,显然第五个节点上的懒标记已经无法胜任这个任务了,那就将懒标记下放,所谓下放懒标记就是让左子节点和右子节点的lazy标签加上该节点的lazy标签,然后将该节点的lazy标签归零

  代码实现如下

void down(int num)
{
	tree[num*2].lazy+=tree[num].lazy;
	tree[num*2].sum+=(tree[num*2].r-tree[num*2].l+1)*tree[num].lazy;
	tree[num*2+1].lazy+=tree[num].lazy;
	tree[num*2+1].sum+=(tree[num*2+1].r-tree[num*2+1].l+1)*tree[num].lazy;
	tree[num].lazy=0;
}

  关于懒标记的内容已经介绍完毕了,现在应当将它放入线段树了

三、区间查询修改

  洛谷上的线段树模板题

  这个板块主要考虑一下关于实现的代码细节

  建树部分不用多讲,和不带懒操作的没什么不同

  区间修改需要注意如果某个节点的范围完全在修改范围内,给这个节点打上标记,sum加上(r-l+1)*增加的数值,即这个节点的sum增加的总值

  区间修改回溯的时候每个节点的sum仍然是两个子节点sum之和,代码实现如下

void upd(int l,int r,int ad,int num)//左边界 右边界 增加值 该节点编号 
{
	if(tree[num].l>r||tree[num].r<l) return ;//如果该节点范围与修改范围完全没有交集就没必要向下递归了 
	if(tree[num].l>=l&&tree[num].r<=r)//如果节点的范围在修改范围内,打上懒标签,修改sum值,回溯 
	{
		tree[num].lazy+=ad;
		tree[num].sum+=(tree[num].r-tree[num].l+1)*ad;
		return ;
	}
	if(tree[num].lazy>0) down(num);//如果该节点有懒标签,下放标签 
	upd(l,r,ad,num*2);//递归左子树 
	upd(l,r,ad,num*2+1);//递归右子树 
	tree[num].sum=tree[num*2].sum+tree[num*2+1].sum;//更新sum 
}

  查询的方法也类似,只不过不需要修改节点元素,代码实现如下

long long sech(int l,int r,int num)//左边界 有边界 该节点编号 
{
	if(tree[num].l>=l&&tree[num].r<=r) //跟不带懒操作的线段树一样 
	{
		return tree[num].sum; 
	}
	if(tree[num].l>r||tree[num].r<l) 
	{
		return 0;
	}
	if(tree[num].lazy>0) down(num);//下发标签 
	return sech(l,r,num*2)+sech(l,r,num*2+1);//查询左右子树 
}

  (题目好像也只要求做区间查改)

  完整代码如下

#include<bits/stdc++.h>
using namespace std;
const int mm=1000005;
int n,m,a[mm];
struct node{
	long long l,r,lazy,sum;
}tree[mm*4];
void build(int l,int r,int num)//建树 
{
	tree[num].l=l;
	tree[num].r=r;
	if(l==r)
	{
		tree[num].sum=a[l];
		return ;
	}
	int mid=(l+r)/2;
	build(l,mid,num*2);
	build(mid+1,r,num*2+1);
	tree[num].sum=tree[num*2].sum+tree[num*2+1].sum;
	return ;
}
void down(int num)//下放标签 
{
	tree[num*2].lazy+=tree[num].lazy;//将lazy下放到左子树 
	tree[num*2].sum+=(tree[num*2].r-tree[num*2].l+1)*tree[num].lazy;//更新左子树的sum 
	tree[num*2+1].lazy+=tree[num].lazy;//下放到右子树 
	tree[num*2+1].sum+=(tree[num*2+1].r-tree[num*2+1].l+1)*tree[num].lazy;//更新右子树的sum 
	tree[num].lazy=0;//下放玩后该节点便不再有懒标记 
}
void upd(int l,int r,int ad,int num)//左边界 有边界 增加值 该节点编号 
{
	if(tree[num].l>r||tree[num].r<l) return ;//如果该节点范围与修改范围完全没有交集就没必要向下递归了 
	if(tree[num].l>=l&&tree[num].r<=r)//如果节点的范围在修改范围内,打上懒标签,修改sum值,回溯 
	{
		tree[num].lazy+=ad;
		tree[num].sum+=(tree[num].r-tree[num].l+1)*ad;
		return ;
	}
	if(tree[num].lazy>0) down(num);//如果该节点有懒标签,下放标签 
	upd(l,r,ad,num*2);//递归左子树 
	upd(l,r,ad,num*2+1);//递归右子树 
	tree[num].sum=tree[num*2].sum+tree[num*2+1].sum;//更新sum 
}
long long sech(int l,int r,int num)//左边界 有边界 该节点编号 
{
	if(tree[num].l>=l&&tree[num].r<=r) //跟不带懒操作的线段树一样 
	{
		return tree[num].sum; 
	}
	if(tree[num].l>r||tree[num].r<l) 
	{
		return 0;
	}
	if(tree[num].lazy>0) down(num);//下发标签 
	return sech(l,r,num*2)+sech(l,r,num*2+1);//查询左右子树 
}
int main()
{
	//freopen("in.txt","r",stdin);
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	build(1,n,1);
	for(int i=1;i<=m;i++)
	{
		int in;
		cin>>in;
		if(in==1)
		{
			int x,y,k;
			cin>>x>>y>>k;
			upd(x,y,k,1);//区间修改 
		}
		else if(in==2)
		{
			int x,y;
			cin>>x>>y;
			cout<<sech(x,y,1)<<endl;//区间查询 
		}
	}
	return 0;
}

  以上就是关于带懒操作线段树区间查改的内容了。如果你觉得我讲得不错,就请点赞关注;如果我有什么疏漏,请在评论区留言,我会尽量即使改正/bx

 

posted @ 2023-07-05 14:51  球君  阅读(268)  评论(0编辑  收藏  举报
View Code