前置知识: 前缀和。
前缀和即:用
[s_i = sum_{j=1}^i a_j
]
然后
[sum_{i=l}^r a_i = s_r - s_{l-1}
]
树状数组是基于前缀和的一种数据结构。
它主要可以维护区间和、单点改、区间改、单点查。
和线段树相比,码量较少、空间复杂度较少是一大优势;但功能不全(比如区间排名,这权值线段树可以搞,但树状数组无法实现)也是弱点。
说实话,前人的智慧是无穷的,能想到树状数组这种超能的武器!
树状数组的原理:
每一个节点存储以自己结尾的若干节点的和。那么“若干节点”是多少个呢?下面考虑一个 lowbit
.
inline int lowbit(int x) {
//求2^{x末尾0的个数}
return x & (-x);
}
至于它怎么求的,你不用管;只需要知道这是求什么就行,代码中注释了。
那么,看个图:
你会发现,如果 (S=lowbit(x))
此时 (x) 的紧靠着左边的那个节点就是 (x-S)
紧靠着右边的那个节点就是 (x+S) (这其实也是自己的父亲)
然后呢,查询 (1) ~ (n) 的和只需要这几步:
-
找出 (lowbit(n))的值
-
然后 (n) 变为自己紧靠着的左边的那个节点
-
如果 (n leq 0) 结束;否则回到第一步。
(代码中 (c) 数组就是树状数组)
inline ll sum(int x) {
//求n的前缀和
ll s=0; while(x>0) {
s+=c[x];
x-=lowbit(x);
//这是x节点紧贴着的前一个区间
} return s;
}
那么,我们需要知道:怎么建树呢?
如果想建树,不外两种:
-
不断单点修改
-
强行构树(比如线段树)
我们要考虑如何单点修改。
其实不难,做三步:
-
找到 (lowbit(n)) 并暴力加
-
然后 (n) 变为自己紧靠着的右边的那个节点
-
如果 $x > n $ 结束;否则回到第一步。
这和求和差不多。一个往前,一个往后,异曲同工。
下面,我们可以解决第一个模板。
#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+1;
typedef long long ll;
inline int read(){char ch=getchar();int f=1;while(ch<'0' || ch>'9') {if(ch=='-') f=-f; ch=getchar();}
int x=0;while(ch>='0' && ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();return x*f;}
int a[N]; ll c[N];
int n,m,x,y;
inline int lowbit(int x) {
//求2^{x末尾0的个数}
return x & (-x);
}
inline ll sum(int x) {
//求n的前缀和
ll s=0; while(x>0) {
s+=c[x];
x-=lowbit(x);
//这是x节点紧贴着的前一个区间
} return s;
}
inline void update(int x,int k) {
a[x]+=k; while(x<=n) {
c[x]+=k; //暴力加和
x+=lowbit(x);
//这是x节点紧贴着的后一个区间
}
}
int main(){
n=read(); m=read();
for(int i=1;i<=n;i++) {
x=read(); update(i,x);
} while(m--) {
int opt=read(); x=read(),y=read();
if(opt==1) update(x,y); //单点修改
else printf("%lld
",sum(y)-sum(x-1)); //利用前缀和思想
}
return 0;
}