2019 9 25 初級資料結構 樹狀陣列

2022-05-08 20:06:08 字數 4018 閱讀 4879

一、樹狀陣列基礎

學oi的同學都知道,位運算(對二進位制的運算)比普通運算快很多。同時我們接觸到了狀態壓縮的思想(即將0-1的很多個狀態壓縮成十進位制的乙個數,每乙個二進位制位表示乙個狀態)。由於在實際做題過程當中,由於資料範圍限制,我們必須採用更高效的儲存、查詢方法,於是樹狀陣列應運而生。

首先我們檢查傳統的儲存狀態。對於陣列的每乙個下標i,其所儲存的有效資訊只有乙個a[i](這是傳統陣列)。而對於樹狀陣列,我們每一位下標可以儲存lowbit(i)個有效資訊。這個lowbit運算一會再說。所以儘管樹狀陣列很難壓縮真正的儲存空間,但查詢的時候可以把n的複雜度壓縮成log(n)。這裡的查詢指區間查詢和單點修改。

為什麼有這樣的查詢效率?對於乙個區間[1,x],我們把它分成log(x)個小區間。設x=2i1+2i2+……2im(這不由得讓我們想起快速冪的分解方法),不妨設i1>i2>……>im,則我們可以把[1,x]分成以下小區間:

(1)長度為2i1的小區間[1,2i1]

(2)長度為2i2的小區間[2i1+1,2i1+2i2]

(log(n))長度為2im的小區間[2i

m-1+1,……+2im]

比如[1,6]可以分成[1,4]和[5,6]

所以我們如果想修改這個區間[1,x]的和,我們只需要修改這log(n)個區間的的和即可。

先上個圖:

所以我們現在的問題在於,如果我們要查詢a[idx]的字首和,假設最開始idx是6:

則我們先加上最後一段小區間,令idx=6-21;

再加上第一段小區間,令idx=6-21-22。

應當注意的是,我們每次多減去的那個2i,實際上那個i是idx的二進位制最靠近個位的那一位1對應到的數的編號。

舉個例子:

10的二進位制是1010,最靠近個位的乙個1是在第二位,所以我們第一次減掉22;

得到的新數是8,二進位制是1000,最靠近個位的乙個1是在第四位,所以我們減掉24得到0。

我們減掉2k這個過程,我們把它量化,記原來的陣列是a,我們開的陣列是c,則每次我們加上的區間就是c[idx],然後idx往前移動到c[idx]儲存的區間的前一位,繼續這個步驟。

比如說,我們要計算a[1]到a[6]的和,我們讓idx=6,我們發現剛才我們拆分[1,6]的結果最後乙個小區間是[5,6],所以我們用c[6]記錄a[5]+a[6],然後跳到c[4],用c[4]記錄a[1]+a[2]+a[3]+a[4],即可得到答案。c陣列就是我們說的樹狀陣列。

那到底這個[1,n]的小區間怎麼拆分?因為我們每次減掉的那個2i的i都是離個位最近的乙個1所在的位置,所以我們只需要按照上述規則從後往前遍歷n的所有二進位制位,就可以遍歷儲存這個數的所有小區間。

比如說我們從6(110)遍歷到4(100)(其中經過了[5,6]),然後遍歷到0,其中經過了[1,4]。

也就是說,我們只需要找到每次要減掉的2i即可。

重點來了!!!

我們定義每次減掉的數是lowbit(i)(其中i是進行減法操作之前的數),也就是說lowbit(i)=2m,其中m是i的二進位制最靠個位乙個1的位置。定義了這個,我們實際上就是定義c[i]是以a[i]結尾長度為lowbit(i)的a陣列內的區間,即c[i]=a[i-lowbit(i)+1]+a[i-lowbit(i)+2]+……+a[i]。這樣每次idx跳躍乙個lowbit(idx),就相當於恰好跳過了需要被加上的一段小區間,而加上這一段小區間的和只需要o(1)的複雜度

怎麼算這個lowbit(i)?這就要用到科學家們的智慧型。

lowbit(i)=x&(-x)。

為什麼這個數就能滿足我們的要求?

我們來補充一點二進位制的知識:

我們在儲存乙個數時,int型別是乙個32位有符號整數,其中第一位是符號位,1表示負數,0表示正數。

這裡利用的負數的儲存特性,負數是以補碼儲存的,對於整數運算 x&(-x)有

● 當x為0時,即 0 & 0,結果為0;

●當x為奇數時,最後乙個位元位為1,取反加1沒有進製,故x和-x除最後一位外前面的位正好相反,按位與結果為0。結果為1。

●當x為偶數,且為2的m次方時,x的二進位制表示中只有一位是1(從右往左的第m+1位),其右邊有m位0,故x取反加1後,從右到左第有m個0,第m+1位及

其左邊全是1。這樣,x& (-x) 得到的就是x。 

●當x為偶數,卻不為2的m次方的形式時,可以寫作x= y * (2^k)。其中,y的最低位為1。實際上就是把x用乙個奇數左移k位來表示。這時,x的二進位制表示最

右邊有k個0,從右往左第k+1位為1。當對x取反時,最右邊的k位0變成1,第k+1位變為0;再加1,最右邊的k位就又變成了0,第k+1位因為進製的關係變成了1。

左邊的位因為沒有進製,正好和x原來對應的位上的值相反。二者按位與,得到:第k+1位上為1,左邊右邊都為0。結果為2^k。

總結一下:x&(-x),當x為0時結果為0;x為奇數時,結果為1;x為偶數時,結果為x中2的最大次方的因子。

一定注意,lowbit(0)會陷入死迴圈。

所以我們只需要利用這種運算,就可以在log(n)的複雜度內分割小區間,從而計算從a[1]到a[n]的和。

同樣我們考慮修改乙個值a[idx],因為c陣列儲存a陣列的值有重複,所以我們必須將所有包含a[idx]的c[i]都進行修改。所以,同剛才的步驟,我們只需要根據某種順序順次查詢每個包含a[idx]的c[i]即可。

這個順序非常容易想到:

剛才我們已經知道,我們用c[i]表示從i往前數lowbit(i)個a[i]的總和。根據樹狀陣列的特殊結構,我們查詢的步驟如下:

找到c[i](肯定包含a[i])----->找到第乙個包含c[i]的c[j]--------->找到第乙個包含c[j]的c[k]……

一定注意這個步驟,這是因為樹狀陣列的本質是一棵樹,乙個點只有乙個父親,這個我們之後會說。

根據lowbit的定義,同時根據二進位制的特性,有以下兩個特徵:

(1)lowbit每向左移一位,其表示的lowbit(i)(也就是對應的區間長度,見前文)就乘以2;

(2)根據上文,我們要找到的數實際上是比他大的第乙個lowbit往前移動一位的數(比如說5(101),6(110),lowbit移動了一位)。

所以我們怎麼找這樣的數???

要解決這個問題,我們必須回到二進位制的加法:

比如說6+2,在二進位制裡的表示是這樣的:

1 1 0

+0 1 0

---1-------

1 0 0 0

這難道不是我們剛才要的lowbit移動的方法??我們已經把最後乙個1進製到了前面。我們只需要找到第二個和它相加的那個數即可。

因為要進製最後乙個1,同時我們需要使底下那個數最小,所以我們只需要找到乙個數,它前面後面都是0,中間那個與第乙個數最後乙個1對上的那一位是1.不明白的參見上面的例子。

有沒有覺得上面這個很熟悉????

綜上所述,我們得到:

(1)樹狀陣列求字首和:求1-i的和,每次加上c[i],然後減去lowbit(i)。

(2)樹狀陣列單點修改:修改a[i]時修改c[idx],然後idx+=lowbit(idx),最開始idx=i。

二、樹狀陣列的幾何認識

剛才我們已經了解了樹狀陣列的基本操作,下面我們從樹本身的角度認識樹狀陣列的性質和操作。

根據上面的圖我們可以知道,樹狀陣列具有以下幾個性質:

(1)紅色節點和其父親之間的橫向距離是lowbit(i),這是樹狀陣列構建的基礎,所以我們每次修改乙個a[i]的值其實是修改所有以c[i]為根的且包含a[i]的子樹的c[i]的值,也就是說從a[i]每次向上找到其父親,並修改a[i]每一級父親的值即可。

(2)查詢乙個點的字首和時,相當於從這個點每次找到它的兒子覆蓋的區間,而每個點覆蓋的區間是從這個點到它最左邊的兒子,所以每次減去lowbit(i)即可。

高階用法待填坑。

暖 墟 初級資料結構 分塊

分塊的概念與運用 例題1 單點修改 區間查詢 例題2 區間修改 單點查詢 例題3 區間修改 區間查詢 例題4 bzoj 2724 蒲公英 例題5 bzoj 2038 小z的襪子 通過適當的劃分,預處理一部分資訊並儲存下來。用空間換取時間,達到時空平衡。易實現。將查詢區間分成中間整塊 零散兩邊,此時兩...

初級資料持久化

沙盒機制 列印沙盒的各個資料夾路徑 void path 簡單物件的寫入 系統的類例項出來的物件 叫簡單物件 例如 字串 陣列 字典 二進位制物件 nsdata 如果要寫入乙個陣列 或者 字典 等容器類物件 那麼這個容器當中 也要儲存是 簡單物件才能寫入 void writefile dic writ...

資料結構系列( )樹狀結構

樹 是有乙個或乙個以上的節點組成,存在乙個特殊的節點,稱為樹根。每個節點是一些資料和指標組合而成的記錄。除了樹根,其餘節點可分為n 0個互斥的集合,其中每乙個子集合本身也是一種樹狀結構,即此根節點的子樹。此外一棵合法的樹,節點間可以互相連線,但不能形成無出口的迴路。度數 每個節點所有子樹的個數。層數...