树状数组学习笔记
前置知识: 前缀和。
前缀和即:用
\[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\n",sum(y)-sum(x-1)); //利用前缀和思想
}
return 0;
}
简易的代码胜过复杂的说教。

浙公网安备 33010602011771号