数据结构——树状数组详解

一.概念

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

  这种数据结构(算法)并没有C++和Java的库支持,需要自己手动实现。在Competitive Programming的竞赛中被广泛的使用。树状数组和线段树很像,但能用树状数组解决的问题,基本上都能用线段树解决,而线段树能解决的树状数组不一定能解决。相比较而言,树状数组效率要高很多。——百度百科 
  首先明确一点,树状数组的本质还是数组。
二.问题的引入
  先来看这样一道题目:
   

    要想查看本题,请点这里(有一些区别,本质相同)

       如果直接用数组进行模拟,修改的时间复杂度是O(1),查询是O(n)m次查询操作的时间复杂度就是O(mn),时间复杂度过高。

   树状数组就可以轻松处理这类问题,它是一个查询和修改的复杂度都为log(n)的数据结构。

    来看一下树状数组的样子:
图片出自水印。

 

    A代表原来的数组,C代表树状数组。为什么树状数组要长成这样?

   明确一点:树状数组是对二进制的应用。

   我们不妨把所有的数字都转换为二进制。来观察一下数字特征:

    

    举几个例子:

   用C来代表树状数组,用A来代表原数组:

   C(2) = A(1)+ A(2) 对应二进制 C(0010) = A(0010) + A(0001)

   C(4) = A(4)+A(2)+A(3) 对应二进制C(0100) = A(0100)+A(0010)+  A(0011)

   C(8) = A(8)+A(4)+A(6)+A(7)对应二进制C(1000) = A(1000)+A(0100)+A(0110)+A(0111)

   ......

   不难发现,对于任意的C[i]写成二进制的形式,都等于原来的数组A[i]的值本身,加上把i转换成二进制后,把首位变成0,并且开始逐位向后把0变成1,相加。

   例如: 1000(二进制)向后逐位变化,即1000--> 0100-->0110>0111

   ......

   构建出这样的树状数组后:

   查询A1到A8的和,只需要返回C8

   查询A1到A7,只需返回C7+C6+C4

   查询A1到A6,只需要返回C6+C4

   以此类推。

   这样便优化了询问的时间复杂度。

   那么说的这么好听,如何做到修改A的值时,顺便更新C的所有关于A的值呢?(例如,修改A[1]的时候更新C1,C2,C4,C8)

三.补码、lowbit函数

  1.补码

   首先说一说补码。

    

                                                       ——百度百科。

   补码是计算机表示符号数的一种方式,概念内容太杂乱,对于我们树状数组没有太大的用处,只需要了解两个事情:

   ·正整数的补码和原码(原来的二进制的代码)相同。

   ·负整数的补码将其正整数的原码所有为取反(0变1,1变0),之后加一。

   2.lowbit函数

   lowbit函数便是帮助我们找到二进制表达式中最低位1所对应的值。

   比如,6的二进制是110,所以lowbit(6)=2。 

   lowbit的实现方式一共两种:个人推荐下面这种写法:  

  int lowbit(int x)
  {
      return x&(-x);
  }

     &符号意思为按位与,把两个数的二进制一位一位比较,都为1,结果则为1,否则就是0。

    这个函数什么意思?将一个数x的原码和补码按位与?返回的就是最后一位1?

    的确是这样。

    举个例子(example):

      6 = 0110(二进制)

      它的补码为0010。按位与之后得到的确实就是最后一位1。

    正是因为补码在取反后+1,才有了lowbit函数的产生。

    您不妨打开计算器,切换到程序员模式,试上几组,或许会有新的领悟。

    毕竟“纸上得来终觉浅,绝知此事要躬行”。

    ......

  3.lowbit查询与更新的应用:

    首先先来运行一段代码:

  #include<stdio.h>
  int main()
  {
      int i,j;
      for(i=1;i<=8;i++)
      {
          printf("%d:",i);
          for(j=i;j<=8;j+=j&-j)
              printf("%d  ",j);
          printf("\n");
      }
      return 0;
  }    

    Output:
   1: 1 2 4 8
   2: 2 4 8
   3: 3 4 8
   4: 4 8
   5: 5 6 8
   6: 6 8
   7: 7 8
   8: 8

     

 

    图片数字对比来看更新A1,需要更新C1,C2,C4,C8。

   更新A2,需要更新C2,C4,C8。更新A3,需要更新C3,C4,C8.....

   正好与我们代码运行出来的结果一致。这就为我们向上更新提供了条件。

      再来看一下查询。如果查询A1到A8和,只需要返回C8,查询A1到A7,只需要返回C7+C6+C4

      再来看下面这组代码:

  #include<stdio.h>
  int main()
  {
      int i,j;
      for(i=1;i<=8;i++)
      {
          printf("%d:",i);
          for(j=i;j;j-=j&-j)
              printf("%d ",j);
          printf("\n");
      }
      return 0;
  }

    

  Output:
  1: 1
  2: 2
  3: 3 2
  4: 4
  5: 5 4
  6: 6 4
  7: 7 6 4
  8: 8

   这段代码只是将之前的i+=lowbit(i)修改为了i-=lowbit(i)

  再对比之前原图,查询A1到A8,只需要返回C8,查询A1到A7,只需要返回C7,C6,C4与我们代码运行的效果一致,这就为我们向下查询提供了条件。

 四.树状数组应用

  再回头看之前引出树状数组的题目,这时候就可以有一定的思路了。

    树状数组主要的函数分为两个,即更新函数和查询函数。

    

    void fix(int x)
    {
        int i;
        for(i=x;i<=n;i+=i&-i)    //向上更新
            e[i]++;        //一维树状数组e
    }
    //注意fix()的形参值x<=0时死循环。
    int getsum(int x)
    {
        int ret=0,i;       //返回值为ret,初值为0
        for(i=x;i;i-=i&-i)//向下查询
            ret+=e[i];
        return ret;    
    }
    //注意getsum()的形参值x<0时e[ ]数组越界。
  

 

    void fix(int x)
    {
        int i;
        for(i=x;i;i-=i&-i)    //向下更新
            e[i]++;        
    }
    int getsum(int x)
    {
        int ret=0,i;    
        for(i=x;i<=n;i+=i&-i)//向上查询
            ret+=e[i];
        return ret;    
    }

   树状数组可以有两个方向,1.向下更新,向上查询 2.向上更新,向下查询。本题用的是向上更新,向下查询。

           注意:树状数组更新时是增加量,初始时候更新量就是它本身。

   所以开始时利用更新函数,将每一个点更新,之后只要输出就行了。

  代码:

  

#include<stdio.h>
int a[100005];
int c[100005];
int n;
int lowbit(int x)
{
    return x&(-x);
}
void add(int x,int ad)
{
    for(int i = x;i<=n;i+=lowbit(i))
    {
        c[i]+=ad;
    }    
}
int getsum(int x)
{
    int ans = 0;
    for(int i = x;i>0;i-=lowbit(i))
    {
        ans+=c[i];
    }
    return ans;
}
int main()
{
    scanf("%d",&n);
    for(int i = 1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        add(i,a[i]);
    }
    int m;
    scanf("%d",&m);
    for(int i = 1;i<=m;i++)
    {
        char s[2];
        int x,y;
        scanf("%s%d%d",s,&x,&y);
        if(s[0]=='C')
        {
            add(x,y-a[x]);
            a[x] = y;
        }else
        {
            printf("%d\n",getsum(y) - getsum(x-1));
        }
    }
    return 0;
}

    树状数组其他应用,之后会陆续补充。若对于其有新的理解,也会加入到其中。

   更新时间(2018.12.6)

   


去超越自己不认同的人,去追赶自己理想的人。我想所谓的成长,就是不断的重复这些吧。

 

posted @ 2018-12-06 16:28  lizitong  阅读(782)  评论(0编辑  收藏  举报