樹狀數組簡介

樹狀數組是一個查詢和修改複雜度都爲log(n)的數據結構,假設數組a[1...n],那麼查詢a[1] + …… + a[i] 的時間是log級別的,而且是一個在線的數據結構,支持隨時修改某個元素的值,複雜度也爲log級別。
來觀察一下這個圖:
<!--[if !vml]--><!--[endif]-->
令這棵樹的結點編號爲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
……
C2^n=a1+a2+….+a2^n

對於序列a,我們設一個數組C定義C[t] = a[t – 2^k + 1] + … + a[t],k爲t在二進制下末尾0的個數。
K的計算可以這樣:
2^k=t and (t xor (t-1))
以6爲例
               (6)10=(0110)2
xor    6-1=(5)10=(0101)2
                        (0011)2
and          (6)10=(0110)2
                        (0010)2


這裏有一個有趣的性質:
設節點編號爲x,那麼這個節點管轄的區間爲2^k(其中k爲x二進制末尾0的個數)個元素。因爲這個區間最後一個元素必然爲Ax,所以很明顯:
Cn = A(n – 2^k + 1) + …… + An
算這個2^k有一個快捷的辦法,定義一個函數如下即可:
int lowbit(int x){
return x & (x ^ (x – 1)); //return x & (-x);
}

當想要查詢一個SUM(n)時,可以依據如下算法即可:
step1: 令sum = 0,轉第二步;
step2: 假如n <= 0,算法結束,返回sum值,否則sum = sum + Cn,轉第三步;
step3: 令n = n – lowbit(n),轉第二步。


可以看出,這個算法就是將這一個個區間的和全部加起來,爲什麼是效率是log(n)的呢?以下給出證明:
n = n – lowbit(n)這一步實際上等價於將n的二進制的最後一個1減去。而n的二進制裏最多有log(n)個1,所以查詢效率是log(n)的。

那麼修改呢,修改一個節點,必須修改其所有祖先,最壞情況下爲修改第一個元素,最多有log(n)的祖先。所以修改算法如下(給某個結點i加上x):
step1: 當i > n時,算法結束,否則轉第二步;
step2: Ci = Ci + x, i = i + lowbit(i)轉第一步。

i = i +lowbit(i)這個過程實際上也只是一個把末尾1補爲0的過程。
//修改過程必須滿足減法規則!

 


樹狀數組是一個可以很高效的進行區間統計的數據結構。在思想上類似於線段樹,比線段樹節省空間,編程複雜度比線段樹低,但適用範圍比線段樹小。

以簡單的求和爲例。設原數組爲a[1..N],樹狀數組爲c[1..N],其中c[k] = a[k-(2^t)+1] + ... + a[k]。比如c[6] = c[5] + c[6]。也就是說,把k表示成二進制1***10000,那麼c[k]就是1***00001 + 1***00010 + ... + 1***10000這一段數的和。設一個函數lowestbit(k)爲取得k的最低非零位,容易發現,根據上面的表示方法,從a[1]到a[k]的所有數的總和即爲sum[k] = c[k] + c[k-lowestbit(k)] + c[k-lowestbit(k)-lowestbit(k-lowestbit(k))] + ... 於是可以在logk的時間內求出sum[k]。當數組中某元素髮生變化時,需要改動的c值是c[k],c[k+lowestbit(k)], c[k+lowestbit(k)+lowestbit(k+lowestbit(k))] ... 這個複雜度是logN (N爲最大範圍)

擴展到多維情況:以二維爲例,用c[k1][k2]表示c[k1-(2^t1)+1][k2-(2^t2)+1] + ... + c[k1][k2]的總和。可以用類似的方法進行處理。複雜度爲(logn)^k (k爲維數)

樹狀數組相比線段樹的優勢:空間複雜度略低,編程複雜度低,容易擴展到多維情況。劣勢:適用範圍小,對可以進行的運算也有限制,比如每次要查詢的是一個區間的最小值,似乎就沒有很好的解決辦法。

多維情況的幾道題目:

POJ 2155 Matrix
URAL 1470 UFOs

其中POJ 2155是一道很不錯的題目,表面上看,這題的要求似乎和樹狀數組的使用方法恰好相反,改變的是一個區間,查詢的反而是一個點。實際上可以通過一個轉化巧妙的解決。

首先對於每個數A定義集合up(A)表示{A, A+lowestbit(A), A+lowestbit(A)+lowestbit(A+lowestbit(A))...} 定義集合down(A)表示{A, A-lowestbit(A), A-lowestbit(A)-lowestbit(A-lowestbit(A)) ... , 0}。可以發現對於任何A<B,up(A)和down(B)的交集有且僅有一個數。

於是對於這道題目來說,翻轉一個區間[A,B](爲了便於討論先把原問題降爲一維的情況),我們可以把down(B)的所有元素的翻轉次數+1,再把down(A-1)的所有元素的翻轉次數-1。而每次查詢一個元素C時,只需要統計up(C)的所有元素的翻轉次數之和,即爲C實際被翻轉的次數。

實際實現時,由於只考慮奇偶,因此無須統計確切的翻轉次數。另外,如果翻轉up(A)和up(B+1),查詢down(C),也是同樣的效果。這種方法可以很容易地擴展到二維情況。比起線段樹、四分樹之類的常規思路,無論編程複雜度還是常數速度上都有很大優勢。

PS:
int lowbit(int t)
{
    return t & (-t);
}
void ...()
{    ...
    pos+=lowbit(pos); //如果pos=0,那麼這個地方pos將永遠是0
}

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/oopos/archive/2007/10/14/1824583.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章