树状数组的原理分析及使用

问题

给定集合S,包含了N个元素,每个元素都在区间[1, 1000]内,
用户输入两个参数x, y,求解任意区间[x, y]内,其中j <= k,出现的元素和。

树状数组原理解析

区间树表示

区间树地完整表示如下,可以看到它就是一树满二叉树,为能能够表达所有区间的信息,需要多创建2^(k-1)个结点。

树1:
                      (0)
                     [1,8]
          (1)                     (2)
         [1,4]                   [5,8]
    (3)         (4)         (5)         (6)
   [1,2]       [3,4]       [5,6]       [7,8]
 (7)   (8)   (9)   (10)  (11)  (12)  (13)  (14)
[1,1] [2,2] [3,3] [4,4] [5,5] [6,6] [7,7] [8,8]

但其实右孩子的和,可以通过减法来得到,即 右 孩 子 = 父 结 点 − 左 孩 子 右孩子=父结点-左孩子 =因此我们区间树中所有的右孩子删除(这里说删除,实际上是指被合并到了其父结点),可以得到如树2,所有[x,x]表示的结点记录的区间信息,都被逐层地向上合并到了父结点上。

树2:
                      (0)
                     [1,8]
          (1)                     (2)
         [1,4]                   [x,x]
    (3)         (4)         (5)         (6)
   [1,2]       [x,x]       [5,6]       [x,x]
 (7)   (8)   (9)   (10)  (11)  (12)  (13)  (14)
[1,1] [x,x] [3,3] [x,x] [5,5] [x,x] [7,7] [x,x]

仔细分析上面删除了所有右孩子结点的树,可以发现这些被删除的结点的索引都是2的倍数,0除外。例如索引为4、10的结点被删除后,只保留了索引9的结点,而索引9的结点对应于区间[3,3]。
我们将所有的合并信息都整理下来,就得到了如下的描述:

7 保留
8 合并到3,3保留
9 保留
10合并到4,4合并到1,1保留
11保留
12合并到5
13保留
14合并到6,6合并到2,2合并到0,0保留

至此,所有的索引为2的倍数的右孩子都合并到了其父结点上。

树状数组雏形

但怎么样才能将这些删除的结点重新组装成一树可以搜索的二叉树呢??
我们回过头看看上面的文字描述,是不是感觉很有规律?像一棵被右对齐的树?
我们结合文字的描述的合并过程,将树2重新调整,就得到了树3,很有趣是吧,树2被整个右对齐了!!!这不难想象呀,因为我们是将所有的右孩子都合并到了父结点嘛,也就相当于自底向上,逐层右移对齐右孩子。

树3:
                                           (0)
                                          [1,8]
                   (1)                     (2)
                  [1,4]                   [x,x]
       (3)         (4)         (5)         (6)
      [1,2]       [x,x]       [5,6]       [x,x]
 (7)   (8)   (9)   (10)  (11)  (12)  (13)  (14)
[1,1] [x,x] [3,3] [x,x] [5,5] [x,x] [7,7] [x,x]

这也启发了我们如何去表示树2的结构。不难看出,如果定义有N个数,将所有的右孩子合并之后,只需要N个结点就可以表达所有区间的求和信息。

我们再次将[1, 8]个数与树3,整合到一起分析,这里将合并的所有树结点,重新按编号,如下所示:

区间:                                          [1,8]
区间:                  [1,4]                   [x,x]
区间:      [1,2]       [x,x]       [5,6]       [x,x]
区间:[1,1] [x,x] [3,3] [x,x] [5,5] [x,x] [7,7] [x,x]
索引: (1)   (2)   (3)   (4)   (5)   (6)   (7)   (8)

由之前得到的分析,知道所有偶数索引的结点被合并到了父结点,这里分别在2、4、6、8的位置上进行了合并。

树状数组索引关系定义

现在问题是如何定义这些区间的父子关系,以便通过加法、减法来得到任意区间上的和????

以索引为6的结点分析,它表示的区间是[5,6]的区间上的和,而如果想要得到[1,6]区间上的和,就需要加上索引为4的结点,即区间[1,4]上的和,因此可以得到式子:
s u m ( 1 , 6 ) = s u m ( 1 , 4 ) + s u m ( 5 , 6 ) + v a l u e ( 6 ) = i n d e x ( 4 ) + i n d e x ( 6 ) + v a l u e ( 6 ) sum(1,6) = sum(1,4) + sum(5,6) + value(6) = index(4) + index(6) + value(6) sum(1,6)=sum(1,4)+sum(5,6)+value(6)=index(4)+index(6)+value(6)

注意,对应于树2的结果,[5,6]区间结点的父结点是[5,8],而[5,8]的父结点是[1,8],经过合并操作后,[5,6]区间的父结点是[1,8],而[1,4]区间的父结点也是[1,8]。

以索引为7的结点分析,有式子:
s u m ( 1 , 7 ) = s u m ( 1 , 4 ) + s u m ( 5 , 6 ) + s u m ( 7 ) = i n d e x ( 4 ) + i n d e x ( 6 ) + v a l u e ( 7 ) sum(1,7) = sum(1,4) + sum(5,6) + sum(7) = index(4) + index(6) + value(7) sum(1,7)=sum(1,4)+sum(5,6)+sum(7)=index(4)+index(6)+value(7)

以索引为8的结点分析,有式子:
s u m ( 1 , 8 ) = s u m ( 1 , 4 ) + s u m ( 5 , 6 ) + s u m ( 7 ) + v a l u e ( 8 ) sum(1,8) = sum(1,4) + sum(5,6) + sum(7) + value(8) sum(1,8)=sum(1,4)+sum(5,6)+sum(7)+value(8)

最终得到所有前缀和区间和索引之间的关系,如下列式子,其中index(…)表示树结点索引,而value(…)表示单值:
s u m ( 1 , 1 ) = v a l u e ( 1 ) = i n d e x ( 1 ) s u m ( 1 , 2 ) = i n d e x ( 1 ) + v a l u e ( 2 ) = i n d e x ( 2 ) s u m ( 1 , 3 ) = i n d e x ( 2 ) + i n d e x ( 3 ) = i n d e x ( 2 ) + v a l u e ( 3 ) s u m ( 1 , 4 ) = i n d e x ( 2 ) + i n d e x ( 3 ) + v a l u e ( 4 ) = i n d e x ( 4 ) s u m ( 1 , 5 ) = i n d e x ( 4 ) + i n d e x ( 5 ) = i n d e x ( 4 ) + v a l u e ( 5 ) s u m ( 1 , 6 ) = i n d e x ( 4 ) + i n d e x ( 6 ) = i n d e x ( 4 ) + i n d e x ( 5 ) + v a l u e ( 6 ) s u m ( 1 , 7 ) = i n d e x ( 4 ) + i n d e x ( 6 ) + i n d e x ( 7 ) = i n d e x ( 4 ) + i n d e x ( 6 ) + v a l u e ( 7 ) s u m ( 1 , 8 ) = i n d e x ( 4 ) + i n d e x ( 6 ) + i n d e x ( 7 ) + v a l u e ( 8 ) = i n d e x ( 8 ) \begin{aligned} sum(1,1) &= &value(1) &= index(1)\\ sum(1,2) &= index(1) + &value(2) &= index(2)\\ sum(1,3) &= index(2) + index(3) = index(2) + value(3)\\ sum(1,4) &= index(2) + index(3) + &value(4) &= index(4)\\ sum(1,5) &= index(4) + index(5) = index(4) + value(5)\\ sum(1,6) &= index(4) + index(6) = index(4) + index(5) + value(6)\\ sum(1,7) &= index(4) + index(6) + index(7) = index(4) + index(6) + value(7)\\ sum(1,8) &= index(4) + index(6) + index(7) + &value(8) &= index(8) \end{aligned} sum(1,1)sum(1,2)sum(1,3)sum(1,4)sum(1,5)sum(1,6)sum(1,7)sum(1,8)==index(1)+=index(2)+index(3)=index(2)+value(3)=index(2)+index(3)+=index(4)+index(5)=index(4)+value(5)=index(4)+index(6)=index(4)+index(5)+value(6)=index(4)+index(6)+index(7)=index(4)+index(6)+value(7)=index(4)+index(6)+index(7)+value(1)value(2)value(4)value(8)=index(1)=index(2)=index(4)=index(8)

通过分析上面的等式,并结合树的结构,有8个数,共8个树结点,树高为4,可以得到2个有趣的结论:

  1. 索引为2的幂次结点的前缀和,就是前2^(k-1)个数的和,即有sum([1,8]) = index(8)
  2. 非2幂次的结点前缀和,可以由多个小于且等于它的结点组合得到,例如sum([1,7]) = index(4) + index(6) + index(7),这就是一种动态规划思想的求解过程。

上面两个有趣的结论,能够帮助我们基于前缀和,求得任意区间上的和,时间复杂度为O(logn)。

树状数组求解前缀和

以求前缀和的问题为引,给定一个索引N,期望得到前N个数的和,我们应该怎么做呢?
假设N=7,由上面的结论,为了求sum([1,7]),我们应首先知道index(4)、index(6)、index(7),那如何根据给定的索引号7,推导出所关联的其子问题索引号4、6、7呢?

这里就引需要引入低位位LOWBIT(n)技术

LOWBIT(n) = n & ( -n)

大家可以GOOGLE相关概念,这里简单描述一下它的意思:给定一个数n,找到其最低位出现1的位置i,返回将1左移(i-1)位得到的结果,即(1 << 尾部0个数)。

那么利用LOWBIT函数,可以得到如下的等式:
7 − L O W B I T ( 7 ) = 7 − 1 = 6 6 − L O W B I T ( 6 ) = 6 − 2 = 4 4 − L O W B I T ( 4 ) = 4 − 4 = 0 7 - LOWBIT(7) = 7 - 1 = 6\\ 6 - LOWBIT(6) = 6 - 2 = 4\\ 4 - LOWBIT(4) = 4 - 4 = 0 7LOWBIT(7)=71=66LOWBIT(6)=62=44LOWBIT(4)=44=0

我们可以通过递归的方式,来查询当前索引N,应该累加的子索引,伪代码如下:

getSum(N):
  int ret = 0;
  for i = N if i > 0:
    // index(i)表示索引号i对应的值
    ret += index(i)
    i -= LOWBIT(-i)
  return ret

树状数组求解任意区间和

前面的方法可以根据输入的索引号N,只能简单求得前缀和,即[1, N]的和。如何求解任意区间,即[x, y]的和呢?

很简单,调用两次两次getSum函数,求差就好了,伪代码如下:

// 假定x <= y,求区间[x, y]和
getSum(x, y):
  return getSum(x-1) - getSum(y)

树状数组更新

前面只是对于固定数组的查询操作,不可避免地需要更新操作,而更新的过程,是对查询过程的逆过程,即给定一个子结点的值,如何更新父结点???

例如给定数字2,我们应该如何更新包含了2的所有区间的统计信息呢,我们知道
2的父结点是4,祖先结点是8、16…,因此伪代码如下:

update(N, MAX):
  for i = N if i <= MAX:
      index(i) += N;
      i += LOWBIT(i)

总结

通过前面的讲解,应该可以构建自己的树状数组来求解,基于前缀和的区间问题了,虽然数状数组要比线段树或区间树在结构、操作上都要简单,但也因此它的使用是有局限性的,不能完全取代区间树。

你可能感兴趣的:(算法/数据结构,算法,数据结构)