2015.7.14 树状数组及其初步应用

来源:互联网 发布:魔神英雄传 知乎 编辑:程序博客网 时间:2024/06/02 03:58

一、概述

树状数组(binary indexed tree),是一种设计新颖的数组结构,它能够高效地获取数组中连续n个数的和。概括说,树状数组通常用于解决以下问题:数组{a}中的元素可能不断地被修改,怎样才能快速地获取连续几个数的和?

二、树状数组基本操作

举个简单的例子:
*现在有一个长度为n的数组,数组内存有数据,对于这些数据一般有两类操作:
①修改数据第i个元素
②查询一个区间[p,q]内元素的和*

方法一:使用数组a[i]存储数据的第i个元素

①直接修改,时间复杂度为O(1)
②从p循环累加至q,时间复杂度为O(n)

方法二:使用数组a[i]存储数组第i个元素,并维护一个数组s,s[i]表示数组a前i项和

①直接修改a[i],但是对于维护s,需要时间复杂度为O(n)。
②a[p]~a[q]=s[q]-s[p]即为所求,时间复杂度为O(1)。

由此可见,不同数据结构之间没有绝对优劣之分,这取决于算法的需求。

在方法二中,我们需要维护一个数组的前缀和S[i]=A[1]+A[2]+…+A[i]。但是不难发现,如果我们修改了任意一个A[i],S[i]、S[i+1]…S[n]都会发生变化。可以说,每次修改A[i]后,调整前缀和S在最坏情况下会需要O(n)的时间。

但方法二的思想已经给我们了启发。对于有关“区间”的问题,如果我们只在单个元素上做文章,可能不会有太大的收获。但是如果对于这些数据元素进行合理的划分(如方法二将其化为n个前缀),然后对于整体进行操作,往往会有神奇的功效。
为了让树状数组更直观,我们先看下面这张图:
这里写图片描述
传统数组(共n个元素)的元素修改和连续元素求和的复杂度分别为O(1)和O(n)。树状数组通过将线性结构转换成伪树状结构(线性结构只能逐个扫描元素,而树状结构可以实现跳跃式扫描),使得修改和求和复杂度均为O(lgn),大大提高了整体效率。

树状数组定义:

给定序列(数列)A,我们设一个数组C满足:
C[i] = A[i–2^k+ 1] + … + A[i]
其中,k指i在二进制时末尾0的个数,或者说是i用2的幂方和表示时的最小指数。


如果对概念不是太理解,可以用以下例子来理解:
如23的二进制为10111,即:23 =1 *2^4+0 *2^3+1 *2^2+1 *2^1+1 *2^0
即23用2的幂方和表示为
2^4+2^2+2^1+2^0


操作一:修改第i个元素(上)

从图示中我们可以看出,修改第i个元素,为了维护数组C的意义,需要修改C[i]以及C[i]的全部祖先,而非C[i]的祖先的节点则对于第i个元素的修改,不会发生改变。所以时间复杂度为O(logn)。

操作二:区间[p,q]元素和查询(上)

要求区间[p,q]元素和,可求[1,q]、[1,p]作差。则问题转化为如何查询一个区间[1,p]的元素和,即求s[p]。

对于求数列的前n项和,只需找到n以前的所有最大子树,把其根节点的C加起来即可。不难发现,这些子树的数目是n在二进制时1的个数,或者说是把n展开成2的幂方和时的项数。因此,求和操作的复杂度也是O(logn)。

例如:23的二进制为10111,即:

1 0 1 1 1 (2) = 23 C[23] = A[23]
1 0 1 1 0(2) = 22 C[22] = A[21] + A[22]
1 0 1 0 0(2) = 20 C[20] = A[17] + … + A[20]
1 0 0 0 0(2) = 16 C[16] = A[1] + … + A[16]

则S[23] = C[16] + C[20] + C[22] + [23]
易见,对于任意p,求s[p]只需将若干树状数组上节点c进行加和即可,因为所加节点个数等于p的二进制形式中1的个数,因此统计过程的复杂度为O(Log N)。

树状数组具体操作(C语言实现)

通过上面的介绍,可以发现,实现树状数组的关键,在于求一个数p的二进制时末尾0的个数k(用2的幂方和表示时的最小指数)。而2^k就是修改(和统计)时指针滑动的距离,我们定义这个值为p的lowbit。
更具体的说,正整数p的lowbit为将p二进制中最后一个1按位抽取的结果。比如,23(1011 1 )的lowbit为1(0000 1 ),20(10 1 00)的lowbit为4(00 1 00)。

对于一个二进制数p,以111010000为例。
将这个数减1,即p-1为111001111。
将这两个数取异或,即p^(p-1),为000011111。
将原数与这个数取和,即p&(p^(p-1)),为000010000。
至此已经成功将p二进制的最后一个1按位抽取出来了。

lowbit(p) = p & ( p ^ ( p - 1 ) )

根据有符号整数的补码规则,我们可以发现(p^(p-1))恰好等于-p(具体就不证明了)
即lowbit的求取公式可以更为简练:
lowbit(p) = p & -p

记住这个结论,这是实现树状数组的核心内容。
操作一:修改第i个元素(下)

修改第i个元素:修改第i个元素,需要更新C[i]以及C[i]的全部祖先。根据树状数组的逻辑结构,易见C[i]的祖先为C[i + lowbit(i)]。要对第i个位置进行修改,修改C[i]后再向上对其父亲进行递归修改,直至树根。下面给出一个对第i个元素进行加法操作(x位置加num)的例子。

void plus(int x, int num){    while ( x <= n)    {        c[ x ] += num;        x += lowbit( x );    }}

操作二:区间[p,q]元素和查询(下)

查询区间[p,q]的元素和:要求[p,q]的元素和,可求s[q]、s[p]作差。要求s[p],只需将p反复减其lowbit并对其c进行加和即可(读者可参照第18页的例子帮助理解)。下面给出求s函数的范例:

int sum(int x){    int s = 0;    while ( x )    {        s += c[ x ];        x -= lowbit( x );    }    return s;}

以上就是树状数组的具体实现,它解决的问题是:
(1)改变某一个元素的值
(2)查询某一个区间内所有元素的和。

在此基础上,经过简单的变形可以变成支持另一组操作:
(1)把一个区间内所有元素都加上一个值
(2)查询某一个元素的值。
如果你真正的理解了树状数组区间划分的思想,那么你会发现这种变形很显然,只需要将上面的2个基本操作反过来用就可以了,即把一个区间内所有的元素都加上一个值时,向前递归修改;查询某一个元素的值时,沿树向上递归累加,因为它的祖先记录了所有与它有关的区间整体修改操作。

0 0
原创粉丝点击