引入
已知一個數列,你需要進行下面兩種操作:
-
將某一個數加上 xx (修改)
-
求出某區間每一個數的和 (查詢)
如何解決這個問題呢?
如果直接使用陣列來做,那麼修改操作複雜度是 \(O(1)\) ,查詢複雜度是 \(O(N)\).
而如果使用字首和陣列來做,那麼修改操作複雜度是 \(O(N)\) ,查詢複雜度是 \(O(1)\).
上面兩種方法的總複雜度都是 \(O(N)\)
那麼有沒有更快的方法呢?有,樹狀陣列便是一種解決方法,它的複雜度是 \(O(logN)\) (在後面我們會說明為什麼)
原理
樹狀陣列,是基於二進位制的數位的特性的資料結構。所謂的特性指的是什麼呢?簡單地說就是可以逐位拆解出 \(1\) ,直到整個串被如此表示。
舉個例子:給出一個二進位制串 \(100101\) ,它可以被分解為 \(100000+100+1\) 。
這樣便為求字首和提供了一條道路:按照拆出來的數分塊求字首和。
比如說要求陣列前 \(12\) 個元素的和,利用 \(12_{(10)}=1100_{(2)}=1000_{(2)}+100_{(2)}=8_{(10)}+4_{(10)}\) 。
先求出後 \(4\) 個元素的和(即 \(9-12\) ),再求出除了這 \(4\) 個元素之外後 \(8\) 個元素的和(即 \(1-8\) )再把它們加起來即可。
這樣我們便解決了求字首和(查詢)操作。
講到這裡,我想引入這張圖片幫助理解:(我們記 \(x\) 所對應的區間為 c[x]
)這張圖完美地解釋了 \(x\) 能夠維護 \(lowbit(x)\) 個元素 ( \(lowbit(x)\) 指 \(x\) 在二進位制下最後一個 \(1\) 以及它後面所有的 \(0\) 構成的二進位制串所對應的數,比如說 \(lowbit(6)=lowbit(110_{(2)})=10_{(2)}=2\) )
下面來講講更新操作:
核心問題便是:一個數改變了,需要改變什麼相關的區間?
答案是:改變維護的區間包括這個數的區間,還是舉例來說,如果 a[9]
改變了,由上圖可以看出,c[9]
,c[10]
,c[12]
( \(12\) 之後的不考慮 )均要改變。事實上,c[x]
上面的區間是c[x+lowbit(x)]
(比如 \(9_{(10)}=1001_{(2)}\) 於是 \(9\) 上面一層區間便是\(1001_{(2)}+lowbit(1001_{(2)})=1010_{(2)}=10_{(10)})\) 依次類推)。
這樣我們便知道如何更新了。
程式碼實現
//修改 p指的是當前位置,k指的是加上k(如果tree[p]=k則是修改為k)
void modify(int p,int k){
for(;p<=n;p+=lowbit(p)) tree[p]+=k;
}
//查詢
int query(int p){
int res=0;
for(;p;p-=lowbit(p)) res+=tree[p];
return res;
}
複雜度分析
結合程式碼,複雜度分析可以更便於理解:
以查詢為例,複雜度無疑取決於那個for
迴圈會執行多少次。根據lowbit
的定義,
即使p
所對應的二進位制串
都是 \(1\),也不過是迴圈 \(1\) 的個數次。
具體地說,\(N=111...111_{(2)}\)(有 \(x\) 個 \(1\) ),而 \(N\) 可以近似看成是 \(2^x\) ,故迴圈的次數為 \(x=logN\),
故對應的複雜度是 \(O(logN)\) 。
類似的,修改的操作亦然。