【题目描述】
这是一道模板题。
给定数列 a[1],a[2],...,a[n],你需要依次进行 q 个操作,操作有两类:
1、lrx:给定 l,r,x,对于所有 i∈[l,r],将 a[i] 加上 x(换言之,将 a[l],a[l+1],...,a[r] 分别加上 x);
2、lr:给定 l,r,求 ∑ri=la[i] 的值(换言之,求 a[l]+a[l+1]+⋯+a[r] 的值)。
【输入】
第一行包含 2 个正整数 n,q,表示数列长度和询问个数。保证 1≤n,q≤106 。
第二行 n 个整数 a[1],a[2],...,a[n],表示初始数列。保证 ∣∣a[i]∣≤106 。
接下来 q 行,每行一个操作,为以下两种之一:
1、lrx:对于所有 i∈[l,r],将 a[i] 加上 x;
2、lr:输出 ∑ri=la[i] 的值。
保证 1≤l≤r≤n,∣x∣≤106 。
【输出】
对于每个 2lr 操作,输出一行,每行有一个整数,表示所求的结果。
【输入样例】
5 10
2 6 6 1 1
2 1 4
1 2 5 10
2 1 3
2 2 3
1 2 2 8
1 2 3 7
1 4 4 10
2 1 2
1 4 5 6
2 3 4
【输出样例】
15
34
32
33
50
【提示】
数据范围与提示:
对于所有数据,1≤n,q≤10^6,∣a[i]∣≤10^6,1≤l≤r≤n,∣x∣≤10^6 。
一、 题目背景与分析
核心需求:给定一个长度为N的数组,要求支持两种极其频繁的操作:
-
区间修改 :给区间
[L,R]内的每一个数加上一个值x。 -
区间查询 :求区间
[L,R]内所有数的总和。
数据规模:数组长度N≤10^6,操作次数Q≤10^6。
面对这种百万级别的数据,任何带有O(N)层级的操作都会遭遇超时。普通数组区间修改太慢,前缀和数组单点/区间修改太慢。我们需要一种能将修改和查询都死死压制在O(logN) 级别的数据结构------线段树。
二、 核心与思考过程:为什么需要"懒标记"?
线段树的区间查询很容易理解,但区间修改 是初学者最大的噩梦。 如果我们要给 [1,100000] 的每个数加1,难道要顺着线段树跑到最底层的100000个叶子节点去逐一修改吗?那时间复杂度直接退化成O(N),线段树就失去了意义。
破局点:懒标记(Lazy Tag)------ 伟大的"非必要不执行"思想
-
截留 :当修改区间完全包围 了当前节点的管辖区间时,我们直接修改当前节点的总账,并在该节点门上贴一个"懒标记"(记录下面每个小弟应该加多少钱),然后**直接return,**不往下走。
-
下放(PushDown) :只有当下一次操作(无论是修改还是查询)被迫要劈开当前区间、访问其子节点时,我们才把门上的懒标记撕下来,传给左右两个儿子。
通过懒标记,我们将极其昂贵的底层遍历,转化为了精准的O(1)局部更新。
三、 算法设计:线段树的两大实战流派
在竞赛实战中,线段树有两种极其经典的写法,它们在时间复杂度上完全一致,但在工程风格上各有千秋:
-
写法一:传统传参 。节点只存
val和lazy。每次递归都把当前管辖的[l, r]作为参数传下去。优点是极限省空间,是OI届最主流的写法。 -
写法二:胖节点封装 。在结构体里直接存入该节点管辖的
l、r和len。优点是函数传参极其干净清爽(只需传目标区间和节点编号),大幅减轻大脑的记忆负担。
四、 时空复杂度分析
-
时间复杂度:建树O(N);单次区间修改O(logN);单次区间查询O(logN)。整体时间复杂度 O(QlogN)。
-
空间复杂度 :由于是二叉树的底层铺设,需要开辟原数组大小4倍的空间,空间复杂度O(N)。
五、 易错点总结
-
乘法溢出 :更新总账时
val+=x*len,如果x和len都是int,乘积会撑爆临时变量导致变负数,必须写成1ll*x*len。 -
新官不理旧账 :
PushDown下放标记时,子节点的标记必须是累加 (lazy+=),绝不能是直接赋值(lazy=),否则会覆盖掉历史账单。 -
忘报总账 :
update函数最后必须pushup(rt),否则底层数据改了,高层情报不同步。
六、 完整代码
写法一:传统传参
cpp
//区间修改 区间查询 线段树模版1
#include <iostream>
using namespace std;
const int maxn=1000010;
int n,q;
int a[maxn];//原数列
struct node{//线段树节点封装
long long val;//节点值
long long lazy;//懒标记
}tree[maxn*4];//段树必须开4倍空间
//向上更新 汇总左右儿子的情报给父亲
void pushup(int rt){
//父节点的值等于左右儿子值之和
tree[rt].val=tree[rt<<1].val+tree[rt<<1|1].val;
}
//建树
void build(int l,int r,int rt){
//初始化懒标记为0
tree[rt].lazy=0;
if(l==r){
//已经递归到叶子节点 直接赋予原数列的值
tree[rt].val=a[l];
return ;
}
int m=(r+l)>>1;
build(l,m,rt<<1);//递归构造左子树
build(m+1,r,rt<<1|1);//递归构造右子树
pushup(rt);//左右子树构造完毕,向上更新
}
//下放懒标记 清理历史账单 确保子节点数据是最新的
//ln 左儿子区间长度 rn 右儿子区间长度
void pushdown(int ln,int rn,int rt){
if(tree[rt].lazy){//只有有标记才需要下放
//左子树
tree[rt<<1].val+=tree[rt].lazy*ln;
//右子树
tree[rt<<1|1].val+=tree[rt].lazy*rn;
//左子树
tree[rt<<1].lazy+=tree[rt].lazy;
//右子树
tree[rt<<1|1].lazy+=tree[rt].lazy;
}
//清除当前节点懒标记
tree[rt].lazy=0;
}
//[L,R]区间每个数加上x 当前节点为rt rt所管辖区间为[l,r]
void update(int L,int R,int x,int l,int r,int rt){
//如果当前管辖区间被目标区间完全包围
if(L<=l&&R>=r){
tree[rt].val+=1ll*x*(r-l+1);
tree[rt].lazy+=x;//贴上懒标记
return;
}
int m=(l+r)>>1;
//没有被完全包围 说明必须劈开 劈开前必须先下放历史标记
//下推以后 才能准确更新子节点
pushdown(m-l+1,r-m,rt);
if(L<=m) update(L,R,x,l,m,rt<<1);
if(R>m) update(L,R,x,m+1,r,rt<<1|1);
pushup(rt);
}
//区间查询
long long query(int L,int R,int l,int r,int rt){
//完全包围 直接交出总账
if(L<=l&&R>=r) return tree[rt].val;
if(L>r||R<l) return 0;
int m=(l+r)>>1;
//下推以后才可以准确查询子节点
pushdown(m-l+1,r-m,rt);
long long ans=0;
if(L<=m) ans+=query(L,R,l,m,rt<<1);
if(R>m) ans+=query(L,R,m+1,r,rt<<1|1);
return ans;
}
int main(){
//IO加速
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>q;
//原数列赋值
for(int i=1;i<=n;i++) cin>>a[i];
//建树
build(1,n,1);
//总共q次操作
while(q--){
int k;
cin>>k;
if(k==1){//对于所有i∈[l,r],将a[i]加上x
int l,r,x;
cin>>l>>r>>x;
update(l,r,x,1,n,1);
}
else{//求a[l]+a[l+1]+⋯+a[r]的值
int l,r;
cin>>l>>r;
cout<<query(l,r,1,n,1)<<"\n";
}
}
}
写法二:胖节点封装
cpp
//区间修改 区间查询 线段树模版2
#include <iostream>
using namespace std;
int n,q;
const int maxn=1000010;
//线段树封装节点
struct node{
long long val;
long long lazy;
//当前节点管辖的左边界、右边界、区间长度
int l,r,len;
}tree[maxn<<2];
int a[1000010];//原数列
//向上更新
void pushup(int rt){
tree[rt].val=tree[rt<<1].val+tree[rt<<1|1].val;
}
//建树,[l,r]为当前节点rt所代表的区间,rt为当前节点
void build(int l,int r,int rt){
//初始化每个节点懒标记为0
tree[rt].lazy=0;
//每个节点所代表区间的长度
tree[rt].len=r-l+1;
//当前节点左端点
tree[rt].l=l;
//当前节点右端点
tree[rt].r=r;
if(l==r){
tree[rt].val=a[l];
return;
}
int m=(l+r)>>1;
build(l,m,rt<<1);//递归构造左子树
build(m+1,r,rt<<1|1);//递归构造右子树
//左右子树构造完毕,向上更新
pushup(rt);
}
//rt为当前节点 ln为当前节点所代表左区间长度
//rn为当前节点所代表右区间长度
void pushdown(int ln,int rn,int rt){
//下推懒标记 更新rt左儿子节点值
tree[rt<<1].val+=tree[rt].lazy*ln;
//下推懒标记 更新rt右儿子节点值
tree[rt<<1|1].val+=tree[rt].lazy*rn;
//更新rt左儿子懒标记
tree[rt<<1].lazy+=tree[rt].lazy;
//更新rt右儿子懒标记
tree[rt<<1|1].lazy+=tree[rt].lazy;
//rt懒标记消除
tree[rt].lazy=0;
}
//[l,r]区间增加x,rt为根节点
void update(int l,int r,int x,int rt){
if(l<=tree[rt].l&&r>=tree[rt].r){
tree[rt].val+=1ll*x*tree[rt].len;
tree[rt].lazy+=x;
return;
}
if(r<tree[rt].l||l>tree[rt].r) return;
int m=(tree[rt].r+tree[rt].l)>>1;
//懒标记下推
if(tree[rt].lazy)
pushdown(m-tree[rt].l+1,tree[rt].r-m,rt);
if(l<=m) update(l,r,x,rt<<1);
if(r>m) update(l,r,x,rt<<1|1);
pushup(rt);
}
//rt为当前节点,[l,r]为要查询区间
long long query(int l,int r,int rt){
if(l<=tree[rt].l&&r>=tree[rt].r)
return tree[rt].val;
int m=(tree[rt].l+tree[rt].r)>>1;
//懒标记下推
if(tree[rt].lazy)
pushdown(m-tree[rt].l+1,tree[rt].r-m,rt);
long long ans=0ll;
if(l<=m) ans+=query(l,r,rt<<1);
if(r>m) ans+=query(l,r,rt<<1|1);
return ans;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>q;
for(int i=1;i<=n;i++) cin>>a[i];
//建树
build(1,n,1);
//总共执行q次操作
while(q--){
int k;
cin>>k;
if(k==1){//对于所有i∈[l,r],将a[i]加上x
int l,r,x;
cin>>l>>r>>x;
update(l,r,x,1);
}
else{//求a[l]+a[l+1]+⋯+a[r]的值
int l,r;
cin>>l>>r;
cout<<query(l,r,1)<<"\n";
}
}
return 0;
}