如果给你一个数组,让你求某个区间的和,你很自然会想到遍历一遍数组,复杂度是O(n),但是如果有多次询问呢,你也许会想到用前缀数组,通过O(n)的预处理,达到O(1)的查询,但是如果要更新某个元素的值呢,如果用前缀和的思想,每更新一个元素就会更新前缀数组,每次复杂度是O(n),如果有n次更改,复杂度为O(n^2)。有没有更快的呢,这时候树状数组就排上用场了,树状数组可以用来解决动态数组连续和的问题。
树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree):是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询)。
树状数组大致就长下面这个模样。
那么树状数组到底是怎么把修改和求和都做到O(log(n))呢
当我么求和的时候,c[1]=A[1],c[2]=c[1]+A[2],c[4]=c[2]+c[3]+A[4],当我们更新A[2]的时候,我们只需要更新c[2],c[4],c[8],所以相当与每层我们只操作一个结点,即O(log(n))。
当我们更新的时候c[i]的时候,就从c[i]开始往右走,边走边往上爬。比如我们更新c[2]的时候,我们先更新c[2],然后更新c[4]、c[8]、c[16]。
当我们要求和的时候,我们从c[i]开始往左走,边走边往上爬。比如我们求前7项和,先找到c[7],然后加上c[6],最后加上c[4]。
那我们怎么才能做到这种奇怪的走法呢,
图中灰色结点表示c[i],白色结点表示以i结尾的和,很神奇,灰色结点c[4]=A[1]+A[2]+A[3]+A[4]表示以4结尾的和,我们很好理解,但是为什么灰色结点c[6]表示的表示的只有A[5]+A[6] 我们怎么来控制个数呢,对于c[4],我们相当于从下标4开始,往前数了4个数,对于c[6],我们从下标6开始往前数两个数,有个公式c[i]=A[i-2^k+1]+A[i-2^k+2]......+A[i],其中k表示i的二进制末尾0的个数
i | 2进制 | k | 下标取值范围 | |
1 | 0001 | 0 | i-2^k+1...i=1...1 | c[1]=A[1] |
2 | 0010 | 1 | i-2^k+1...i=1...2 | c[2]=A[1]+A[2]=c[1]+A[2] |
3 | 0011 | 0 | i-2^k+1...i=3...3 | c[3]=A[3] |
4 | 0100 | 2 | i-2^k+1...i=1...4 | c[4]=A[1]+A[2]+A[3]+A[4]=c[2]+c[3]+A[4] |
5 | 0101 | 0 | i-2^k+1...i=5...5 | c[5]=A[5] |
6 | 0110 | 1 | i-2^k+1...i=5...6 | c[6]=A[5]+A[6]=c[5]+A[6] |
所以我们求和的时候我们就先找i,然后往前数2^k个数,既然k表示i的二进制末尾0的个数,其实就是前一位对应的权值,比如4的二进制为0100,末尾有2个0,所以2^k=4,从后往前数第一位不为0的出现在第三位,权值为100,即4。
那我们我们怎么计算这个权值呢,lowbit(i)=2^k=x&-x,先给出代码
int lowbit(int x)
{
return x&(-x);
}
还是以4为栗子:lowbit(4)=4,4二进制为0100,-4的二进制为4的二进制按位取反然后加1,-4=1100,所以4&(-4)=0100 即为4;
所以c[i]等于以i开始从后往前数i&(-i)个数。
当我们更新c[i]的时候,就从c[i]开始往右走,边走边往上爬。所以i逐渐变大但小于等于n,即更新时的下标变化:i+=lowbit(i)
当我们要求和的时候,我们从c[i]开始往左走,边走边往上爬。所以i逐渐变小但大于0,即求和时下标变化:i-=lowbit(i)
预处理方法:先把A和c数组都清空,然后执行n次add操作,所以时间复杂度为O(nlog(n))。
树状数组主要是用来维护动态数组连续和问题。
HDU 1166 敌兵布阵
题意:有n个营地,每个营地开始都有一定数量的敌军,然后有下面三种命令
(1) Add i j,i和j为正整数,表示第i个营地增加j个人(j不超过30)
(2)Sub i j ,i和j为正整数,表示第i个营地减少j个人(j不超过30);
(3)Query i j ,i和j为正整数,i<=j,表示询问第i到第j个营地的总人数
题解:由于n=50000比较大,而且命令多达40000,所以可以用树状数组来解决,当然也可以用线段树来维护区间和,只不过线段树代码量比较大,所以对于这种单点更新求区间和问题最好使用树状数组,写起来节约时间,也不容易出错。下面分别给出树状数组和线段树的代码。
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=50000+7;
int tree[maxn<<1]; //树状数组
int n;
char s[10];
int lowbit(int x)
{
return x&(-x);
}
void add(int x,int d)
{
while(x<=n){
tree[x]+=d;
x+=lowbit(x);
}
return ;
}
int sum(int x)
{
int s=0;
while(x>0){
s+=tree[x];
x-=lowbit(x);
}
return s;
}
int main()
{
int T;
scanf("%d",&T);
for(int t=1;t<=T;t++){
memset(tree,0,sizeof(tree));
scanf("%d",&n);
for(int i=1;i<=n;i++){
int num;
scanf("%d",&num);
add(i,num); //开始默认所有的点都是0 每读到一个数就开始往上更新
}
printf("Case %d:\n",t);
int x,y;
while(scanf("%s",s)==1&&strcmp(s,"End")){
if(!strcmp(s,"Query")){
scanf("%d %d",&x,&y);
printf("%d\n",sum(y)-sum(x-1));
}
else if(!strcmp(s,"Add")) {
scanf("%d %d",&x,&y);
add(x,y);
}
else {
scanf("%d %d",&x,&y);
add(x,-y);
}
}
}
return 0;
}
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn =1e5+7;
long long int segtree[maxn<<2],a[maxn];
int n,x,y;
char s[10];
void pushup(int now) //维护的信息 维护区间和
{
segtree[now]=segtree[now<<1]+segtree[(now<<1)|1];
return ;
}
void build_tree(int l,int r,int now)
{
if(l==r){ //递归到叶子结点赋值
segtree[now]=a[l];
return ; //记得递归一定要renturn 不然就会疯狂递归
}
int mid=(l+r)>>1;
build_tree(l,mid,now<<1);
build_tree(mid+1,r,(now<<1)|1);
pushup(now);
}
long long int query(int ql,int qr,int l,int r,int now)
{
if(ql<=l&&r<=qr){ //如果当前区间完全包含在查询区间 就return 当前递归子函数不再向下递归
return segtree[now];
}
long long int ans=0;
int mid=(l+r)>>1;
if(ql<=mid)ans+=query(ql,qr,l,mid,now<<1);
if(qr>mid)ans+=query(ql,qr,mid+1,r,(now<<1)|1);
return ans;
}
void update(int pos ,int c,int l,int r,int now)
{
if(l==r){
segtree[now]+=c;
return ;
}
int mid=(l+r)>>1;
if(pos<=mid)update(pos,c,l,mid,now<<1);
else update(pos,c,mid+1,r,(now<<1)|1);
pushup(now);
}
int main()
{
int T;
scanf("%d",&T);
for(int t=1;t<=T;t++){
scanf("%d",&n);
memset(segtree,0,sizeof(segtree));
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
build_tree(1,n,1);
printf("Case %d:\n",t);
while(scanf("%s",s)==1&&strcmp(s,"End")){
if(!strcmp(s,"Query")){
scanf("%d %d",&x,&y);
printf("%lld\n",query(x,y,1,n,1));
}
else if(!strcmp(s,"Add")){
scanf("%d %d",&x,&y);
update(x,y,1,n,1);
}
else {
scanf("%d %d",&x,&y);
update(x,-y,1,n,1);
}
}
}
return 0;
}