1. 程式人生 > >B.I.T(樹狀陣列)的初步學習(包懂)

B.I.T(樹狀陣列)的初步學習(包懂)

目錄

》樹狀陣列的定義《

》樹狀陣列的構建《

》正式開始建造《

》區間和《

》板《


》樹狀陣列的定義《

樹狀陣列(Binary Indexed Tree(B.I.T)也稱作Fenwick Tree)是一區間查詢單點修改複雜度都為log(n)的資料結構。主要用於查詢任意兩點之間的所有元素之和。

如圖:樹狀陣列

》樹狀陣列的構建《

A為原陣列,C為樹狀陣列,那麼問題來了,如何構建樹狀陣列呢?

首先,我們觀察一下每一個i的二進位制

i 1 2 3 4 5 6 7 8 9
二進位制 0001 0010 0011 0100 0101 0110 0111 1000 1001

 

 

 

我們可以發現,2是1的父節點,4是2的父節點,又是3的父節點,那麼,如何找一個節點的父節點呢?

其實我們可以找到一下規律:1 + 1 = 2,沒錯吧(廢話)    2 + 2 = 4 , 3 + 1 = 4, 5 + 1 = 6 , 6 + 2 =8 , 7 + 1 = 8

來吧,上表格

i 1 2 3 4 5 6 7
父節點 2 4 4 8 6 8 8
差值 1 2 1 4 1 2 1

 

 

 

 

相信有的讀者已經找出了規律,沒找出的也不要著急,接下來我們把 i 的二進位制位與差值的二進位制位來比較(來了!來了!)

i 1 2 3 4 5 6 7
i的二進位制 001 010 011 100 101 110 111
差值 1 2 1 4 1 2 1
差的二進位制 001 010 001 100 001 010 001

怎麼樣,是不是一目瞭然,將 i 的二進位制位與 差的二進位制位 進行比較,我們可以發現 i 與它的父節點的差值就是 i的二進位制位從右往左數第一個1出現的二進位制位所表示的值,我們稱之為lowbit(i) ,舉個例子,lowbit(6) = 2(二進位制位10),因為6的二進位制位為 110,所以從左往右數第1個1出現的位置為2(從左往右),所以lowbit(6)= 2(十進位制)= 10(二進位制)。再舉個例子,7,因為7的二進位制位為111,所以lowbit(7)=1(自己照上面的推)。

那麼,如何確定lowbit(i)呢?

我們先來熟悉一下位運算中的按位與&:0 & 0=0        0 & 1=0        1 & 0=0       1 & 1=1

二進位制位上相同的就得相同的,不同的都為1,簡單來說只有1&1時,值為1,其他都為零

1、法一

程式碼

#define lowbit(x) (x-(x&(x-1))
//或者如下
int lowbit(int x){
    return (x-(x&(x-1));
}

說明:x的二進位制可以看做A1B(A是最後一個1之前的部分,B是最後一個1之後的0)

            x-1的二進位制可以看做A0C(C是和B一樣長的1)

            x & (x - 1)的二進位制就是A1B & A0C = A0B

            x – (x & (x - 1))的二進位制就是A1B – A0B = 0…0(長A個0)10…0(長B個0)

2、法二

程式碼

#define lowbit(x) (x&(-x))
//或者如下
int lowbit(int x){
    return (x&(-x));
}

例如:lowbit(22)=2(實在不好推,只好舉例子(推不出)

            22的二進位制原碼011010,正數的補碼等於它的原碼011010

            -22的二進位制原碼111010,負數的補碼等於它的原碼取反加1,為100110

            011010  & 100110 = 000010 正數轉換成原碼後依然是000010

            所以lowbit(22)=2

》正式開始建造《

我們先回到樹狀陣列的圖片

我們可以發現,當一個節點改變後,它的父節點,父節點的父節點……都要改變,怎麼搞?怎麼搞?

我們上面已經探討了一個節點與父節點的關係 (i的父節點 = i + lowbit(i)),所以我們可以用一個迴圈一直找父節點的編號,直到大於了整個陣列的長度,程式碼:

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

int main(){
    scanf("%d",&n);
    for(int i = 1; i <= n; i++){
        scanf("%d",&a[i]);
        update(i,a[i]);
    }
}

update(i,j)指單點修改,修改 i ,修改的值為j。

為什麼輸入時就構建,而不再輸完以後再構建呢? 

我們想:我們輸入 a[i] ,其實就等同於向 c[i] 中加 a[i] ,而 i 的所有前輩都要加上 a[i] 所以我們輸入的時候就可以把樹狀陣列建好,以免再去用另一個迴圈浪費時間。

 

》區間和《

首先,我們來複習一下字尾和,就是 sum[i] 就為位置 i 以前的所有元素之和,其程式碼也極其簡單,就不再贅述。

所以區間 l 到 r 的和就為 sum[r] - sum[l-1]。

那麼,如何用樹狀陣列呢?

首先我們探討一下 i - lowbit(i) 是 i 的什麼,從圖中可以看出,i - lowbit(i)就是從 i 向左數第一個比它高的(暫且這麼稱呼)

而一直這樣下去就是以 i 為尾的字尾和,我就不再推了(推不出)

程式碼:

int Sum(int x){
    int he = 0;
    for(int i = x; i > 0; i -= lowbit(i))
        he += c[i];
    return he;
}

而我們可以發現,i 的字首和只與 i 前面的元素有關,所以我們可以在輸入時就構建出來(板)

》板《

程式碼中有我自己的個人習慣,不喜勿噴。

其中 a 為原陣列,c 為樹狀陣列,b 為字尾和。

#include <cstdio>
#include <iostream>

#define lowbit(x) (x&(-x))
using namespace std;

int n;

int a[105],b[105],c[105];

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

int Sum(int x){
    int he = 0;
    for(int i = x; i > 0; i -= lowbit(i))
        he += c[i];
    return he;
}

int main(){
    scanf("%d",&n);
    for(int i = 1; i <= n; i ++){
        scanf("%d",&a[i]);
        update(i,a[i]);
        b[i] = Sum(i);
    }
}