What · 是什么
扩展树状数组有两类核心用途:① 区间加 + 区间求和:用两个 BIT 维护差分数组 d[i] 与 d[i]·i,推导得前缀和 sum(1..r)=sum_B1(r)·r−sum_B2(r),区间加只做 4 次点加;② 区间加 + 单点查:单个 BIT 维护差分,区间加两次点加,单点查询一次前缀和。③ 维护不可差分信息(区间最值等):因 BIT 单点更新需要『从区间移除旧值』做逆运算,对不可差分信息改为 O(log²n) 的块内重构式更新。二维 BIT 则是树状数组套树状数组,支持二维单点改/区间查。
Why · 为什么
差分把『区间加』变成两次『单点加』:区间 [l,r] 加 v ⇔ d[l]+=v, d[r+1]−=v,而前缀和就是对差分数组求和,于是区间修改转成了单点修改,直接套 BIT。但区间查询需要 ∑a[i],即差分 d 的『二重前缀和』——展开后每项 d[j] 被加 (r−j+1) 次,拆成 ∑d[j]·(r+1)−∑d[j]·j 两部分,各用一个 BIT 维护即可。不可差分信息因为无法『减回去』,只能把受影响的 c 区间用其儿子+单点重构。
设 d[i]=a[i]−a[i−1],则 a[i]=∑_{j≤i}d[j],前缀和 ∑_{i=1}^r a[i]=∑_{j=1}^r d[j]·(r−j+1)=∑d[j]·(r+1)−∑d[j]·j。故用 B1 维护 d[j]、B2 维护 d[j]·j,pre(r)=sumB1(r)·r−sumB2(r)。区间加 [l,r] 加 v:B1: add(l,v), add(r+1,−v);B2: add(l,v·(l−1)), add(r+1,−v·r)。区间求和 = pre(r)−pre(l−1)。区间加+单点查是特例(一个 BIT)。不可差分(最值):query 沿 lowbit 跳但防止越过 l,单点更新用 c 的儿子们+点 a[x] 重构,均 O(log²n)。
树状数组扩展能力由竞赛社区在上世纪 90~00 年代逐步完善:差分法解决区间修改源自『差分+前缀和』思想的 BIT 化;区间加+区间求和的『双 BIT』trick 在 CPH、USACO、TopCoder 教程中广泛流传;二维 BIT 则是 BIT 的维度推广。
How · 实现
伪代码
class FenwickRange:
b1, b2 (both BIT)
range_add(l,r,x):
b1.add(l,x); b1.add(r+1,-x)
b2.add(l,x*(l-1)); b2.add(r+1,-x*r)
prefix_sum(i) = b1.sum(i)*i - b2.sum(i)
range_sum(l,r) = prefix_sum(r)-prefix_sum(l-1)
class FenwickDiff:
range_add(l,r,x): add(l,x); add(r+1,-x)
point(i) = prefix_sum(i)Python 实现
class FenwickRange:
"""扩展树状数组: 区间加 + 区间求和。双 BIT 维护差分 d[i] 与 d[i]*i。"""
def __init__(self, n, init=None):
self.n = n
self.b1 = [0] * (n + 1)
self.b2 = [0] * (n + 1)
if init:
for i, v in enumerate(init, 1):
self.range_add(i, i, v)
def _add(self, bit, i, x):
while i <= self.n:
bit[i] += x
i += i & -i
def _sum(self, bit, i):
s = 0
while i > 0:
s += bit[i]
i -= i & -i
return s
def range_add(self, l, r, x):
self._add(self.b1, l, x)
self._add(self.b1, r + 1, -x)
self._add(self.b2, l, x * (l - 1))
self._add(self.b2, r + 1, -x * r)
def prefix_sum(self, i):
return self._sum(self.b1, i) * i - self._sum(self.b2, i)
def range_sum(self, l, r):
return self.prefix_sum(r) - self.prefix_sum(l - 1)
class FenwickDiff:
"""扩展树状数组: 区间加 + 单点查 (单 BIT 维护差分)"""
def __init__(self, n):
self.n = n
self.bit = [0] * (n + 1)
def add(self, i, x):
while i <= self.n:
self.bit[i] += x
i += i & -i
def range_add(self, l, r, x):
self.add(l, x)
self.add(r + 1, -x)
def point(self, i):
s = 0
while i > 0:
s += self.bit[i]
i -= i & -i
return s
if __name__ == "__main__":
import random
a = [random.randint(-100, 100) for _ in range(20)]
n = len(a)
fr = FenwickRange(n, a)
assert fr.range_sum(1, n) == sum(a)
assert fr.range_sum(5, 10) == sum(a[4:10])
fr.range_add(3, 7, 5)
for i in range(3, 8):
a[i - 1] += 5
assert fr.range_sum(1, n) == sum(a)
assert fr.range_sum(4, 6) == sum(a[3:6])
fd = FenwickDiff(n)
b = [0] * n
for _ in range(100):
l = random.randint(1, n)
r = random.randint(l, n)
x = random.randint(-10, 10)
fd.range_add(l, r, x)
for i in range(l - 1, r):
b[i] += x
for i in range(n):
assert fd.point(i + 1) == b[i]
print("fenwick-extended OK")
| 时间复杂度 | 区间加+区间查 O(log n);区间加+单点查 O(log n);不可差分最值 O(log²n) |
|---|---|
| 空间复杂度 | O(n) |
易错点
- b2 在 r+1 的更新写成 −x*(r+1) 而非 −x*r,公式全错
- 区间加+单点查与区间加+区间查混用同一实现
- 前缀和公式 pre(i)=sumB1(i)*i−sumB2(i) 乘的是 i(查询下标)不是 l
- 二维 BIT 坐标 +1 与差分矩阵偏移处理遗漏
- 把不可差分信息用普通 BIT 的『直接赋值 max』更新,会得到错误(旧值残留)
例题
| OJ | 题号 | 难度 | 考点 | 链接 |
|---|---|---|---|---|
| Luogu | P3372 | 3 | 【模板】线段树1(区间加+区间求和):双 BIT 差分方案可解(或线段树) | https://www.luogu.com.cn/problem/P3372 |
| Luogu | P4514 | 4 | 上帝造题的七分钟:二维 BIT(区间加+区间求和),二维差分扩展 | https://www.luogu.com.cn/problem/P4514 |
| LOJ | 135 | 3 | 二维树状数组3(区间修改+区间查询):二维差分双 BIT 模板 | https://loj.ac/p/135 |
信源
- OI Wiki✓ https://oi-wiki.org/ds/fenwick/
- CP-Algorithms✓ https://cp-algorithms.com/data_structures/fenwick.html