树状数组 全网最详细详解

树状数组概念:

  树状数组是一种非常优秀&神奇的数据结构。可以做到区间查询、单点修改,两种操作的复杂度都为log(n),其空间复杂度为O(n)。

  理解树状数组的关键在于理解二进制,曾有一个大神对我说:“这个世界本来就是二进制的,人非要主观的构建一个十进制” ,我并没有能力证明这句话的正确性,但我认为这句话放在树状数组这里是非常助于理解的(因为这种数据结构就是基于二进制的)。

  先从百度借张图,没错的,红色部分就是传说中的树状数组了。

   观察片刻,不难发现:

  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

  然而为什么会有这样的规律呢?当然是因为树状数组奇妙的原理了。

原理:

  提到树状数组原理,通常我们首先想到的就是lowbit函数,这个函数贯穿树状数组的所有功能和实现,同样也是理解树状数组的关键。

lowbit():

  随便打开一个树状数组的代码,我们一定可以一眼找到一个宏定义或者函数,形如:

#define lowbit(x) (x&-x)               //宏定义写法

int lowbit(int x) { return x&-x; }   //函数写法

  两种写法显然本质上是相同的,但是很多OIers其实并不理解为什么要这么写,而只是背过了代码。对于lowbit的理解就涉及到树状数组最根本的原理了。我们看下图,举个栗子。

  栗子:我们以下图c[7]为例,我们该如何判断c[6]所代表区间的范围呢?

       首先写出c[7]的二进制:0111,然后取二进制下该数从右向左的第一个1及后面的0取出,得到1,也就是十进制下的1,即c[7]所包含的区间范围长度。其实转为二进制后的7每一个1都代表树状数组上的一个节点。具体模拟一下,我们想要将最后一个1及后面的0取出,0111可以看作0110+0001,将0110转为十进制后等于6,则c[7]的范围就是 [6 + 1, 7] ,同理继续求c[6]的范围,0110 = 0100 + 0010 将0100转为十进制后等于4,则c[6]的范围就是 [4 + 1, 6]。最后求一下c[4]的取值范围(有点特殊) 0100 = 0100 + 0000 将0000转为十进制后等于0,则c[4]的范围就是 [0 + 1, 4]。

  看到这,问题的关键就只剩然后把某个数从右向左的第一个1及后面的0取出了,也就是lowbit函数所做的事情。

  lowbit()这行代码到底在做什么?我们需要先明白一些小知识。

  1、反码 = 原码每一位取反。

  2、补码 = 反码 + 1。

  3、计算机中,负数使用补码来表示的。

  那么再来看一下上面的代码:

return x & -x;

  我们以76为例,我们来做一下以上操作 76 & -76

  忽略符号位后效果如下:

   76转二进制:0100 1100

  -76的二进制:1011 0100

     0100 1100

 &   1011 0100

   =    0000 0100

  神奇的大功告成了。

  这样有道理吗?肯定是有的,由于反码 = 原码取反 补码 = 反码 + 1,则补码与原码除了最后加的1,其余部分一定相反,与运算后都是0。而最后加1并进位后,一定会使末尾一段再次反过来,直到遇到0,无法进位为止,而第一个遇到的0,一定就是原码中从右向左第一个1。

  最后我们也得出 c[l] 所包含的数为a[i - lowbit(i) + 1] ~ a[i] 共lowbit(i)个数字。

区间查询:

   首先树状数组本身维护的就是区间和,但是查询时有一个问题就是:查询区间并不一定是树状数组所维护的整区间。

      借上图,比如我们要求区间 [4,7] 的区间和。

   于是我们先运用前缀和的思想,将问题简化为求[1,7]的区间和 - [1,4]的区间和。

   然后我们来思考[1,n]区间和的求法:我们已知树状数组上的节点c[i]代表原数组上a[i - lowbit(i) + 1]到a[i]的和,那么我们考虑求1~n的区间和,则最后的ans中一定不包含a[n + 1],而不包含a[n + 1]的位置我们首选c[n](c[n] 包含的区间一定在c[n + 1] 之前),现在ans += c[n] 我们就已经统计了部分答案,我们也知道我们刚刚统计上的答案一定是原数组上 a[i - lowbit(n) + 1] 到 a[n] 的和,共计lowbit(n)个数被统计到了,于是问题转化为求[1,n - lowbit(n)]的区间和。以此类推,我们可以通过lowbit将[1,n]的区间和求出。

   总结一下:sum[i][j] = sum[1][j] - sum[1][i];

    for (int i = n; i != 0; i -= lowbit(i)) ans += c[i];

    sum[1][n] = ans;

代码如下:

int Query(int x)
{
    int sum = 0;
    for(int i = x; i; i -= lowbit(i)) //注意循环终止条件
        sum += tree[i];
    return sum;
}

单点修改:

  由于树状数组维护的是前缀和,单点修改时我们还要考虑修改包含该节点的点,将这些点全部修改完成后,单点修改完成。

  举个栗子:我们对第P个元素进行修改。我们需要找到许多包含P的c[i],将他们一一修改。那么那些c[i]需要修改呢?

   首先需要修改的c[i]编号必然大于P,范围必然包括P,且lowbit(i) 一定大于 lowbit(P)。

   于是我们得出 i >= P > i - lowbit(i)

   我们设P的二进制为 0101 1010

   我们先从小到大推测一下i可能是多少。

   设i为abcd efgh,由于lowbit(i) 一定大于 lowbit(P),得出i为abcd ef00,后二位确定。其他六位若本来不是1则也可能为1(原因是原来为1的话按此方法推i会小于P),如果这时f = 0,又因为P > i - lowbit(i) 我们得知i为0101 1100(为了满足P > i - lowbit(i))。

   我们继续推出abcde为1的情况,同样为最后一个1后面的都是0,1前面的与P相同。

   我们列出所有可能:

   0101 1100

   0110 0000

   1000 0000

   我们发现这些符合要求的i是通过不断加本身的lowbit找出的。

   没错就是这样!!!(逃!)大家可以枚举试一试。

代码如下:

void Add(int x, int k)
{
    for (int i = x; i <= n; i += lowbit(i)) //注意循环终止条件
        tree[i] += k;
}

 代码实现:

 顺便说一下树状数组的初始化:直接用单点修改做就行了。没什么问题。

#include<iostream>
#include<cstdio>
#define lowbit(x) (x&-x)
const int MAXN=500010;
int n,m;
int x,y,z;

int tree[MAXN];

void Add(int x,int k)
{
    for(int i=x;i<=n;i+=lowbit(i))
        tree[i]+=k;
}

int Query(int x)
{
    int sum=0;
    for(int i=x;i;i-=lowbit(i))
        sum+=tree[i];
    return sum;
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i)
    {
        scanf("%d",&x);
        Add(i,x);
    }
    for(int i=1;i<=m;++i)
    {
        scanf("%d%d%d",&x,&y,&z);
        if(x==1)
            Add(y,z);
        else
            std::cout<<Query(z)-Query(y-1)<<std::endl;
    }
    return 0;
    
}

 总结:

树状数组确实是一种优美到令人惊叹的数据结构。不过它也不是万能的,有不少优点但也有缺点。

优点:代码简单、好写、好调。修改查询时间复杂度都是O(logN),常数还比线段树小。

缺点:必须满足区间减法,一定转化成两个前缀相减。这就使得很多题无法用树状数组解决。

                                                                                                                                              

猜你喜欢

转载自www.cnblogs.com/yanyiming10243247/p/9322938.html