树状数组

来源:互联网 发布:高新区网络问政平台 编辑:程序博客网 时间:2024/06/15 20:44

结合这两个博客看懂的:

点击打开链接

点击打开链接

关于树状数组的实现

    数据结构 - 树状数组 ( Binary Indexed Tree,BIT,二分索引树 ),它只有两种基本操作,并且都是操作线性表的数据的:

      1、add( i, 1 )      (1<=i<=n)                       对第i个元素的值自增1           O(logn)
      2、sum( i )         (1<=i<=n)                       统计[1...i]元素值的和             O(logn)
      试想一下,如果用HASH来实现这两个函数,那么1的复杂度是O(1),而2的复杂度就是O(n)了,而树状数组实现的这两个函数可以让两者的复杂度都达到O(logn),具体的实现先卖个关子,留到第二节着重介绍。
      有了这两种操作,我们需要将它们转化成之前设计的数据结构的那三种操作,首先:
      1、插入(Insert),对应的是 add(i, 1),时间复杂度O( logn )
      2、删除(Delete), 对应的是 add(i, -1), 时间复杂度O( logn )
      3、询问(Query), 由于sum( i )能够统计[1...i]元素值的和,换言之,它能够得到我们之前插入的数据中小于等于i的数的个数,那么如果能够知道sum(i) >= r + 1的最小的i,那这个i就是所有插入数据的中位数了(因为根据上文的条件,插入的数据时刻保证有2r+1个)。因为sum(i)是关于 i 的递增函数,所以基于单调性我们可以二分枚举i (1 <= i <= n),得到最小的 i 满足sum(i) >= r + 1,每次的询问复杂度就是 O( logn * logn )。 一个logn是二分枚举的复杂度,另一个logn是sum函数的复杂度。


节点的含义:

每个整数都能表示为一些2的幂次方的和,比如13,其二进制表示为1101,所以它能表示为:13 = 2^0 + 2^2 + 2^3 .类似的,累积频率可表示为其子集合之和。在本文的例子中,每个子集合包含一些连续的频率值,各子集合间交集为空。比如累积频率c[13]=f[1]+f[2]+…+f[13],可表示为三个子集合之和(数字3是随便举例的,下面的划分也是随便举例的),c[13]=s1+s2+s3,其中s1=f[1]+f[2]+…+f[4],s2=f[5]+f[6]+…+f[12],s3=f[13]。

idx记为BIT的索引,r记为idx的二进制表示中最右边的1后面0的个数,比如idx=1100(即十进制的12),那么r=2。tree[idx]记为f数组中,索引从(idx-2^r +1)到idx的所有数的和,包含f[idx-2^r +1]和f[idx]。即:tree[idx]=f[idx-2^r +1]+…+f[idx],见表1.1和1.2,你就会一目了然。我们也可称idx对索引(idx-2^r +1)到索引idx负责。(We also write that idx is responsible for indexes from (idx-2^r +1)to idx)

关于2的k次方的求解:

这里介绍一种O(1)的方法计算2k的方法。

       来看一段补码小知识:
         清楚补码的表示的可以跳过这一段,计算机中的符号数有三种表示方法,即原码反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。这里只讨论整数补码的情况,在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。整数补码的表示分两种:
                  正数:正数的补码即其二进制表示;
                           例如一个8位二进制的整数+5,它的补码就是 00000101 (标红的是符号位,0表示"正",1表示“负”)
                  负数:负数的补码即将其整数对应的二进制表示所有位取反(包括符号位)后+1;
                           例如一个8位二进制的整数-5,它的二进制表示是00000101,取反后为11111010,再+1就是11111011,这就是它的补码了。
         下面的等式可以帮助理解补码在计算机中是如何工作的
                      +5 + (-5) = 00000101 + 11111011 = 1 00000000 (溢出了!!!) = 0 
         这里的加法没有将数值位和符号位分开,而是统一作为二进制位进行计算,由于表示的是8进制的整数,所以多出的那个最高位的1会直接舍去,使得结果变成了0,而实际的十进制计算结果也是0,正确。
       补码复习完毕,那么来看下下面这个表达式的含义:           x & (-x)       (其中 x >= 0)
         首先进行&运算,我们需要将x和-x都转化成补码,然后再看&之后会发生什么,我们假设 x 的二进制表示的末尾是连续的 k 个 0,令x的二进制表示为  X0X1X2…Xn-2Xn-1,  则 {X= 0 | n-k <= i < n}, 这里的X0表示符号位。
       x 的补码就是由三部分组成:               (0)(X1X2…Xn-k-1)(k个0)   其中Xn-k-1为1,因为末尾是k个0,如果它为0,那就变成k+1个0了。
      -x的补码也是由三部分组成:      (1)(Y1Y2…Yn-k-1)(k个0)     其中Yn-k-1为1,其它的XiYi相加为1,想补码是怎么计算的就明白了。
   那么 x & (-x) 也就显而易见了,由两部分组成 (1)(k0)表示成十进制为 2k 啦。
      由于&的优先级低于-,所以代码可以这样写:
int lowbit(int x) {       return x & -x;    }


关于求和的sum函数:

想要求i-index的和值

tree数组是这么定义的:tree[idx] = f[idx-2^r +1] +…+ f[idx]. 上面的程序sum加上tree[idx]后,去掉idx最后的1,假设变为idx1,那么有idx1 = idx-2^r , sum接下来加上tree[idx1]= f[idx1-2^r1 +1] +…+ f[idx1] = f[idx1-2^r1 +1] +…+ f[idx-2^r ],我们可以看到tree[idx1]表达示的最右元素为f[idx-2^r ],这与tree[idx]表达式的最左元素f[idx-2^r +1]无缝地连接了起来。所以,只需要这样操作下去,即可求得f[1]+…+f[idx],即c[idx]的结果。

所以代码如下

int sum(int idx){    int res = 0;   while(idx > 0)    {        res += tree[idx];        idx -= (idx & -idx);    }    return res;}

关于更新的add函数:

更新操作就是之前提到的add(i, 1) 和 add(i, -1),更加具体得,可以推广到add(i, v),表示的其实就是 f[i] = f[i] + v。但是我们不能在原数组f上操作,而是要像求和操作一样,在树状数组tree上进行操作。

      那么其实就是求在fi改变的时候会影响哪些Ci,看图二-1-1的树形结构就一目了然了,fi的改变只会影响treei及其祖先结点,f5的改变影响的是tree5、tree6、tree8;而f1的改变影响的是tree1、tree2、tree4、tree8。
      也就是每次add(i, v),我们只要更新treei以及它的祖先结点,之前已经讨论过两个结点父子关系是如何建立的,所以给定一个x,一定能够在最多log(n) (这里的n是之前提到的值域) 次内更新完所有x的祖先结点,add(i, v)的主体代码(去除边界判断)也只有一行代码:
void add(int idx,int val){    while(idx <= n)    {        tree[idx] +=val;        idx += (idx & -idx);    }}


最后奉上一个求逆序数的题目吧:
HDU(1394)
#include <iostream>#include<string>#include<algorithm>#include<cstdio>#include<cstring>using namespace std;const int maxn=5000+5;const int inf=0x3f3f3f3f;int n;int s[maxn];int tree[maxn*2];int sum(int idx){    int res = 0;   while(idx > 0)    {        res += tree[idx];        idx -= (idx & -idx);    }    return res;}void add(int idx,int val){    while(idx <= n)    {        tree[idx] +=val;        idx += (idx & -idx);    }}int main(){ while(cin>>n)    {        int ans=inf;        int res=0;        memset(tree,0,sizeof(tree));        for(int i=0;i<n;i++)        {            scanf("%d",&s[i]);            res+=sum(n)-sum(s[i]+1);            add(s[i]+1,1);        }        if(res<ans)            ans=res;        for(int i=n-1;i>=0;i--)        {            res=res+s[i]-(n-s[i]-1);            if(res<ans)                ans=res;        }        printf("%d\n",ans);    }    //cout << "Hello world!" << endl;    return 0;    return 0;}





原创粉丝点击