1. 程式人生 > >淺談樹狀陣列(解析+模板)

淺談樹狀陣列(解析+模板)

也不知道是什麼時候開始,對於曾經學過的演算法都不太用了

遇到區間修改,區間最值就知道用線段樹,什麼樹狀陣列啊,st表啊都忘得差不多了

最近幾次模考被卡翻了,於是又想起這些老朋友

來填個坑


 

首先我們要明確一點,樹狀陣列只能維護求和,不能維護區間最值

樹狀陣列利用了分治的思想,層數為logn,所以查詢和修改都是logn,總複雜度為詢問次數m乘logn

也就是mlogn,最關鍵的是和線段樹比起來,常數要小得多,跑的飛快

而空間複雜度則是n的,只用開一維,還不用結構體

但是樹狀陣列的應用範圍也相對較小

通常分為兩種

(1)單點修改+取件查詢

(2)區間修改+單點查詢

具體為什麼,我們一會兒會說到

首先來張圖片吧

這是比較流行的一種圖

顯而易見,樹狀陣列是上面的C陣列,而下面的A則是全陣列,練出來的線代表每個節點的值是由那幾個點組成的

例如:C[4]=C[2]+C[3]+A[3]

而我們如何找到組成當前節點的每一個點呢,或者說如何找到當前點的父親呢

這就引出了我們今天的重中之重

lowbit函式

來看一下這個函式長什麼樣子

 

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

 

對的,只要壓一下行就只有一行的小小的函式,就是整個樹狀陣列的核心了

雖然短,但是蘊含的內容卻很不好理解,這個函式所求的是x化為二進位制之後從末尾開始一共有幾個零

x加上這個數之後,就得到了他的父親節點的下標

減去這個數之後,就得到了上一個與x的子樹不相交的根節點(因為建立是就是這樣定義的)

具體的原因與二進位制中的補碼有關,我們在這裡就不詳細說了,當個模板來背即可

例如上圖中,6+2=8    6-2=4

而這兩種不同的運算也就對應了樹狀陣列中的兩種操作

操作一:單點修改

首先我們可以知道當前要修改的點在原陣列中的下標i,同時知道要加上(減去)的值v

根據lowbit函式的定義我們可以知道,包含原陣列中的值的節點的下標不可能小於原陣列的下標

同時改變某個點的值只會對其父親有影響,所以理所應當的加上lowbit(i),直到根節點

對於經過的每個節點,將權值加上v

void build(int i,int v){for(;i<=n;i+=lowbit(i)) c[i]+=v;}

同樣是壓行之後只有一行

操作二:區間查詢

和樹狀陣列的含義有關,當前的樹狀陣列中存的是類似於字首和的東西

所以我們很難得到一段區間的值,但是我們可以知道從1到x的值

假設要查詢的區間為[x,y],我們可以得到a[1]+a[2]+……+a[x-1],也可以同理得1到y

做一下差會可以了

具體的實現流程就是從當前點開始,不斷減去lowit(i),知道節點1,將路徑上的每一個點的值累加

特別一題,樹狀陣列中的下標不能為0,否則lowbit函式就會炸掉

放一下操作程式碼(同樣很簡潔,這也是樹狀陣列的優點之一)

int solve(int i){
    int sum=0;
    for(;i>=1;i-=lowbit(i)) sum+=c[i];
    return sum;
}

以上就是樹狀陣列的單點修改和區間查詢

來一道完整的題:樹狀陣列模板1

附上AC程式碼:

#include<iostream>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<algorithm>
using namespace std;
inline int min(int a,int b){return a<b?a:b;}
inline int max(int a,int b){return a>b?a:b;}
inline int rd(){
    int x=0,f=1;
    char ch=getchar();
    for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=-1;
    for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0';
    return x*f;
}
inline void write(int x){
     if(x<0) putchar('-'),x=-x;
     if(x>9) write(x/10);
     putchar(x%10+'0');
     return ;
}
int n,m;
int c[500006];
int lowbit(int x){return x&(-x);}
void build(int i,int v){for(;i<=n;i+=lowbit(i)) c[i]+=v;}
int solve(int i){
    int sum=0;
    for(;i>=1;i-=lowbit(i)) sum+=c[i];
    return sum;
}
int main(){
    n=rd(),m=rd();
    for(int i=1;i<=n;i++){
        int x=rd();
        build(i,x);
    }
    for(int i=1;i<=m;i++){
        int f=rd();
        int x=rd(),y=rd();
        if(f==1) build(x,y);
        else write(solve(y)-solve(x-1)),puts("");
    }
    return 0;
}

 

然後就是一個小小的修改了

如何用樹狀陣列來維護區間修改+單點查詢

大家可以先自己想一想(反正我當時是沒有想出來的)

不太會的同學不要擔心

因為這裡的樹狀陣列和我們剛才講的不太一樣

哪裡不一樣呢,就是這裡的C陣列不是用來存和的

而是被用來存一個叫做差分的東西

什麼是差分呢,就是對於一個數組

我們不維護每個地方的值,而是維護一個字首和

使得從下標1加到下標x,就剛好可以得到原組的第x個元素

雖然查詢變慢了,但是區間修改只需要O(1)的時間

為什麼如此神奇呢,我們來舉個例子

現在我們需要將2到4的區間加上1

我們就把差分陣列下標為2的地方加1,下標為4+1的地方減1

就變成了:

計算字首和,得到序列0 1 1 1 0 0 和原陣列保持一致

是不是很神奇呢

而區間修改則是用樹狀陣列來維護差分

雖然把修改變成了logn

但是相應的,單點查詢也變成了logn

看似血虧,實則血賺

經過了上文的講解,這裡的具體操作我就不贅述了

再來一道題:樹狀陣列模板2

附上AC程式碼:

#include<iostream>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<algorithm>
using namespace std;
inline int min(int a,int b){return a<b?a:b;}
inline int max(int a,int b){return a>b?a:b;}
inline int rd(){
    int x=0,f=1;
    char ch=getchar();
    for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=-1;
    for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0';
    return x*f;
}
inline void write(int x){
     if(x<0) putchar('-'),x=-x;
     if(x>9) write(x/10);
     putchar(x%10+'0');
     return ;
}
int n,m;
int c[500006];
int lowbit(int x){return x&(-x);}
void build(int i,int v){for(;i<=n;i+=lowbit(i)) c[i]+=v;}
int solve(int i){
    int sum=0;
    for(;i>=1;i-=lowbit(i)) sum+=c[i];
    return sum;
}
int main(){
    n=rd(),m=rd();
    int set=0;
    for(int i=1;i<=n;i++){
        int x=rd();
        build(i,x-set);
        set=x;
    }
    for(int i=1;i<=m;i++){
        int f=rd();
        if(f==1){
            int x=rd(),y=rd(),k=rd();
            build(x,k);build(y+1,-k);
        }
        else{
            int x=rd();
            write(solve(x)),puts("");
        }
    }
    return 0;
}

總而言之,樹狀陣列還是很好的一種資料結構

只要利用得當,每一種資料結構都能夠煥發出耀眼的光芒,給程式碼帶來無限生機