樹狀陣列(Binary Indexed Tree) 總結(ing)
推薦一篇很好的部落格:http://www.cppblog.com/menjitianya/archive/2015/11/02/212171.html
一、樹狀陣列的定義
基本定義:樹狀陣列是利用二分的思想使得查詢和修改的複雜度都為 log(n) 的資料結構,樹狀陣列是通過字首和思想,用來完成單點更新和區間查詢的資料結構。
如上圖,不難看出樹狀陣列是一個不斷地二分的過程。
如上圖,其中A為普通陣列,C為樹狀陣列(C在物理空間上和A一樣都是連續儲存的)。樹狀陣列的第4個元素C4的父結點為C8 (4的二進位制表示為"100",所以k=2,那麼4 + 2^2 = 8,下面會具體講解樹狀陣列節點的含義),C6和C7同理。C2和C3的父結點為C4,同樣也是可以用上面的關係得出的,那麼從定義出發,奇數下標一定是葉子結點
談到樹狀陣列就很有必要談一下線段樹,下圖就是一個線段樹:
於是刪除了線段樹的所有右兒子,發現就形成了樹狀陣列。與線段樹相比,所用空間更小,速度更快,而且程式設計的複雜難度也大大減小。但是這裡有一個前提條件,也是樹狀陣列的缺陷:節點的資料必須是可加減,而且要滿足a + b = c,c - a = b,比如集合運算,是不能通過父節點和左子節點 來計算右子節點的。
和ST相比,樹狀陣列可以動態更新資料。
二、結點的含義
這時就要說到樹狀陣列的一個很重要操作:lowbit(x),返回引數轉為二進位制後,最後一個1的位置所代表的數值。計算方法:
int lowbit(int x) {
return x & (-x);
}
現在來看樹狀陣列上的結點Ci具體表示什麼,我們定義Ci的值為它的所有子結點的值 和 Ai 的總和,之前提到當i為奇數時Ci一定為葉子結點,所以有Ci = Ai ( i為奇數 )。從圖中可以得出:
C1 = A1
C2 = C1 + A2 = A1 + A2
C3 = A3
C4 = C2 + C3 + A4 = A1 + A2 + A3 + A4
C5 = A5
C6 = C5 + A6 = A5 + A6
C7 = A7
C8 = C4 + C6 + C7 + A8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
建議直接看C8,因為它最具代表性。
我們從中可以發現,其實Ci還有一種更加普適的定義,它表示的其實是一段原陣列A的連續區間和。根據定義,右區間是很明顯的,一定是i,即Ci表示的區間的最後一個元素一定是Ai,那麼接下來就是要求Ci表示的第一個元素是什麼。從圖上可以很容易的清楚,其實就是順著Ci的最左兒子一直找直到找到葉子結點,那個葉子結點就是Ci表示區間的第一個元素。
更加具體的,如果i的二進位制表示為1000,那麼它最左邊的兒子就是 0100,這一步是通過結點父子關係的定義進行逆推得到,並且這條路徑可以表示如下:
1000 => 0100 => 0010 => 0001
這時候,0001已經是葉子結點了,所以它就是 Ci 能夠表示的第一個元素的下標,那麼我們發現,如果用 k 來表示 i 的二進位制末尾0的個數,Ci能夠表示的A陣列的區間的元素個數為 2^k,又因為區間和的最後一個數一定是Ai,所以有如下公式:
Ci = sum{ A[j] | i - 2^k + 1 <= j <= i } (幫助理解:將 j 的兩個端點相減+1 等於2^k)
所以,樹狀陣列中一定不能有0元,如果題目中有要注意處理。通常是整體資料加1,這樣後同時也要注意變化後的資料上限
三、更新操作
更新操作就是之前提到的add(i, x) 和 add(i, -x),也就是單點的加減操作。
那麼其實就是求在Ai改變的時候會影響哪些Ci,看上面樹形結構Ai的改變只會影響Ci及其祖先結點,即A5的改變影響的是C5、C6、C8,而A1的改變影響的是C1、C2、C4、C8。也就是每次add(i, x),我們只需要更新Ci以及它的祖先結點。
int add(int p, int x) {
for(int i = p; i <= n; i += lowbit(i)) presum[i] += x;
}
四、求和操作
求區間 [l, r] 的和時,只需要利用樹狀陣列求出 [1, r] 的字首和再減去 [1, l] 的字首和即可。
int sum(int x){
int ans =0;
for(int i = x; i > 0; i -= lowbit(i)) ans += presum[i];
return ans;
}
五、小結
樹狀陣列的本質上是單點修改,區間查詢。樹狀陣列的c[i]儲存的是它所屬的下屬元素的累加和,每次查詢區間的時候都是從1到x這個區間裡面的所有元素的和,每次在更新的時候也是不斷的將控制它的c[i]也更新。
六、樹狀陣列的經典模型
1、點更新,段求和
【例題1】一個長度為n(n <= 500000)的元素序列,一開始都為0,現給出三種操作:
1. add x v : 給第x個元素的值加上v; ( a[x] += v )
2. sub x v : 給第x個元素的值減去v; ( a[x] -= v )
3. sum x y: 詢問第x到第y個元素的和; ( print sum{ a[i] | x <= i <= y } )
這是樹狀陣列最基礎的模型,1和2的操作就是對應的單點更新,3的操作就對應了字首求和。1和2只要分別呼叫 add(x, v) 和 add(x, -v),而3則是輸出 sum(y) - sum(x-1) 的值。
2、段更新,點求值
【例題2】一個長度為n(n <= 500000)的元素序列,一開始都為0,現給出兩種操作:
1. add l, r, x : 給第 l 個元素到第 r 個元素的值都加上 x( a[i] += x, 其中 l <= i <= r )
2. get x: 詢問第x個元素的值(print a[x] )
這類問題對樹狀陣列稍微進行了一個轉化,對於操作1我們只需要執行兩個操作,即 add(l, x) 和 add(y+1, -v),而操作2則是輸出sum(x)的值。這樣就把區間更新轉化成了單點更新,單點求值轉化成了區間求和。
3、逆序模型
【例題3】給定一個長度為n(n <= 500000)的排列a[i],求它的逆序對對數。1 5 2 4 3 的逆序對為(5,2)(5,3)(5,4)(4,3),所以答案為4。
樸素演算法,列舉任意兩個數,判斷他們的大小關係進行統計,時間複雜度O(n^2)。
來看一個給定n個元素的排列 X0, X1, X2……Xn,其中第 i 個元素 Xi,如果想知道以他為首的逆序對的數目(形如(Xi, Xj)這樣的數對),就是需要計算Xi+1, ……, Xn 這個序列中小於 Xi 的元素個數。那麼我們只需要對這個排列從後往前列舉,每次列舉到 Xi 元素時,執行 ans += sum(Xi - 1),然後再執行add(Xi, 1),n個元素列舉完畢,得到的 ans 就是我們要求的逆序數了。總的時間複雜度O(nlogn)。
這個模型和之前的區別在於它不是將原陣列的下標作為樹狀陣列的下標,而是將元素本身作為樹狀陣列的下標。
這裡就需要引入另一道求逆序對的題了:
【例題4】POJ 2299 Ultra-QuickSort
與這道不同的是陣列元素最大值可以達到 999,999,999,那麼也就無法將元素本身作為樹狀陣列的下標,這時就需要利用離散化思想,由於最多隻有 500000 個數,可以把原始的數對映為1-n一共n個數,具體請看另一篇部落格:https://blog.csdn.net/Jasmineaha/article/details/81449834。
【例題5】給定N(N <= 100000)個區間,定義兩個區間 (Si, Ei) 和 (Sj, Ej) 的 '>' 規定如下:
如果Si <= Sj 並且 Ej <= Ei 並且 Ei - Si > Ej - Sj,則 (Si, Ei) > (Sj, Ej),現在要求每個區間有多少區間 '>' 它
將上述三個關係式化簡,可以得到 區間 i > 區間 j 的條件是:區間 i 完全覆蓋 區間 j,並且兩者不相等。
首先對區間進行排序,排序規則為:按區間的右端點從大到小排序,如果右端點相同,則左端點小的排在前面
那麼當讀取到第 i 個牛的 Si 和 Ei時,之前(假設任意兩個區間不會完全相同)的牛的 Sj(j<=i-1) <= Si 的這些牛就都比 i 號牛強壯了。列舉區間,不斷插入區間左端點,因為區間右端點是保持遞減的,所以對於某個區間(Si, Ei),只需要查詢樹狀陣列中 [1, Si] 這一段有多少已經插入的資料,就能知道有多少個區間是比它大的,這裡需要注意的是多個區間相等的情況,因為有排序,所以它們在排序後的陣列中一定是相鄰的,所以在遇到有相等區間的情況時,當前的區間的答案就等於上一個相等的區間的答案。
這裡的插入即 add(Sj, 1),統計則是 sum(Si) (其中j < i)。
參考部落格:https://blog.csdn.net/Jasmineaha/article/details/81474680
4、二分模型