跳转至

树状数组(Fenwick Tree)

当数组经常修改,同时又需要反复查询区间和时,每次遍历区间会产生重复工作。树状数组通过保存部分区间的和,把单点增加和前缀和查询都降低到 O(log n)。

lowbit 保存了什么?

使用 1 开始的下标,lowbit(i) = i & -i 表示 i 的二进制中最低的有效位对应的值。

tree[i] 保存原数组下标区间 [i - lowbit(i) + 1, i] 的和。例如:

下标 i lowbit(i) 保存的区间
4 4 [1, 4]
6 2 [5, 6]
7 1 [7, 7]
8 8 [1, 8]

查询前 7 项时,依次访问 7、6、4,把 [7,7]、[5,6]、[1,4] 三段相加。更新第 5 项时,依次更新包含它的节点 5、6、8。

一个完整实现

接口约定:

  • add(i, delta):第 i 项增加 delta,1 ≤ i ≤ n。
  • prefix_sum(i):前 i 项之和,允许 i = 0。
  • range_sum(left, right):查询闭区间 [left, right]。
class FenwickTree:
    def __init__(self, n):
        if n < 0:
            raise ValueError("n must be non-negative")
        self.n = n
        self.tree = [0] * (n + 1)

    def add(self, i, delta):
        if not 1 <= i <= self.n:
            raise IndexError("update index out of range")
        while i <= self.n:
            self.tree[i] += delta
            i += i & -i

    def prefix_sum(self, i):
        if not 0 <= i <= self.n:
            raise IndexError("prefix index out of range")
        result = 0
        while i > 0:
            result += self.tree[i]
            i -= i & -i
        return result

    def range_sum(self, left, right):
        if not 1 <= left <= right <= self.n:
            raise IndexError("invalid query interval")
        return self.prefix_sum(right) - self.prefix_sum(left - 1)


values = [2, 1, 3, 4, 5]
bit = FenwickTree(len(values))
for i, value in enumerate(values, 1):
    bit.add(i, value)

assert bit.prefix_sum(0) == 0
assert bit.range_sum(2, 4) == 8
bit.add(3, 2)
assert bit.range_sum(2, 4) == 10

按上面的逐项插入方法建树耗时 O(n log n),空间 O(n)。单次更新和查询都耗时 O(log n)。

“增加”与“赋值”不同

add(3, 2) 表示第三项增加 2,不是变为 2。若需要将当前值 old 改成 new,应执行 add(3, new - old),并同步保存原数组。

下标 0 不能用于更新

lowbit(0) 等于 0,没有边界校验时,更新循环可能无法前进。本实现会拒绝无效下标。

怎样选择?

数组不变时,前缀和能以 O(1) 查询区间和;动态单点更新配合区间和时,可以考虑树状数组。更一般的区间最值或区间更新需求,可以继续阅读线段树。