【题目描述】
给定一个全部为零的数列,规定有两种操作,一是修改某个元素,二是求区间的连续和。
【输入】
输入数据第一行包含两个正整数n,m(n≤100000,m≤500000),以下是m行,
每行有三个正整数k,a,b(k=0或1,a,b≤n).k=0时表示将a处数字加上b,k=1时表示询问区间[a,b]内所有数的和。
【输出】
对于每个询问输出对应的答案。
【输入样例】
10 20
0 1 10
1 1 4
0 6 6
1 4 10
1 8 9
1 4 9
0 10 2
1 1 8
0 2 10
1 3 9
0 7 8
0 3 10
0 1 1
1 3 8
1 6 9
0 5 5
1 1 8
0 4 2
1 2 8
0 1 1
【输出样例】
10
6
0
6
16
6
24
14
50
41
题目分析与思考过程
面对"修改与查询交替进行"的序列问题,我们首先要在脑海中进行暴力算法的推演与排除:
-
普通数组(暴力法):
-
单点修改极其迅速,时间复杂度O(1)。
-
区间求和需要使用
for循环遍历,时间复杂度O(N)。 -
在m=500000的数据量下,最坏时间复杂度逼近O(N×M),即5×10^10次运算,绝对会 超时。
-
-
前缀和数组:
-
区间求和利用前缀和相减(
sum[R]-sum[L-1]),时间复杂度降为O(1)。 -
但单点修改灾难了,改了一个位置,它后面的所有前缀和全都要重新计算,时间复杂度退化为O(N),同样会超时。
-
解法 :我们需要一种数据结构,能够在这两个极端之间找到平衡。树状数组和线段树应运而生,它们都能将修改和查询的时间复杂度同时稳定在极速的O(logN)级别。
解题思路与算法设计
方法一:树状数组
树状数组的核心在于**lowbit(位运算)**。它巧妙地利用二进制特征,将一个数字划分为多个不同长度的管辖区。
-
单点修改 :不断向右上方寻找包含当前位置的"大管家"(
x+=lowbit(x)),并给它们加上该数值。 -
区间求和 :依然是经典的前缀和思想
query(R)-query(L-1)。求前缀和时,不断向左下方聚拢小管家(x-=lowbit(x))把账本加起来。 -
特点 :代码精简,常数小,速度快,是本题的最优解。
方法二:线段树
线段树运用了经典的二分分治思想。将区间不断从中间劈开,直到叶子节点(长度为 1)。
-
建树(Build) :递归到叶子节点赋值,再自底向上
PushUp汇总结点信息。 -
单点修改(Update) :像狙击枪一样,一路根据中点劈开,直插到对应的叶子节点修改,归来时顺路
PushUp更新沿途的父节点。 -
区间查询(Query):如果当前节点的管辖区间被查询区间完全包围,直接交出总账;否则继续向下劈开查找。
-
特点:虽然代码较长、常数略大,但它是后续处理"区间修改"、"区间取最值"等复杂操作的万能框架。
时空复杂度分析
| 算法 | 单次修改时间 | 单次查询时间 | 总体时间复杂度 | 空间复杂度 |
|---|---|---|---|---|
| 树状数组 | O(logN) | O(logN) | O(MlogN) | O(N) (单倍数组) |
| 线段树 | O(logN) | O(logN) | O(MlogN) | O(N) (需开 4 倍数组) |
易错点总结
-
十年OI一场空,不开long long见祖宗 :100000 个数累加,哪怕每个数不大,多次修改后的区间和也很容易突破
int范围(约 21 亿)。前缀和与线段树的节点值必须使用long long。 -
线段树的空间血泪史 :线段树是一棵二叉树,底层叶子节点为了完全铺开,数组大小必须开到原数组大小的4倍 (
maxn<<2)。 -
输入输出流引发的血案 :本题查询与修改总次数m高达500000。如果使用C++原生的
cin/cout,可能因为I/O太慢而超时,在main函数开头加上ios::sync_with_stdio(false); cin.tie(0);来解除同步。 -
递归的死亡黑洞 :线段树单点修改寻找到叶子节点(
l==r)并修改后,必须return。如果不写return,程序会继续劈开区间进入死循环,导致栈溢出。
完整代码
版本一:树状数组
cpp
//树状数组做法
#include <iostream>
using namespace std;
int n,m;
long long c[100010];//树状数组 维护前缀和
//树状数组核心 返回x二进制表示下最低位1所代表的整数
int lowbit(int x){
return x&(-x);
}
//单点修改 在a的位置上加b
void update(int a,int b){
//从底层一路向右上方更新所有管辖着位置a的上级节点
while(a<=n){
c[a]+=b;
a+=lowbit(a);
}
}
//查询前缀和:返回1到x的区间总和
long long query(int x){
long long ret=0;//记录前缀和
//从位置x一路向左下方累加经过的每一个管辖区
while(x>0){
ret+=c[x];
x-=lowbit(x);
}
return ret;
}
int main(){
//io加速
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>m;
//总共m个操作
while(m--){
int k,a,b;
cin>>k>>a>>b;
if(k==0){//k=0时表示将a处数字加上b
update(a,b);
}
else{//k=1时表示询问区间[a,b]内所有数的和
cout<<query(b)-query(a-1)<<"\n";
}
}
return 0;
}
版本二:线段树
(注:代码中的 lazy 标记在"单点修改"中无需使用,但为了向"区间修改"过渡,我予以保留并注释说明)
cpp
//线段树
#include <iostream>
using namespace std;
const int maxn=100010;//元素总个数最大值
//线段树节点封装
struct node{
long long val;//节点值(区间总和)
//懒标记(单点修改不需要用到,作为区间修改的扩展预留)
int lazy;
}tree[maxn<<2];//线段树开四倍最大总元素个数
int aa[maxn];//原数列
int n,m;
//PushUp向上更新
//子节点信息合并,向上更新父节点总和
void pushup(int rt){
//父节点总和=左儿子总和+右儿子总和
//rt<<1等价于rt*2, rt<<1|1等价于rt*2+1
tree[rt].val=tree[rt<<1].val+tree[rt<<1|1].val;
}
//建树 rt为当前节点编号,l、r 为当前节点管辖区间
void build(int rt,int l,int r){
tree[rt].lazy=0;//初始化懒标记为0
if(l==r){
//已经递归到叶子节点,直接赋予原数列的值
tree[rt].val=aa[l];
return;
}
int m=(l+r)/2;
build(rt*2,l,m);//递归构造左子树
build(rt*2+1,m+1,r);//递归构造右子树
//左右子树构造完毕,向上更新
pushup(rt);
}
//单点修改 将位置L的数字加上x
//rt表示当前线段树的根节点编号
//l,r为当前节点管辖区间
void update(int L,int x,int l,int r,int rt){
if(l==r){//叶子节点 代表已经找到要修改的位置 直接修改
//已经找到最底层的单人节点,直接修改
tree[rt].val+=x;
return;
}
int m=(l+r)/2;
//判断目标L在左半区还是右半区,精准递归搜索
if(L<=m) update(L,x,l,m,rt*2);
else update(L,x,m+1,r,rt*2+1);
pushup(rt);//回溯 顺路向上更新沿途的父节点
}
//区间查询 求[L, R]的总和
//L R代表本次查询的区间 l r代表当前节点代表区间 rt表示当前线段树的根节点编号
long long query(int L,int R,int l,int r,int rt){
//1:如果当前节点的管辖区间[l,r]被查询区间[L,R]完全包围,直接交出总账
if(L<=l&&R>=r) return tree[rt].val;
//2:区间无重叠部分
if(L>r||R<l) return 0;
int mid=(l+r)/2;
long long ans=0;
//左子区间与[L,R]有重合 递归处理
if(L<=mid) ans+=query(L,R,l,mid,rt*2);
//右子区间与[L,R]有重合 递归处理
if(R>mid) ans+=query(L,R,mid+1,r,rt*2+1);
return ans;
}
int main(){
//IO加速
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>m;
//原数列每个值都是0
for(int i=1;i<=n;i++) aa[i]=0;
build(1,1,n);//建树
//总共执行m次操作
while(m--){
int k,a,b;
cin>>k>>a>>b;
if(k==0){//k=0时表示将a处数字加上b
update(a,b,1,n,1);
}
else{//k=1时表示询问区间[a,b]内所有数的和
cout<<query(a,b,1,n,1)<<"\n";
}
}
return 0;
}