【题目描述】
原题来自:AHOI 2009
老师交给小可可一个维护数列的任务,现在小可可希望你来帮他完成。
有长为 n 的数列,不妨设为 a1,a2,⋯,an 。有如下三种操作形式:
把数列中的一段数全部乘一个值;
把数列中的一段数全部加一个值;
询问数列中的一段数的和,由于答案可能很大,你只需输出这个数模 P 的值。
【输入】
第一行两个整数 n 和 P;
第二行含有 n 个非负整数,从左到右依次为 a1,a2,⋯,an ;
第三行有一个整数 M,表示操作总数;
从第四行开始每行描述一个操作,输入的操作有以下三种形式:
操作 1:1tgc,表示把所有满足 t≤i≤g 的 ai 改为 ai×c;
操作 2:2tgc,表示把所有满足 t≤i≤g 的 ai 改为 ai+c;
操作 3:3tg,询问所有满足 t≤i≤g 的 ai 的和模 P 的值。
同一行相邻两数之间用一个空格隔开,每行开头和末尾没有多余空格。
【输出】
对每个操作 3,按照它在输入中出现的顺序,依次输出一行一个整数表示询问结果。
【输入样例】
7 43
1 2 3 4 5 6 7
5
1 2 5 5
3 2 4
2 3 7 9
3 1 3
3 4 7
【输出样例】
2
35
8
【提示】
样例说明:
初始时数列为 {1,2,3,4,5,6,7};
经过第 1 次操作后,数列为 {1,10,15,20,25,6,7};
对第 2 次操作,和为 10+15+20=45,模 43 的结果是 2;
经过第 3 次操作后,数列为 {1,10,24,29,34,15,16};
对第 4 次操作,和为 1+10+24=35,模 43 的结果是 35;
对第 5 次操作,和为 29+34+15+16=94,模 43 的结果是 8。
数据范围与提示:
对于全部测试数据,1≤t≤g≤n,0≤c,a[i]≤10^9,1≤P≤10^9 。
测试数据规模如下表所示:
|------|----|-------|-------|---------|---------|---------|---------|-------|
| 数据编号 | 1 | 2,3 | 4 | 5 | 6 | 7 | 8 | 9,10 |
| n= | 10 | 10^3 | 10^4 | 6×10^4 | 7×10^4 | 8×10^4 | 9×10^4 | 10^5 |
| M= | 10 | 10^3 | 10^4 | 6×10^4 | 7×10^4 | 8×10^4 | 9×104^ | 10^5 |
一、 题目分析
本题要求我们维护一个长度为N的数列,支持三种操作:
-
区间乘法:将某一段区间内的所有数乘上一个常数c。
-
区间加法:将某一段区间内的所有数加上一个常数c。
-
区间求和:查询某一段区间内所有数字的和,并对指定的数P取模。
核心痛点 :这是极其经典的"多重懒标记"线段树问题。如果只有加法,或者只有乘法,问题都很简单。但当加法和乘法交织在一起时,一个节点上可能同时存在"历史加法标记"和"新来的乘法标记",先算加法还是先算乘法?标记下放时如何互相影响? 这成为了这道题最大的难点。
二、 思考过程与解题思路(核心数学推导)
面对两个互相干扰的标记,我们需要在数学上确立一个绝对的优先级(铁律)。
在线段树中,我们人为规定:永远遵循"先乘后加"的原则。
即对于底层的任意一个真实数字x,它经过当前节点的懒标记作用后,其真实值永远表示为:
真实值 =
交接仪式的代数魔法:
假设当前子节点(儿子)手里原本就有自己的旧账本:旧的乘法标记 和旧的加法标记
。
此时,父节点执行pushdown,下放了新的指令:整体乘上,再加上
。
我们把子节点的旧状态作为一个整体,代入父节点的新指令中,利用乘法分配律展开:
最新状态 =
最新状态 =
经过提取公因式,我们得到了极其优美的状态转移方程:
-
子节点的新乘法标记 =
-
子节点的新加法标记 =
这就是本题的灵魂:当父节点下放乘法标记时,子节点的旧加法标记必须跟着一起翻倍。
三、 算法设计
基于上述推导,我们的线段树设计如下:
-
节点结构 :维护
val(区间和)、lazym(乘法标记,初始化为1)、lazys(加法标记,初始化为0)。 -
下放操作 (
pushdown):-
先用父节点的标记更新左右儿子的
val。注意:加法标记对区间和的影响必须乘以区间长度。 -
利用推导出的公式更新左右儿子的
lazym和lazys。 -
清空父节点标记(
lazym=1, lazys=0)。
-
-
更新操作 (
update):-
乘法更新:当前节点的
val,lazym,lazys统统乘上常数c。 -
加法更新:当前节点的
val加上c区间长度,
lazys加上c。 -
只要跨越节点(分裂),必须先
pushdown。
-
四、 易错点总结
-
加法标记 :在
pushdown时,更新子节点的lazys必须写成子lazys=子lazys*父lazym+父lazys,千万不能忘记乘上父节点的乘法标记。 -
区间长度 :加法操作对区间和(
val)的影响,必须乘以当前区间的长度(r-l+1),而乘法操作直接对val乘即可。 -
取模大爆炸 :本题数据范围极大,每一次加法和乘法运算后(包括更新标记时),都必须立刻取模
%p,并且所有涉及数值的变量务必开long long,防止中间过程溢出。 -
局部变量覆盖 :在分裂区间找中点时,有同学习惯性写
int m=(l+r)>>1;容易与全局变量m(操作次数)发生冲突,强烈建议计算中点统一使用int mid。
五、 时空复杂度分析
-
时间复杂度 :建树操作为O(N);得益于懒标记和势能剪枝,每次区间修改和区间求和的时间复杂度均为
。对于M次操作,总时间复杂度为
,完美通过 10^5级别的数据。
-
空间复杂度:线段树需要开辟原数组4倍的空间,空间复杂度为O(N)。
六、 完整代码
cpp
//线段树 区间修改 区间查询
//这道题要用到分配律 永远遵循先乘后加
#include <iostream>
using namespace std;
int n,p,m;
const int maxn=100010;
int a[maxn];//原数列
//线段树节点封装
struct node{
long long val;
long long lazym;//乘懒标记
long long lazys;//加懒标记
}tree[maxn<<2];//线段树要开四倍元素大小
//懒标记下放 当前节点rt 管辖区间为[l,r]
void pushdown(int rt,int l,int r){
int mid=(l+r)>>1;
//先修改左右儿子的值
tree[rt<<1].val=(tree[rt<<1].val*tree[rt].lazym+tree[rt].lazys*(mid-l+1))%p;
tree[rt<<1|1].val=(tree[rt<<1|1].val*tree[rt].lazym+tree[rt].lazys*(r-mid))%p;
//再修改左右儿子懒标记 然后清空rt懒标记
tree[rt<<1].lazym=(tree[rt<<1].lazym*tree[rt].lazym)%p;
tree[rt<<1|1].lazym=(tree[rt<<1|1].lazym*tree[rt].lazym)%p;
tree[rt<<1].lazys=(tree[rt<<1].lazys*tree[rt].lazym+tree[rt].lazys)%p;
tree[rt<<1|1].lazys=(tree[rt<<1|1].lazys*tree[rt].lazym+tree[rt].lazys)%p;
//清空父节点的懒标记
tree[rt].lazym=1;
tree[rt].lazys=0;
}
//向上更新
void pushup(int rt){
tree[rt].val=(tree[rt<<1].val+tree[rt<<1|1].val)%p;
}
//当前节点是rt,rt管辖的区间为[l,r]
void build(int l,int r,int rt){
//初始化乘懒标记为1
tree[rt].lazym=1;
//初始化加懒标记为0
tree[rt].lazys=0;
if(l==r){
tree[rt].val=a[l]%p;
return;
}
int m=(l+r)>>1;
build(l,m,rt<<1);
build(m+1,r,rt<<1|1);
//通过子节点更新当前节点
pushup(rt);
}
//乘更新操作
//[t,g]区间每个数都*c,当前节点为rt,rt管辖区间为[l,r]
void updatem(int t,int g,int c,int l,int r,int rt){
//如果当前节点管辖区间属于[t,g]
if(t<=l&&g>=r){
tree[rt].val=(tree[rt].val*c)%p;
tree[rt].lazym=(tree[rt].lazym*c)%p;
tree[rt].lazys=(tree[rt].lazys*c)%p;
return;
}
if(t>r||g<l) return;
//递归求左半区间和右半区间时先懒标记下放
pushdown(rt,l,r);
int m=(l+r)>>1;
if(t<=m) updatem(t,g,c,l,m,rt<<1);
if(g>m) updatem(t,g,c,m+1,r,rt<<1|1);
pushup(rt);
}
//加更新操作
//[t,g]区间每个数都+c,当前节点为rt,rt管辖区间为[l,r]
void updates(int t,int g,int c,int l,int r,int rt){
//如果当前节点管辖区间属于[t,g]
if(t<=l&&g>=r){
tree[rt].val=(tree[rt].val%p+1ll*(r-l+1)*c%p)%p;
tree[rt].lazys=(tree[rt].lazys+c)%p;
return;
}
if(t>r||g<l) return;
//懒标记下放
pushdown(rt,l,r);
int m=(l+r)>>1;
if(t<=m) updates(t,g,c,l,m,rt<<1);
if(g>m) updates(t,g,c,m+1,r,rt<<1|1);
pushup(rt);
}
//查询[t,g]区间的和%p的值 当前节点为rt rt所管辖区间为[l,r]
long long query(int t,int g,int l,int r,int rt){
if(t<=l&&g>=r){
return tree[rt].val%p;
}
if(t>r||g<l) return 0;
//懒标记下放
pushdown(rt,l,r);
int m=(l+r)>>1;
long long ans=0;
if(t<=m) ans=(ans+query(t,g,l,m,rt<<1))%p;
if(g>m) ans=(ans+query(t,g,m+1,r,rt<<1|1))%p;
return ans%p;
}
int main(){
//io加速
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>p;
for(int i=1;i<=n;i++) cin>>a[i];
cin>>m;
//建树
build(1,n,1);
//总共m次操作
while(m--){
int k;
cin>>k;
if(k==1){//表示把所有满足t≤i≤g的a[i]改为a[i]×c
int t,g,c;
cin>>t>>g>>c;
updatem(t,g,c,1,n,1);
}
else if(k==2){//表示把所有满足t≤i≤g的a[i]改为a[i]+c
int t,g,c;
cin>>t>>g>>c;
updates(t,g,c,1,n,1);
}
else{//询问所有满足t≤i≤g的a[i]的和模P的值。
int t,g;
cin>>t>>g;
cout<<query(t,g,1,n,1)<<"\n";
}
}
return 0;
}