数状数组

树状数组

树状数组或二元索引树(Binary Indexed Tree),又以其发明者命名为Fenwick树,最早由Peter M. Fenwick于1994年以A New Data Structure for Cumulative Frequency Tables为题发表在SOFTWARE PRACTICE AND EXPERIENCE。
其初衷是解决数据压缩里的累积频率(Cumulative Frequency)的计算问题,现多用于高效计算数列的前缀和, 区间和。它可以以O(log n)的时间得到任意前缀和,并同时支持在O(log n)时间内支持动态单点值的修改。空间复杂度O(n)

怎么理解树状数组要解决的问题呢?

举例来说,树状数组所能解决的典型问题就是存在一个长度为n的数组,我们如何高效进行如下操作:

要解决以上问题,可以有以下几种思路

数组数组这种数据结构能平衡求前缀和和更新操作,使之都是O(logn)时间复杂度,虽然需要额外的O(n)空间

怎么做到的

image

数状数组逻辑上可以看作一颗多叉树,父节点等于子节点之和,但是实际存储还是按照数组的形式来存储数组的。 树状数组的父节点和子节点的下标关系是parent = son + 2^k,其中 k 是子节点下标对应二进制末尾 0 的个数,也就是说一个节点的子节点是符合以下条件的节点:子节点的下标加上下标二进制形式最末尾的1所代表的数等于父节点的下标。

比如8的二进制是 0b1000,那么有 7(0b111)加上最末尾的1正好等于8、6(0b110)加上最末尾的1也就是2(0b10)正好等于8、4(0b100)加上最末尾的1也就是4(0b100)正好等于8……

再观察上图,可以发现一些规律

可以发现树状节点很重要的一个操作是要求序号二进制最后一位1,也就是lowbit,可以定义一个函数:

func lowBit(x int) int {
    return x & -x
}

这里用到了位运算的技巧,因为数值在计算机中都是用补码表示的,负数等于正数原码按位取反再+1,再和原数进行 & 操作就可以得到lowbit,随便找个数换成二进制形式算一下就能理解了。

那数状数组怎么求前缀和呢?上面规律的第三点很关键,可以理解成,树状节点的值代表对应原数组节点序号减去其lowbit的下一个数到此序号的累加。去掉lowbit就可以得到紧挨着的前一个范围内的范围和,一直递归到最开始,就可以累加得到前缀和,举个例子:

求原数组0-14的前缀和:

数学证明引用大佬的一张图(看参考链接):

image

所以可以发现这其实是个递归的操作,那么可以实现PrefixSum查询:


// 时间复杂度O(logn)
func PrefixSum(tree []int, idx int) int {
       sum := 0
       for idx >= 1 {
              sum += tree[idx]
              idx -= lowBit(idx)
       }
       return sum
}

从原数组建树有两种方式,一种O(nlogn)复杂度,一种O(n)复杂度

// O(nlogn)时间复杂度建树
func BuildBinaryIndexedTree(raw []int) []int {
       tree := make([]int, len(raw)+1)
       for i := 1; i <= len(raw); i++ {
              for j := i - lowBit(i); j <= i-1; j++ {
                     tree[i] += raw[j]
              }
       }
       return tree
}

func getParent(idx int) int {
       low := lowBit(idx)
       return idx + low
}

// O(n)时间复杂度建树
func BuildBinaryIndexedTree2(raw []int) []int {
       tree := make([]int, len(raw)+1)
       for i := 1; i <= len(raw); i++ {
              tree[i] += raw[i-1]
              parent := getParent(i)
              if parent <= len(raw) {
                     tree[parent] += tree[i]
              }
       }
       return tree
}

因为树状数组父节点和子节点的下标关系是parent = son + 2^k,所以更新一个节点的时候,要递归更新所有其祖先节点的值:


// O(logn)时间复杂度
func Add(tree []int, idx int, val int) {
       for idx <= len(tree) {
              tree[idx] += val
              idx += lowBit(idx)
       }
}

上面已经实现了获取节点父节点操作,接下来实现获取所有子节点下标的操作

// O(logn)时间复杂度
func getChildren(idx int) []int {
       low := lowBit(idx)
       children := []int{idx}

       base := idx - low
       for low > 1 {
              low >>= 1
              base += low
              children = append(children, base)
       }
       return children
}

如果不是求前缀和,而是求一个范围的累加和RangeSum,比如[i,j],因为PrefixSum求的是[1, i]的累加值,所以RangeSum(i,j) = PrefixSum(j) - PrefixSum(i-1)

// O(logn)时间复杂度
func rangeSum(tree []int, start, end int) int {
       return PrefixSum(tree, end) - PrefixSum(tree, start-1)
}

参考链接

https://zh.m.wikipedia.org/zh-hans/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84
https://halfrost.com/binary_indexed_tree/#toc-0
https://oi-wiki.org/ds/fenwick/

© 2019 - 2022 · Firsy · Theme Simpleness Powered by Hugo ·