線段樹的知識

雖然網上有很多介紹了,我還是要寫一下吧,儘量從它的起源,如何被發現,以及爲什麼應該是這樣的來寫,單純的使用很簡單,也不是學習的目的,理解有助於記憶吧,當然可能還有理解不到的地方。

起源


樹狀數組英文名稱是Binary Indexed Trees,最早由Peter M. Fenwick於1994年MARCH以A New Data Structure for Cumulative Frequency Tables爲題發表在SOFTWARE PRACTICE AND EXPERIENCE。最初是爲了解決數據壓縮裏的累積頻率(Cumulative Frequency)的計算問題提出來的。再此之前採用的方法在這篇論文裏都有提到比如MTF(move-to-front),HEAP,SPLAY。現在則因爲實現簡單,結構單一,作爲計算前綴和的在線數據結構被廣泛採用。

按照Peter M. Fenwick的說法,該想法產生大概類比了整數與二進制的關係.
Each integer can be represented as sum of powers of two. In the same way, cumulative frequency can be represented as sum of sets of subfrequencies. In our case, each set contains some successive number of non-overlapping frequencies.
也就是說就像所有的整數都可以表示成2的次方的和那樣,我們也可以考慮把一串序列表示成一系列子序列的和。而實際上也正是採用這個想法,將一個前綴和劃分成了多個子序列的和,而劃分的方法與數的2的冪和具有及其相似的方式。首先子序列的個數也是其二進制表示中1的個數,同時子序列代表的f[i]的個數也是2的冪,這些與Integer都很類似。

之所以命名爲Binary indexed tree,Peter M. Fenwick有這樣的一段解釋,
In recognition of the close relationship between the tree traversal algorithms and the binary representation of an element index,the name "binar indexed tree" is proposed for the new structure.

前綴和的拆分:

比如我們假設用C[i]表示f[1]...f[i]的和,而用tree[idx]表示子序列,按照定義實際上tree[idx]是那些indexes from (idx - 2^r + 1) to idx的f[index]的和,其中r是idx最右邊的那個非零位到右邊末尾的0的個數。也就是說實際上tree[idx]的子序列中f[index]的個數也是2的冪,這點與integer也類似。通過對tree[idx]的這種定義,便可以方便的把C[idx]用若干個tree[idx]表示出來。實際上這樣C[idx]可以類比成某個Integer,而tree[idx]的f[i]個數可以類比成用來組合成該Integer的2的冪。
舉個例子:比如我們想求C[13]也就是f[1]+...+f[13];則C[13] = tree[13]+tree[12]+tree[8],另:
tree[13] = f[13]
tree[12] = f[12]+f[11]+f[10]+f[9]
tree[8] = f[8]+....f[1]
對應於Integer 13 = 1+4+8,上面tree[13],tree[12],tree[8]的f個數剛好也是1 4 8,完全統一。

{--------------------------------------------------------------------------------------------------------------------------------------
來一個引用自別人的圖和圖的解釋:
Binary indexed tree-樹狀數組 - 星星 - 銀河裏的星星
令這棵樹的結點編號爲C1,C2...Cn。令每個結點的值爲這棵樹的值的總和,那麼容易發現:
C1 = A1
C2 = A1 + A2
C3 = A3
C4 = A1 + A2 + A3 + A4
C5 = A5
C6 = A5 + A6
C7 = A7
C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
...
C16 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 + A9 + A10 + A11 + A12 + A13 + A14 + A15 + A16

這裏有一個有趣的性質,下午推了一下發現:
設節點編號爲x,那麼這個節點管轄的區間爲2^k(其中k爲x二進制末尾0的個數)個元素。因爲這個區間最後一個元素必然爲Ax,
所以很明顯:Cn = A(n – 2^k + 1) + ... + An

-----------------------------------------------------------------------------------------------------------------------------------------}


前綴和的計算:

姑且這樣猜測一下,我想Peter M. Fenwick當初也是最先根據這個猛然間的類比,做出了這樣的分解的嘗試,逐步把前綴和分解爲部分和,寫出了上面的表示方式。因爲如果可以分解成這種方式,就意味着如果所有的tree[idx]可以得到,那麼任意的C[i]就可以在lgn時間內算出了。而對於其他屬性的發現,則是這樣的嘗試之後必然存在的,不過他發現了它們。也就是文章本天成,妙手偶得之。或者說it exist in The Book.

更新的維護:

然後繼續考慮,當某個f[i]改變的時候,如何才能維護該f[i]所涉及到的tree[idx]就可以了,也就是找到那些包含了f[i]的tree[idx]元素,只要更新它們就可以了。

然後初步觀察,可以發現每個f[i]所隸屬的tree[idx]的數目實際上不會超過lgn,下面的任務只有找出這些tree[idx]就可以了。這樣基本上就完成了這個數據結構所應該做的工作。爲什麼每個f[i]所隸屬的tree[idx]不會超過lgn呢?

雖然不是顯而易見的,但也不是很隱蔽的。對於每個tree[idx] = f[idx]+f[idx-1]....+f[idx-2^r+1],如果f[i]屬於tree[idx]中的一員,應該滿足 idx-2^r+1  =< i <= idx,看起來還不是很明顯,也就是說idx滿足了這個條件,就可以包含i了,所以如果求包含i的tree[idx]的數目,實際上是求滿足這個不等式的idx的個數。

對於這個條件來說,轉換成更明顯的文字來說,就是idx應當具有如下性質在剪掉末尾1之前,它應當大於等於i,剪掉末尾1後,要小於等於i。對一系列的idx:a1000..,如果要使i處在a1000和a0000之間需要滿足如下條件,即a0000 <= i <= a1000,則a應當是固定的了,我們的idx具有這樣的特徵它的左半段a跟我們的i是相同的,剩下的就是1000了,因此1的位置只有Lgn種可能,而且只有該位置的二進制表示爲0時,纔可能存在對應的idx。有一點需要注意的是當a1000...長度大於i時,是一種特殊情況,可以單獨討論,當然也可以把i的前導0補足,如下例。

以5(00101)爲例,則包含它的idx有6(00110) 8(01000) 16(10000)這些,觀察6 8 16可以發現,它們實際上就是把5的某個非0位變成了1,同時把它右面的所有二進制位都變成了0。而在進一步的探索過程中,實際上這樣的一個過程便是通過不斷的i=i+lowbit(i),來實現的。

實現
需要支持的操作有:
計算某個前綴和:Read cumulative frequency
更新某個元素f[i]的值:Change frequency at some position and update tree
讀取某個位置的f值:Read the actual frequency at a position
縮放整個數組,比如數值全部減半:Scaling the entire tree by a constant factor
找到具有給定值的f[i]:Find index with given cumulative frequency

最常用的前三個,後兩個均可以增加一部分存儲f[i]元素的空間實現。
進行這些操作之前有一個必備的基本運算,lowbit,即計算整數裏最右邊的那個非零位,在Peter M. Fenwick的論文中提到了三種方法。

   return x-(x&(x–1));

   return x&(-x);

   return x&(2^k-x);2^k is a power of 2 greater than the table size.

 


如果要統計f[1]f[idx]之間的和,可以通過調用如下函數實現

int read(int idx){
int sum = 0;
while (idx > 0){
sum += tree[idx];
idx -= (idx & -idx);
}
return sum;
}

如果要把f[idx]增加var,可以通過調用如下函數實現

void update(int idx ,int val){
while (idx <= MaxVal){
tree[idx] += val;
idx += (idx & -idx);
}
}

讀取某個位置的值f[i]:
一般我們可以用sum[i]-sum[i-1],根據此結構還可以繼續優化。即計算兩個sum的時候,它們的路徑可能會相遇。

int readSingle(int idx){
int sum = tree[idx]; // sum will be decreased
if (idx > 0){ // special case
int z = idx - (idx & -idx); // make z first
idx--; // idx is no important any more, so instead y, you can use idx
while (idx != z){ // at some iteration idx (y) will become z
sum -= tree[idx];
// substruct tree frequency which is between y and "the same path"
idx -= (idx & -idx);
}
}
return sum;
}

樹的所有元素*一個常數因子,可以通過兩種方式實現
void scale(int c){
for (int i = 1 ; i <= MaxVal ; i++)
update(-(c - 1) * readSingle(i) / c , i);
}
void scale(int c){
for (int i = 1 ; i <= MaxVal ; i++)
tree[i] = tree[i] / c;
}
尋找具有給定值的tree[i],最簡單的方法當然還是將所有的tree[i]計算一遍,當然對於包含負數的數組,我們只能這樣做
但是對於,那些單調的tree[i]序列來說,完全可以採用二分查找的方法。
// if in tree exists more than one index with a same
// cumulative frequency, this procedure will return
// some of them (we do not know which one)

// bitMask - initialy, it is the greatest bit of MaxVal
// bitMask store interval which should be searched

int find(int cumFre){
int idx = 0; // this var is result of function

while ((bitMask != 0) && (idx < MaxVal)){ // nobody likes overflow :)
int tIdx = idx + bitMask; // we make midpoint of interval
if (cumFre == tree[tIdx]) // if it is equal, we just return idx
return tIdx;
else if (cumFre > tree[tIdx]){
// if tree frequency "can fit" into cumFre,
// then include it

idx = tIdx; // update index
cumFre -= tree[tIdx]; // set frequency for next loop
}
bitMask >>= 1; // half current interval
}
if (cumFre != 0) // maybe given cumulative frequency doesn't exist
return -1;
else
return idx;
}



// if in tree exists more than one index with a same
// cumulative frequency, this procedure will return
// the greatest one

int findG(int cumFre){
int idx = 0;

while ((bitMask != 0) && (idx < MaxVal)){
int tIdx = idx + bitMask;
if (cumFre >= tree[tIdx]){
// if current cumulative frequency is equal to cumFre,
// we are still looking for higher index (if exists)

idx = tIdx;
cumFre -= tree[tIdx];
}
bitMask >>= 1;
}
if (cumFre != 0)
return -1;
else
return idx;
}


總結
BIT很容易編碼實現
所有的查詢均花費logn或者常數時間
需要線性數量級的空間
可以擴展到n維的情況,BIT可以作爲一種多維的數據結構,即可擴展到多維。以上只是一維的情況。

發佈了72 篇原創文章 · 獲贊 1 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章