5.4 树状数组

5.4 树状数组

树状数组我个人理解上就是能够解决线段树的一部分问题的一个简化版本的线段树,通过二进制的一些运算进行查询和更新。

单点修改,区间查询BIT

假设对于长度为n的数列{a1,a2······an},有如下两种操作:

1.修改元素add(k,x):
2.求和sum(x):求a1到ax的和(ai到aj的和可以用两个sum值相减)。

树状数组的代码如下:

#define lowbit(x) ((x)&-(x)))
void add(int k,int x){
	while (k<=n){tree[k]+=x; k+=lowbit(k);}
}
int sum(int x){int sum=0;
	while (x>0){sum+=tree[x]; x-=lowbit(x);} return sum;
} 

我没有写注释,从代码看起来非常的简洁,初始化时tree数组为0,不断地通过add(i,ai)的方式进行读取,这即是“单点修改,区间查询BIT的模板”。

这其中代码的关键在于lowbit()操作,-x相当于x对应二进制码的取反再+1,之前有提到过x&(x-1)能够消除当前二进制数的最后一个1,x&(-x)的功能相当于找到x二进制数的最后一个1。

例如x=1100111011100,那么lowbit(x)=100=4。

那么对于n=9的情况,tree数组的值如下:

tree[1]=a[1]。

tree[2]=a[1]+a[2]。

tree[3]=a[3]。

tree[4]=a[1]+a[2]+a[3]+a[4]。

tree[5]=a[5]。

tree[6]=a[5]+a[6]。

tree[7]=a[7]。

tree[8]=a[1]+a[2]······+a[8]。

tree[9]=a[9]。

a数组与tree数组之间的关系如下图所示:

在这里插入图片描述
(这里的c数组就是上面说的tree数组)

可以很明显的发觉,树状数组构成的关系图和线段树的关系图是非常类似的,只是可能去掉了一些对求和运算没什么影响的区间。


区间修改,单点查询BIT

首先我们对a数组进行一个差分处理,d[i]=a[i]-a[i-1],d[1]=a[1]。那对d[1]到d[i]进行求和,得到的结果即是a[i],于是sum[i]就做到了单点查询的功能。

对[L,R]区间内的a[i]都加上x,然而实际上对于进行过差分处理的d[i]数组,真正发生改变的就只有d[L]和d[R+1],因为对于L<i<=R,d[i] (新)=(a[i]+x)-(a[i-1]+x)=a[i]-a[i-1]=d[i]。所以对于区间修改,我们只需要进行两次单点修改即可,模板如下:

void init(int n) {for(int i=1;i<=n;i++) tree[i]=0;}//初始化共有n个点
void add(int pos,int val){//单点更新,pos为更新的下标,val为增加的值 
    while(pos<=n) {tree[pos]+=val; pos+=lowbit(pos);}
}//区间更新,a[l]到a[r]加x,实际上就是d[l]加上x,d[r+1]减去x 
void range_add(int l,int r,int x){add(l,x); add(r+1,-x);}
int sum(int pos){int ans=0;//查询a[pos]的值,即为d[1]到d[pos]的和 
	while(pos>0) {ans+=tree[pos]; pos-=lowbit(pos);} return ans;
}

上面参与add运算,range_add运算,sum运算的数组都是差分过后的d数组而不是原数组。


区间修改,区间查询BIT

和区间修改,单点查询的BIT多一个区间查询,a[1]+a[2]······+a[i]=d[1]*i+d[2]*(i-1)·······+d[i]=(i+1)*(d[1]+d[2]······+d[i])-d[1]*1-d[2]*2·····-d[i]*i。

于是可以新定义一个tree2数组来存储d[i]*i。

void init(int n){memset(tree1,0,sizeof(tree1)); memset(tree2,0,sizeof(tree2));}
void add(int pos,int val){//单点更新,tree2存储的是d[i]*i,所以加上的val乘的是pos 
    for(int i=pos;i<=n;i+=lowbit(i)) {tree1[i]+=val; tree2[i]+=val*pos;} 
}//区间更新,只对l和r+1进行修改 
void range_add(int l,int r,int x){add(l,x); add(r+1,-x);}
int sum(int pos){int ans=0;//求和,求出来的是a[1]到a[i]的和 
    for(int i=pos;i>0;i-=lowbit(i)) ans+=(pos+1)*tree1[i]-tree2[i];
    return ans;
}//查询a[1]到a[r]的和,a[1]到a[l-1]的和,两个和相减,即是a[l]到a[r]的和 
int range_sum(int l,int r){return sum(r)-sum(l-1);}

二维树状数组

就是循环里面在套一层循环,不过和二维线段树与一维线段树的区别上从还是有一定的差别。

单点修改 区间查询
void init(int n,int m){memset(tree,0,sizeof(tree));}//初始化矩阵 
void add(int x,int y,int val){//在点(x,y)加上val
    for(int i=x;i<=n;i+=lowbit(i)) for(int j=y;j<=m;j+=lowbit(j)) tree[i][j]+=val;
}//求左上角为(1,1)右下角为(x,y)的矩阵的元素和
int sum(int x,int y){int ans=0;
    for(int i=x;i>0;i-=lowbit(i)) for(int j=y;j>0;j-=lowbit(j)) ans+=tree[i][j];
    return ans;
}
区间修改 单点查询

同样也是用差分的方法,令d[i][j]=a[i][j]−(a[i−1][j]+a[i][j−1])+a[i−1][j−1],当你为了查询某个点(x,y)的值时,a[x][y]即等于以d[1][1]为左下角,d[x][y]为右上角的矩阵的元素之和。

区间修改将左上角为(x1,y1)右下角为(x2,y2)的矩阵全部加上x,同样的只有d[x1][y1],d[x1][y2+1],d[x2+1][y1],d[x2+1][y2+1]发生了真正的改变,只需要对4个端点值进行修改:

void init(int n,int m){memset(tree,0,sizeof(tree));}//初始化矩阵
void add(int x,int y,int val){//单点更新 
    for(int i=x;i<=n;i+=lowbit(i)) for(int j=y;j<=m;j+=lowbit(j)) tree[i][j]+=val;
}//左上角为(x1,y1)右下角为(x2,y2)的矩阵全部加上x,实际上只需要对4个端点的值进行修改 
void range_add(int x1,int y1,int x2,int y2,int x){
	add(x1,y1,x); add(x1,y2+1,-x); add(x2+1,y1,-x); add(x2+1,y2+1,x); 
}//查询点(x,y)的值 
int ask(int x,int y){int ans=0;
    for(int i=x;i>0;i-=lowbit(i)) for(int j=y;j>0;j-=lowbit(j)) ans+=tree[i][j];
    return ans;
}
区间修改,区间查询

如果是区间查询,就需要你能够求出以(1,1)为左下角,(x,y)为右上角的矩阵的和。化简能够得到:

在这里插入图片描述

于是比起一维的树状数组,一共需要4个属性分别去存储d[i][j],d[i][j]*i,d[i][j]*j,d[i][j]*i*j。

最后求以(x1,y1)为左下角,(x2,y2)为右上角的矩阵的和,就是用以(1,1)为左下角,(x2,y2)为右上角的大矩阵,减去两个下面和左边两个小矩阵,再加上左下角最小矩阵的值。

void init(int n,int m){//初始化矩阵
	memset(tree1,0,sizeof(tree1)); memset(tree2,0,sizeof(tree2));
    memset(tree3,0,sizeof(tree3)); memset(tree4,0,sizeof(tree4));
}//tree1-tree4分别存储d[i][j],d[i][j]*i,d[i][j]*j,d[i][j]*i*j 
void add(int x,int y,int val){
    for(int i=x;i<=n;i+=lowbit(i)) for(int j=y;j<=m;j+=lowbit(j)){
        tree1[i][j]+=val; tree2[i][j]+=val*x; tree3[i][j]+=val*y; tree4[i][j]+=val*x*y;
    }
}//左上角为(x1,y1)右下角为(x2,y2)的矩阵全部加上x
void range_add(int x1,int y1,int x2,int y2,int x){//实际上只对四个端点进行操作 
    add(x1,y1,x); add(x1,y2+1,-x); add(x2+1,y1,-x); add(x2+1,y2+1,x);
}//查询左上角为(1,1)右下角为(x,y)的矩阵的元素和,按照给出的算式进行加减 
int sum(int x,int y){int ans=0;
    for(int i=x;i>0;i-=lowbit(i)) for(int j=y;j>0;j-=lowbit(j)){
        ans+=(x+1)*(y+1)*tree1[i][j]; ans-=(y+1)*tree2[i][j]+(x+1)*tree3[i][j]; ans+=tree4[i][j];
    } return ans;
}//几何手段求出最终的值 
int range_ask(int x1,int y1,int x2,int y2){return sum(x2,y2)-sum(x1-1,y2)-sum(x2,y1-1)+sum(x1-1,y1-1);}

习题

书上的话只有最简单的,单点修改,区间查询的一维树状数组,后面的区间修改,区间查询,二维树状数组都是我在别人的博客学习的。

树状数组的习题,由于数据结构考的不多,而且大部分的题目可以用线段树去做,线段树那里除了综合的难题我基本都解决了,所以先搁置在这里。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值