Binary Indexed Trees[二進制索引樹]

英文原文鏈接:鏈接地址

藍色是筆者註釋,高手請忽略

簡介

爲了使我們的算法更快,我們總是需要一些數據結構。在這篇文章中我們將討論二進制索引樹(Binary Indexed Tree)。依據Peter M. Fenwick,這個數據結構首先用於數據壓縮。現在它多用於存儲頻率和操作累計頻率表。


問題定義如下:我們有N個盒子。通常的操作是

1. 在第i個盒子中加入球

2. 求從盒子l到盒子k中球的總和


最天真的做法對於操作1而言時間複雜度是O(1),對於操作2的時間複雜度是O(n)。假設我們查詢m次,最壞情況下操作的時間複雜度是O(m*n)。使用一些數據結構(例如RMQ我也不知道是什麼東西)可以將這個問題的最差時間複雜度控制在O(m*lg n)。另一種解決方式就是使用Binary Indexed Tree數據結構,最壞情況下的時間複雜度依然是O(m*lg n),然是Binary Indexed Tree更容易編碼,也有更小的空間使用量,相比RMQ而言。

註記

BIT Binary Indexed Tree 二進制索引樹
MaxVal  maximum value which will have non-zero frequency 非零最大值
f[i] frequency of value with index i, i = 1 .. MaxVal 這個可以理解爲每個盒子中小球的個數
c[i] cumulative frequency for index i (f[1] + f[2] + ... + f[i])
tree[i] sum of frequencies stored in BIT with index i (latter will be described what index means); sometimes we will write tree frequency instead sum of frequencies stored in BIT  
在BIT中存儲的頻率(小球個數)的和;有時在BIT中我們使用tree 頻率來替代頻率和
num¯ complement of integer num (integer where each binary digit is inverted: 0 -> 1; 1 -> 0 )  求num的反
  NOTE: Often we put f[0] = 0, c[0] = 0, tree[0] = 0, so sometimes I will just ignore index 0.





基本思路

每個整數都可以表示爲2的次冪的和。同理,累計的頻率也可以表示爲子頻率集合的和。在我們這篇文章裏,每一個集合含有一些連續但互補重合的頻率子集。

idx是BIT的索引,r是idx以二進制表示後最右側的0的位置( 很繞口,解釋一下哈。比如idx爲12,二進制爲1100,則r=2。再來一個,idx=9,二進制1001, 則r=0)。那麼tree[idx]是從 ( idx  - 2^ r  + 1)到idx的平率和(看錶1.1)(即f[idx - 2^r + 1]+...f[idx])。同時我們還說idx是負責(responsible)從(idx - 2^r + 1)到idx的索引(  注意,這裏是算法的關鍵,也是操作tree的方法)

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
f 1 0 2 1 1 3 0 4 2 5 2 2 3 1 0 2
c 1 1 3 4 5 8 8 12 14 19 21 23 26 27 27 29
tree 1 1 2 4 1 4 0 12 2 7 2 11 3 4 0 29

  table 1.1

(Tips:不要嘗試去推理f(i)的值,因爲這是給定的例子。c[i]和tree[i]是計算的結果,需要理解)


  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
tree 1 1..2 3 1..4 5 5..6 7 1..8 9 9..10 11 9..12 13 13..14 15 1..16

table1.2 responsibility 表 (即tree[i]表示是f[]~f[]的和,例如tree[10]=f[9]+f[10])

image 1.3 tree 負責的index(bar顯示的是累加的頻率)
 
image 1.4 帶有tree頻率和的tree

假設我們要尋找index 13的累加頻率。在二進制表示中,13表示爲1101。據此我們計算c[1101]=tree[1101]+tree[1100]+tree[1000]。

找出最後的1

我們需要多次的從二進制數中獲得最後一個1,所以我們需要一個高效的方法。假設我們想從num中獲取最後的1.在二進制中num可以表示爲a1b,a代表最後一個1之前的所有二進制位,b表示在這個1之後的0.
-num  = (a1b)¯+ 1 = a¯0b¯ + 1.  由於b全市由0構成,所以b¯全部是1.由此可得
-num = (a1b)¯ + 1 = a¯0b¯ + 1 = a¯0(0...0)¯ + 1 = a¯0(1...1) + 1 = a¯1(0...0) = a¯1b.
我們現在可以簡單的獲得最後一個1,讓num和-num做位與運算:

           a1b
&      a¯1b
--------------------
= (0...0)1(0...0)


讀取累計的頻率和
如果我們需要讀取整數idx的頻率累計和,我們可以讓sum加上tree[idx]的值,然後讓idx減去最有一個1(我們也可以說移走最後的1,使最後的1變爲0),然後重複上述過程直至idx爲0.我們可以使用下面這段代碼(C++)。
1 int read(int idx){
2     int sum = 0;
3     while (idx > 0){
4         sum += tree[idx];
5         idx -= (idx & -idx);
6     }
7     return sum;
8 }

舉例: idx=13,sum=0:


iteration idx position of the last digit idx & -idx sum
1 13 = 1101 0 1 (2 ^0) 3
2 12 = 1100 2 4 (2 ^2) 14
3 8 = 1000 3 8 (2 ^3) 26
4 0 = 0 --- --- ---

 
image 1.5 箭頭指示了在遍歷過程中使用的數據.

所以我們的結果是26.這個函數中遍歷的次數是idx含有的1的個數,最大的便利次數也就是log MaxVal。
時間複雜度: O(log MaxVal).
代碼的複雜度: 如上代碼

改變一些位置的頻率並更新tree

當改變某些位置的頻率時,所有tree中負責該位置的都需要更新。在讀取idx的累計和時我們移走idx最後的1並且循環繼續。修改tree中的一些值val時,我們需要增加當前idx的tree值tree[idx],增加idx最後一位的1(例如idx爲6,該值增加了val,當tree[6]增加了val後。6的最後等於1的一位是2,所以6+2=8,需要繼續修改tree[8]的值)並且循環繼續之前的過程,只要idx小於MaxVal.C++寫的函數如下
1 void update(int idx ,int val){
2     while (idx <= MaxVal){
3         tree[idx] += val;
4         idx += (idx & -idx);
5     }
6 }
例如idx=5:
iteration idx position of the last digit idx & -idx
1 5 = 101 0 1 (2 ^0)
2 6 = 110 1 2 (2 ^1)
3 8 = 1000 3 8 (2 ^3)
4 16 = 10000 4 16 (2 ^4)
5 32 = 100000 --- ---

image 1.6 更新idx爲5的頻率時遍歷的順序
使用如上算法我們可以更新整個BIT。
時間複雜度: O(log MaxVal)
代碼長度:最長10行

讀取某個位置的頻率值

(未翻譯)

整個樹乘以或除以某個常數

(未翻譯)

給定累計的頻率值,找出index(翻譯這麼多終於到我要用的地方了)

最笨最天真的解決方法就是遍歷整個索引,計算累計頻率,檢查是否等於給定的值。如果考慮存在負數的話,這是唯一解決方案。但是如果我們只有非負的頻率值的話(也就是說對於遞增的index,累計頻率值不減少)我們可以找到指數級的算法,這個算法由二分搜索修改而來。逐步遍歷所有的位(從最高爲開始),比較當前index的累計頻率和給出的值,依據大於小於結果選擇高一半或者低一半(就像二分查找)。C++寫的函數如下:
01 // if in tree exists more than one index with a same
02 // cumulative frequency, this procedure will return
03 // some of them (we do not know which one)
04  
05 // bitMask - initialy, it is the greatest bit of MaxVal
06 // bitMask store interval which should be searched
07 int find(int cumFre){
08     int idx = 0; // this var is result of function
09      
10     while ((bitMask != 0) && (idx < MaxVal)){ // nobody likes overflow :)
11         int tIdx = idx + bitMask; // we make midpoint of interval
12         if (cumFre == tree[tIdx]) // if it is equal, we just return idx
13             return tIdx;
14         else if (cumFre > tree[tIdx]){
15                 // if tree frequency "can fit" into cumFre,
16                 // then include it
17             idx = tIdx; // update index
18             cumFre -= tree[tIdx]; // set frequency for next loop
19         }
20         bitMask >>= 1; // half current interval
21     }
22     if (cumFre != 0) // maybe given cumulative frequency doesn't exist
23         return -1;
24     else
25         return idx;
26 }
27  
28  
29  
30 // if in tree exists more than one index with a same
31 // cumulative frequency, this procedure will return
32 // the greatest one
33 int findG(int cumFre){
34     int idx = 0;
35      
36     while ((bitMask != 0) && (idx < MaxVal)){
37         int tIdx = idx + bitMask;
38         if (cumFre >= tree[tIdx]){
39                 // if current cumulative frequency is equal to cumFre,
40                 // we are still looking for higher index (if exists)
41             idx = tIdx;
42             cumFre -= tree[tIdx];
43         }
44         bitMask >>= 1;
45     }
46     if (cumFre != 0)
47         return -1;
48     else
49         return idx;
50 }

當cumFre爲21 時調用find的情況:

First iteration tIdx is 16; tree[16] is greater than 21; half bitMask and continue
Second iteration tIdx is 8; tree[8] is less than 21, so we should include first 8 indexes in result, remember idx because we surely know it is part of result; subtract tree[8] of cumFre (we do not want to look for the same cumulative frequency again - we are looking for another cumulative frequency in the rest/another part of tree); half bitMask and contiue
Third iteration tIdx is 12; tree[12] is greater than 9 (there is no way to overlap interval 1-8, in this example, with some further intervals, because only interval 1-16 can overlap); half bitMask and continue
Forth iteration tIdx is 10; tree[10] is less than 9, so we should update values; half bitMask and continue
Fifth iteration tIdx is 11; tree[11] is equal to 2; return index (tIdx)

時間複雜度: O(log MaxVal)
代碼長度: 小於20行
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章