1 minute read     Posted on:     Updated on:     Views: Loading...

Binary Indexed Tree

又名 Fenwick Tree, 是一種特殊資料結構,適用於需要大範圍的紀錄更新資料
像是下圖,假設我想要知道,達到 20% 採購率有哪些國家,達到 50% 的又有哪些
一般的作法是我可能開一個 map 去紀錄對吧 看起來會像以下這樣

1
2
3
4
5
6
7
8
map[rate][]string{
    20: []string{
        "Finland", "United States",
    },
    30: []string{
        "Spain",
    },
}

這種作法看似沒問題,但是當資料一多起來,你的程式跑起來將會非常的緩慢
線段樹的資料結構可以有效的利用最小空間,紀錄起這些龐大的資料,並且查詢速度相當的快速

Introduce to Binary Indexed Tree

Binary Indexed Tree(簡稱 BIT) 的核心思想就是建立一個表格,透過預先計算的方式,建構出完整的資料
BIT 透過將資料進行 分組 並紀錄於 一維陣列 當中
當使用者需要特定範圍的資料時,它可以以最小步數重建資料
重建資料? 這樣不會太慢嗎

BIT 是以二進位的方式建立的,並且每個格子都存放著 不完全的累進的資料

為什麼是不完全? 因為如果每個格子都紀錄完全的累進資料,那不就跟你用暴力法紀錄的一樣了(一樣慢阿)

Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var (
    size              = 100
    binaryIndexedTree = make([]int, size + 1)
)

func sum(index int) int {
    sum := 0

    for i := index; i >= 1; i -= (i & -i) {
        sum += binaryIndexedTree[i]
    }

    return sum
}

func update(index, value int) {
    for i := index; i <= size; i += (i & -i) {
        binaryIndexedTree[i] += value
    }
}

他的實作就只有上面這樣,非常的簡單阿
透過定義簡單的一維陣列,並初始化為 0
sum 計算從 1 ~ index 的累進數值
update 更新從 index ~ size 的累進數值

看到這,為什麼 update 要把後面的全部都更新呢?
它不能只更新 1 ~ index 就好嗎?
ㄟ還真的不行 且讓我娓娓道來

How does Binary Indexed Tree Works

前面提到,BIT 是透過將資料進行分組以達到高速計算的
那他是怎麼分組的?

BIT 借用了二進位的特性,亦即,所有正整數都可以以二進位的方式寫出來
考慮 19 這個數字,想像有一個大小為 19 的陣列且每個元素值為 1(下圖第一行)
接下來,19 這個數字可以被改寫成 $19 = 2^4 + 2^1 + 2^0$, 用顏色區分就會是 橘色 藍色 以及紅色(下圖第二行)
重點來了,我如果用迴圈慢慢加(i.e. 1+1+1+…+1) 是不是可以改寫成 16 個 1 + 2 個 1 + 1 個 1 呢?
那我為什麼不直接把數字直接標注在相對應的位子上呢!(下圖第三行)
仔細解析出來看就是

1
2
3
19: 0b10011
18: 0b10010
16: 0b10000

所以 19 的累進資料的算法是 (19 到 19) + (18 到 17) + (16 到 1) 上面個別紀錄不完全的累進資料相加就是了
再看一個例子

1
2
3
26: 0b11010
24: 0b11000
16: 0b10000

所以 26 的累進資料的算法是 (26 到 25) + (24 到 17) + (16 到 1) 加總


仔細觀察你就會發現,每一次的往下更新
它都是 移除最右邊數值為 1 的 bit
直到為 0

移除最右邊數值為 1 的 bit 可以用下列公式寫出來

重建的公式是 $x + (x\ {\&}\ {-x})$
取累進數值是 $x - (x\ {\&}\ {-x})$
你可以嘗試手算一下 19 跟 26 的推導累進

所以你可以看到,建資料的成本不高,複雜度才 $O(Log\ n)$


回到上一節提到的問題
為什麼重建不能從 index ~ 1 而偏偏要 index ~ size 呢?
道理其實很簡單,因為 BIT 紀錄的是 累進資料
假設你要 update(10, 1) 好了,他的重點是,當我查詢的範圍有包含到 10 的時候,必須要算到
如果往下重建資料,它會連 1 ~ 9 都被 + 1
很明顯這不是正確的結果

LeetCode 1854. Maximum Population Year

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
var (
    offset = 1949
    size = 110
)

func sum(binaryIndexedTree []int, index int) int {
    mysum := 0
    for i := index; i >= 1; i -= (i & -i) {
        mysum += binaryIndexedTree[i]
    }
    return mysum
}

func update(binaryIndexedTree []int, index, value int) {
    for i := index; i < size; i += (i & -i) {
        binaryIndexedTree[i] += value
    }
}

func maximumPopulation(logs [][]int) int {
    binaryIndexedTree := make([]int, size + 1)

    if len(logs) == 1 {
        return logs[0][0]
    }

    for _, log := range logs {
        update(binaryIndexedTree, log[0] - offset, 1)
        update(binaryIndexedTree, log[1] - offset, -1)
    }
    
    maxYear := 0
    maxPopulation := 0
    for i := 1950; i <= 2050; i++ {
        population := sum(binaryIndexedTree, i - offset)
        if population > maxPopulation {
            maxPopulation = population
            maxYear = i
        }
    }

    return maxYear
}

1854 這一題其實也可以使用 binary indexed tree 解
題目要求是要求出人口最多的所在年份是哪一年
然後他有提供每一個人的出生以及死亡日期

所以我的想法是,建立一個 array,每個欄位都儲存該年份,有多少人
因為 binary indexed tree 的特性,他是更新 n ~ size 的資料欄位
每個人的資料時長不同,僅會存在於 birth ~ dead 之間,因此我們要把這個操作改成

  1. birth ~ size 是 1
  2. dead ~ size 是 -1(要把它扣回來)

其中 birth < dead

最後當我們把陣列建立完成之後,在從頭掃過一遍就好了

因為實際上的資料維度只有 100(1950 ~ 2050)
所以實際上不用將陣列大小開到 2000

References

Leave a comment