應用
一個用來快速計算數組前綴和的數據結構,就像一個數可以用2的多次冪的組合相加表示,一個數組的前綴和也可以是多個序列的加和表示,二者之間也存在着一些巧妙的聯繫。
如下圖所示,A數組是原數組,C數組是前綴和數組,從這個結構來看,C數組的和計算是一個樹狀的形式。
C1 = A1
C2 = C1 + A2 = A1 + A2
C3 = A3
C4 = C2 + C3 + A4 = A1 + A2 + A3 + A4
C5 = A5
C6 = C5 + A6 = A5 + A6
C7 = A7
C8 = C4 + C6 + C7 + A8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
樹狀數組的好處
樹狀數組c保存的是部分序列和,對於區間求和,比如求A[1] ~ A[6]我們需要使用C[4] + c[5] + A[6];
有人說我們可以使用這樣一個數組t, t[i] 保存 A[1] ~ A[i - 1]的和,這樣豈不是O(1)時間就可以得到結果?
的確,這樣的確更快,但是如果我們需要修改原數組中的一個值,比如說A[k], 那麼所有包含A[k]的T元素都應該修改,這樣的代價就邊成了O(n),而樹狀數組只需要修改部分元素,所以樹狀元素其實是對查詢和修改的一個折中方案。
詳細原理
樹狀數組的核心是c數組,c數組的原理是存儲部分序列的和,那麼這些序列是怎麼劃分的,怎麼知道c數組元素記錄的是哪段序列的和呢?
我們先講結論
c數組定義
對於c數組元素下標 i 爲0x###100 (1是從右到左第一個1),那麼他記錄的區間是(0x###000,0x###100],
即起始位置爲0x###000 + 1(對應的1變爲0),長度爲100 (最右的1和它右邊0構成) 的序列的和。
例如,對於上節圖中才c[6],6 = 0x110,他記錄的區間位置爲 (0x100, 0x110]。
這就是他樹狀數組核心c數組的定義,就這麼簡單。
但是,正如你想要深入瞭解,特殊的構造背後,是一些優美的性質。
c數組下標的一些性質
我們從一開始的圖中發現,c數組的元素構成了一個邏輯上的樹,這是由於他下標和定義帶來的優美性質。
性質 #1
c數組中,下標爲0x###100的元素,他有唯一的父節點,其下標爲0x###100 + 0x100(最右邊1和0構成的值) = 0x##1000。
例如:
0x###100的父節點是 0x##1000, 0x###110的父節點也是 0x##1000, 0x###111的父節點也是 0x##1000。
性質 #2
c數組中,下標爲0x##1000的元素,他有如下兒子節點,其下標爲 0x###100 、 0x###110 、0x###111和元素組中的A[0x###100]
根據上一節對c數組的定義我們知, c數組裏0x###100 、 0x###100 、0x###111構成了對0x##1000所表示區間的不重疊劃分。
而0x###100和0x###100我們也可以再遞歸拆分成幾個序列的和。
實現操作
我們發現,上面兩條性質中,有一個重要的操作就是要獲取最右邊1和所有0構成的值,有什麼好的實現呢?
代碼中,我們可以用如下操作實現:
int lowbit(int x){
return x&(-x);
}
計算機中,採用補碼形式表示int值,補碼的性質在於取相反數時,原數中所有二進制位都取反,然後末尾+1。
比如二進制中6的相反數-6的求取過程如下:
6 = 0x110;
-6 = 0x001 + 0x1= 0x10;
而6 & -6 我們發現正好等於0x10,即6的最右邊1和所有0構成的值。這個很好理解,因爲-6對6原位都取了反,取反+1之後從右往左的1就是原數左右邊的1,自己腦子裏多過遍就很好理解。
全部代碼
int c[100];
int a[100]; // 注意a[0] 和 c[0]是不被使用的
int n = 10;
int lowbit(int x){
return x&(-x);
}
void init(){ // 初始化c數組,c數組的原理對應我們之前講的性質1
for(int i = 0;i < n;i++){
c[i] += a[i];
int j = i + lowbit(i);
if(j <= n) c[j] += c[i];
}
}
int getSum(int x){ // 求取 a[1] - a[x]的和,性質利用的是之前講的性質2
int ans =0;
while(x >= 1){
ans = ans + c[x];
x = x- lowbit(x);
}
return ans;
}
// 偷個懶,只需要修改上級,而不修改原來的數組
void add(int x, int k){
while(x <= n){
c[x] = c[x] + k; // x下標要增加
x = x + lowbit(x); // 對應的父節點也要增加
}
}
int main() {
int b[] = {1,2,4,6,8,10,12,14,16};
for(int i = 1;i < 10;i++){
a[i] = b[i - 1];
}
init();
cout << getSum(1);
return 0;
}