树状数组
发布日期:2021-06-29 05:37:51 浏览次数:4 分类:技术文章

本文共 4239 字,大约阅读时间需要 14 分钟。

树状(Binary Indexed Tree(BIT), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个的值(如果加入多个辅助数组则可以实现区间修改与区间查询)。——百度百科

树状数组作用就是给定一个数组,在log(n)时间内求数组内某个区间的和,并且可以在log(n)时间内修改其中的某个元素。本文只研究修改单个值,区间求和的情况。

 

需要维护一个C数组,如下图所示:

 

令这棵树的结点编号为C1,C2...Cn。令每个结点的值为这棵树的值的总和,那么容易发现:

C1 = A1

C2 = A1 + A2

C3 = A3

C4 = A1 + A2 + A3 + A4

C5 = A5

C6 = A5 + A6

C7 = A7

C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8

...

C16 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 + A9 + A10 + A11 + A12 + A13 + A14 + A15 + A16

这里有一个有趣的性质:

设节点编号为x,那么这个节点管辖的区间为2^k(其中k为x二进制末尾0的个数)个元素。因为这个区间最后一个元素必然为Ax,

所以很明显:Cn = A(n – 2^k + 1) + ... + An

例如:C[6]这个结点,6的二进制为110,末尾有1个0,所以它的管辖区间是2^1 = 2个元素,即5和6。

 

求2^k的简便方法是使用位运算:

int lowerbit(int x)

{
return x&(x^(x–1));
}

 

利用机器补码特性,也可以写成:

int lowerbit(int x)

{
    return x&-x;
}

解释:lowbit(k)就是把k的二进制的高位1全部清空,只留下最低位的1,比如10的二进制是1010,则lowbit(k)=lowbit(1010)=0010(2进制),介于这个lowbit在下面会经常用到,这里给一个非常方便的实现方式,比较普遍的方法lowbit(k)=k&-k,这是位运算,我们知道一个数加一个负号是把这个数的二进制取反+1,如-10的二进制就是-1010=0101+1=0110,然后用1010&0110,答案就是0010了,也就是2^k的结果。

 

修改某个值时:

 

如果要修改一个值,那么需要修改所有管辖这个数的C[]数组的值。

比如要修改a[3],由于a[3]同时被c[3]、c[4]、c[8](假设只有8个数字)管辖,那么更新a[3]的时候,就需要同时修改c[3]、c[4]、c[8]。

修改代码:

 

void add(int k,int num)  //将第k个元素加上num{         while(k<=n)          {              tree[k]+=num;              k+=k&-k;          }  }

 

 

求和:

如果求1~n这个区间和的话,只需要将log(n)个C[]数组中的值想加即可。比如要求1~6这个区间的和,那就只需将C[6]和cC[4]相加即可。

求和代码:

 

int read(int k)//1~k的区间和  {         int sum=0;          while(k)          {              sum+=tree[k];              k-=k&-k;          }          return sum;  }

参考:

 

整体代码:

 

#include 
#include
#define N 1005int c[N], n;int lowbit(int x){ return x&(-x);}void add(int k, int x){ while(k <= n){ c[k] += x; k += lowbit(k); }}int sum(int k)//求前k项和{ int s=0; while(k>0) { s+=c[k]; k-=lowbit(k); } return s;}int main(){ int q, x, a, b; while(~scanf("%d%d", &n, &q)){ memset(c, 0, sizeof(c)); for(int i = 1; i <= n; i ++){ scanf("%d", &x); add(i,x); } while(q--){ scanf("%d%d", &a, &b); int ans = sum(b) - sum(a-1); printf("%d\n", ans); } } return 0;}

 

 

 

上面讲的都是一维树状数组,树状数组也可以扩展到二维,即求和时改为求某个子矩阵的和。

举个例子来看看C[][]的组成。 

     设原始二维数组为: 
  A[][]={
{a11,a12,a13,a14,a15,a16,a17,a18,a19}, 
         {a21,a22,a23,a24,a25,a26,a27,a28,a29}, 
         {a31,a32,a33,a34,a35,a36,a37,a38,a39}, 
         {a41,a42,a43,a44,a45,a46,a47,a48,a49}}; 
那么它对应的二维树状数组C[][]呢? 
记: 
  B[1]={a11,a11+a12,a13,a11+a12+a13+a14,a15,a15+a16,...} 这是第一行的一维树状数组 
  B[2]={a21,a21+a22,a23,a21+a22+a23+a24,a25,a25+a26,...} 这是第二行的一维树状数组 
  B[3]={a31,a31+a32,a33,a31+a32+a33+a34,a35,a35+a36,...} 这是第三行的一维树状数组 
  B[4]={a41,a41+a42,a43,a41+a42+a43+a44,a45,a45+a46,...} 这是第四行的一维树状数组 
那么: 
C[1][1]=a11,C[1][2]=a11+a12,C[1][3]=a13,C[1][4]=a11+a12+a13+a14,c[1][5]=a15,C[1][6]=a15+a16,... 
   这是A[][]第一行的一维树状数组 
C[2][1]=a11+a21,C[2][2]=a11+a12+a21+a22,C[2][3]=a13+a23,C[2][4]=a11+a12+a13+a14+a21+a22+a23+a24, 
C[2][5]=a15+a25,C[2][6]=a15+a16+a25+a26,... 
   这是A[][]数组第一行与第二行相加后的树状数组 
C[3][1]=a31,C[3][2]=a31+a32,C[3][3]=a33,C[3][4]=a31+a32+a33+a34,C[3][5]=a35,C[3][6]=a35+a36,... 
   这是A[][]第三行的一维树状数组 
C[4][1]=a11+a21+a31+a41,C[4][2]=a11+a12+a21+a22+a31+a32+a41+a42,C[4][3]=a13+a23+a33+a43,... 
    这是A[][]数组第一行+第二行+第三行+第四行后的树状数组 

C[]数组在二维树状数组中需要同时管辖行和列,管辖的范围与一维相似,不论是行还是列都是管辖lowbit(i)的范围。

 

修改代码:

 

void modify(int x,int y,int data){    for(int i=x;i

 

求和代码:

 

int get_sum(int x,int y){    int res=0;    for(int i=x;i>0;i-=lowbit(i))        for(int j=y;j>0;j-=lowbit(j))            res+=c[i][j];    return res;}

整体代码:

 

 

#include
#include
#include
#include
using namespace std;const int MAXN = 1010;int c[MAXN][MAXN];int lowbit(int x){ return x & (-x);}void modify(int x,int y,int data){ for(int i=x;i
0;i-=lowbit(i)) for(int j=y;j>0;j-=lowbit(j)) res+=c[i][j]; return res;}int main(){ int n,x,y,x1,y1, date; char str[5]; scanf("%d",&n); memset(c,0,sizeof(c)); while(n--) { scanf("%s",str); if(strcmp(str,"add")==0) { scanf("%d%d%d",&x,&y, &date); modify(x,y,date); } else { scanf("%d %d %d %d",&x,&x1,&y,&y1); int ans=get_sum(x1,y1)-get_sum(x-1,y1)-get_sum(x1,y-1)+get_sum(x-1,y-1); printf("%d\n",ans); } } return 0;}

 

 

 

 

 

 

 

 

转载地址:https://blog.csdn.net/zhj_fly/article/details/76643638 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:度度熊与邪恶大魔王——dp
下一篇:RMQ算法

发表评论

最新留言

表示我来过!
[***.240.166.169]2024年04月22日 10时08分42秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章

Gradle for Android(七)——一些使用技巧 2019-04-29
Android Bugs——Caused by: android.util.AndroidRuntimeException: Calling startActivity() from outside 2019-04-29
Android——6.0 Scrollview嵌套Recyclerview导致显示不全,滑动卡顿问题解决 2021-07-02
Android Bugs——RecyclerView.Adapter java.lang.IllegalStateException: The specified child already has 2021-07-02
Materail Design 入门(六)—— TabLayout之设置自定义指示器宽度(3) 2021-07-02
Android内存优化——使用SparseArray和ArrayMap代替HashMap 2021-07-02
Android——仿ios阻尼回弹动画 2021-07-02
ExoPlayer实现设置画面比例功能 2021-07-02
Android——DialogFragment(一)用法介绍 2021-07-02
(六)RecycleView 回收复用机制总结 2021-07-02
Android 中对Java对象深拷贝的方法 2021-07-02
Mac Charles 替换和改写接口地址、环境、参数、状态码等 2021-07-02
高并发解决思路 2021-07-02
单片机实验四:定时器控制发光二极管的亮灭+简单输出连续矩形脉冲 2021-07-02
微机接口实验一:七段数码管循环动态显示00~99 2021-07-02
微机接口实验二:键盘显示控制实验(翻转法实现) 2021-07-02
微机接口实验三:交通灯控制实验(C口置位/复位控制字的使用) 2021-07-02
微机接口实验四:可编程定时器/计数器8254 2021-07-02
浮点加减运算中关于结果规格化的思考 2021-07-02
DL入门(4):长短期记忆网络(LSTM) 2021-07-02