8.3 樹狀陣列BIT#
概念說明#
樹狀陣列(Binary Indexed Tree,又稱 Fenwick Tree) 是「前綴和」專用的精簡結構:支援單點更新與前綴和查詢,兩者皆 O(log n)。若只需要「和」類可逆運算,它比線段樹程式碼短十倍、常數更小。
它的巧妙之處在於用索引的二進位結構組織資料:定義 lowbit(i) = i & (-i)(i 的二進位最低位的 1 所代表的數值),tree[i] 負責管理「以 i 結尾、長度 lowbit(i)」的這段元素。例如 tree[12](1100)管理長度 4 的段落、tree[11](1011)管理長度 1。前綴和 sum(i) 就是把 i 依二進位拆成幾段,各段取自 tree——拆解的方式恰好是「每次砍掉最低位的 1」。
操作步驟#
- add(i, d):把位置 i 增加 d。沿途所有「管得到 i」的節點都要更新:
i += i & (-i),直到超出 n。 - prefix(i):求前 i 項和。累加
tree[i]後i -= i & (-i),直到 i 為 0。 - range_sum(l, r):
prefix(r) - prefix(l - 1)——這就是為什麼只處理「和」即可。
Python 實作#
class BIT:
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1) # 索引從 1 開始
def add(self, i, d): # 位置 i(1-based)增加 d
while i <= self.n:
self.tree[i] += d
i += i & (-i) # 跳到下一個管得到 i 的節點
def prefix(self, i): # 前 i 項的和
s = 0
while i > 0:
s += self.tree[i]
i -= i & (-i) # 砍掉最低位的 1
return s
def range_sum(self, l, r):
return self.prefix(r) - self.prefix(l - 1)
# 動態計分板:隨時更新分數、隨時查前綴與區間統計
scores = [0, 5, 3, 8, 1, 9] # 1 號~5 號選手
bit = BIT(len(scores) - 1)
for i in range(1, len(scores)):
bit.add(i, scores[i])
print(bit.prefix(3)) # 16(5+3+8)
print(bit.range_sum(2, 5)) # 21(3+8+1+9)
bit.add(3, -3) # 3 號扣 3 分
print(bit.range_sum(2, 5)) # 18
逆序對計數(BIT 的經典應用):由左往右掃描,每看到元素 x 就查「目前已出現的數中,比 x 大的有幾個」,累加即逆序對數。用值當 BIT 索引,即可在 O(n log n) 內完成——暴力法是 O(n²)。
def count_inversions(a):
n = max(a)
bit = BIT(n)
inv = 0
for x in reversed(a): # 由右往左:查比 x 小的已出現數
inv += bit.prefix(x - 1)
bit.add(x, 1)
return inv
print(count_inversions([3, 1, 4, 2])) # 3:(3,1) (3,2) (4,2)
print(count_inversions([5, 4, 3, 2, 1])) # 10(完全逆序)
BIT、線段樹、前綴和的取捨#
| 需求 | 前綴和陣列 | BIT | 線段樹 |
|---|---|---|---|
| 建構 | O(n) | O(n) | O(n) |
| 前綴查詢 | O(1) | O(log n) | O(log n) |
| 單點更新 | O(n) | O(log n) | O(log n) |
| 區間 min/max | ✗ | ✗ | ✓ |
| 區間更新 | ✗ | 需兩棵差分 BIT | ✓(懶標記) |
複雜度分析#
add 與 prefix 都沿二進位位數行走,至多 O(log n) 步;空間恰為 O(n),程式碼極短、常數極小。建構可逐點 add(O(n log n))或「父節點 = 子節點相加」的 O(n) 版本。
練習#
- 實作 O(n) 建構版 BIT:先複製原陣列,再用
tree[i] += tree[i - lowbit(i)]式的推導一次建好。 - 用 BIT 實作「隊伍插隊」:支援在第 k 個空位插入一人(提示:BIT 存 0/1 佔位,二分搜找第 k 個 1 的位置)。
- 對比實驗:n=10⁵ 隨機資料下,比較「樸素陣列(更新 O(1)、查詢 O(n))」與「BIT」在「5 萬次更新 + 5 萬次區間和查詢」混合負載下的總耗時。