英文原文鏈接:鏈接地址
藍色是筆者註釋,高手請忽略
簡介
爲了使我們的算法更快,我們總是需要一些數據結構。在這篇文章中我們將討論二進制索引樹(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++)。
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){ |
例如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++寫的函數如下:
10 |
while ((bitMask
!= 0) && (idx < MaxVal)){ |
11 |
int tIdx
= idx + bitMask; |
12 |
if (cumFre
== tree[tIdx]) |
14 |
else if (cumFre
> tree[tIdx]){ |
33 |
int findG( int cumFre){ |
36 |
while ((bitMask
!= 0) && (idx < MaxVal)){ |
37 |
int tIdx
= idx + bitMask; |
38 |
if (cumFre
>= tree[tIdx]){ |
當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行