樹狀數組

應用

一個用來快速計算數組前綴和的數據結構,就像一個數可以用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;
}

參考

1.樹狀數組-wiki

2.oiwiki-樹狀數組

發佈了72 篇原創文章 · 獲贊 37 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章