樹狀陣列確實是個好東西啊,以前搞比賽的時候了解過它,會套用模版,但確沒有深入理解這個東西,先學會用輪子,然後再學造輪子嘛,這段時間再回頭研究了一下,發現二進位制在演算法中真的是的好東西,它可以使演算法的時間複雜度降到n
的二進位制表示中的1
相關,大家都知道,求乙個二進位制中的1
的個數,這個時間複雜度為o(logn)o(logn)。
有時候覺得樹狀陣列難以理解,我覺得根本原因是:你還在用十進位制的視角來看待樹狀陣列,下面的講解我會時刻提醒你轉換到二進位制的視角,而且我也不會先給你上圖,因為你的視角在二進位制,你就會發現樹狀陣列就是乙個普通的東西,不需要圖你就能理解。
首先我們搞明白樹狀陣列是用來幹嘛的,現在有乙個這樣的問題:有乙個陣列a
,下標從0
到n-1
,現在給你w
次修改,q
次查詢,修改的話是修改陣列中某乙個元素的值;查詢的話是查詢陣列中任意乙個區間的和,w + q < 500000
。
這個問題很常見,首先分析下樸素做法的時間複雜度,修改是o(1)o(1)的時間複雜度,而查詢的話是o(n)o(n)的複雜度,總體時間複雜度為o(qn)o(qn);可能你會想到字首和來優化這個查詢,我們也來分析下,查詢的話是o(1)o(1)的複雜度,而修改的時候修改乙個點,那麼在之後的所有字首和都要更新,所以修改的時間複雜度是o(n)o(n),總體時間複雜度還是o(qn)o(qn)。
可以發現,兩種做法中,要麼查詢是o(1)o(1),修改是o(n)o(n);要麼修改是o(1)o(1),查詢是o(n)o(n)。那麼就有沒有一種做法可以綜合一下這兩種樸素做法,然後整體時間複雜度可以降乙個數量級呢?有的,對,就是樹狀陣列。
這裡我們先不管樹狀陣列這種資料結構到底是什麼,先來了解下lowbit
這個函式,你也先不要問這個函式到底在樹狀陣列中有什麼用;
顧名思義,lowbit
這個函式的功能就是求某乙個數的二進位制表示中最低的一位1
,舉個例子,x = 6
,它的二進位制為110
,那麼lowbit(x)
就返回2
,因為最後一位1
表示2
。
那麼怎麼求lowbit
呢?
兩種方法對應的**依次如下:
int lowbit(x)
int lowbit(x)
在樹狀陣列的問題模型中已經有所提及了,就是那兩種不同做法的乙個綜合;
先定義一些東西:arr
是原陣列,c
是新開的乙個陣列,這個陣列代表字尾和(問題模型中是用的字首和,這裡要用字尾和,具體原因馬上就知道了);
二進位制的視角:乙個數n
,假設n = 6
,它的二進位制為110
,我們把它表示成累加的形式110 = 100 + 10
,這樣是可以的,那麼我們要求前6(110)
項的和是不是可以這樣求:
∑i=16=(arr1+arr2+arr3+arr4)+(arr5+arr6)∑i=16=(arr1+arr2+arr3+arr4)+(arr5+arr6)
注意括號中的元素個數,是不是4(100)
個加2(10)
個,和110 = 100 + 10
是不是很像,不知你們發現了嗎,10
就是lowbit(110)
的結果,100
是lowbit(100)
的結果。求和的時候我們總是把∑ni=1∑i=1n拆分成這樣的幾段區間和來計算,而如何去確定這些區間的起點和長度呢?就是根據n
的二進位制來的(不懂的可以再看下上面舉的例子),二進位制怎麼拆的,你就怎麼拆分,而拆分二進位制就要用到上面說的lowbit
函式了。這裡也可以順理成章得給出c
陣列的表示了。
這裡也可以順理成章得給出c
陣列的表示了,c[i]
表示從第i
個元素向前數lowbit(i)
個元素,這一段的和,這就是上面說的區間和,只不過這個區間是靠右端點的;你可能又會想,不是說區間是靠右端點的嗎,是字尾和啊,那中間的這些區間怎麼定義?其實遞迴定義就好了,比如說∑6i=1=(arr1+arr2+arr3+arr4)+(arr5+arr6)=∑6i=1=(arr1+arr2+arr3+arr4)+c[6]∑i=16=(arr1+arr2+arr3+arr4)+(arr5+arr6)=∑i=16=(arr1+arr2+arr3+arr4)+c[6],你把c[6]
去掉,不就是∑4i=1=(arr1+arr2+arr3+arr4)∑i=14=(arr1+arr2+arr3+arr4),這個區間不就靠右端點了嗎,∑4i=1=c[4]=c[6−lowbit(6)]∑i=14=c[4]=c[6−lowbit(6)]。
其實你把所有的數字都看成二進位制,很好理解的。
設計一種資料結構,需要的操作無非就是」增刪改查「,這裡只討論查詢和修改操作具體是怎麼實現的;
這裡說的查詢是查詢任一區間的和,由於區間和具有可加減性,故轉化為求字首和;
查詢字首和剛剛在樹狀陣列的思想中已經說過了,就是把大區間分成幾段長度不等的小區間,然後求和。區間的個數為o(logn)o(logn),所以查詢的時間複雜度為o(logn)o(logn)。
修改某一位置上的元素的時間複雜度為o(1)o(1),但是要更新c
陣列,不然查詢的時間複雜度就會變高。更新的方法就要提一下樹狀陣列的性質了和樹狀陣列那張經典的了。
這張中已經把c
陣列的字尾和這個含義已經表達得很清楚了。這個時候你再把查詢操作對應到這張圖上,然後看著二進位制來操作,是不是就可以很直白地理解上面所說的查詢操作了!
我們從這張圖中可以得到樹狀陣列的如下性質:
我暫時就寫這麼多吧,這個時候我們再來說更新操作;
更新的時候只要更新修改這個點會影響到的那些字尾和(c
陣列),假設現在修改6(110)
這個點,依據樹狀陣列的性質三,它影響的直系父層就是c[6(110) + lowbit(6(110))] = c[8(1000)]
,但是它肯定不是只影響直系父層,上面所有包含這一層和的層都要更新,但是我們把這個更新傳遞給直系父層c[8]
,8
這個點的直系父層是c[16]
,依次類推地更新就行了。
這裡我留乙個問題給大家,如何尋找某一層的所有直系子層,大家可以看這個圖思考一下,想一想。
int sum(int x, arrayint c, int n)
void update(int x, int val, arrayint c, int n)
先給乙個題目背景,然後運用樹狀陣列來高效解決這個問題。
輸入乙個陣列,然後給你一些操作,操作有查詢和修改兩種。具體見輸入格式。
第一行輸入n
,表示陣列長度;第二行輸入
n
個整數;第三行輸入
m
,表示操作的次數;接下來
m
行,每行輸入三個東西,字元ch
,x
,y
。ch='f'
表示查詢x
到y
這段區間和;ch='s'
表示修改第x
個元素為y
。
對於每個查詢,輸出結果。之前好多同學和我說看不懂c++的**,主要還是stl那塊,所以我用c來寫了這個**。
#include #include #include #define lowbit(x) ((x) & -(x))
typedef int *arrayint;
int sum(int x, arrayint c, int n)
void update(int x, int val, arrayint c, int n)
int main()
}return 0;
}
查詢區間和以前的做法要麼就是查詢很慢,修改很快,那怎麼辦呢,那就儲存字首和來提高查詢速度,但這樣一來修改了之後要更新這些字首和,更新又很慢;
陣列陣列就完美地綜合了這兩種做法,儲存字尾和,更新字尾和,通過lowbit
來限定字尾和的長度,利用二進位制使得查詢、更新的時間複雜度都在o(logn)o(logn)。
**:
樹狀陣列簡單易懂的詳解
2018年01月25日 19 29 05 樹狀陣列確實是個好東西啊,以前搞比賽的時候了解過它,會套用模版,但確沒有深入理解這個東西,先學會用輪子,然後再學造輪子嘛,這段時間再回頭研究了一下,發現二進位制在演算法中真的是的好東西,它可以使演算法的時間複雜度降到n的二進位制表示中的1相關,大家都知道,求...
樹狀陣列 詳解
對於普通陣列,其修改的時間複雜度位o 1 而求陣列中某一段的數值和的時間複雜度為o n 因此對於n的值過大的情況,普通陣列的時間複雜度我們是接受不了的。在此,我們引入了樹狀陣列的資料結構,它能在o logn 內對陣列的值進行修改和查詢某一段數值的和。假設a陣列為儲存原來的值得陣列,c為樹狀陣列。我們...
樹狀陣列詳解
樹狀陣列求區間和的一些常見模型 樹狀陣列在區間求和問題上有大用,其三種複雜度都比線段樹要低很多 有關區間求和的問題主要有以下三個模型 以下設a 1.n 為乙個長為n的序列,初始值為全0 1 改點求段 型,即對於序列a有以下操作 修改操作 將a x 的值加上c 求和操作 求此時a l.r 的和。這是最...