解析樹狀陣列

HinanawiTenshi發表於2021-02-10

引入

已知一個數列,你需要進行下面兩種操作:

  • 將某一個數加上 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)\)
類似的,修改的操作亦然。

相關文章