线段树的题2033

pt.1 前言
这是一道线段树题

而且是一道线段树裸题

而且是一道线段树模板题!!!
链接 : p3373 线段树2

你会惊奇的发现 , 只有输入部分的一个数据的位置不一样 :

操作总数mm跑到数列a[i]a[i]后面去了而已 , 其他几乎完全一样

关于线段树 , 可以去OI Wiki康康 , 我觉得那里的图很好

pt.2 解题
不难看出 , 我们只要对这个区间进行区间修改(加 , 乘)和区间查询(求和)即可

2.1 建树
对于区间修改和查询 , 既然是loglog的操作 , 那么肯定是要二分的

我们知道 , 线段树将每个长度不为1的区间划分成左右两个区间递归求解 ,

把整个线段划分为一个树形结构 ,

通过合并左右两区间信息来求得该区间的信息 ,

这种数据结构可以方便的进行大部分的区间操作。

假设我们用一个数组sum[i]sum[i]存线段树

那么我们可以以1为根节点存储1到n的和(即sum[1]sum[1]代表1到n所有数的和)

然后再递归到其左子节点和右子节点缩小区间

(即sum[2]sum[2]存1到n/2的和 , $sum[3]存n/2+1到n的和 , 如此类推 , 直到递归到左右端点相等为止)

由此过程可得对于线段树上每一个节点 , 要么没有儿子 , 要么有2个儿子 ,

且这两个子节点编号分别为2i2∗i (下用ls(i)代替) 和 2i+12∗i+1 (下用rs(i)代替)

那么我们就可以容易的建立一棵线段树啦

void build(ll p,ll l,ll r){//当前建立节点p , 建树区间l到r
lll[p]=l,rrr[p]=r; //两个数组分别记录当前节点左右代表的区间
if(l==r){
sum[p]=a[l]%md; //左右端点相等 , 到达递归边界
return;
}
ll mid=(l+r)>>1;
build(ls§,l,mid); //递归到它的左子
build(rs§,mid+1,r); //递归到它的右子
sum[p]=sum[ls§]+sum[rs§]%md; //计算出当前节点的求和
}
2.2 区间修改
2.2.1 懒标记

而对于区间修改 , 我们引入了一个叫懒标记的东西

所谓懒标记 , 简单来讲 , 就是当加法操作将要从父节点传给子节点时 ,

为了节省操作 , 先将加法操作以标记的形式存着 ,

等到需要修改时在把标记下传

一个从OI Wiki上找的小故事 , 有助于理解懒标记

A 有两个儿子,一个是 B,一个是 C。

有一天 A 要建一个新房子,没钱。刚好过年嘛,有人要给 B 和 C 红包,
两个红包的钱数相同都是1元,然而因为 A 是父亲所以红包肯定是先塞给 A 咯~

理论上来讲 A 应该把两个红包分别给 B 和 C,
但是……缺钱嘛,A 就把红包偷偷收到自己口袋里了。

A 高兴地说:「我现在有2份红包了!我又多了2*1=2元了!哈哈哈~」

但是 A 知道,如果他不把红包给 B 和 C,那 B 和 C 肯定会不爽,
致家庭矛盾最后崩溃,所以 A 对儿子 B 和 C 说:

「我欠你们每人1份1元的红包,下次有新红包给过来的时候再给你们!
这里我先做下记录……嗯……我欠你们各1元……」

儿子 B、C 有点恼怒:「可是如果有同学问起我们我们收到了多少红包咋办?
你把我们的红包都收了,我们还怎么装?」

父亲 A 赶忙说:「有同学问起来我就会给你们的!我欠条都写好了不会不算话的!」

这样 B、C 才放了心。
在懒标记下传的同时需注意的点 :

懒标记不能重复下传 , 传完必须清零 ; 但是作为最终答案的区间和不用清零
懒标记下传的时候 , 由于一个节点可以对应多个原数组元素所对应的区间 , 所以传标记的时候这个区间包含了几份元素就要下传多少个标记
那么我们就可以容易的进行懒标记下传啦

void pushdown(ll p){ //从p节点开始下传
	sum[ls(p)]=(mu[p]*sum[ls(p)]+(rrr[ls(p)]-lll[ls(p)]+1)*add[p]%md)%md; 
   //将区间和从p传递到其左子 , 下同
	sum[rs(p)]=(mu[p]*sum[rs(p)]+(rrr[rs(p)]-lll[rs(p)]+1)*add[p]%md)%md; 
   //将区间和从p传递到其右子 , 下同
	mu[ls(p)]=(mu[p]*mu[ls(p)])%md;
	mu[rs(p)]=(mu[p]*mu[rs(p)])%md;
   //乘法运算懒标记下传
	add[ls(p)]=(mu[p]*add[ls(p)]+add[p])%md;
	add[rs(p)]=(mu[p]*add[rs(p)]+add[p])%md; 
   //加法运算懒标记下传
	mu[p]=1,add[p]=0; 
   //清空懒标记(注意:由于0乘任何数都得0,所以乘法标记传为1,加法标记传为0)
}

2.2.2 加法和乘法的顺序

有了懒标记 , 我们就可以在lognlogn的时间里进行区间修改了

但是区间修改还有一个注意事项 , 那就是先乘后加 , 乘法运算会影响加法运算 ,

因为如果先加后乘的话 , 在进行加法操作时 ,

还需要给乘法标记进行乘法的逆运算----除法 , 容易造成奇怪的精度损失

所以在区间乘的时候需要修改加法标记

那么我们就可以容易的写出区间加和区间乘的代码啦

void mul(ll p,ll l,ll r,ll k){  //区间乘,p表示当前节点,l和r表示询问区间
	if(lll[p]>=l && rrr[p]<=r){ 
		add[p]*=k,add[p]%=md;
		mu[p]*=k,mu[p]%=md;
		sum[p]*=k,sum[p]%=md;
		return;  
	}// 当前区间被修改区间包含时直接修改当前节点值,然后打标记,结束修改
	pushdown(p); //传标记
	sum[p]=(sum[ls(p)]+sum[rs(p)])%md; //求和
	ll mid=(lll[p]+rrr[p])>>1;
	if(l<=mid)mul(ls(p),l,r,k);  //递归到左子节点
	if(mid<r)mul(rs(p),l,r,k);   /递归到右子节点
	sum[p]=(sum[ls(p)]+sum[rs(p)])%md;  //再次更新节点值
}
void addd(ll p,ll l,ll r,ll k){  //区间加,和区间乘大致相同
	if(lll[p]>=l && rrr[p]<=r){
		add[p]+=k;
		add[p]%=md;
		sum[p]+=(rrr[p]-lll[p]+1)*k;
		sum[p]%=md;
		return;
	}
	pushdown(p);
	sum[p]=(sum[ls(p)]+sum[rs(p)])%md;
	ll mid=(lll[p]+rrr[p])>>1;
	if(l<=mid)addd(ls(p),l,r,k);
	if(mid<r)addd(rs(p),l,r,k);
	sum[p]=(sum[ls(p)]+sum[rs(p)])%md;
}

2.3 区间查询
而对于区间查询 , 我们可以通过找对应的sumsum数组的值来求和

比如对于这幅从OI Wiki上copy的图中 , 我们可以直接找到1到3的和 , 但是找不到1到4的和

对于这种情况 , 我们只要把区间[1,4][1,4]看成区间[1,3][1,3]和区间[4,4][4,4]的和即可

也就是说 , 一般地 , 如果要查询的区间是[l,r][l,r] ,

则可以将其拆成最多为lognlogn个子区间 , 合并这些区间即可求出[l,r][l,r]的答案

那么我们就可以容易的进行区间查询求和啦

ll query(ll p,ll l,ll r){  //[l,r]为查询区间,lll[p]和rrr[p]分别记录当前p节点代表的左右区间 , p为当前节点的编号
	if(lll[p]>=l && rrr[p]<=r)return sum[p]; //当前节点区间被包含时直接返回
    	pushdown(p);  //区间求和前必须把节点值更新完毕
	ll v=0,mid=(lll[p]+rrr[p])>>1; //v记录查询子区间的总和
	if(l<=mid)v+=query(ls(p),l,r),v%=md; //递归到左半边找子区间
	if(mid<r)v+=query(rs(p),l,r),v%=md;  //递归到右半边找子区间
	return v;  //返回查询区间的总和
}

pt.3 一些注意事项
线段树的空间要开到一般数组的4倍 , 原因见下图
由于位运算比普通四则运算快 , 所以可以用a<<1a<<1和a<<1|1a<<1∣1来代替2a2a和2a+12a+1 , 用(l+r)>>1(l+r)>>1代替(l+r)/2 , 可适当优化线段树
建树时记录每个节点所对应的区间,就不需要每次计算当前节点的左右端点了,减小代码常数 (众所周知 , 线段树常数不小)
在叶子节点处无需下放懒惰标记,所以懒惰标记可以不下传到叶子节点。
pt.4 上代码 !
献上码风奇特的代码

#include <bits/stdc++.h>
using namespace std;
#define ll long long
int n,m,md;
ll a[400005],sum[400005],mu[400005],add[400005],lll[400005],rrr[400005];
#define ls(x) x<<1
#define rs(x) x<<1|1
void build(ll p,ll l,ll r){
	lll[p]=l,rrr[p]=r,mu[p]=1;
	if(l==r){
		sum[p]=a[l]%md;
		return;
	}
	ll mid=(l+r)>>1;
	build(ls(p),l,mid);
	build(rs(p),mid+1,r);
	sum[p]=sum[ls(p)]+sum[rs(p)]%md;
}
void pushdown(ll p){
	sum[ls(p)]=(mu[p]*sum[ls(p)]+(rrr[ls(p)]-lll[ls(p)]+1)*add[p]%md)%md;
	sum[rs(p)]=(mu[p]*sum[rs(p)]+(rrr[rs(p)]-lll[rs(p)]+1)*add[p]%md)%md;
	mu[ls(p)]=(mu[p]*mu[ls(p)])%md;
	mu[rs(p)]=(mu[p]*mu[rs(p)])%md;
	add[ls(p)]=(mu[p]*add[ls(p)]+add[p])%md;
	add[rs(p)]=(mu[p]*add[rs(p)]+add[p])%md;
	mu[p]=1,add[p]=0;
}
void addd(ll p,ll l,ll r,ll k){
	if(lll[p]>=l && rrr[p]<=r){
		add[p]+=k;
		add[p]%=md;
		sum[p]+=(rrr[p]-lll[p]+1)*k;
		sum[p]%=md;
		return;
	}
	pushdown(p);
	sum[p]=(sum[ls(p)]+sum[rs(p)])%md;
	ll mid=(lll[p]+rrr[p])>>1;
	if(l<=mid)addd(ls(p),l,r,k);
	if(mid<r)addd(rs(p),l,r,k);
	sum[p]=(sum[ls(p)]+sum[rs(p)])%md;
}
void mul(ll p,ll l,ll r,ll k){
	if(lll[p]>=l && rrr[p]<=r){
		add[p]*=k,add[p]%=md;
		mu[p]*=k,mu[p]%=md;
		sum[p]*=k,sum[p]%=md;
		return;
	}
	pushdown(p);
	sum[p]=(sum[ls(p)]+sum[rs(p)])%md;
	ll mid=(lll[p]+rrr[p])>>1;
	if(l<=mid)mul(ls(p),l,r,k);
	if(mid<r)mul(rs(p),l,r,k);
	sum[p]=(sum[ls(p)]+sum[rs(p)])%md;
}
ll query(ll p,ll l,ll r){ 
	if(lll[p]>=l && rrr[p]<=r)return sum[p];
	pushdown(p);
	ll v=0,mid=(lll[p]+rrr[p])>>1;
	if(l<=mid)v+=query(ls(p),l,r),v%=md;
	if(mid<r)v+=query(rs(p),l,r),v%=md;
	return v;
}
int main() {
	cin>>n>>md;
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	build(1,1,n);
	cin>>m;
	while(m--){
		ll x,y,typ;
		scanf("%lld%lld%lld",&typ,&x,&y);
		if(typ==1){
			ll z;
			scanf("%lld",&z);
			mul(1,x,y,z);
		}else if(typ==2){
			ll z;
			scanf("%lld",&z);
			addd(1,x,y,z);
		}else printf("%lld\n",query(1,x,y));
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值