洛谷P3373 [ 模板] 线段树 (乘法和加法)

本文详细介绍了一种线段树的高级实现方式,包括区间加法、乘法操作及标记下放过程。通过实例说明了为何标记下放时需要遵循先乘后加的原则,并提供了完整的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

       andy的小伙伴acer(WA_哈_哈)已经写好【模板】线段树1啦,但是仅仅支持区间加法和查询,这对

于oier们当然是远远不够的,所以本蒟蒻在此奉上线段树的区间加法,乘法的实现,以及对乘法标记的下放,

查询的实现;

        解释下标记下放要先乘后加的原因:(我们约定 lazy1 为加法标记,lazy2 为乘法标记)

            设有区间 [x,y] 现执行操作:将该区间内所有值 加 n;

   [x,y] -> [ax+n....ay+n];在线段树里则表示为 该区间父节点的lazy1标记 +n,即为:[x,y] -> [ax..ay](lazy1 + n);

            如果此时 再进行操作 将该区间内 所有值 乘 m;

   [x,y] -> [ax*m....ay*m];在线段树里则表示为 该区间父节点的lazy2标记*m,即为:[x,y]->[ax..ay](lazy2*m);

   但是 lazy1 标记呢?

            我们来看,原区间经过2次操作后,应变为-> [(ax+n)*m...a(ay+n)*m] -> [ax*m+m*n...ay*m+m*n];

            由此可见,加法标记(lazy1)变为了m*n;

            那为什么要先乘后加呢,现在看起来先加再乘也没什么问题;

            在线段树实现里,我们每进行一次操作,实际上等于对该 若干区间的父节点上(以及父区间)运

            算,所以 lazy1,lazy2是分开运算的,在下放标记时,会先对lazy1*lazy2,再向下下放,对子区间而言,

            会先得 到乘法标记,再得到加法标记,所以先乘后加。 

            其实,先加后乘也不是不可以,但是子区间节点也需要继承父亲的 lazy1,lazy2,先乘后加就不用对子区

            间的lazy1,再乘 lazy2;同时要记得下放标记后,父节点的标记重新置为 lazy1=0,lazy2=1;

        以及在此附上acer的链接(他的是结构体实现),看各位喜好啦:

        http://blog.youkuaiyun.com/acerandaker/article/details/79371464

#include<bits/stdc++.h>
using namespace std;
#define M 100050
int m,n,p,xx,yy,zz,s;//本代码%p是因为题目需求,直接去掉就好,1ll* 是强转long long,add是加法标记,mul是乘法标记;
int tree[M<<2],mul[M<<2],add[M<<2];//tree :树的加和|mul乘法懒标记|add加法懒标记
void build(int pos,int l,int r){
	mul[pos]=1;
	if(l==r){//叶子节点 
	scanf("%d",&tree[pos]);
	return ;//Online 建树 
	}
	int mid=(l+r)>>1,lson=pos<<1,rson=pos<<1|1;//等价于 mid=(l+r)/2,lson=pos/2,rson=pos/2+1;
	build(lson,l,mid),build(rson,mid+1,r);//递归建树
	tree[pos]=(1ll*tree[lson]+tree[rson])%p;//更新父节点
} 
void pushdown(int pos,int l,int r){//下放标记,无论乘法还是加法都遵循 先乘后加 的原则
	int mid=(l+r)>>1,lson=pos<<1,rson=pos<<1|1;//等价于 mid=(l+r)/2,lson=pos/2,rson=pos/2+1;
	tree[lson]=1ll*tree[lson]*mul[pos]%p,tree[rson]=1ll*tree[rson]*mul[pos]%p;
	mul[lson]=1ll*mul[lson]*mul[pos]%p,mul[rson]=1ll*mul[rson]*mul[pos]%p;
	add[lson]=1ll*add[lson]*mul[pos]%p,add[rson]=1ll*add[rson]*mul[pos]%p;
	tree[lson]=(tree[lson]+1ll*add[pos]*(mid-l+1))%p,tree[rson]=(tree[rson]+1ll*add[pos]*(r-mid))%p;
	add[lson]=(add[pos]+1ll*add[lson])%p,add[rson]=(add[pos]+1ll*add[rson])%p;
	mul[pos]=1,add[pos]=0;//一定是mul=1;不然在对下一次做乘法标记时会出问题;
}
int query(int pos,int l,int r,int L,int R){//查询区间和运算,
	if(L<=l&&r<=R){return tree[pos];}//如果完全包含,则取查找的区间的值
	pushdown(pos,l,r);//这就是我们打标记的目的,因为操作时直接运算,复杂度会爆掉
	int mid=(l+r)>>1,lson=pos<<1,rson=pos<<1|1;
	if(L>mid){return query(rson,mid+1,r,L,R)%p; }
	if(mid>=R){return query(lson,l,mid,L,R)%p;}
	else return (1ll*query(lson,l,mid,L,R)+query(rson,mid+1,r,L,R))%p; //左右递归查询,罗列各小区间,拼凑目标区间
}
void update(int pos,int l,int r,int c,int L,int R){//加法运算
	if(L<=l&&r<=R){
		tree[pos]=(tree[pos]+1ll*c*(r-l+1))%p;
		add[pos]=(add[pos]+1ll*c)%p;
		return;
	}
	pushdown (pos,l,r); 
	int mid=(l+r)>>1,lson=pos<<1,rson=pos<<1|1;
	if(mid<L)update(rson,mid+1,r,c,L,R);//完全超出左边,往右动动 
	else if(mid>=R)update(lson,l,mid,c,L,R);//完全超出右边,往左动动 
	else update(lson,l,mid,c,L,R),update(rson,mid+1,r,c,L,R);//不然,在区间内很大一部分但是不完全(有可能)覆盖 
	tree[pos]=(1ll*tree[lson]+tree[rson])%p; // pushup 一下 
}
void update2(int pos,int l,int r,int x,int L,int R){//乘法运算
	if(L<=l&&r<=R){
		tree[pos]=1ll*tree[pos]*x%p;
		mul[pos]=1ll*mul[pos]*x%p;
		add[pos]=1ll*add[pos]*x%p;//一起改掉加法标记
		return; 
	}
	pushdown(pos,l,r); 
	int mid=(l+r)>>1,lson=pos<<1,rson=pos<<1|1; 
	if(mid<L)update2(rson,mid+1,r,x,L,R);
	else if(mid>=R) update2(lson,l,mid,x,L,R);
	else update2(rson,mid+1,r,x,L,R),update2(lson,l,mid,x,L,R);
	tree[pos]=(1ll*tree[rson]+tree[lson])%p; 
} 
int main(void){
	scanf("%d%d%d",&n,&m,&p),build(1,1,n);
	while(m--){
	scanf("%d%d%d",&s,&xx,&yy);
	if(s==3)printf("%d\n",query(1,1,n,xx,yy));
	else scanf("%d",&zz),s==1?update2(1,1,n,zz,xx,yy):update(1,1,n,zz,xx,yy);
	}
}
题目原型:洛谷P3373,【模板】线段树2,日后更新其他的图论,数据结构等内容,就酱 :)




   


### 线段树实现区间加法乘法操作 #### 结构定义 为了支持区间的加法乘法操作,线段树中的每个节点除了存储左右边界`l`, `r`以及当前区间的总`sum`外,还需要额外维护两个懒惰标签`add`用于记录待处理的加法操作,`mul`用于记录待处理的乘法操作。 ```cpp struct Node { int l, r; long long sum; long long mul; // 乘法懒标记 long long add; // 加法懒标记 } tree[N << 2]; ``` #### 延迟更新函数 当访问某个节点时,如果该节点存在未处理的操作,则将其应用于子节点上,并清除自身的延迟标志位。对于乘法来说,在传递给子节点之前要确保已有的增值也被相应扩大;而对于加法则只需简单地累即可[^2]。 ```cpp void pushdown(int p) { if (tree[p].mul != 1 || tree[p].add != 0) { int mid = (tree[p].l + tree[p].r) >> 1; // 更新左孩子 tree[p<<1].sum *= tree[p].mul; tree[p<<1].sum += (mid-tree[p].l+1)*tree[p].add; tree[p<<1].mul *= tree[p].mul; tree[p<<1].add *= tree[p].mul; tree[p<<1].add += tree[p].add; // 更新右孩子 tree[(p<<1)|1].sum *= tree[p].mul; tree[(p<<1)|1].sum += (tree[p].r-mid)*tree[p].add; tree[(p<<1)|1].mul *= tree[p].mul; tree[(p<<1)|1].add *= tree[p].mul; tree[(p<<1)|1].add += tree[p].add; // 清除父节点的懒标记 tree[p].mul = 1; tree[p].add = 0; } } ``` #### 查询与修改 无论是查询还是修改都需要遵循自顶向下遍历的原则,即每次到达一个新的内部结点都要调用一次`pushdown()`方法以保证数据的一致性。具体到本题中: - **单点/区间求**:通过递归方式计算目标区间的累积值。 - **区间增**:将增量附至对应区间的根节点并设置其`add`属性。 - **区间相**:同样作用于指定区间的根节点但这次是调整它的`mul`属性[^4]。 ```cpp // 区间加法 void update_add(int L, int R, long long val, int p=1){ if(L<=tree[p].l && tree[p].r<=R){ tree[p].sum+=(tree[p].r-tree[p].l+1)*val; tree[p].add+=val; return ; } pushdown(p); int m=(tree[p].l+tree[p].r)>>1; if(L<=m)update_add(L,R,val,p*2); if(R>m)update_add(L,R,val,p*2+1); tree[p].sum=tree[p*2].sum+tree[p*2+1].sum; } // 区间乘法 void update_mul(int L,int R,long long val,int p=1){ if(L<=tree[p].l&&tree[p].r<=R){ tree[p].sum*=val; tree[p].mul*=val; tree[p].add*=val; return ; } pushdown(p); int m=(tree[p].l+tree[p].r)>>1; if(L<=m)update_mul(L,R,val,p*2); if(R>m)update_mul(L,R,val,p*2+1); tree[p].sum=tree[p*2].sum+tree[p*2+1].sum; } ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值