说起树状数组,那就不得不提到线段树,它们可以说都可以完成区间修改和区间查询,但是树状数组的常数小,还好写,但是缺点是不能实现其他的高端操作,因此我们应该把这两种方法都掌握。对于那些简单的操作,可以用树状数组来写,反之就得用线段树了。
树状数组的主要思想跟线段树差不多,都是采用分治,但是他们的代码实现并不一样,主要是线段树是真真正正的树形结构,而树状数组则是虚拟的树形结构。
比如下面的这张图:
A数组是我们输入的数据,C数组便是我们的树状数组。
我们可以看到,树状数组的个数跟我们的输入数据的个数一样。
但是线段树却比树状数组用的空间大。
且我们可以找规律:
c1=a1;
c2=a1+a2;
c3=a3;
c4=a1+a2+a3+a4;
这个似乎看不出来关系,但是我们可以把数字转变为二进制。
c0001=a0001
c0010=a0001+a0010
c0011=a0011
c0100=a0001+a0010+a0011+a0100
我们就可以看出C[这个数的下标再加上该数二进制从右数第一个1位的右边的数的和]+=C[这个数]。
这个搞明白了,那单点修改就会了,如果我们想区间查询,那可以用前缀和维护。
代码(luoguP3374):
#include<iostream> #include<cstdio> #include<cstring> using namespace std; #define lowbit(x) (x&-x) int a[1000100],ans[1000010],n,m,b; void update(int x,int y) { while(x<=n) { ans[x]+=y; x+=lowbit(x); } } int query(int x) { int sum=0; while(x!=0) { sum+=ans[x]; x-=lowbit(x); } return sum; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); update(i,a[i]); } for(int j=1;j<=m;j++) { int x,y; scanf("%d%d%d",&b,&x,&y); if(b==1) update(x,y); else printf("%d ",query(y)-query(x-1)); } return 0; }
这只能实现单点修改,如果想实现区间修改可以用差分数组,所谓差分数组可以看一下下面的例子。
数组a[]={1,6,8,5,10},那么差分数组b[]={1,5,2,-3,5}
可以知道差分数组b[i]=b[i]-b[i-1]。
并且如果差分一下的话比如要修改3-5都加上3,那b[]={1,5,5,-3,2}。
可以发现如果要修改一个区间那可以把b[左区间端点]加上这个数,b[右区间端点+1]减去这个数.
这样就可以实现此时的树状数组就是差分数组所以我们就可以跟上文的代码一样,只是在修改的时候不一样罢了。
还要注意的一点是易证差分数组的前缀和恰恰是这个位置的ans值。
因此上文的query函数是求第x位置的前缀和,所以我们如果用差分数组的话那query函数依旧有用
代码():
#include<iostream> #include<cstring> #include<cstdio> #include<algorithm> #define lowbit(x) (x&(-x)) using namespace std; int ans[1000100],a,n,m,now,last; void update(int x,int add) { while(x<=n) { ans[x]+=add; x+=lowbit(x); } } int query(int x) { int res=0; while(x) { res+=ans[x]; x-=lowbit(x); } return res; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) { scanf("%d",&a); last=now; now=a; update(i,now-last); } for(int i=1;i<=m;i++) { int flag,x,y,k; scanf("%d",&flag); if(flag==1) { scanf("%d%d%d",&x,&y,&k); update(x,k); update(y+1,-k); } else { scanf("%d",&x); printf("%d ",query(x)); } } return 0; }