樹狀數組---整理網上的資料

http://blog.sina.com.cn/s/blog_8627bf080100sq46.html 我查過的資料這個最詳細。

http://community.topcoder.com/tc?module=Static&d1=tutorials&d2=binaryIndexedTrees topcoder上講的這個。注意c[i]在不同的文章裏表示的意思也不同。有的表示數組,有的表示求和數組。


第01講 什麼是樹狀數組?

樹狀數組用來求區間元素和,求一次區間元素和的時間效率爲O(logn)。

有些同學會覺得很奇怪。用一個數組S[i]保存序列A[]的前i個元素和,那麼求區間i,j的元素和不就爲S[j]-S[i-1],那麼時間效率爲O(1),豈不是更快?

但是,如果題目的A[]會改變呢?例如:

我們來定義下列問題:我們有n個盒子。可能的操作爲

1.向盒子k添加石塊

2.查詢從盒子i到盒子j總的石塊數

自然的解法帶有對操作1爲O(1)而對操作2爲O(n)的時間複雜度。但是用樹狀數組,對操作1和2的時間複雜度都爲O(logn)。


第02講 圖解樹狀數組C[]

現在來說明下樹狀數組是什麼東西?假設序列爲A[1]~A[8]

[轉載]樹狀數組

網絡上面都有這個圖,但是我將這個圖做了2點改進。

(1)圖中有一棵滿二叉樹,滿二叉樹的每一個結點對應A[]中的一個元素。

(2)C[i]爲A[i]對應的那一列的最高的節點。

現在告訴你:序列C[]就是樹狀數組。

那麼C[]如何求得?

C[1]=A[1];

C[2]=A[1]+A[2];

C[3]=A[3];

C[4]=A[1]+A[2]+A[3]+A[4];

C[5]=A[5];

C[6]=A[5]+A[6];

C[7]=A[7];

C[8]= A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];

以上只是枚舉了所有的情況,那麼推廣到一般情況,得到一個C[i]的抽象定義:

因爲A[]中的每個元素對應滿二叉樹的每個葉子,所以我們乾脆把A[]中的每個元素當成葉子,那麼:C[i]=C[i]的所有葉子的和。

現在不得不引出關於二進制的一個規律:

先仔細看下圖:

[轉載]樹狀數組

將十進制化成二進制,然後觀察這些二進制數最右邊1的位置:

1 --> 00000001

2 --> 00000010

3 --> 00000011

4 --> 00000100

5 --> 00000101

6 --> 00000110

7 --> 00000111

8 --> 00001000

1的位置其實從我畫的滿二叉樹中就可以看出來。但是這與C[]有什麼關係呢?

接下來的這部分內容很重要:

在滿二叉樹中,

以1結尾的那些結點(C[1],C[3],C[5],C[7]),其葉子數有1個,所以這些結點C[i]代表區間範圍爲1的元素和;

以10結尾的那些結點(C[2],C[6]),其葉子數爲2個,所以這些結點C[i]代表區間範圍爲2的元素和;

以100結尾的那些結點(C[4]),其葉子數爲4個,所以這些結點C[i]代表區間範圍爲4的元素和;

以1000結尾的那些結點(C[8]),其葉子數爲8個,所以這些結點C[i]代表區間範圍爲8的元素和。

擴展到一般情況:

i的二進制中的從右往左數有連續的x個“0”,那麼擁有2^x個葉子,爲序列A[]中的第i-2^x+1到第i個元素的和。

終於,我們得到了一個C[i]的具體定義:

C[i]=A[i-2^x+1]+…+A[i],其中x爲i的二進制中的從右往左數有連續“0”的個數。


第03講 利用樹狀數組求前i個元素的和S[i]

理解了C[i]後,前i個元素的和S[i]就很容易實現。

從C[i]的定義出發:

C[i]=A[i-2^x+1]+…+A[i],其中x爲i的二進制中的從右往左數有連續“0”的個數。

我們可以知道:C[i]是肯定包括A[i]的,那麼:

S[i]=C[i]+C[i-2^x]+…

也許上面這個公式太抽象了,因爲有省略號,我們拿一個具體的實例來看:

S[7]=C[7]+C[6]+C[4]

因爲C[7]=A[7],C[6]=A[6]+A[5],C[4]=A[4]+A[3]+A[2]+A[1],所以S[7]=C[7]+C[6]+C[4]

(1)i=7,求得x=0,那麼我們求得了A[7];

(2)i=i-2^x=6,求得x=1,那麼求得了A[6]+A[5];

(3)i=i-2^x=4,求得x=2,那麼求得了A[4]+A[3]+A[2]+A[1]。

講到這裏其實有點難度,因爲S[i]的求法,如果要講清楚,那麼得寫太多的東西了。所以不理解的同學,再反覆多看幾遍。

從(1)(2)(3)這3步可以知道,求S[i]就是一個累加的過程,如果將2^x求出來了,那麼這個過程用C++實現就沒什麼難度。

現在直接告訴你結論:2^x=i&(-i)

證明:設A’爲A的二進制反碼,i的二進制表示成A1B,其中A不管,B爲全0序列。那麼-i=A’0B’+1。由於B爲全0序列,那麼B’就是全1序列,所以-i=A’1B,所以:

i&(-i)= A1B& A’1B=1B,即2^x的值。

所以根據(1)(2)(3)的過程我們可以寫出如下的函數:

int Sum(int i) //返回前i個元素和

{

       int s=0;

       while(i>0)

       {

              s+=C[i];

              i-=i&(-i);

       }

       return s;

}

第04講 更新C[]

正如第01講提到的小石塊問題,如果數組A[i]被更新了怎麼辦?那麼如何改動C[]?

如果改動C[]也需要O(n)的時間複雜度,那麼樹狀數組就沒有任何優勢。所以樹狀數組在改動C[]上面的時間效率爲O(logn),爲什麼呢?

因爲改動A[i]只需要改動部分的C[]。這一點從第02講的圖中就可以看出來:

[轉載]樹狀數組

如上圖:

    假如A[3]=3,接着A[3]+=1,那麼哪些C[]需要改變呢?

    答案從圖中就可以得出:C[3],C[4],C[8]。因爲這些值和A[3]是有聯繫的,他們用樹的關係描述就是:C[3],C[4],C[8]是A[3]的祖先。

    那麼怎麼知道那些C[]需要變化呢?

    我們來看“A”這個結點。這個“A”結點非常的重要,因爲他體現了一個關係:A的葉子數爲C[3]的2倍。因爲“A”的左子樹和右子樹的葉子數是相同的。因爲2^x代表的就是葉子數,所以C[3]的父親是A,A的父親是C[i+2^0],即C[3]改變,那麼C[3+2^0]也改變。

我們再來看看“B”這個結點。B結點的葉子數爲2倍的C[6]的葉子數。所以B和C[6+2^1]在同一列,所以C[6]改變,C[6+2^1]也改變。

    推廣到一般情況就是:

如果A[i]發生改變,那麼C[i]發生改變,C[i]的父親C[i+2^x]也發生改變。

這一行的迭代過程,我們可以寫出當A[i]發生改變時,C[]的更新函數爲:

void Update(int i,int value)  //A[i]的改變值爲value

{

       while(i<=n)

       {

              C[i]+=value;

              i+=i&(-i);

       }

}

第05講 一維樹狀數組的應用舉例

廢了4講的話,我們終於把一維樹狀數組的2個不到5行的代碼給搞定了。現在要正式投入到應用當中。

題目鏈接:http://poj.org/problem?id=2352

題意:按照y升序給你n個星星的座標,如果有m個星星的x,y座標均小於等於星星A的座標,那麼星星A的等級爲m。

分析:是一道樹狀數組題。舉例來說,以下是題目的輸入:

5

1 1

5 1

7 1

3 3

5 5

由於y座標是升序的且座標不重複,所以在星星A後面輸入的星星的x,y座標不可能都小於等於星星A。假如當前輸入的星星爲(3,3),易得我們只需要去找樹狀數組中小於等於3的值就可以了,即GetSum(3)。注意:A[i]表示x座標爲i的個數,C[]爲A[]的樹狀數組,那麼GetSum(i)就是序列中前i個元素的和,即x小於等於i的星星數。

本題還是一點要注意:星星座標的輸入可以是(0,0),所以我們把x座標統一加1,然後用樹狀數組實現。


第06講 二維樹狀數組

BIT可用爲二維數據結果。假設你有一個帶有點的平面(有非負的座標)。你有三個問題:

1.在(x , y)設置點

2.從(x , y)移除點

3.在矩形(0 , 0), (x , y)計算點數 - 其中(0 , 0)爲左下角,(x , y)爲右上角,而邊是平行於x軸和y軸。

對於1操作,在(x,y)處設置點,即Update(x,y,1),那麼這個Update要怎麼寫?很簡單,因爲x,y座標是離散的,所以我們分別對x,y進行更新即可,函數如下:

void Update(int x,int y,int val)

{

       while(x<=n)

       {

              int y1=y;

              while(y1<=n)

              {

                     C[x][y1]+=val;

                     y1+=y1&(-y1);

              }

              x+=x&(-x);

       }

}

那麼根據Update可以推得:GetSum函數爲:

int GetSum(int x,int y)

{

       int sum=0;

       while(x>0)

       {

              int y1=y;

              while(y1>0)

              {

                     sum+=C[x][y1];

                     y1-=y1&(-y1);

              }

              x-=x&(-x);

       }

       return sum;

}

第07講 二維樹狀數組的應用舉例

題目鏈接:http://poj.org/problem?id=2155

我們先討論POJ2155的一維情況,如下:

有一個n卡片的陣列。每個卡片倒放在桌面上。你有兩個問題:

  1. T i j (反轉從索引i到索引j的卡片,包括第i張和第j張卡——面朝下的卡將朝上;面朝上的卡將朝下)

  2. Q i (如果第i張卡面朝下回答0否則回答1)

解決:

解決問題(1和2)的方法有時間複雜度O(log n)。在數組f(長度n + 1)我們存儲每個問題T(i, j)——我們設置f[i]++和f[j + 1]--。對在i和j之間(包括i和j)每個卡k求和f[1] + f[2] + ... + f[k]將遞增1,其他全部和前面的一樣(看圖2.0清楚一些),我們的結果將描述爲和(和累積頻率一樣)模2。

[轉載]樹狀數組

圖 2.0

使用BIT來存儲(增加/減少)頻率並讀取累積頻率。

理解了一維的情況,POJ2155就是其二維的版本,易得只需要更(x1,y1),(x1,y2+1),(x2+1,y1),(x2+1,y2+1)四個點的C[]的值就可以了,最後的結果依然是GetSum(x,y)%2


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