樹狀數組入門(Binary Indexed Tree)
樹狀數組是一種利用數的二進制特徵進行檢索的樹狀結構。是一種高效地對一個數字的列表進行更新及求前綴和的數據結構。
-
樹狀數組
在學習樹狀數組前,先看一下樹狀數組的結構:
A[n] 數組是原數組
tree[n] 數組就是樹狀數組,包含如下關係:tree[1] = A[1] tree[2] = A[1]+A[2]
tree[3] = A[3] tree[4] = A[1]+A[2]+A[3]+A[4]
tree[5] = A[5] tree[6] = A[5]+A[6]
tree[7] = A[7] tree[8] = A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8]看完樹狀數組的結構之後你可能完全不知道這其中存在什麼關係或是好奇爲什麼會有這樣的結構,接下來我們就學習一下樹狀數組的知識。
-
lowbit()操作
在學習樹狀數組之前,先學習一下樹狀數組的前置知識—lowbit() 操作。
這個lowbit() 操作實際上是用來取得一個十進制數用二進制表達時,最低位的 1 及其後面的零轉化成十進制所對應的值。例如 (6)10 的二進制表達爲 (1010)2 ,那麼 lowbit(6) 就等於 (10)2 ,對應十進制就是 (2)10 。
lowbit(x) = ((x) & (-x)) ,這個式子就完成了上述操作,其原理利用了計算機內部一個數的負數用其二進制補碼錶示。例如 x = (6)10 的二進制爲 (1010)2 ,它的補碼就是 (0110)2 ,所以 ((x) & (-x)) = (1010)2 & (0110)2 = (0010)2 = (2)10 。 -
構造樹狀數組
瞭解了lowbit() 操作之後,我們來看這個樹狀數組是如何構造出來的。
這裏我們令 m = lowbit(x) ,定義 tree[x] 爲 A[x] 和 他前面 m 個數相加的結果。例如 lowbit(6) = 2 ,則 tree[6] = A[6] + A[5] 。通過這個定義,就不難理解文章一開始給出 tree[n] 數組的關係了。那麼問題來了,前面說過,這個數據結構可以幫助我們快速高效地進行更新和求前綴和,那我們怎麼才能應用這個樹狀數組呢?接下來我們就學習一下樹狀數組的區間求和和單點更新操作。
-
區間求和
首先我們先看一個例子,求前六個數的和,即 sum(6) = A[1]+A[2]+…+A[6] 。
結合文章前面給出的樹狀數組的關係,可以看出,sum(6) = tree[6] + tree[4] 可以快速求得前六個值的和。
到這裏有沒有看出什麼規律來呢。其實,(6)10 對應的二進制表示爲 (1010)2 ,
首先 sum(6) = 0 + tree[6] ,
然後對 (6)10 進行下面操作 6-lowbit(6) ,那麼現在就是 6-2=4 ,sum(6) 再加上 tree[4] ,此時 sum(6) = 0 + tree[6] + tree[4] 。
然後再進行上述操作 4-lowbit(4) = 0, 我們的數組 A[n] 是從 1 開始的,所以此時求和操作就結束了。
所以最後的結果是 sum(6) = tree[6] + tree[4] 。
從上面這個例子可以看出來,區間和是如何通過 樹狀數組 和 lowbit() 操作得到了,並且將複雜度降到了 O(log2n) 。下面是代碼實現。int sum(int x){ int sum = 0; while(x>0){ sum += tree[x]; x -= lowbit(x); } return sum; }
通過上面的介紹可以看出,所謂的求區間和實際上是求前綴和,並不像線段樹一樣,可以求任意區間的和,樹狀數組默認求和區間是 [1…x] 。當需要求任意區間的時候可以通過區間和相減的方法完成。
相關線段樹的內容可以看一下另一篇文章 線段樹入門 -
單點更新
說完了區間求和的問題,我們來說一下樹狀數組單點更新(這裏的更新只涉及加上或減去某個值)的問題。
通過介紹樹狀數組的構建,我們知道,樹狀數組中某個元素 tree[x] 是原數組中一系列包含 A[x] 及其前面 lowbit(x) 個數的和,所以 A[x] 的改變關係到了 tree[x] 和後面某些元素的值,所以,當我們更新原數組中的 A[x] 時,要同時更新 tree[x] 和與他相關的元素。例如改變 A[3] 的值,同時要更新 tree[3] 、tree[4]、 tree[8] 的值。
那如何確定到底需要更新哪些元素呢。這裏依舊使用到了 lowbit() 操作。還是以 A[3] 爲例:
首先更新 tree[3] ,
然後 3 + lowbit(3) = 4 ,更新 tree[4] ,
然後 4 + lowbit(4) = 8 ,更新 tree[8] ,
重複進行,直到更新到 tree[n] 。
下面是代碼實現:void add(int x, int d){ //d是要加上或者減去的數 while(x<=n){ tree[x] += d; x += lowbit(x); } }
-
區間更新
學過線段樹的話,我們會知道還有一個操作叫區間更新,如果我們的操作是多次更新區間的值(區間內每個元素同時加上或減去某個值),使用樹狀數組的話,複雜度是很高的,遠遠超過了直接更新原數組的值。線段樹的區間更新需要藉助 lazy 標記。同樣,一個序列的區間更新可以藉助 差分數組 ,其時間複雜度遠遠低於直接更新。差分數組請見下一篇博客:差分數組。