• 沒有找到結果。

Fenwick tree (Binary indexed tree) 線段樹可以動態的在

N/A
N/A
Protected

Academic year: 2022

Share "Fenwick tree (Binary indexed tree) 線段樹可以動態的在"

Copied!
5
0
0

加載中.... (立即查看全文)

全文

(1)

Fenwick tree (Binary indexed tree)

線段樹可以動態的在 O(log n) 時間內詢問一個區間內的元素總合,以及修改其中 一個元素的值。然而線段樹這個資料結構有點大,且遞迴時也有些複雜,有沒有另外 的資料結構可以處理這種問題呢?我們發現,「區間 [a, b] 的總合」可以轉成「區間 [1, b] 的總合」減掉「區間 [1, a− 1] 的總合」,因此我們可以把問題簡化成:如何快速 的求得一個陣列的前綴和,以及更改其中一個元素的值?

Fenwick tree 是一個處理前綴時很有效率的資料結構,他也被稱為 Binary indexed tree,或簡稱 BIT,在中國被稱做樹狀數组。以下我們都簡稱它為 BIT。和線段樹相 同,對於一個長度為 n 的陣列,BIT 可以在 O(n) 的時間初始化,在 O(log n) 時間詢 問一個前綴的訊息 (例如前綴和),以及在 O(log n) 的時間修改其中一個值。雖然複雜 度和線段樹相同,但 BIT 的時間常數比線段樹小得多 (時間複雜度會把常數拿掉,但 實際上還是有影響的),空間也比較小 (只需要多開一個大小恰好為 n 的陣列即可),

程式碼也非常精簡 (初始化 + 修改 + 詢問,全部約 20 行)。一般來說,如果問題的性 質可以使用 BIT,效率會和使用線段樹有明顯的差別。當然,BIT 的缺點就是有些問 題無法轉為前綴之間的運算,例如區間 [a, b] 的最大值就無法由區間 [1, b] 和 [1, a− 1]

的最大值算出,這時候就無法使用它了。以下將詳細介紹 BIT 的操作。

首先,我們先介紹一個函數:lowbit(x),表示 x 在二進位表示時,最接近 LSB(Least Significant Bit),也就是最靠右邊的 1 所對應的值。例如十進位下的數字 6(10),在二 進位的寫法是 110(2),其中的兩個 1 分別表示 22 和 21,因此 lowbit(6) = 21 = 2。同 理,20(10) = 10100(2),所以 lowbit(20) = 22 = 4。lowbit 函數可以用位元運算在 O(1) 時間內得到,在作業中會再多做說明。

和線段樹一樣,BIT 先記錄了一些區間的訊息,並由這些預先處理過的區間拼出 真正詢問的區間,而在單點修改時,也要另外修改包含該點的區間。假設原始陣列長 度為 n,且索引值為 1 至 n(1-based),線段樹會記錄大約 2n 至 4n 個區間 (這也和實 作方法有關),而 BIT 只需記錄恰好 n 個區間。BIT 的這 n 個區間的右界即是 1 到 n,若一個區間的右界為 x,其左界就是 x− lowbit(x) + 1。意即,BIT 的區間集合為

{ [x − lowbit(x) + 1, x] | 1 ≤ x ≤ n }

畫成圖之後可以發現,BIT 其實是棵刪除掉一些區間的線段樹,如下圖所示:

(2)

因為每個範圍的右界都不同,我們可以只使用右界來表示這些區間,並以 range(x) 表示,也就是

range(x) = [x− lowbit(x) + 1, x]

以下我們用前綴和當例子:已知一個長度為 n 的陣列 arr,我們希望建立一顆 BIT 的陣列 bit,並滿足 bit[x] 為 arr 陣列中相對應範圍的元素和,即:

bit[x] =

i∈range(x)

arr[i] =

x i=x−lowbit(x)+1

arr[i]

定義完 range(x) 和 bit[x] 之後,我們討論三種操作:詢問,單點修改,以及初始 化時,如何快速的得到答案或完成操作。

1. 詢問前綴和

詢問 [1, x] 的區間和很容易,因為該區間可以拆成 [1, x−lowbit(x)] 和 [x−lowbit(x)+

1, x] 兩個區間和的加總。區間 [x− lowbit(x) + 1, x] 的和就是 bit[x],另外一個區 間則遞迴處理。由於 x− lowbit(x) 在二進位表示法中會比 x 少一個 1,因此最多遞⌈log2x⌉ 次之後就會終止,詢問的複雜度也就是 O(log n)。(雖然概念上另外一個 區間是遞迴處理,實作時只要使用迴圈就可以了,這也是 BIT 常數比較小的原因 之一。)

1 int query(int x) { 2 int sum = 0;

3 for(int i = x; i > 0; i -= lowbit (i))

4 sum += bit[i];

5 return sum;

6 }

2. 單點修改

將 arr[x] 的值增加 val 之後,bit 陣列中所有對應區間包含 x 的值都要改變。由 BIT 的結構圖可以觀察到,需要更新的區間為 range(x), range(x + lowbit(x)),· · · 。 因為每往上遞迴一次,對應區間大小就會變為原本的 2 倍,因此最多遞迴 ⌈log2n⌉ 次之後就會終止,單點修改的複雜度也就是 O(log n)。

(3)

除了 arr[x] 這項以外,區間內其餘元素的和可以用前面已經算好的 bit 較快的得 到答案,如 bit[8] = arr[8] + bit[7] + bit[6] + bit[4]。此做法雖然看起來計算單一 個 bit[x] 的時間複雜度不是 O(1),但是總時間複雜度計算之後是 O(n)。演算法程 式碼如下:

1 void init(int n) {

2 for(int x = 1; x <= n; ++x) {

3 bit[x] = arr[x];

4 int y = x - lowbit (x);

5 for(int i = x -1; i > y; i -= lowbit (i))

6 bit[x] += bit[i];

7 }

8 }

另一個做法則是計算完 bit[x] 之後,「主動」往上更新上一層的值。該做法和上一 個做法原理完全一樣,由程式碼就可以輕易看出這是個 O(n) 的演算法:

1 void init2(int n) {

2 for(int x=1; x<=n; ++x)

3 bit[x] = 0;

4 for(int x=1; x<=n; ++x) { 5 bit[x] += arr[x];

6 int y = x + lowbit (x);

7 if(y <= n) arr[y] += arr[x];

8 }

9 }

(4)

習題

1. (10 pts) 請證明在二補數系統下 (即 −x ≡ ~x + 1),lowbit(x) = x&(−x)。(假設 x > 0)。

2. 有一個初始化 BIT 的方法是一開始先把 bit 陣列歸零,並對於 arr 陣列中的每個 元素都呼叫一次 update(x, arr[x]),如以下程式碼所示:

1 void init3(int n) {

2 for(int x=1; x<=n; ++x)

3 bit[x] = 0;

4 for(int x=1; x<=n; ++x) 5 update (x, arr[x]);

6 }

這個演算法的時間複雜度顯然是 O(n log n),然而這只是一個上界,實際上也許並 不會那麼差,因為更新時不一定會改到滿滿的 log n 個區間,也許均攤後也是 O(n) 呢!對此我們需要更細的計算一下,以下將證明該做法的複雜度是 Ω(n log n),也 就是均攤之後還是比較差。

為了方便起見,先假設 n = 2m,其中 m 為非負整數。令 f (m) 表示當 arr 長度為 2m 時,用這個初始化做法需要更新幾次區間,也就是操作次數。由 BIT 的結構可 以發現,大小為 2m 的 BIT 最上層一定是區間 [1, 2m],且不論更改哪個位置的值,

這個區間一定會被更改到。將最上層的區間拿走之後,下面剩下的剛好是大小為 2m−1, 2m−2,· · · , 21, 20 的 BIT,於是就得到

f (m) = 2m+

m−1 k=0

f (k), f (0) = 1

(a) (10 pts) 請證明:∑n

i=0

i· 2i−1 = (n− 1) · 2n+ 1。

(b) (20 pts) 請證明:f (m) ≥ m · 2m−1 , for m ≥ 0。(即:f(m) ≥ n2 · log2n , for n = 2m, m≥ 0)

(5)

(a) (10 pts) 請給出一組會讓時間複雜度超過 O(log n) 的詢問。

(b) (15 pts) 請問該做法中,詢問的時間複雜度為何?

4. 給定原始陣列 arr,我們定義一個差分陣列 dif,滿足

dif[x] =

{arr[1] , if x = 1 arr[x]− arr[x − 1] , if x ̸= 1 接著再定義另外一個陣列 dif2,滿足

dif2[x] = dif[x]× x

並且定義以下函式,時間複雜度皆為 O(log n),可以想像內部就是用 BIT 實作的:

• query(dif, x):回傳x

i=1

dif[i] 的值

• query(dif2, x):回傳x

i=1

dif2[i] 的值

• update(dif, x, val):將 dif[x] 的值加上 val

• update(dif2, x, val):將 dif2[x] 的值加上 val 請回答以下問題:

(a) (10 pts) 請問如何使用給定的函式,在 O(log n) 時間內得到 arr[x] 的值?

(b) (15 pts) 現在將 arr 陣列中的一段區間 [a, b] 內的數都加上 val,請問如何在 O(log n) 時間內,使用給定的函式改變 dif 和 dif2 陣列以滿足定義?

(c) (15 pts) 請以 dif 陣列表示 arr 陣列的前綴和,也就是 ∑x

i=1

arr[i]。(答案中可 以有很多項 dif[i],甚至包含

,只要能表示就可以了。) (d) (15 pts) 請使用給定的函式,在 O(log n) 時間內得到x

i=1

arr[i]。

參考文獻

相關文件

In the implementation, a reservation buffer and a node translation table are proposed to reduce the number of unnecessary and frequent updates of information over flash-memory

In the attention heat maps of the original Transformer, we also ob- serve that it learns hierarchical structures in the first layer (Figure 7(a)) that the words mostly at- tend to

– Maintain a heavy path: Instead of recalculate all ances tors' value, only the corresponding overlapping subpa th will be recalculated. It cost O(1) time for each verte x, and

Binary tree based data structure Data structure based on binary

[r]

[r]

[r]

[r]