二分索引樹與線段樹分析
二分索引樹是一種樹狀數組,其全名為Binary Indexed Tree。二分索引樹可以用作統計作用,用於計某段連續區間中的總和,並且允許我們動態變更區間中存儲的值。二分索引樹和線段樹非常相似,二者都享有相同的O(log2(n))時間復雜度的更新操作和O(log2(n))時間復雜度的查詢操作,區別在於二分索引樹更加簡潔高效,而線段樹則較冗雜低效,原因在於對二分索引樹的操作中是使用了計算機中整數存儲的特性來進行加速,而線段樹中由於使用的是比較操作,因此性能不及二分索引樹。那麽為什麽我們不拋棄線段樹呢?原因在於所有二分索引樹能解決的問題,線段樹也都可以解決,但是線段樹還能解決許多二分索引樹無法解決的問題。下面將先後討論二分索引樹和線段樹。
要敘述二分索引樹,我們必須先說明二分索引樹高效的關鍵,lowbit操作,其中lowbit(x)=x&-x。我們知道在計算機中,對於一個帶符號n位整數b,其由n個二進制位表示,可以記為(b[n-1],b[n-2],...,b[0]),其中對於任意0<=i<n,b[i]為0或1。而一個這樣的向量,其實際表示的整數值為$$ value\left(b\left[n-1\right],\cdots ,b\left[0\right]\right)=-b\left[n-1\right]\cdot 2^{n-1}+\sum_{i=0}^{n-2}{b\left[i\right]\cdot 2^i} $$對於一個任意正整數X,我們先對其按位取反(除了首位),得到一個新的數Y,很容易可以得知X+Y=2^(n-1)-1,因此X+Y+1=2^(n-1),換言之Y+1-2^(n-1)=-X,因此我們得到-X的二進制表示,其後n-1位為X按位取反後加1得到的(與Y+1的後n-1位相同),且首位為1。接下來考慮X&-X的實際含義,註意由於X為非負正數,因此X的首位為0,而運算為且運算,因此我們可以忽略-X的符號位帶來的影響。我們不妨認為X的後面k位均為0,且X[k]=1,對應的Y的後面k位均為1,而Y[k]=0。而Y+1的後面k位均為0,而(Y+1)[k]=1,之後的前面n-2-k位與Y一致,與X相反。因此X&-X得到的值應該為2^k,到此我們可以得出一個結論X&-X得到的數為以二進制視角從後數起第一個為1的二進制位所代表的整數值。事實上這個結論對於X為0時也是成立的,並且由於且運算滿足交換律,因此當X為負數時也可以得到正確結果,這些只是補充說明而已,我們要用到的只是lowbit應用到正整數情況下表現出的性質而已。
接下來我們用二分索引樹表示一段長度為L的數組A,且從1開始計數,即我們認為下標分別為1,2,...,L。二分索引樹支持兩種操作,修改某個A[i]的值,以及查詢S[j]=A[1]+A[2]+...+A[j]的加總和,兩種操作允許以任意次序交錯執行。
不難知道使用原始數組存取,我們的查詢時間復雜度將達到O(L),在查詢密集的情況下是一個噩夢,而如果我們緩存S[j]的值,這樣每次更新的時間復雜度將達到O(L),這也是不允許的。我們可以利用一個精致的機制,我們首先定義一個長度為L的數組data,其中data[i]表示S[i]-S[i-lowbit(i)]的值,換言之,data[i]表示A在區間(i-lowbit[i],i]範圍內所有元素的加總值。這樣我們的查詢操作可以用下面的偽代碼實現:
query(x)//查詢A[1]+...+A[x] sum = 0 for(i = x; i > 0; i = i - lowbit(i)) sum = sum + data[i] return sum
我們定義函數f(x):=x-lowbit(x)。
在整個流程中我們匯總了下標為f^0(x),f^1(x),...,f^k(x)的data中的值,我們對其進行加總得到:$$ data\left[f^0\left(x\right)\right]+data\left[f^1\left(x\right)\right]+\cdots +data\left[f^k\left(x\right)\right] $$ $$ =S\left[f^0\left(x\right)\right]-S\left[f^1\left(x\right)\right]+S\left[f^1\left(x\right)\right]-S\left[f^2\left(x\right)\right]+\cdots +S\left[f^k\left(x\right)\right]-S\left[0\right] $$ $$ =S\left[f^0\left(x\right)\right]=S\left[x\right] $$
到此我們說明了query返回的結果是正確的,而時間復雜度,我們每次調用函數f都會導致i最靠後的為1的二進制位被修改為0,而i中最多有log2(i)個為1的二進制位,因此時間復雜度為O(log2(i)),在i=L的情況下達到最大O(log2(L))。
說明完了查詢,下面說明如何存儲數據。當我們修改了A[i]中的值,我們勢必需要修正所有滿足f(x)<i<=x的x對應的data[x]中存儲的值,以保證data中數據的正確。
命題1:對於一個給定數k和i,最多只存在一個同時滿足lowbit(x)=2^k且f(x)<i<=x的數x。
證明:假設x與y為兩個不同的滿足條件的值,則有x-2^k<i<=x與y-2^k<i<=y成立,不妨設x<y,則有y-x>2^k(由於2^k同時是x與y的約數,後面不再說明),而y-2^k>x>i>y-2^k將成立,這是不可能的,因此命題成立。
命題2:若兩個不同的數x,y同時滿足f(x)<i<=x,f(y)<i<=y,則由lowbit(x)<lowbit(y)可以得出x<y。
證明:記2^p=lowbit(x),則x>y可以得出x-y>=2^p,即i>x-2^p>=y,這與前提相悖,因此命題成立。
命題3:若某個數x滿足f(x)<i<=x,且i的後面t個二進制位為0,且i[t]=1,則lowbit(x)>=2^t
證明:設lowbit(x)=2^p<2^t,我們可以記y為x將後面t位均設置為0後的值,因此有y+2^t>x>=i,即i-y<2^t,從而可得i<=y,而x-2^p>=y>=i,因此命題成立。
命題4:若某個數x滿足f(x)<i<=x,那麽y=x+lowbit(x)是大於x中滿足f(y)<i<=y的最小下標
證明:證明y=x+lowbit(x)滿足f(y)<i<=y比較簡單,這裏不證明。而對於任意z,若z>x,且f(z)<i<=z,則根據命題3可知lowbit(z)>lowbit(x),由此可知z-x>=lowbit(x),即z>=lowbit(x)+x=y。
這裏我們定義g(x)=x+lowbit(x)。由上面4個命題可以得出一系列需要更新的下標從小到大排序為g^0(x),g^1(x),...,g^k(x)。因此我們給出下面代碼:
update(j, val) //令A[j]增加val for(i = j; i <= L; i = i + lowbit(i)) data[i] = data[i] + val
這裏使用的是加法,和前面提及的直接賦值有所不同,但是可以將直接對A[j]賦值v對應的修改為令A[j]增加v-A[j]即可。
由於i在每次循環,其lowbit值不斷增大,因此update的時間復雜度也是log2(L)。
線段樹有空再補上...
二分索引樹與線段樹分析