线段树,添加懒标记解决区间修改和区间查找的问题

写的很乱,可能只有自己看得懂ORZ

对于区间修改,单点查找问题以及单点修改,区间查找问题使用树状数组可以在O(logN)的时间复杂度上很好解决,但是对于区间修改,区间查找时,就不好解决了。因此使用到了一种叫做线段数的数据结构,类似于堆(结点的编号为n,则左孩子编号为2n,右孩子的编号为2n+1)。原数组为A[1~10]。线段树的结构如下图,图片来自洛谷
在这里插入图片描述根据上图可以看到是根节点表示的是t[1]表示A[1]+A[2]+…+A[10]
根节点的左节点表示t[2]=A[1]+…+A[mid] (mid = (1+10)/2)
根节点的右节点表示t[3]=A[mid+1]+…+A[10]
因此,当为了求区间(nl,nr)内的数字和时,可以从根结点开始查找。
区间累加的步骤如下:

如果当前结点包含的区间在(nl,nr)之间,则可以直接返回该结点的值。否则进行拆分,递归找他的左右孩子。可以很轻松的写出递归代码。

单点修改的步骤如下:

如果需要单点修改则只能从根节点一直遍历到该点对应的叶子结点,修改这条路径上的所有结点。

区间修改

但是如果需要区间修改则复杂度会不止O(logN)。
这时如果给树加上一个懒标记(tag数组,每个结点都有一个懒标记的值)就可以很好的解决区间修改问题。下面解释懒标记的作用。

  • 示例
  • 第一步:当我们需要给A[1~10] (nl=1,nr=10)增加2的时候,我们从根节点(结点编号1)开始遍历,根节点的范围被(nl,nr)包含了,这时直接c[1]增加2*10(因为c[1]表示的是A[1-10]的和),再令tag[1]加2,表示了该结点范围的数字都需要增加2。结束遍历。如下图。
    在这里插入图片描述
  • 第二步:当需要给A[1~6] (nl=1,nr=6)增加3的时候,因为nl,nr无法包含结点1的范围[1-10],这时就需要从结点1向结点2和3遍历。这时候进行的操作是,先把tag[1]的结点传递(乘以下一个结点元素的个数)给结点2(1-5)和结点3(6-10),再把tag[1]置为0。再进行上面描述的第一步判断。对于结点2,因为[1-6]包含了结点2的范围[1-5].因此,直接给结点2再增加,并且令tag[2]=3即可,用tag[2]先存着下面结点需要加的数。跳出对结点2的递归。在判断结点3的过程中,则需要一直遍历到最下面的6结点才能让其被(nl,nr)包含。在递归遍历两个孩子结点结束的时候,需要返回两个孩子结点的和增加到父结点上,即t[1] = t[2]+t[3] (此时tag[1]已经被清零了)进行回溯。
  • tag结点的含义:tag[1]=4 表示,该节点(1)包含范围的所有元素[1-10]都需要加4。

区间查找

解决了区间修改的问题后,区间查找的步骤只需要稍微修改。

在遍历到的结点不能被(nl,nr)包含时,即需要分割时,先把懒标记传递下去,再进行递归孩子即可。

例题1

P3372 【模板】线段树 1
AC代码如下

#include<cstdio>

#define ll long long
const int maxn = 300005;	//树的结点要至少多开一倍
//用一个存结点范围和值的结构体似乎也不错!
ll t[maxn];	//线段树 
ll tag[maxn];	//为了加快区间插入的的速度,引入懒标记

void f(int node,int left,int right,ll v){	
	//node区间的每个元素都加v
	tag[node]+=v;
	t[node]+=v*(right-left+1);
	return;
}

void push_down(int node,int left,int right){
	//把node的懒标记传递到子节点
	int mid = (left+right)>>1;
	f(node*2  ,left ,mid  ,tag[node]);
	f(node*2+1,mid+1,right,tag[node]);
	tag[node] = 0;
	return;
}

void update(int node,int left,int right,int nl,int nr,ll v){	//区间修改[nl~nr]都加v 
	//node:当前结点;left,right结点的取值范围;nl,nr原数组的范围
	if(nl<=left&&right<=nr){
		t[node] += v*(right-left+1);
		tag[node] += v;
		return;
	}
	push_down(node,left,right);	//把node结点的懒标记传递下去
	int mid = (left+right)>>1;
	if(nl<=mid) update(node*2,left,mid,nl,nr,v);
	if(mid+1<=nr) update(node*2+1,mid+1,right,nl,nr,v);
	t[node] = t[node*2]+t[node*2+1];//回溯 
	return;
}

ll getsum(int node,int left,int right,int nl,int nr){	//区间求和 
	if(nl<=left&&right<=nr)
		return t[node];	//该结点范围被需要求和的区间覆盖
    int mid=(left+right)>>1;
    push_down(node,left,right);	//把懒标记传递下去
    ll res=0;
    if(nl<=mid)
		res+=getsum(node*2,left,mid,nl,nr);
    if(mid+1<=nr) 
		res+=getsum(node*2+1,mid+1,right,nl,nr);
    return res;
}

int n,m;//数字个数,操作个数 
int main(){
	scanf("%d %d",&n,&m);
	int o,x,y;
	ll k;
	for(int i=1;i<=n;i++){
		scanf("%lld",&k);
		update(1,1,n,i,i,k);//nums[i]加上k 
	}
	for(int i=1;i<=m;i++){
		scanf("%d",&o);
		if(o==1){
			scanf("%d %d %lld",&x,&y,&k);//[x~y]内的加上k 
			update(1,1,n,x,y,k);
		}else{
			scanf("%d %d",&x,&y);
			printf("%lld\n",getsum(1,1,n,x,y));
		}
	}
	return 0;
}

例题2

P3373 【模板】线段树 2
在这里插入图片描述
观察题目,发现只需要设置两个懒标记表示乘法和加法即可。之后要规定一个优先级,规定方法如下:

①加法优先,即规定好segtree[root2].value=((segtree[root2].value+segtree[root].add)segtree[root].mul)%p,问题是这样的话非常不容易进行更新操作,假如改变一下add的数值,mul也要联动变成奇奇怪怪的分数小数损失精度,我们内心是很拒绝的;
②乘法优先,即规定好segtree[root
2].value=(segtree[root2].valuesegtree[root].mul+segtree[root].add*(本区间长度))%p,这样的话假如改变add的数值就只改变add,改变mul的时候把add也对应的乘一下就可以了,没有精度损失,看起来很不错。 —zhuwanman

因此选择乘法优先。按照线段树例题1的例子可以稍微修改即可。
AC代码。

#include<cstdio>

#define ll long long
const int maxn = 300005;	//树的结点要至少多开一倍
//用一个存结点范围和值的结构体似乎也不错!
ll t[maxn];	//线段树 
ll tag_mul[maxn]; //乘懒标记 
ll tag_add[maxn]; //加懒标记
int p;
//加法优先,即规定好segtree[root*2].value=((segtree[root*2].value+segtree[root].add)*segtree[root].mul)%p,
//问题是这样的话非常不容易进行更新操作,假如改变一下add的数值,mul也要联动变成奇奇怪怪的分数小数损失精度,我们内心是很拒绝的;
//乘法优先,即规定好segtree[root*2].value=(segtree[root*2].value*segtree[root].mul+segtree[root].add*(本区间长度))%p,
//这样的话假如改变add的数值就只改变add,改变mul的时候把add也对应的乘一下就可以了,没有精度损失,看起来很不错。
//https://www.luogu.com.cn/problemnew/solution/P3373根据分析选择先乘再加
 
void f(int node,int left,int right,ll v_mul,ll v_add){	//接受乘和加懒标记 
	//node区间的每个元素都先乘以用乘懒标记再用加懒标记 
	tag_add[node] = (tag_add[node] * v_mul+v_add)%p;
	tag_mul[node] = (tag_mul[node] * v_mul)%p;
	t[node] = (v_mul*t[node] +v_add*(right-left+1))%p;
	return;
}

void push_down(int node,int left,int right){
	//把node的懒标记传递到子节点
	int mid = (left+right)>>1;
	f(node*2  ,left ,mid  ,tag_mul[node],tag_add[node]);
	f(node*2+1,mid+1,right,tag_mul[node],tag_add[node]);
	tag_add[node] = 0;
	tag_mul[node] = 1;
	return;
}

void update_add(int node,int left,int right,int nl,int nr,ll v){	//区间修改[nl~nr]都加v 
	//node:当前结点;left,right结点的取值范围;nl,nr原数组的范围
	if(nl<=left&&right<=nr){
		t[node] =(t[node] + v*(right-left+1))%p;
		tag_add[node] += v;
		return;
	}
	push_down(node,left,right);	//把node结点的懒标记传递下去
	int mid = (left+right)>>1;
	if(nl<=mid) update_add(node*2,left,mid,nl,nr,v);
	if(mid+1<=nr) update_add(node*2+1,mid+1,right,nl,nr,v);
	t[node] = (t[node*2]+t[node*2+1])%p;//回溯 
	return;
}

void update_mul(int node,int left,int right,int nl,int nr,ll v){	//区间修改[nl~nr]都乘以v 
	//node:当前结点;left,right结点的取值范围;nl,nr原数组的范围
	if(nl<=left&&right<=nr){
		t[node] =(t[node]*v)%p;
		tag_add[node] *= v;
		tag_mul[node] *= v;
		return;
	}
	push_down(node,left,right);	//把node结点的懒标记传递下去
	int mid = (left+right)>>1;
	if(nl<=mid) update_mul(node*2,left,mid,nl,nr,v);
	if(mid+1<=nr) update_mul(node*2+1,mid+1,right,nl,nr,v);
	t[node] = (t[node*2]+t[node*2+1])%p;//回溯  
	return;
}


ll getsum(int node,int left,int right,int nl,int nr){	//区间求和 
	if(nl<=left&&right<=nr)
		return t[node];	//该结点范围被需要求和的区间覆盖
    int mid=(left+right)>>1;
    push_down(node,left,right);	//把懒标记传递下去
    ll res=0;
    if(nl<=mid)
		res+=getsum(node*2,left,mid,nl,nr);
    if(mid+1<=nr) 
		res+=getsum(node*2+1,mid+1,right,nl,nr);
    return res%p;
}

int n,m;//数字个数,操作个数 
int main(){
	for(int i=1;i<=maxn;i++)
		tag_mul[i] = 1;
	scanf("%d %d %d",&n,&m,&p);
	int o,x,y;
	ll k;
	for(int i=1;i<=n;i++){
		scanf("%lld",&k);
		update_add(1,1,n,i,i,k);//nums[i]加上k 
	}
	for(int i=1;i<=m;i++){
		scanf("%d",&o);
		if(o==1){
			scanf("%d %d %lld",&x,&y,&k);//[x~y]内的加上k 
			update_mul(1,1,n,x,y,k);
		}else if(o==2){
			scanf("%d %d %lld",&x,&y,&k);//[x~y]内的加上k 
			update_add(1,1,n,x,y,k);
		}else{
			scanf("%d %d",&x,&y);
			printf("%lld\n",getsum(1,1,n,x,y));
		}
	}
	return 0;
}
发布了22 篇原创文章 · 获赞 8 · 访问量 2232

猜你喜欢

转载自blog.csdn.net/weixin_44520881/article/details/104784568
今日推荐