樹狀陣列簡單易懂的詳解

2021-08-22 08:34:00 字數 4669 閱讀 2573

2023年01月25日 19:29:05

樹狀陣列確實是個好東西啊,以前搞比賽的時候了解過它,會套用模版,但確沒有深入理解這個東西,先學會用輪子,然後再學造輪子嘛,這段時間再回頭研究了一下,發現二進位制在演算法中真的是的好東西,它可以使演算法的時間複雜度降到n的二進位制表示中的1相關,大家都知道,求乙個二進位制中的1的個數,這個時間複雜度為o(logn)o(logn)。

有時候覺得樹狀陣列難以理解,我覺得根本原因是:你還在用十進位制的視角來看待樹狀陣列,下面的講解我會時刻提醒你轉換到二進位制的視角,而且我也不會先給你上圖,因為你的視角在二進位制,你就會發現樹狀陣列就是乙個普通的東西,不需要圖你就能理解。

首先我們搞明白樹狀陣列是用來幹嘛的,現在有乙個這樣的問題:有乙個陣列a,下標從0n-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)的結果,100lowbit(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行,每行輸入三個東西,字元chxych='f'表示查詢xy這段區間和;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)。

我已經把我所理解的樹狀陣列盡可能詳細地寫出來了,之中可能有些表述不清楚的地方,大家可以諒解

樹狀陣列簡單易懂的詳解

樹狀陣列確實是個好東西啊,以前搞比賽的時候了解過它,會套用模版,但確沒有深入理解這個東西,先學會用輪子,然後再學造輪子嘛,這段時間再回頭研究了一下,發現二進位制在演算法中真的是的好東西,它可以使演算法的時間複雜度降到n的二進位制表示中的1相關,大家都知道,求乙個二進位制中的1的個數,這個時間複雜度為...

樹狀陣列 詳解

對於普通陣列,其修改的時間複雜度位o 1 而求陣列中某一段的數值和的時間複雜度為o n 因此對於n的值過大的情況,普通陣列的時間複雜度我們是接受不了的。在此,我們引入了樹狀陣列的資料結構,它能在o logn 內對陣列的值進行修改和查詢某一段數值的和。假設a陣列為儲存原來的值得陣列,c為樹狀陣列。我們...

樹狀陣列詳解

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