树状数组基础知识

from: http://old.blog.edu.cn/user3/Newpoo/archives/2007/1712628.shtml

树状数组

树状数组是一个可以很高效的进行区间统计的数据结构。 频繁的对数组元素进行修改,同时又要频繁的查询数组内任一区间元素之和的时候,可以考虑使用树状数组.

树状数组是一个查询和修改复杂度都为log(n)的数据结构,假设数组a[1..n], 那么查询a[1]+...+a[n]的时间是log级别的,而且是一个在线的数据结构,支持随时修改某个元素的值,复杂度也为log级别。
 
 来观察这个图:

  令这棵树的结点编号为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

 
 

 

假设a[1...N]为原数组,定义c[1...N]为对应的树状数组:
c[i] = a[i - 2^k + 1] + a[i - 2^k + 2] + ... + a[i]
其中k为i的二进制表示末尾0的个数,所以2^k即为i的二进制表示的最后一个1的权值.
所以2^k可以表示为n&(n^(n-1))或更简单的n&(-n):
 
int lowbit(int n)
{return n& (-n);}
 
对a[n]进行修改后,需要相应的修改c数组中的p1 , p2 , p3 ...等一系列元素
其中p1 = n,
       pi+1 = pi + lowbit(pi )
所以修改原数组中的第n个元素可以实现为:
void Modify(int n, int delta)
{
    while(n <= N)
    { c[n] += delta; n += lowbit(n);}
}
 
当要查询a[1],a[2]...a[n]的元素之和时,需要累加c数组中的q1 , q2 , q3 ...等一系列元素
其中q = n,
       qi+1 = qi  - lowbit(qi )
所以计算a[1] + a[2] + .. a[n]可以实现为:
int Sum(int n)
{
     int result = 0;
     while(n != 0)
     { result += c[n]; n -= lowbit(n); }
    return result;
}

    树状数组的最基本功能就是求比某点x小的点的个数(这里的比较是抽象的概念,可以使数的大小,坐标的大小,质量的大小等)。

       比如给定个数组a[5] = {2, 5, 3, 4, 1},求b[i] = 位置i左边小于等于a[i]的数的个数.如b[5] = {0, 1, 1, 2, 0},这是最正统的树状数组的应用,直接遍历遍数组,每个位置先求出Getsum(a[i]),然后再修改树状数组Update(a[i], 1)即可。当数的范围比较大时需要进行离散化,即先排个序,再重新编号。如a[] = {10000000, 10, 2000, 20, 300},那么离散化后a[] = {5, 1, 4, 2, 3}。

       但我们想个问题,如果要求b[i] = 位置i左边大于等于a[i]的数的个数呢?当然我们可以离散化时倒过来编号,但有没有更直接的方法呢?答案是有。几乎所有教程上树状数组的三个函数都是那样写的,但我们可以想想问啥修改就是x不断增加,求和就是x不断减少,我们是否可以反过来呢,答案是肯定的。

 

这时候,我们要改变一下树状数组的构成,仍以开始的例子说明:

C[i]=a[i]+a[i+1]+...+a[i+2^k-1]。

比如:

C1=a1

C2=a2+a3

C3=a3

C4=a4+a5+a6+a7

C5=a5

C6=a6+a7

C7=a7

C8=a8

这时候,数组C[i]中保存的是从i开始到i+2^k-1位置的元素之和。这时对a[n]进行修改后,需要相应的修改c数组中的p1 , p2 , p3 ...等一系列元素

其中p1 = x,
       pi+1 = pi - lowbit(pi )
当要查询a[1],a[2]...a[n]的元素之和时,需要累加c数组中的q1 , q2 , q3 ...等一系列元素
其中q1  = x,
       qi+1 = qi  + lowbit(qi )

void Update(int x, int c)
{
    int i;
    for (i = x; i >= 1; i -= Lowbit(i))
    {
        tree[i] += c;
    }
}

int Getsum(int x)
{
    int i;
    int temp(0);
    for (i = x; i < maxn; i += Lowbit(i))
    {
        temp += tree[i];
    }
    return temp;
}
       我们只是将两个函数中的循环语句调换了下,现在每次要修改点x的值,就要修改(1, …x-Lowbit(x-Lowbit(x))), x-Lowbit(x), x)路径,而求和就变成求(x, x+Lowbit(x), x+Lowbit(x+Lowbit(x))),…)这条路径的点。而这不正好就是大于等于x的点的求和吗?

       所以我们既可以修改x增大的路,求和x减小的路;也可以修改x减小的路,求和x增大的路,根据题目的需要来决定用哪种。





树状数组可以扩充到二维。在二维情况下:

C[x][y] = ∑ a[i][j], 其中,
x-lowbit(x) + 1 <= i <= x,
y-lowbit(y) + 1 <= j <= y.


在二维情况下,对应的更新和查询函数为:

void Modify(int x, int y, int delta)
{
    for(int i = x; i <= N; i += lowbit(i))
       for(int j = y; j <= N; j += lowbit(i))
          C[x][y] += delta;
}

int Sum(int i, int j)
{
    int result = 0;
    for(int x = i; x > 0; x -= lowerbit(x))
    {
        for(int y = j; y > 0; y -= lowerbit(y))
        {
            result += C[x][y];
        }
    }

    return result;
}




树状数组可以解决更新线段区间,查询某个点的问题。
在这种情况下,更新线段区间和查询点的时间复杂度仍然为O(logn).

设A[1,N]为我们要处理的数组。
另外设置一个数组B[1,N],使得B[1] + B[2] + .. B[i] = A[i], 其中1 <= i <= N.

即B[i] = A[i] - A[i-1]

当要查询A[i]的时候,我们只需在数组B上使用一般的树状数组操作查询区间[1,i]即可
当我们要给区间A[a~b]加上delta时,只需要在数组B上对B[a]进行一般树状数组的更新delta的操作,同时对数组B上对B[b+1]进行 - delta操作。

这种使用方式同样可以扩展到二维。
二维情况下,
B[i][j] = A[i][j] - A[i-1][j] - A[i][j-1]
当要查询A[i][j]的时候,只需在数组B上查询B[0][0]到B[i][j]的和即可。
当 要给矩形A[x1][y1],A[x2][y2]加上delta的时候,只需要在数组B上对B[x1][y1], [x2+1][y2+1]进行一般树状数组的更新delta的操作,同时对B[x1][y2+1], B[x2+1][y1]进行一般树状数组的更新-delta的操作即可


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值