樹狀數組————BIT(Binary Index Tree),又稱二分索引樹。不得不承認,二分索引樹的名字更能反應它的本質,而樹狀數組這個名字則更加的直觀。
與其說是一種算法,不如說是一種數據結構。從名字可以直觀的看出,這是一種像是樹一樣的數組。這就具有很多優點,能夠在log(n)的時間內進行查詢、求和等操作。
下面看這張圖(提到樹狀數組必須離不開這個圖)
其中以A表示原數組,C表述樹狀數組。
下面給出一個構造的原則(或者說樹狀數組的每一個結點的含義吧):C[i] = A[i-2^k+1] + .........+A[i]
其中這個k的含義是什么呢?
即表示將i轉化為2進制后,從右往左數,0的個數。(大家大可不必問為什么,這個證明應該是數學問題了,找了一下沒有發現相關資料於是我就作罷了...)
比如說,i = 8
8的二進制表示為00001000,從右往左數到第一個1為止共有三個0,則k = 3
那么C[i]表示的是從A[8 - 2 ^ 3 + 1] 到 A[8]也即A[1......8]的值
下面給出求一個函數LowBit(int index)用來求2^k
相信這個地方還是很難理解,我舉幾個例子看看。
例如,index = 6 經過LowBit運算后得出結果為2那么C[6] = A[5] + A[6]
(k == 2 剛好是表示A[i-1 + 1] + A[i - 1 + 1]這兩個數的和,和前邊的構造原則是一致的)
有一個簡單的記憶方式,6的因子有1,2,3,6,這四個因子中有一個2是2的n次方,所以這個2就是我們想要求的那個k
在打一個比方,8的因子有1,2,4,8,其中有三個數可以表示成2的n次方(2,4,8),但那個最大的8是我們想要的k.
又如7,其因子1,7沒有一個數可以表示成2的n次方,得出的結果k = 1,那么,可以得出一致結論,當index為奇數時,C[i] = A[i]
(以上紅字部分可以加深對LowBit的理解)
<span style="font-family:Microsoft YaHei;font-size:14px;">int LowBit(int index) { return index & (-index); }</span>
(至於為什么這么算,可以看BYvoid的鏈接https://www.byvoid.com/blog/binary-index-tree)
要時刻明白,LowBit這個函數,求得的是index的所有因子中滿足以下條件的那個數值:
1.最大的因子
2.該因子可以表示成2的整數次冪。
巧合的是,這個LowBIt求得的值,就是當前結點的管轄范圍。
什么意思呢?
大家看上圖:同樣以index = 8 為例。index = 8的這個葉節點的管轄范圍是原數組中的1----8,這8個結點都可以看做是index結點的子結點
實際上,我們有這樣的重要定理:
index += LowBit(index)得到的值,正是子節點index的父節點。
如:index = 4時 index += LowBit(index) = 8,也就是管轄4這個結點的父節點。
相應的index -= LowBit(index)可以得到index管轄的子節點。
下面看一個例子:
敵兵布陣
Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total Submission(s): 45999 Accepted Submission(s): 19553
中央情報局要研究敵人究竟演習什么戰術,所以Tidy要隨時向Derek匯報某一段連續的工兵營地一共有多少人,例如Derek問:“Tidy,馬上匯報第3個營地到第10個營地共有多少人!”Tidy就要馬上開始計算這一段的總人數並匯報。但敵兵營地的人數經常變動,而Derek每次詢問的段都不一樣,所以Tidy不得不每次都一個一個營地的去數,很快就精疲力盡了,Derek對Tidy的計算速度越來越不滿:"你個死肥仔,算得這么慢,我炒你魷魚!”Tidy想:“你自己來算算看,這可真是一項累人的工作!我恨不得你炒我魷魚呢!”無奈之下,Tidy只好打電話向計算機專家Windbreaker求救,Windbreaker說:“死肥仔,叫你平時做多點acm題和看多點算法書,現在嘗到苦果了吧!”Tidy說:"我知錯了。。。"但Windbreaker已經掛掉電話了。Tidy很苦惱,這么算他真的會崩潰的,聰明的讀者,你能寫個程序幫他完成這項工作嗎?不過如果你的程序效率不夠高的話,Tidy還是會受到Derek的責罵的.
每組數據第一行一個正整數N(N<=50000),表示敵人有N個工兵營地,接下來有N個正整數,第i個正整數ai代表第i個工兵營地里開始時有ai個人(1<=ai<=50)。
接下來每行有一條命令,命令有4種形式:
(1) Add i j,i和j為正整數,表示第i個營地增加j個人(j不超過30)
(2)Sub i j ,i和j為正整數,表示第i個營地減少j個人(j不超過30);
(3)Query i j ,i和j為正整數,i<=j,表示詢問第i到第j個營地的總人數;
(4)End 表示結束,這條命令在每組數據最后出現;
每組數據最多有40000條命令
對於每個Query詢問,輸出一個整數並回車,表示詢問的段中的總人數,這個數保持在int以內。
1 10 1 2 3 4 5 6 7 8 9 10 Query 1 3 Add 3 6 Query 2 7 Sub 10 2 Add 6 3 Query 3 10 End
Case 1: 6 33 59
這里,再給出兩個重要的函數。
1.
____________________________________________________________________________________________________________________________________
<span style="font-family:Microsoft YaHei;font-size:14px;">void Update(int index,int value) { while(index <= N){ tree[index] += value;//這里不一定要加,可以給出必要的更改 index += LowBit(index); } }</span>
其中N是表示原數組的容量
(這個地方其實有一容易被忽略的地方,就是C語言數組下標一般從0開始。不過LowBit(0) == 0 ,那么在這個函數中 0 += LowBit(0) 會一直得到0的結果始終<=N就會變成死循環!所以在構造原數組的時候一定要注意!)
好了,下面說明一下這個函數的功能。
因為樹狀數組的特殊性——某一個結點的管轄結點可能有很多個,修改子節點的時候,必須上溯到它的父節點,這一條路徑的上的值都要更改,所以有了這個Update函數。
____________________________________________________________________________________________________________________________________
2.
<span style="font-family:Microsoft YaHei;font-size:14px;">int Sum(int index) { int sum = 0; while(index){ sum += tree[index]; index -= LowBit(index); } return sum; }</span>
函數很簡單,求index結點所對應的子節點和。
(其實就是從1----index的原數組的和)
看懂了圖就看懂了一大半,下面給出題目代碼,有興趣的可以研究一下
HDU 1166
<span style="font-family:Microsoft YaHei;font-size:14px;">/*-------------------------------------------------------------------- 12二進制: 00001100 取反: 11110011 負數補碼形式哪負數補碼呢 我先看看負數補碼何表示【負數補碼其原碼逐位取反符號位除外;整數加1】 我返弄: 先11110011-1=11110010 符號位外取反:10001101 看看除符號外數:0001101 13 所數-13 PS: 1 & (-1) == 1 */ #include <stdio.h> #include <string.h> #define maxn 50005 int num[maxn]; int TreeC[maxn],N;//樹狀數組 void Initialize() { memset(TreeC,0,sizeof(TreeC)); memset(num,0,sizeof(num)); } int LowBit(int index) { return index & (-index); } int Sum(int index) { int ans = 0; while(index > 0){ ans += TreeC[index]; index -= LowBit(index); } return ans; } void Update(int index,int value)//維護樹狀數組 { while(index <= N){ TreeC[index] += value; index += LowBit(index); } } int main() { int Casenum,a,b; char str[10]; scanf("%d",&Casenum); for(int cases = 1 ; cases <= Casenum ; ++cases){ Initialize(); scanf("%d",&N); bool over = false; for(int i = 1 ; i <= N ; ++i){ scanf("%d",&num[i]); Update(i,num[i]);//讀取一組數構建一次樹狀數組 } printf("Case %d:\n",cases); while(scanf("%s",str) != EOF){ switch(str[0]){ case 'Q' : { scanf("%d%d",&a,&b) ; printf("%d\n",Sum(b)-Sum(a-1)) ; } ; break; case 'A' : { scanf("%d%d",&a,&b) ; Update(a,b) ;} ; break; case 'S' : { scanf("%d%d",&a,&b) ; Update(a,-b) ;} ; break; case 'E' : over = true;break; } if(over) break; } } return 0; }</span>