樹狀數組

來自:http://www.cnblogs.com/Creator/archive/2011/09/10/2173217.html

 

在一個數組中。若你需要頻繁的計算一段區間內的和,你會怎麼做?,最最簡單的方法就是每次進行計算,但是這需要O(N)的時間複雜度,如這個需求非常的頻繁,那麼這個操作就會佔用大量的CPU時間,進一步想一想,你有可能會想到使用空間換取時間的方法,把每一段區間的值一次記錄下來,然後存儲在內存中,將時間複雜度降低到O(1),的確,對於目前的這個需求來說,已經能夠滿足時間複雜度上的要求,儘管帶來了線性空間複雜度的提升.

但若是我們的源數據需要頻繁的更改怎麼辦?使用上面的方案,我們需要大量的更新我們保存到內存中的區間和,而且這中間的很多更新的影響是重疊的,我們需要重複計算。例如對於數組array[10],更新了array[4]值,需要更新區間[4,5],[4,5,6],在更新[4,5,6]需要又一次的計算[4,5],這樣的更新帶來了非常多的重複計算,爲了解決這一問題,樹狀數組應運而生了。

當要頻繁的對數組元素進行修改,同時又要頻繁的查詢數組內任一區間元素之和的時候,可以考慮使用樹狀數組.樹狀數組是一種非常優雅的數據結構.先來看看一張樹狀結構的圖片

QQ截圖未命名1

圖中C[1]的值等於A[1],C[2]的值等於C[1]+A[2]=A[1]+a[2],C[4]的值=C[2]+C[3]=A[1]+A[2]+A[3]+A[4],假設我們現在需要更改元素a[2],那麼它將隻影響到得c數組中的元素有c[2],c[4],c[8],我們只需要重新計算這幾個值即可,減少了很多重複的操作。這就是樹狀結構大致的一個存貯示意圖,下面看看他的定義:

假設a[1...N]爲原數組,定義c[1...N]爲對應的樹狀數組:

c[i] = a[i - 2^k + 1] + a[i - 2^k + 2] + ... + a[i] (其中k爲i的二進制表示末尾0的個數)

下面枚舉出i由1...5的數據,可見正是因爲上面的a[i - 2^k + 1]...a[i]的計算公式保證了我們C數組的正確意義,至於證明過程,大家可以翻閱相關資料..

p_w_picpath

基本操作:

對於C[i]=a[i - 2^k + 1]...a[i]的定義中,比較難以逐磨的k,他的值等於i這個數的二進制表示末尾0的個數.如4的二進制表示0100,此時k就等於2,而實際上我們還會發現2^k就是前一位的權值,即0100中,2^2=4,剛好是前一位數1的權值.所以所以2^k可以表示爲n&(n^(n-1))或更簡單的n&(-n),例如:

爲了表示簡便,假設現在一個int型爲4位,最高位爲符號位

int i=3&(-3); 此時i=1,3的二進制爲0011,-3的二進制爲1101(負數存的是補碼)所以0011&1101=1

int j=4&(-4); 此時j=4,理由同上..

所以計算2^k我們可以用如下代碼:

 

1 int lowbit(int x)//計算lowbit
2 {
3 return x&(-x);
4 }

 

求和操作:

在上面的示意圖中,若我們需要求sum[1..7]個元素的和,僅需要計算c[7]+c[6]+c[4]的和即可,究竟時間複雜度怎麼算呢?一共要進行多少次求和操作呢?

求sum[1..k],我們需查找k的二進制表示中1的個數次就能得到最終結果,具體爲什麼,請見代碼i-=lowbit(i)註釋

 

1 int sum(int i)//求前i項和
2 {
3 int s=0;
4 while(i>0)
5 {
6 s+=c[i];
7 i-=lowbit(i); //這一步實際上等價於將i的二進制表示的最後一個1剪去,再向前數當前1的權個數(例子在下面),
8 //而n的二進制裏最多有log(n)個1,所以查詢效率是log(n)
9 //在示意圖上的操作即可理解爲依次找到所有的子節點
10 }
11 return s;
12 }

 

以求sum[1..7]爲例,二進制爲0111,右邊第一個1出現在第0位上,也就是說要從a[7]開始向前數1個元素(只有a[7]),即c[7];

然後將這個1舍掉,得到6,二進制表示爲0110,右邊第一個1出現在第1位上,也就是說要從a[6]開始向前數2個元素(a[6],a[5]),即c[6];

然後舍掉用過的1,得到4,二進制表示爲0100,右邊第一個1出現在第2位上,也就是說要從a[4]開始向前數4個元素(a[4],a[3],a[2],a[1]),即c[4].

所以s[7]=c[7]+c[6]+c[4]

給源數組加值操作:

在上面的示意圖中,假設更改的元素是a[2],那麼它影響到得c數組中的元素有c[2],c[4],c[8],我們只需一層一層往上修改就可以了,這個過程的最壞的複雜度也不過O(logN);

 

void add(int i,int val)
{
while(i<=n)
{
c[i]
+=val;
i
+=lowbit(i); //i+(i的二進制中最後一個1的權值,即2^k),在示意圖上的操作即爲提升一層,到上一層的節點.
//這個過程實際上也只是一個把末尾1後補0的過程(例子在下面)
}
}

 

以修改a[2]元素爲例,需要修改c[2],2的二進制爲0010,末尾補0爲0100,即c[4]

4的二進制爲0100,在末尾補0爲1000即c[8]。所以我們需要修改的有c[2],c[4],c[8]

 

POJ上面有一個這方面的水題,可以幫助理解 http://poj.org/problem?id=2352

解題報告http://hi.baidu.com/acmerskycoding/blog/item/40af1b2585dd310a4c088d95.html

 

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