1. 程式人生 > >[樹狀陣列] 基礎

[樹狀陣列] 基礎

問題引入

我們先來看這樣一道題:

現在有一個長度為n的數列a,我們要對數列進行m次操作,操作分為兩種,對其中第k個元素進行修改,或者是計算出數列中一個區間[l,r]中所有元素的和

很容易想到用字首和的思想,即用一個sum陣列,sum[i]表示第1個元素到第i個元素的和,那麼此時要求區間[l,r]中所有元素的值就可以寫為sum[r]-sum[l-1]

但如果資料範圍太大,豈不是就會超時了嗎?所以我們就要引入一個新的資料結構——樹狀陣列




樹狀陣列



WHAT

樹狀陣列(Binary Indexed Tree(B.I.T), Fenwick Tree)是一個查詢和修改複雜度都為log(n)的資料結構。主要用於查詢任意兩位之間的所有元素之和,但是每次只能修改一個元素的值;經過簡單修改可以在log(n)的複雜度下進行範圍修改,但是這時只能查詢其中一個元素的值(如果加入多個輔助陣列則可以實現區間修改與區間查詢)。

這裡只介紹對樹狀陣列的單點修改和查詢操作

我們先用一個圖來了解樹狀陣列:

這個題很直觀的展現了一個樹狀陣列,在上圖C陣列中,C[i]表示以C[i]為根節點的樹中所有子葉節點的和,此時樹狀陣列的好處就充分的體現出來了

假設有一個長度為8的數列A(如上圖),如果此時我們要修改A[1],如果用B陣列來用普通的方法儲存字首和,那麼我們總共要對B陣列進行8次修改,但對樹狀陣列C,我們只需要修改4次

現在的關鍵就是如何構造樹狀陣列



HOW


lowbit

相信大家都能發現,構造樹狀陣列的關鍵就是找出這個點有多少個子葉節點,此時我們需要引入lowbit

那麼什麼是lowbit呢?

lowbit(i)的意思是將 i 轉化成二進位制數之後,只保留最低位的1及其後面的0,截斷前面的內容,然後再轉成十進位制數

可能現在不是太好理解,我們就先來看C[4],那麼4的二進位制數為0100,那麼根據上面所說的方法,4的lowbit就為4,而根據上圖可以得出,C[4]的父親是C[8],我們可以驚奇的發現4+4=8,也就是說4加上自己的lowbit就是自己的父親,當然4只是一個例子,大家可以再去多驗證幾個,這裡就不多說了,我們直接來看lowbit的實現

對於lowbit這種神奇的東西,我們有兩種可以求得的方法:

1、lowbit(x) = x -  (x & (x - 1) )

那麼我們來解釋一下這個求法:

我們假設x的二進位制數為A1B(此時的1就是從低位開始的最後一個1)

x - 1的二進位制數就是A0C(熟悉為運算的應該都能看出,C是和B長度相同的1)

x & (x - 1)就是A1B & A0C,可以得出為A0B

x - (x & (x - 1))的二進位制就是A1B - A0B,等於D0E(D是和A長度相同的0,E是和B長度相同的0)

inline int lowbit(int x){
    return x - (x & (x - 1));
}

2、lowbit(x) = x & -x

這個方法比較的簡單,也比較好記,但因為本人能力有限,無法給各位一個嚴密的推理證明

inline int lowbit(int x){
    return x & -x;
}

update

update就是充分利用了lowbit,可以對樹狀陣列進行更新,也就是說,如果在原來的陣列中,第i號元素加上了一個數x,我們就可以用lowbit來對樹狀陣列進行更新

因為我們已經知道第i號元素被修改了,那麼第i號元素的父親也會被修改,也就是說第i + lowbit(i)號元素也會被修改,我們就直接用一個迴圈修改即可

inline void update(int x, int y){
    for(reg int i = x;i <= n;i += lowbit(i))
        bit[i] += y;
}

sum

這就是樹狀陣列應用的體現了,即利用樹狀陣列來求字首和,如果我們要求第i號位的字首和,就意味著我們除了要加上第i號元素之外,還要加上第i - lowbit(i)號元素,就可以用一個迴圈來解決

inline int sum(int x){
    int tot = 0;
    for(reg int i = x;i >= 1;i -= lowbit(i))
        tot += bit[i];
    return tot;
}



總結

那麼此時再來解決最開始的那個問題就很簡單了,我們只需要對樹狀陣列進行一個初始化,然後再用sum函式求區間元素和就可以了

#include<cstdio>
#include<cstring>

#define reg register
#define M 10005

int n, l, r;
int a[M], bit[M];

inline void read(int &x) {
    x = 0; int f = 1; char s = getchar();
    while(s < '0' || s > '9') {if(s == '-') f = -1; s = getchar();}
    while(s >= '0' && s <= '9') {x = (x << 3) + (x << 1) + s - 48; s = getchar();}
    x *= f;
}

inline void wri(int x) {
    if(x < 0) {x = -x; putchar('-');}
    if(x / 10) wri(x / 10);
    putchar(x % 10 + 48);
}

inline void write(int x, char s) {
    wri(x);
    putchar(s);
}

inline int lowbit(int x) {
    return x & -x;
}

inline void update(int k, int x) {
    for(reg int i = k;i <= n;i += lowbit(i))
        bit[i] += x;
}

inline int sum(int x) {
    int tot = 0;
    for(reg int i = x;i >= 1;i -= lowbit(i))
        tot += bit[i];
    return tot;
}

int main() {
    read(n);
    for(reg int i = 1;i <= n;i ++) {
        read(a[i]);
        update(i, a[i]);
    }
    read(l), read(r);
    write(sum(r) - sum(l - 1), '\n');
    return 0;
}

如果想了解更多樹狀陣列的應用,可以看我更新的題解