已發布·持續改進
Algorithm
線段樹能在 O(log n) 時間內完成陣列的區間求和、區間最小值查詢與單點更新;若只需求和,樹狀陣列是更輕量的選擇。
線段樹是一棵二元樹,它把陣列不斷對半切分,並為每個得到的區間儲存和、最小值或最大值等彙總值。根節點負責整個陣列,葉節點負責單一元素,每個內部節點儲存兩個子節點合併後的結果。因此,任何關於連續區間的問題都可以透過合併少數幾個節點來回答。
前綴和可以在 O(1) 時間內求出區間和,但只要修改一個元素就需要 O(n);直接使用陣列時更新很快,但每次查詢都要 O(n)。線段樹把單點更新和區間查詢都控制在 O(log n),適合數值不斷變動、區間查詢又接連而來的情境。加上懶標記(延遲傳播)後,它還能在 O(log n) 內完成替整個區間加上某個值這類區間更新。
建議先弄清楚前綴和的限制,再用一個小陣列親手畫出線段樹,追蹤一次查詢和一次更新分別經過哪些節點。接著自己寫出簡短的迭代版實作與樹狀陣列,並用區間最小值、逆序數對計數、區間加值與區間求和等經典題目反覆練習,就能在程式面試和競賽中熟練運用。
每個節點負責陣列中的一個區間並儲存其和或最小值;只要運算滿足結合律且有單位元素,都能放進同一個結構。
修改一個元素只需重新計算從葉到根的一條路徑,區間查詢在每一層最多合併兩個節點。
區間更新會在覆蓋該區間的節點上留下標記,只有在必須繼續往下走時才把標記下推給子節點,因此仍是 O(log n)。
對於求和這類可用減法還原的運算,樹狀陣列(Fenwick 樹、BIT)只需長度 n + 1 的陣列和幾段短迴圈就能達到相同的複雜度。
這是迭代版線段樹:元素放在長度 2n 陣列的後半部,前半部的每個位置儲存兩個子節點的和。update 從葉節點往上走到根並重新計算和,query 從半開區間 [left, right) 的兩端往上走,只累加邊界上的節點。執行後會依序輸出 26、17、28。
segment_tree.py
class SegmentTree:
"""Iterative segment tree: point update and range sum in O(log n)."""
def __init__(self, values):
self.n = len(values)
self.tree = [0] * self.n + list(values)
for i in range(self.n - 1, 0, -1):
self.tree[i] = self.tree[2 * i] + self.tree[2 * i + 1]
def update(self, index, value):
i = index + self.n
self.tree[i] = value
while i > 1:
i //= 2
self.tree[i] = self.tree[2 * i] + self.tree[2 * i + 1]
def query(self, left, right):
"""Sum of values[left:right] (half-open range)."""
total = 0
lo, hi = left + self.n, right + self.n
while lo < hi:
if lo & 1:
total += self.tree[lo]
lo += 1
if hi & 1:
hi -= 1
total += self.tree[hi]
lo //= 2
hi //= 2
return total
if __name__ == "__main__":
tree = SegmentTree([5, 3, 7, 9, 6, 4, 1, 2])
print(tree.query(2, 6)) # 7 + 9 + 6 + 4 = 26
tree.update(3, 0)
print(tree.query(2, 6)) # 7 + 0 + 6 + 4 = 17
print(tree.query(0, 8)) # 28
python segment_tree.py共六章,帶你從安裝一步步認識 線段樹 的核心概念。
在這裡提問、分享經驗,交流關於 線段樹 的看法。
還沒有討論。來發起第一則吧。
0 則留言
登入 · 登入後即可留言。
來留下第一則留言吧。