樹狀數組與前綴和差分數組以及二維樹狀數組

樹狀數組

基本思想

樹狀數組有稱作Binary Index Tree,顧名思義,就是一種以二進制爲索引的數據結構。令源數組記作AA,現在考慮求取源數組的前綴和。例如求Σi=111Ai\Sigma_{i=1}^{11}{A_{i}}。將1111分解爲二進制,有11=8+2+111=8+2+1。因此,源數組中11個元素之和轉化爲了樹狀數組中3個元素之和。即:
i=111Ai=C11+C10+C8\sum_{i=1}^{11}{A_{i}}=C_{11}+C_{10}+C_8
當然這個式子之所以成立是因爲CC數組中的元素實際上是對應的源數組元素之和。如下圖所示:
在這裏插入圖片描述
實際上有:
C11=A11C10=A10+A9C8=Σi=18AiC_{11}=A_{11} \\ C_{10}=A_{10}+A_9\\C_{8}= \Sigma_{i=1}^{8}{A_{i}}
因此合適安排CC數組的內容,即可以在loglog時間內求取前綴和。因爲任何數的二進制表示中1的數量都是loglog的。當然,如果僅僅只是靜態求和,並不需要樹狀數組,但是樹狀數組還可以支持loglog時間的修改操作。

樹狀數組實現

從實現上說,每個CiC_i都是從AiA_i往前累加一定的數量,例如:
C1=A1C2=A2+A1C3=A3C4=A4+A3+A2+A1C5=A5C6=A6+A5C_1=A_1\\C_2=A_2+A_1\\C_3=A_3\\C_4=A_4+A_3+A_2+A_1\\C_5=A_5\\C_6=A_6+A_5\\\cdots
如何確定數量呢?就是ii的二進制表示的最低位1所代表的數量。例如:10的二進制表示爲1010,所以C10C_{10}是兩個數相加;5的二進制表示爲101,所以C5C_5是一個數相加……這個數量非常容易得到,一般稱之爲lowbitlowbit函數,有兩個版本,本質上是一樣的,如下:

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

這裏面牽涉到補碼與位運算,可以具體推導一下。
查詢的原理直接如上所示,其代碼也非常簡單:

//查詢源數組[1, x]的區間和
int query(int x){
    int sum = 0;
    for(;x;x-=lowbit(x)) sum += C[x];
    return sum;
}

修改的話,需要考慮每當修改了一個AiA_i,有哪些包含了該元素的CjC_j需要修改。結論也很簡單:

//將源數組x位置上的數增加delta,源數組索引範圍1~N
void modify(int x,int delta){
    for(;x<=N;x+=lowbit(x))C[x] += delta;
}

從這裏可以看出,樹狀數組支持單點修改成段求和操作,均可在loglog時間內完成。

初始化

樹狀數組一個很有意思的點是初始化。假設源數組與樹狀數組初始均爲零,則樹狀數組就是通過依次調用modify(i,Ai)modify(i, A_i)來實現的。
同時,其實不需要專門保存源數組,因爲所有源數組的操作與查詢全部都映射到了樹狀數組上,即所有需要用到的信息樹狀數組都有保存,無需額外再保存源數組。在很多數據結構中都是這樣的情況,源數據結構其實不用保存。

差分數組與前綴和數組

成段修改單點查詢

考慮源數組A,令前綴和數組爲S,有Si=ΣAiS_i=\Sigma{A_i}。反過來稱A是S的差分數組。即,如果令A爲源數組,則其差分數組D爲:
D1=A1Di=AiAi1D_1=A_1\\D_i=A_i-A_{i-1}
此時,如果給源數組進行成段修改操作,則相當於差分數組中的兩個單點操作。如下:
在這裏插入圖片描述
因此,如果針對差分數組D建立一個樹狀數組,就可以在loglog時間內完成D上的單點操作,也就相當於可以在loglog時間內完成源數組的成段修改操作。不過因爲此時樹狀數組求的是差分數組的前綴和,實際上就是A中某個元素的值,所以,此時樹狀數組支持的是成段修改、單點查詢。
因此可以發現,樹狀數組要麼支持單點修改、成段查詢,要麼支持成段修改、單點查詢。能不能夠兩個操作都成段呢?

成段修改成段查詢

令源數組爲A,差分數組爲D,則:
i=1nAi=D1+(D1+D2)+(D1+D2+D3)++(D1++Dn)=ni=1nDii=1n(i1)Di\sum_{i=1}^{n}A_i=D_1+(D_1+D_2)+(D_1+D_2+D_3)+\cdots+(D_1+\cdots+D_n)\\=n\cdot{\sum_{i=1}^{n}D_i}-\sum_{i=1}^{n}(i-1)\cdot{D_i}
這個式子實際上是由兩個前綴和構成的,因此可以建立兩個樹狀數組,一個用來操作差分數組,另一個用來操作(i1)Di(i-1)\cdot{D_i}數組。
因此使用2個樹狀數組即可完成對源數組的成段修改、成段求和操作。
POJ3468,很直白的題目,成段修改,成段求和:

/*
    成段修改,區間求和
*/
#include <stdio.h>
typedef long long llt;

int getInt(){
	int sgn = 1;
	char ch = getchar();
	while( ch != '-' && ( ch < '0' || ch > '9' ) ) ch = getchar();
	if ( '-' == ch ) {sgn = 0;ch=getchar();}

	int ret = (int)(ch-'0');
	while( '0' <= (ch=getchar()) && ch <= '9' ) ret = ret * 10 + (int)(ch-'0');
	return sgn ? ret : -ret;
}

int const SIZE = 100010;

//樹狀數組
llt C[SIZE],C2[SIZE];
int N,Q;

inline int lowbit(int n){return n&-n;}
//將x位置增加delta
void modify(int x,llt delta){
    for(;x<=N;x+=lowbit(x))C[x] += delta;
}
//查詢[1,x]的區間和
llt query(int x){
    llt sum = 0;
    for(;x;x-=lowbit(x)) sum += C[x];
    return sum;
}
//第二套樹狀數組
void modify2(int x,llt delta){
    for(;x<=N;x+=lowbit(x))C2[x] += delta;
}
//查詢[1,x]的區間和
llt query2(int x){
    llt sum = 0;
    for(;x;x-=lowbit(x)) sum += C2[x];
    return sum;
}
//回答源數組[1,x]的區間和
inline llt answer(int x){
    return x * query(x) - query2(x);
}
inline llt answer(int s,int e){
    return answer(e) - answer(s-1);
}

llt A[SIZE];
int main(){
    //freopen("1.txt","r",stdin);
    N = getInt();
    Q = getInt();
    for(int i=1;i<=N;++i)A[i]=getInt();

    modify(1,A[1]);
    //建立兩個樹狀數組
    for(int i=2;i<=N;++i){
        modify(i,A[i]-A[i-1]);
        modify2(i,(i-1)*(A[i]-A[i-1]));
    }

    //答問題
    char cmd[3];
    int a,b,c;
    while(Q--){
        scanf("%s",cmd);
        a = getInt();
        if('Q' == *cmd){
            printf("%lld\n",answer(a,getInt()));
        }else{
            b = getInt();
            //修改差分數組
            modify(a,c=getInt());
            modify(b+1,-c);
            //修改(i-1)乘差分數組
            modify2(a,(a-1)*(llt)c);
            modify2(b+1,(llt)b*-c);
        }

    }
    return 0;
}

二維樹狀數組

單點修改成段求和

在一維基礎上,二維樹狀數組非常好理解且容易實現。只需分別按照行列座標相加即可。例如:
在這裏插入圖片描述
C46=(A46+A45)+(A36+A35)+(A26+A25)+(A16+A15)C_{46}=(A_{46}+A_{45})+(A_{36}+A_{35})+(A_{26}+A_{25})+(A_{16}+A_{15})
因此二維數組數組可以解決子矩陣和的問題。

//查詢源數組[(1,1),(r,c)]的子矩陣和
int query(int r, int c){
    int sum = 0;
    for(;r;r-=lowbit(r))for(int j=c;j;j-=lowbit(j))sum += C[r][j];
    return sum;
}

對於單點修改,也是類似的。

//將源矩陣(r,c)位置上的數增加delta,矩陣爲N行M列,從1開始索引
void modify(int r, int c, int delta){
    for(;r<=N;r+=lowbit(r))for(int j=c;j<=M;j+=lowbit(j))C[r][j] += delta;
}

成段修改單點查詢

與一維情況類似,使用差分可以使得樹狀數組支持單點查詢、成段修改操作。考慮源數組A和差分數組D,令:D11=A11Dij=Aij+Ai1,j1Ai1,jAi,j1D_{11}=A_{11}\\D_{ij}=A_{ij}+A_{i-1,j-1}-A_{i-1,j}-A_{i,j-1}
反過來有:Aij=u=1,v=1i,jDu,vA_{ij}=\sum_{u=1,v=1}^{i,j}D_{u,v}
即A是D的前綴和數組。
同樣的,源數組上的單點查詢會變爲差分數組上的求和操作;而源數組上的成段修改操作,會變爲差分數組上的4個單點操作。對差分數組建立樹狀數組即可支持相應的操作。
在這裏插入圖片描述
POJ2155在二維數組上要求支持成段修改、單點查詢,正好使用差分數組實現。而且該數組是01的,只需做異或和即可。

/*
     N×N的矩陣,2種操作
     C x1 y1 x2 y2:子矩陣內的所有元素改變狀態
     Q r y:問(r,y)位置上的元素值
*/
#include <stdio.h>
#include <string.h>

int getUnsigned(){
	char ch = getchar();
	while( ch < '0' || ch > '9' ) ch = getchar();

	int ret = (int)(ch-'0');
	while( '0' <= ( ch = getchar() ) && ch <= '9' ) ret = ret * 10 + (int)(ch-'0');
	return ret;
}

int const SIZE = 1020;
int C[SIZE][SIZE];//樹狀數組

int N,M,Q;

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

int query(int r,int c){
    int ans = 0;
    for(;r;r-=lowbit(r))for(int j=c;j;j-=lowbit(j))ans^=C[r][j];
    return ans;
}

void modify(int r, int c, int delta){
    for(;r<=N;r+=lowbit(r))for(int j=c;j<=M;j+=lowbit(j))C[r][j]^=delta;
}

int main(){
    //freopen("1.txt","r",stdin);
    int nofkase=getUnsigned();

    char cmd[3];
    int x1,x2,y1,y2;
    for(int kase=1;kase<=nofkase;++kase){
        N=M=getUnsigned();
        Q=getUnsigned();

        //初始全0,差分數組也全是0
        memset(C,0,sizeof(C));

        if ( kase > 1 ) putchar('\n');

        while(Q--){
            scanf("%s",cmd);
            x1 = getUnsigned();
            y1 = getUnsigned();
            if ( 'C' == *cmd ){
                x2 = getUnsigned();
                y2 = getUnsigned();
                //4個單點操作
                modify(x1,y1,1);
                modify(x2+1,y1,1);
                modify(x1,y2+1,1);
                modify(x2+1,y2+1,1);
            }else{
                printf("%d\n",query(x1,y1));
            }
        }
    }
    return 0;
}

成段修改成段求和

與一維情況類似,考慮源數組中成段求和的情況,變成了差分數組的四重求和:
i,jAi,j=i=1rj=1cu=1iv=1jDu,v\sum_{i,j}A_{i,j}=\sum_{i=1}^{r}\sum_{j=1}^{c}\sum_{u=1}^{i}\sum_{v=1}^{j}D_{u,v}
統計以後可以發現,對每一個Du,vD_{u,v}在和式中一共出現了(ru+1)(cv+1)(r-u+1)\cdot{(c-v+1)}次。例如D1,1D_{1,1}出現了r×cr\times{c}次,而Dr,cD_{r,c}出現了1次。所以原式可以寫成:
rci=1rj=1cDi,j+i=1rj=1cDi,j(i1)(j1)ci=1rj=1cDi,j(i1)ri=1rj=1cDi,j(j1)r\cdot{c}\cdot\sum_{i=1}^{r}\sum_{j=1}^{c}D_{i,j}+\sum_{i=1}^{r}\sum_{j=1}^{c}D_{i,j}\cdot{(i-1)}\cdot{(j-1)}-c\cdot\sum_{i=1}^{r}\sum_{j=1}^{c}D_{i,j}\cdot{(i-1)}-r\cdot\sum_{i=1}^{r}\sum_{j=1}^{c}D_{i,j}\cdot{(j-1)}
於是,可以使用4個二維樹狀數組來實現這個求和操作。因此可以使用4個樹狀數組來支持源數組的成段修改、成段求和操作。
洛谷4514是一個非常直白的成段修改成段求和的題目。

/*
     N×M的矩陣,2種操作成段修改成段更新
     Luogu提交要開啓O2優化
*/
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;

int getInt(){
	int sgn = 1;
	char ch = getchar();
	while( ch != '-' && ( ch < '0' || ch > '9' ) ) ch = getchar();
	if ( '-' == ch ) {sgn = 0;ch=getchar();}

	int ret = (int)(ch-'0');
	while( '0' <= (ch=getchar()) && ch <= '9' ) ret = ret * 10 + (int)(ch-'0');
	return sgn ? ret : -ret;
}

int const SIZE = 2050;
int C[4][SIZE][SIZE];
int N,M;

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

//在第idx個C上面做查詢
int query(int r,int c,int idx){
    int sum = 0;
    for(;r;r-=lowbit(r))for(int j=c;j;j-=lowbit(j))sum += C[idx][r][j];
    return sum;
}
//在第idx個C上面做修改
void modify(int r,int c,int delta,int idx){
    for(;r<=N;r+=lowbit(r))for(int j=c;j<=M;j+=lowbit(j))C[idx][r][j] += delta;
}
//源數組的[(r1,c1),(r2,c2)]區間全部增加delta
void modify(int r1,int c1,int r2,int c2,int delta){
    //相當於差分數組上的4個單點操作
    //但是要同時修改4個樹狀數組,所以有16個操作
    //修改差分數組
    modify(r1,c1,delta,0);modify(r2+1,c2+1,delta,0);
    modify(r1,c2+1,-delta,0);modify(r2+1,c1,-delta,0);
    //修改(i-1)×(j-1)×D
    modify(r1,c1,delta*(r1-1)*(c1-1),1);modify(r2+1,c2+1,delta*r2*c2,1);
    modify(r1,c2+1,-delta*(r1-1)*c2,1);modify(r2+1,c1,-delta*r2*(c1-1),1);
    //修改(i-1)×D
    modify(r1,c1,delta*(r1-1),2);modify(r2+1,c2+1,delta*r2,2);
    modify(r1,c2+1,-delta*(r1-1),2);modify(r2+1,c1,-delta*r2,2);
    //修改(j-1)×D
    modify(r1,c1,delta*(c1-1),3);modify(r2+1,c2+1,delta*c2,3);
    modify(r1,c2+1,-delta*c2,3);modify(r2+1,c1,-delta*(c1-1),3);
}
//查詢源數組的[(1,1),(r,c)]區間和
int query(int r,int c){
    return r * c * query(r,c,0) - c * query(r,c,2)
         + query(r,c,1) - r * query(r,c,3);
}
//查詢源數組的[(r1,c1),(r2,c2)]區間和
int query(int r1,int c1,int r2,int c2){
    return query(r2,c2) - query(r2,c1-1) + query(r1-1,c1-1) - query(r1-1,c2);
}
int main(){
    //freopen("1.txt","r",stdin);
    char cmd[3];
    scanf("%s",cmd);
    N = getInt(); M = getInt();
    int r1,c1,r2,c2;
    while(EOF!=scanf("%s",cmd)){
        r1=getInt();c1=getInt();r2=getInt();c2=getInt();
        if('k'==*cmd){
            printf("%d\n",query(r1,c1,r2,c2));
        }else{
            modify(r1,c1,r2,c2,getInt());
        }
    }
    return 0;
}

樹狀數組的其他應用

樹狀數組與線段樹等不但可以用來求和,還可以用來進行某種情況的計數。只需要改變i與Ai的含義即可。如果源數據中有一個數是a,則樹狀數組對應的源數組的位置a上加1。這樣,查詢樹狀數組所得到的和其實就是滿足某些條件的值的數量。相當情況下,這樣的處理需要使用到離散化。

逆序對

逆序對問題可以使用經典的分支策略實現,也就是歸併排序的一個流程。設原始數據序列爲DD,令數組A記錄:AiA_i表示D中值爲i的元素的數量。則對D從後往前,對每一個DiD_i,查詢數組A中[1,Di1][1,D_i-1]中的和,即可得到D中以DiD_i爲首的逆序對數量。
洛谷1908是一個基本的逆序對問題,如果用樹狀數組或者線段樹,需要離散化。

/*
     逆序對
*/
#include <bits/stdc++.h>
using namespace std;

int getInt(){
	int sgn = 1;
	char ch = getchar();
	while( ch != '-' && ( ch < '0' || ch > '9' ) ) ch = getchar();
	if ( '-' == ch ) {sgn = 0;ch=getchar();}

	int ret = (int)(ch-'0');
	while( '0' <= (ch=getchar()) && ch <= '9' ) ret = ret * 10 + (int)(ch-'0');
	return sgn ? ret : -ret;
}

int const SIZE = 1000100;
int A[SIZE],T[SIZE];
int C[SIZE];
int N;

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

//查詢[1,r]的區間和
int query(int r){
    int sum = 0;
    for(;r;r-=lowbit(r))sum += C[r];
    return sum;
}
//第r個位置加delta
void modify(int r,int delta){
    for(;r<=N;r+=lowbit(r))C[r] += delta;
}

int main(){
    //freopen("1.txt","r",stdin);
    int n = N = getInt();
    for(int i=0;i<n;++i) A[i] = T[i] = getInt();

    //離散化
    sort(T,T+n);
    N = unique(A,A+n) - A;

    long long int ans = 0;
    for(int a,i=n-1;i>=0;--i){
        a = lower_bound(T,T+N,A[i]) - T + 1;
        //查詢比a小的數量
        ans += query(a-1);
        //將a的數量增加1
        modify(a,1);
    }
    printf("%lld\n",ans);
    return 0;
}

二維平面排序

考慮這樣一個問題:給定平面點集,對每一個點,問其左下(即x、y座標均小於)有多少個點。將點集按任一座標例如按x升序(x相等則按y升序)排序。令數組A記錄:AiA_i表示y座標爲i的點的數量。然後遍歷點集,對每一個點PiP_i,查詢數組A中[1,Pi.y1][1,P_i.y-1]的和,即可知道答案。同理也可以求出左上、右下、右上等各方的點的數量。當然,對於嚴格與不嚴格的情況,處理上有一定的不同,不過並不複雜。
POJ2481給定區間,要求對每一個區間求出其真父集,相當於求左上點。

/**
    給定N個區間[s,e]。
    如果a區間能夠真包含b區間,則稱a比b強壯。
    對每一個區間,問比其強壯的區間有多少個
*/
#include <stdio.h>
#include <algorithm>
using namespace std;

int getUnsigned(){
	char ch = getchar();
	while( ch < '0' || ch > '9' ) ch = getchar();

	int ret = (int)(ch-'0');
	while( '0' <= ( ch = getchar() ) && ch <= '9' ) ret = ret * 10 + (int)(ch-'0');
	return ret;
}

int const SIZE = 100005;
int C[SIZE];
int N;

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

//查詢[1,r]的區間和
int query(int r){
    int sum = 0;
    for(;r;r-=lowbit(r))sum += C[r];
    return sum;
}
//第r個位置加delta
void modify(int r,int delta){
    for(;r<=N;r+=lowbit(r))C[r] += delta;
}

struct _t{
    int s,e,idx;
}Node[SIZE];

//按e降序,e相等則按s升序
bool operator < (_t const&l,_t const&r){
	if ( l.e != r.e ) return l.e > r.e;
	return l.s < r.s;
}
bool operator == (_t const&l,_t const&r){
    return l.e == r.e && l.s == r.s;
}

int Ans[SIZE];
int main(){
    //freopen("1.txt","r",stdin);
    while(N=getUnsigned()){
        //初始化
        fill(C,C+SIZE,0);
        for(int i=0;i<N;++i){
            Node[Node[i].idx = i].s = getUnsigned()+1;
            Node[i].e = getUnsigned() + 1;
        }
        //排序
        sort(Node,Node+N);
        //答問題
        Ans[Node->idx] = 0;
        modify(Node->s,1);
        for(int i=1;i<N;++i){
            //真包含
            Ans[Node[i].idx] = Node[i]==Node[i-1]?Ans[Node[i-1].idx]:query(Node[i].s);
            modify(Node[i].s,1);
        }
        //輸出
        printf("%d",Ans[0]);
        for(int i=1;i<N;++i)printf(" %d",Ans[i]);
        printf("\n");
    }
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章