↓快轉到主要內容
  1. Algorithms/

Segment Tree 與 Fenwick Tree

·2 分鐘· ·
演算法 Segment-Tree Binary-Indexed-Tree Design
Wei Yi Chung
作者
Wei Yi Chung
Working at the contributing of open source, distributed systems, and data engineering.
目錄

介紹
#

Prefix Sum 查詢區間和是 O(1),但只要改一個值,整個前綴陣列都要重算,O(n)。陣列會被修改時,要用能「部分更新」的結構:

結構更新查詢適合
前綴和O(n)O(1)不會修改
Fenwick TreeO(log n)O(log n)單點更新 + 前綴和(可以相減得到區間和)
線段樹O(log n)O(log n)任何可以合併的區間運算(和、最大值、最小值),也能延伸到區間更新

模板一:Fenwick Tree(樹狀陣列)
#

tree[i] 管理一段長度是 i & -i(i 最低的 1)的區間。index 從 1 開始:

class Fenwick:
    def __init__(self, n):
        self.tree = [0] * (n + 1)

    def add(self, i, delta):             # nums[i] += delta,i 從 1 開始
        while i < len(self.tree):
            self.tree[i] += delta
            i += i & -i                  # 往上:負責範圍包含 i 的下一格

    def prefix(self, i):                 # nums[1..i] 的和
        s = 0
        while i > 0:
            s += self.tree[i]
            i -= i & -i                  # 往左:去掉這格負責的範圍
        return s

區間和是 prefix(r) - prefix(l - 1)。題目給的是「改成 val」時,要先算出差值 val - nums[i] 再 add,所以要另外存一份原陣列。

模板二:線段樹
#

節點 node 管理 [start, end],左右小孩是 2 * node + 1、2 * node + 2,陣列開 4n:

def build(node, start, end):
    if start == end:
        tree[node] = nums[start]
        return
    mid = (start + end) // 2
    build(2 * node + 1, start, mid)
    build(2 * node + 2, mid + 1, end)
    tree[node] = tree[2 * node + 1] + tree[2 * node + 2]

def query(node, start, end, l, r):
    if r < start or end < l:             # 完全不重疊
        return 0
    if l <= start and end <= r:          # 完全被包含
        return tree[node]
    mid = (start + end) // 2
    return query(2 * node + 1, start, mid, l, r) + query(2 * node + 2, mid + 1, end, l, r)

更新跟建樹一樣往下找到葉子,改完再一路把父節點重算。

  • 可修改的區間和(LC 307):兩種都可以;Fenwick 程式短,線段樹比較好延伸。

主要參數說明
#

  • Fenwick 的 index 從 1 開始,i & -i 是 i 最低位的 1。
  • 線段樹的區間是包含兩端的 [start, end];mid 屬於左半邊。

複雜度
#

建樹 O(n)(Fenwick 一個一個 add 是 O(n log n)),每次更新和查詢 O(log n),空間 O(n)。

常見錯誤
#

這些都是我練習時實際寫錯過的:

  • 左右小孩算成同一個:右小孩寫成 2 * node + 1,跟左小孩一樣(LC 307)。
  • 遞迴算完沒有 return:左右兩邊都查了,最後忘了回傳相加的結果,得到 None(LC 307)。

相關 LeetCode
#

  • LC 307 Range Sum Query - Mutable

相關文章

LeetCode 307: Range Sum Query - Mutable
·2 分鐘
LeetCode Medium Segment-Tree Binary-Indexed-Tree Design Array
LeetCode 307 解題筆記:單點更新加區間和,前綴和每次更新要 O(n);用線段樹或 Fenwick Tree,兩種操作都是 O(log n)
Queue 與設計題
·2 分鐘
演算法 Queue Design Iterator Lazy-Deletion
環狀佇列、交錯迭代器、FIFO 配對加上取消,以及設計題常用的資料結構組合
Array 陣列掃描
·3 分鐘
演算法 Array Simulation Enumeration
相鄰比較與走過單調段(上坡、下坡、山峰與山谷)、一次掃描維護前綴 / 後綴的最大值與總和、小範圍枚舉配對,以及用 tuple 當 key 處理平手
Bit Manipulation 位元運算
·3 分鐘
演算法 Bit-Manipulation
常用的位元操作、AND 只會減少位元而 OR 只會增加位元、用 n & (n » 1) 找相鄰的 1,以及全部是 1 的數 2^k - 1
Bucket Sort 桶子與鴿籠原理
·1 分鐘
演算法 Bucket-Sort Pigeonhole-Principle Sorting
把值域切成 n - 1 個桶子,最大間距一定不會落在同一個桶子裡,所以每個桶子只要記最小值和最大值,就能線性時間找出排序後的最大相鄰差
Hash Table 雜湊表
·4 分鐘
演算法 Hash-Table Counting Array
計數與「次數的次數」、把陣列本身當雜湊表(負號標記)、多個 key 同時計數(列、欄、對角線)、固定左端點增量計數,以及用正規化後的 key 分組