今天學習了一下樹狀數組,它的主要作用就是對於連續區間值和尋找,雖然修改的複雜度為logN,但是求a[1]~a[n]的和的時間複雜度也為logN.相對於線段樹來說,時間複雜度相同,而空間複雜度更低,而且編程難易度也較線段樹簡單,缺點就是適用範圍沒有線段樹廣.典型的例題可以看一下POJ的2352.
以下文章轉自http://www.cnblogs.com/yykkciwei/archive/2009/05/08/1452889.html
先廢話一下.
嗯 嗯 從今天開始 從基礎一步一步開始 追逐我的夢想!
今天先來討論一下樹狀數組.
問題提出:已知數組a[],元素個數為n,現在更改a中的元素,要求得新的a數組中i到j區間內的和(1<=i<=j<=n).
思考:對於這個問題,我們可以暴力地來解決,從a[i]一直累加到a[j],最壞的情況下複雜度為O(n),對於m次change&querry,合起來的複雜度為O(m*n),在n或m很大的情況下,這樣的複雜度是讓人無法忍受的.另外,如果沒有元素的變更,我們完全可以儲存sum[1,k](k=1,2,……),然後對任意給定的尋找區間[i,j],都可以方便的用ans=sum[1,j]-sum[1,i-1],當然這隻是沒有元素改變的情況下的比較最佳化的解法.那麼對於有元素變更的問題是否有更高效的方法呢?(廢話!沒有我還寫啥?!)可以想一下,每次更改的元素是比較少的,有時候甚至每次只改變一個元素,但是在用暴力方法求區間和的時候,卻對區間內所有的元素都累加了一遍,這樣其實造成了許多無謂的運算.這時候也許會想到如果能把一些結果存起來會不會減少很多運算?答案是肯定的,但問題是怎麼存,存什麼?如果存任意區間的話,n比較大的時候不但記憶體吃不消,而且儲存的量太大,不易更改,反而得不償失;那麼也許可以考慮儲存特定的一些區間(比如說線段樹,其實現在討論的問題用線段樹完全可以解,以後再詳細寫線段樹).那麼現在重新回過頭來,看下這個問題,我們已經確定了要儲存一些特定區間sum的想法,接下來我們要解決的無非是兩個問題:1、減少更改元素後對這些區間裡的sum值的更改時間.2、減少尋找的時間.
好了廢話了這麼半天,無非是想讓自己以及看到的人明白為什麼要用樹狀數組.
接下來正式入題.
首先我們可以借鑒元素不變更問題的最佳化方法,先得到前i-1項之和and前j項之和,以s[i]表示前i項之和,那麼sum[i,j]=s[j]-s[i-1].那麼現在的問題已經轉化為求前i項之和了.另外,我們已經確定要儲存一些特定區間的和,現在就要來揭示這些特定的區間究竟指什麼.
在文字說明之前先引入一個非常經典的,在網上找到的樹狀數組文章裡幾乎都要出現的一個圖片
不難發現,c[k]儲存的實際上是從k開始向前數k的二進位表示中右邊第一個1所代表的數字個元素的和(這麼說可能有點拗口,令lowbit為k的二進位表示中右邊第一個1所代表的數字,然後c[k]裡存的就是從a[k]開始向前數lowbit個元素之和)這麼存有什麼好處呢?無論是樹狀數組還是線段樹,都用到了分塊的思想,而樹狀數組採用這樣的儲存結構我想最主要的還是這樣方便計算,我們可以用位元運算輕鬆地算出lowbit.分析一下這樣做的複雜度:對於更改元素來說,如果第i個元素被修改了,因為我們最終還是要求和,所以可以直接在c數組裡面進行相應的更改,中的例子,假設更改的元素是a[2],那麼它影響到得c數組中的元素只有c[2],c[4],c[8],我們只需一層一層往上修改就可以了,這個過程的最壞的複雜度也不過O(logN);對於尋找來說,如尋找s[k],只需尋找k的二進位表示中1的個數次就能得到最終結果,比如尋找s[7],7的二進位表示中有3個1,也就是要尋找3次,到底是不是呢,我們來看,s[7]=c[7]+c[6]+c[4],可能你還不知道怎麼實現這個過程.
還以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].
代碼
int lowbit(int x)//計算lowbit
{
return x&(-x);
}
void add(int i,int val)//將第i個元素更改為val
{
while(i<=n)
{
c[i]+=val;
i+=lowbit(i);
}
}
int sum(int i)//求前i項和
{
int s=0;
while(i>0)
{
s+=c[i];
i-=lowbit(i);
}
return s;
}
以下文章轉自http://dongxicheng.org/structure/binary_indexed_tree/
1、概述
樹狀數組(binary indexed tree),是一種設計新穎的數組結構,它能夠高效地擷取數組中連續n個數的和。概括說,樹狀數組通常用於解決以下問題:數組{a}中的元素可能不斷地被修改,怎樣才能快速地擷取連續幾個數的和?
2、樹狀數組基本操作
傳統數組(共n個元素)的元素修改和連續元素求和的複雜度分別為O(1)和O(n)。樹狀數組通過將線性結構轉換成偽樹狀結構(線性結構只能逐個掃描元素,而樹狀結構可以實現跳躍式掃描),使得修改和求和複雜度均為O(lgn),大大提高了整體效率。
給定序列(數列)A,我們設一個數組C滿足
C[i] = A[i–2^k+ 1] + … + A[i]
其中,k為i在二進位下末尾0的個數,i從1開始算!
則我們稱C為樹狀數組。
下面的問題是,給定i,如何求2^k?
答案很簡單:2k=i&(i^(i-1)) ,也就是i&(-i)
下面進行解釋:
數組C的具體含義如所示:
當我們修改A[i]的值時,可以從C[i]往根節點一路上溯,調整這條路上的所有C[]即可,這個操作的複雜度在最壞情況下就是樹的高度即O(logn)。另外,對於求數列的前n項和,只需找到n以前的所有最大子樹,把其根節點的C加起來即可。不難發現,這些子樹的數目是n在二進位時1的個數,或者說是把n展開成2的冪方和時的項數,因此,求和操作的複雜度也是O(logn)。
樹狀數組能快速求任意區間的和:A[i] + A[i+1] + … + A[j],設sum(k) = A[1]+A[2]+…+A[k],則A[i] + A[i+1] + … + A[j] = sum(j)-sum(j-1)。
下面給出樹狀數組的C語言實現:
| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849 |
//求2^k int lowbit(int t) { return t & ( t ^ ( t - 1 ) ); } //求前n項和 int sum(int end) { int sum = 0; while(end > 0) { sum += in[end]; end -= lowbit(end); } return sum; } //增加某個元素的大小 void plus(int pos, int num) { while(pos <= n) { in[pos] += num; pos += lowbit(pos); } } |
3、擴充——二維樹狀數組
一維樹狀數組很容易擴充到二維,二維樹狀數組如下所示:
C[x][y] = sum(A[i][j])
其中,x-lowbit[x]+1 <= i<=x且y-lowbit[y]+1 <= j <=y
4、應用
(1) 一維樹狀數組:
參見:http://hi.baidu.com/lilu03555/blog/item/4118f04429739580b3b7dc74.html
(2) 二維樹狀數組:
一個由數字構成的大矩陣,能進行兩種操作
1) 對矩陣裡的某個數加上一個整數(可正可負)
2) 查詢某個子矩陣裡所有數位和
要求對每次查詢,輸出結果
5、總結
樹狀數組最初是在設計壓縮演算法時發現的(見參考資料1),現在也會經常用語維護子序列和。它與線段樹(具體見:資料結構之線段樹)比較在思想上類似,比線段樹節省空間的且編程複雜度低,但使用範圍比線段樹小(如查詢每個區間最小值問題)。
6、參考資料
(1) Binary Indexed Trees:
http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=binaryIndexedTrees
(2) 吳豪文章《樹狀數組》:
http://www.java3z.com/cwbwebhome/article/article19/zip/treearray.zip
(3) 郭煒文章《線段樹和樹狀數組》:
http://poj.org/summerschool/1_interval_tree.pdf