树状数组( B i n a r y I n d e x e d T r e e , B I T Binary Indexed Tree,BIT BinaryIndexedTree,BIT)也是一个区间查询和单点修改复杂度都为log(n)的数据结构。主要用于查询任意两点之间的所有元素之和。
B I T BIT BIT的最基本操作,首先我们要知道 B I T BIT BIT这东东到底是什么,先看一张烂大街的图:
它有这样一个性质: C [ i ] C[i] C[i]一定有 l o w b i t ( i ) lowbit(i) lowbit(i)个子节点(不一定是直接节点)且 C [ i ] = A [ 1 ] + A [ 2 ] + . . . . A [ i ] ( 前 缀 和 ) C[i]=A[1]+A[2]+....A[i](前缀和) C[i]=A[1]+A[2]+....A[i](前缀和),按照这个性质,我们就可以建出这棵树
现在我们回到正题,如何解决单点修改,区间查询?
首先是单点修改,让我们想想,假如修改 A [ 1 ] A[1] A[1],会对 C C C中的那些节点造成影响
就是 A [ 1 ] A[1] A[1]爬上去经过的嘛,那如何求得这些数呢,观察一下C下标,1,2,4,8,好像就是依次 × 2 \times 2 ×2耶,是吗?看看改变A[5]的情况:
爬还是一样的爬,但下标就不一样了,5,6,8,看看规律???
没有???那看看他们的差:1,2???=
没看出来???再看看原下标对应的2进制数???
对了,每次增加的就是lowbit(i),其中i为下标
有了这点,单点修改就明白多了:
int lowbit(int x){//求lowbit
return x & -x;
}
void upd(int k,int x){//单点更新,a[k]+=x;
for(int i=k;i<=n;i+=lowbit(i)){
BIT[i]+=x;
}
}
另外,单点修改也可以用来对BIT初始化:
int t;
for(int i=1;i<=n;i++){
scanf("%d",&t);
upd(i,t);
}
OK,来到我们的区间查询
先说:区间查询其实是假的,它查询的其实是前缀和,但我们可以用前缀和来对出区间和
以查询前缀和[1,5] (为了简便,记A[1]+A[2]+…+A[n]为Sum(n))为例,为了保证查询尽量简单,我们以分割区域尽量少为标准,不难发现Sum[5]=C[4]+C[5]
又到了找规律的时候,找找5和4的关系?
额。。。有点难发现,但结合单点修改+连蒙带猜不难发现4=5-lowbit(5),那就清晰了,每次减lowbit(i)就行了,和单点修改恰恰相反
int Sum(int k,int l){
int s=0;
for(int i=k;i>=1;i-=lowbit(i)){
for(int j=l;j>=1;j-=lowbit(j)){
s+=BIT[i][j];
}
}
return s;
}
前面我们已经知道树状数组可以进行单点修改,区间查询,而 B I T BIT BIT的本质其实就是一个效率更快的前缀和,思考一下,前缀和的互逆运算不就是差分吗,对于查分求前缀和求到的是单点,而对查分数组进行单点修改却改的是区间( a [ l ] + x , a [ r + 1 ] − x a[l]+x,a[r+1]-x a[l]+x,a[r+1]−x),这不就对上了吗
所以:
代码:
upd、Sum函数同上
区间修改 : [ l , r ] :[l,r] :[l,r]中的所有数 + x +x +x
upd(l,x);
upd(r+1,-x);
单点查询:求 A [ i ] A[i] A[i]的值
Sum(i);
P[]仍为A[]的差分数组,那么原数组的前缀和:
A [ 1 ] + A [ 2 ] + … … + A [ n ] A[1]+A[2]+……+ A[n] A[1]+A[2]+……+A[n]
= P [ 1 ] + ( P [ 1 ] + P [ 2 ] ) + ( P [ 1 ] + P [ 2 ] + P [ 3 ] ) + … … + ( P [ 1 ] + P [ 2 ] + … … + P [ n ] ) =P[1]+(P[1]+P[2])+(P[1]+P[2]+P[3])+……+(P[1]+P[2]+……+P[n]) =P[1]+(P[1]+P[2])+(P[1]+P[2]+P[3])+……+(P[1]+P[2]+……+P[n])
= n ∗ P [ 1 ] + ( n − 1 ) ∗ P [ 2 ] + ( n − 2 ) ∗ P [ 3 ] + … … + P [ n ] =n*P[1]+(n-1)*P[2]+(n-2)*P[3]+……+P[n] =n∗P[1]+(n−1)∗P[2]+(n−2)∗P[3]+……+P[n]
= n ∗ ( P [ 1 ] + P [ 2 ] + P [ 3 ] + … … + P [ n ] ) − ( 0 ∗ P [ 1 ] + 1 ∗ P [ 2 ] + 2 ∗ P [ 3 ] + … … + ( n − 1 ) ∗ P [ n ] ) =n*(P[1]+P[2]+P[3]+……+P[n])-(0*P[1]+1*P[2]+2*P[3]+……+(n-1)*P[n]) =n∗(P[1]+P[2]+P[3]+……+P[n])−(0∗P[1]+1∗P[2]+2∗P[3]+……+(n−1)∗P[n])
观察减式两边,分别将P[i]和(i-1)p[i]建立两个树状数组BIT1和BIT2,BIT1就是差分数组,区间修改按上一例进行;BIT2的增量就不是x了,而是x*(i-1)。至于区间查询,我们已经知道原数组前缀和了,直接相减即可查询区间和。
代码:
#include
#include
using namespace std;
int n;
int q;
long long BIT1[1000005],BIT2[1000005];
int a[1000005],p[1000005];
int lowbit(int x){
return x & -x;
}
void upd(int k,int x){
for(int i=k;i<=n;i+=lowbit(i)){
BIT1[i]+=x, BIT2[i] += (long long)(k - 1) * x;
}
}
long long Sum(int k){
long long s=0;
for(int i=k;i>=1;i-=lowbit(i)){
s+=k*BIT1[i]-BIT2[i];
}
return s;
}
int main(){
scanf("%d %d",&n,&q);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
p[i]=a[i]-a[i-1];
upd(i,p[i]);
}
while(q--){
int type;
scanf("%d",&type);
if(type==1){
int l,r,x;
scanf("%d %d %d",&l,&r,&x);
upd(l,x);
upd(r+1,-x);
}
else{
int l,r;
scanf("%d %d",&l,&r);
printf("%lld\n",Sum(r)-Sum(l-1));
}
}
return 0;
}
思考一下二维前缀和以及普通前缀和的关系: