一、什么是树状数组。
将一个正常的数组按照二进制每个位上的和存储的数组为树状数组,类似如下图,
树状数组的好处,求一个数的前缀和变得更快更容易,例如:求S7,只需要把下标为7的二进制各个位置上存储的值求和就可以了就类似于 S7=c[7]+c[6]+c[4],对应的写二进制更便于理解 S(111)=c[111]+c[110]+c[100]。
求前缀和变得容易了之后,那么求一段区间的和只需要求右端点前缀和和左端点前缀和相减就可以了。代码实现如下。
#include<bits/stdc++.h>
#define lowbit(x) x & -x
const int N=1e8;
long long a[N];
long long c[N];
int n;
long long ask(long long x){
long long res=0;
for(;x>0;x-=lowbit(x)){
res+=c[x];
}
return res;
}
int main(){
long long x,y;
scanf("%lld %lld",&x,&y);
printf("%lld\n",ask(y)-ask(x-1));
}
lowbit(x)的含义为取x上的最末端的1及其后面的0所组成的值(也就是上面提到的每个二进制中存储的值)。
那么我们该如何实现构建这个树状数组呢?
同样利用lowbit(x),对每一个读进来的数进行处理,由第一张图我们可以知道,对于树状数组,改变原数组中一个值,就需要对树状数组中它所有的父节点都进行一次修改。也就是每一项二进制存储的值需要修改。代码实现如下。
#include<bits/stdc++.h>
#define lowbit(x) x & -x
const int N=1e8;
long long a[N];
long long c[N];
int n;
void add(long long a,long long b){
for(;a<=n;a+=lowbit(a)){
c[a]+=b;
}
return;
}
int main(){
long long m;
scanf("%lld %lld",&n,&m);
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
add(i,a[i]);
}
while(m--){
long long k,x,y;
scanf("%lld %lld",&x,&y);
add(x,y);
}
}
这样就可以利用树状数组去维护一个需要求多次前缀和的数组了。(也就是最基本的单点修改,区间查询)。
二、区间修改,单点查询。
直接使用树状数组维护原数组的局限性较大,这时,我们思考一下,如果我们对原数组进行处理一下(比如说对原数组进行差分、前缀和等等),再使用树状数组去维护,这时树状数组的应用性就变得更广泛了,比如说,我需要对一个数组中一个区间[l,r]全部+1,再求某一个点的值,一个区间内整体修改,我们可以想到使用差分数组,差分数组[l]+=1,[r+1]-=1,之后要求其中一点的值还需要做求和处理,我们想到用树状数组去求前缀和更加方便,所以就对原数组进行差分处理,然后利用树状数组去维护,求一个点的数时直接利用树状数组求前缀和,问题就解决了。代码如下
#include<bits/stdc++.h>
#define lowbit(x) x & -x
const int N=1e8;
int a[N];
int c[N];
int n;
void add(int a,int b){
for(;a<=n;a+=lowbit(a)){
c[a]+=b;
}
return;
}
int ask(int x){
int res=0;
while(x){
res+=c[x];
x -= lowbit(x);
}
return res;
}
int main(){
int m,b;
b=0;
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
for(int i=m;i>=1;i--){
a[i]=a[i]-a[i-1];
add(i,a[i]);
}
while(m--){
int h;
scanf("%d",&h);
if(h==1){
int x,y,k;
scanf("%d %d %d",&x,&y,&k);
add(x,k);
add(y+1,-k);
}
else{
long long x;
scanf("%d",&x);
printf("%d\n",ask(x));
}
}
}
三、区间修改,区间查询。
现在再拓展一下,如果我们需要对一个数组的区间修改,同时又要求一段区间的和,那怎么办呢?
首先我们想到,修改一个区间的值肯定要用一个树状数组去维护差分数组了,这一点不难,但然后我们有需要对修改过后的区间求和,这个值怎么算呢?
我们思考一下,对于现在的差分树状数组,我们需要的值即为下图中蓝色面积的值。
那么很显然这个值不是很好求。就想到一种办法,构建一个矩形,利用矩形的值去减去多余的值,剩下的就是我们想要的值了。如下图
不难发现,蓝色条的个数+1即为我的矩形边长,假设我求1~n的和,那么其中一边的边长就为(n+1),另一边边长为树状数组中第n个数,即从c[n],这样我们就把大矩形的值算出来了,即为(n+1)*c[n],我们再去想黄色部分的值该怎么算,不难发现,第一个方块的面积为1*c[1],第二个为2*c[2],以此类推,第n个方块的面积即为n*c[n]。再从1到n求和就可以了,又需要求前缀和,那么我们就在构建一个树状数组c2,其中的原数组为i*a[i],用c2维护该原数组,同时进行区间修改,这样我们的区间和就可以求出来了。这时有了1~n的求和方式,寻找每一段区间的求和的话同理,利用1~r的区间和减去1~l-1即为最终的答案了。代码如下
#include<bits/stdc++.h>
const int N=1e8;
int n;
int a[N];
int c[N];
int d[N];
int diff[N];
int lowbit(int x){
return x & -x;
}
void add1(int x,int y){
for(;x<=n;x+=lowbit(x)){
c[x]+=y;
}
return;
}
long long ask1(int x){
long long res=0;
while(x){
res+=c[x];
x-=lowbit(x);
}
return res;
}
void add2(int x,int y){
for(;x<=n;x+=lowbit(x)){
d[x]+=y;
}
return;
}
long long ask2(int x){
long long res=0;
while(x){
res+=d[x];
x-=lowbit(x);
}
return res;
}
int main(){
int m;
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
for(int i=n;i>=1;i--){
diff[i]=a[i]-a[i-1];
}
for(int i=1;i<=n;i++){
add1(i,diff[i]);
add2(i,diff[i]*i);
}
while(m--){
int h,x,y,k;
long long sum1,sum2;
scanf("%d",&h);
if(h==1){
scanf("%d %d %d",&x,&y,&k);
add1(x,k);
add1(y+1,-k);
add2(x,k*x);
add2(y+1,-(y+1)*k);
}
else{
scanf("%d %d",&x,&y);
sum2=(y+1)*ask1(y+1)-ask2(y+1);
sum1=x*ask1(x)-ask2(x);
printf("%d\n",sum2-sum1);
}
}
}