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);
}
}