樹狀陣列 詳解

2021-06-18 00:20:26 字數 3615 閱讀 1060

對於普通陣列,其修改的時間複雜度位o(1),而求陣列中某一段的數值和的時間複雜度為o(n),因此對於n的值過大的情況,普通陣列的時間複雜度我們是接受不了的。

在此,我們引入了樹狀陣列的資料結構,它能在o(logn)內對陣列的值進行修改和查詢某一段數值的和。

假設a陣列為儲存原來的值得陣列,c為樹狀陣列。

我們定義:c[i] = a[i - 2^k + 1] + ..... + a[i]  其中k為i用二進位制表示時的末尾0的個數。例如:i= 10100,則k = 2,i = 11000,則k = 3;為了方便我直接寫的是二進位制的i,請讀者注意。

此時我們可以知道,c[i] 它裡面包含了2^k個a元素,這2^k個元素是從c[i]往後一直遞減的2^k個元素,即i 一直減小的。

其中我們有一種快速的求解2^k的值得方法:

int lowbit(int x)
利用機器的補碼原理也可以寫成這樣:

int lowbit(int x)
下面我們還要求的就是如何快速的修改某乙個元素的值以及求出某一段元素值的和;

先來看它是怎麼快速修改某乙個元素的值的:

我們舉個例子(為了方便i的值直接寫成二進位制了):i = 11000,此時k = 3;

這2^k = 3 個數即為:a[11000],a[10111],a[10110],a[10101],a[10100],a[10011],a[10010],a[10001]

c[11000] = a[11000]+a[10111]+a[10110]+a[10101]+a[10100]+a[10011]+a[10010]+a[10001];

這裡我們會發現:

a[10100]  + a[10011] +  a[10010] + a[10001] = c[10100]

而a[10010] + a[10001] = c[10010]

a[10011] = c[10011]

所以c[10100] = c[10010] + c[10011] + a[10100]

又a[10110] + a[10101] = c[10110]

而a[10101] = c[10101]

所以c[10110] = c[10101] + a[10110]

又a[10111] = c[10111]

至此我們可以得出:

c[11000] = c[10100] + c[10110] + c[10111] + a[11000]

到這裡我們可以得出:

k的值就表示子樹的個數,子樹即為樹狀陣列的元素。

如圖例:

這個個數就是可以從左往右一直加1,怎麼加呢?只要保持這k個位左邊都是1,右邊都是0,且左邊至少有乙個1,右邊可以沒有0,而k那個位,原來是1,在加的過程中,就要變成0.如上例:k個位就是11000的末尾3個0,000;就這3個位而言,左邊至少乙個1,右邊可以沒有0,就有這三種:100,110,111.再就是把原來第k個位的1變成0,這樣就可以得出它的三個子樹為:10100,10110,10111.

這樣得出的個數就是子樹的個數,也等於k的值。

也許有讀者會有疑問了,為什麼就一定是這樣呢?

那是因為,根據樹狀陣列的定義,我們再根據二進位制的加法進製的原則,可以得出任何數的樹狀陣列值都可以用上述分解的子樹的樹狀陣列元素的值累加得到。(可以根據之前的例子進行理解,務必理解清楚)

有了上述的理解基礎了,那麼我們就可以知道如何快速的根據第i個節點求出它的父節點了,求法如下:

i的父節點為p,則p = i + lowbit(i);

根據前面的知識,這一點毋庸置疑。

現在我們再來理解乙個知識:

k 表示的 為i 這個節點表示的樹的深度。

為什麼呢?

我們知道k為i的末尾0的個數,而小於i的節點的值肯定要小於i,那麼它們的k絕對要小於i的k,而最長的就是k,因為它的二進位制表示的數只能允許它右移k位,右移k位之後它就是葉子節點了,就只表示乙個單一的a陣列的值了,同時也是c樹狀陣列的值。

有了這點知識為基礎,那麼我們就可以知道,我們要修改某個元素的值,就會修改c的值,以及它的所有祖先節點的值。

而我們已經知道,它的父節點的節點編號就是i + lowbit[i],一步就可以返回過去,而這個樹的深度只有logn,所以我們往上一步一步的修改它的祖先節點就行了,且最多只要logn步,因此時間複雜度是o(logn)。實現函式**如下:

void plus(int pos , int num) 

}

快速修改已經實現了,那麼接下來就要快速求出某一段元素的值的和了:

我們先來快速求出前n項元素的和,只要能快速求出前n項元素的和,那麼快速求某一段元素的和就只要做一次減法就ok了。

那麼前n項元素的和怎麼快速的求出來呢?

我們來看看i = 11000(2進製),這個數,如何快速求出前i個元素的和。

我們知道:11000 = 2^3 + 2^4

也就是它總共有2^3+2^4 個元素,我們回想一下前面的知識,c[11000]包括了幾個元素,2^3個?對!,就是2^k,k=lowbit(11000) 個,而這2^3個就是11000一直遞減的2^3個,而還有2^4個呢?聰明的讀者會發現那是不是就是c[10000]包括的元素的個數呢?就是2^k,k=lowbit(10000).而這2^4個剛好是接著11000的2^3個之後的2^4個。

所以s[11000] = c[11000] + c[10000],s[i]為前i項的和。

現在我們來想想為什麼就是這麼剛好呢?

我們根據二進位制的組成,可以得出,任何乙個數i都可以表示成2的冪次方的和的乙個組成形式,而樹狀陣列c[i]表示的是2^k,k=lowbit(i)個元素相加的和,這些元素是從第i個元素往下數i一直遞減而得到的。那麼我們是否可以用c的一些元素相加來求得s呢?

答案是可以的,剛好可以,k的值表示的是從這個數開始往下一直數掉2^k個,這2^k個數的值的和存在c[i]裡,而數掉的這2^k個只會影響到第k位上的那個1的變動,變成了0。當數掉了2^k個數之後,

此時如果再往下數,會產生什麼樣的影響呢?會影響到k+1位的1的變動,變成0.我們先想一下,我們數掉2^k個數之後,得到的數不就是乙個末尾是k+1個0個數了麼!

因為2^k個數數完了,且在只有k個0的情況下,數完2^k個數之後末尾又會全是0,而第k位的1也會變成了0,所以此時這個數的末尾0的個數就是k+1,它可以往下數2^(k+1)個數,而這2^(k+1)個數的值的和剛好存在c[i - lowbit(i)]裡。到這裡我想聰明的讀者肯定已經知道如何快速的求出前n項元素的和了。

我們只要一直往下數,數到左邊沒有1可以變動的時候,即我們從i數到了0,那麼這i個數你就數完了,答案也就出來了。

在這個過程中,我們可以知道,我們每次要變動某個1的時候,我們都知道從它開始往下數的2^k,k=lowbit(i)個數的值的和,這個個總和存在了c[i]裡,而每乙個數n它最多能變動的1的個數只有logn個,所以我們在計算前n個元素的和的時候最多只要計算logn次的加法,因此求前n個元素的和的時間複雜度即為o(logn)。

下面給出函式實現**:

int sum(int end) 

return sum;

}

至此,我們實現了在o(logn)的時間內,修改陣列的元素和求得某一段元素的和了。

樹狀陣列詳解

樹狀陣列求區間和的一些常見模型 樹狀陣列在區間求和問題上有大用,其三種複雜度都比線段樹要低很多 有關區間求和的問題主要有以下三個模型 以下設a 1.n 為乙個長為n的序列,初始值為全0 1 改點求段 型,即對於序列a有以下操作 修改操作 將a x 的值加上c 求和操作 求此時a l.r 的和。這是最...

樹狀陣列詳解

比如說,我這裡有一組數1,2,3,2,k。我想知道第i到第j的和 mathop sum limits j v i 是多少?樸素演算法 for int k 0 k n k if k i k j ans v k 類似這種的寫法,雖然在某些點值改變時也依然可以計算 我們稱這種問題為動態問題 但複雜度最高到...

樹狀陣列 詳解

由圖可知,原始的陣列是a陣列,樹狀陣列是e陣列。通俗的說 e 1 a 1 e 2 a 1 a 2 e 3 a 3 e 4 a 1 a 2 a 3 a 4 e 5 a 5 e 6 a 5 a 6 e 7 a 7 e 8 a 1 a 2 a 3 a 4 a 5 a 6 a 7 a 8 為什麼是這樣的規律...