树状数组与前缀和差分数组以及二维树状数组

树状数组与前缀和差分数组以及二维树状数组

  • 树状数组
    • 基本思想
    • 树状数组实现
    • 初始化
  • 差分数组与前缀和数组
    • 成段修改单点查询
    • 成段修改成段查询
  • 二维树状数组
    • 单点修改成段求和
    • 成段修改单点查询
    • 成段修改成段求和
  • 树状数组的其他应用
    • 逆序对
    • 二维平面排序

树状数组

基本思想

树状数组有称作Binary Index Tree,顾名思义,就是一种以二进制为索引的数据结构。令源数组记作 A A A,现在考虑求取源数组的前缀和。例如求 Σ i = 1 11 A i \Sigma_{i=1}^{11}{A_{i}} Σi=111Ai。将 11 11 11分解为二进制,有 11 = 8 + 2 + 1 11=8+2+1 11=8+2+1。因此,源数组中11个元素之和转化为了树状数组中3个元素之和。即:
∑ i = 1 11 A i = C 11 + C 10 + C 8 \sum_{i=1}^{11}{A_{i}}=C_{11}+C_{10}+C_8 i=111Ai=C11+C10+C8
当然这个式子之所以成立是因为 C C C数组中的元素实际上是对应的源数组元素之和。如下图所示:
树状数组与前缀和差分数组以及二维树状数组_第1张图片
实际上有:
C 11 = A 11 C 10 = A 10 + A 9 C 8 = Σ i = 1 8 A i C_{11}=A_{11} \\ C_{10}=A_{10}+A_9\\C_{8}= \Sigma_{i=1}^{8}{A_{i}} C11=A11C10=A10+A9C8=Σi=18Ai
因此合适安排 C C C数组的内容,即可以在 l o g log log时间内求取前缀和。因为任何数的二进制表示中1的数量都是 l o g log log的。当然,如果仅仅只是静态求和,并不需要树状数组,但是树状数组还可以支持 l o g log log时间的修改操作。

树状数组实现

从实现上说,每个 C i C_i Ci都是从 A i A_i Ai往前累加一定的数量,例如:
C 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 ⋯ C_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 C1=A1C2=A2+A1C3=A3C4=A4+A3+A2+A1C5=A5C6=A6+A5
如何确定数量呢?就是 i i i的二进制表示的最低位1所代表的数量。例如:10的二进制表示为1010,所以 C 10 C_{10} C10是两个数相加;5的二进制表示为101,所以 C 5 C_5 C5是一个数相加……这个数量非常容易得到,一般称之为 l o w b i t lowbit lowbit函数,有两个版本,本质上是一样的,如下:

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

修改的话,需要考虑每当修改了一个 A i A_i Ai,有哪些包含了该元素的 C j C_j Cj需要修改。结论也很简单:

//将源数组x位置上的数增加delta,源数组索引范围1~N
void modify(int x,int delta){
    for(;x<=N;x+=lowbit(x))C[x] += delta;
}

从这里可以看出,树状数组支持单点修改成段求和操作,均可在 l o g log log时间内完成。

初始化

树状数组一个很有意思的点是初始化。假设源数组与树状数组初始均为零,则树状数组就是通过依次调用 m o d i f y ( i , A i ) modify(i, A_i) modify(i,Ai)来实现的。
同时,其实不需要专门保存源数组,因为所有源数组的操作与查询全部都映射到了树状数组上,即所有需要用到的信息树状数组都有保存,无需额外再保存源数组。在很多数据结构中都是这样的情况,源数据结构其实不用保存。

差分数组与前缀和数组

成段修改单点查询

考虑源数组A,令前缀和数组为S,有 S i = Σ A i S_i=\Sigma{A_i} Si=ΣAi。反过来称A是S的差分数组。即,如果令A为源数组,则其差分数组D为:
D 1 = A 1 D i = A i − A i − 1 D_1=A_1\\D_i=A_i-A_{i-1} D1=A1Di=AiAi1
此时,如果给源数组进行成段修改操作,则相当于差分数组中的两个单点操作。如下:
树状数组与前缀和差分数组以及二维树状数组_第2张图片
因此,如果针对差分数组D建立一个树状数组,就可以在 l o g log log时间内完成D上的单点操作,也就相当于可以在 l o g log log时间内完成源数组的成段修改操作。不过因为此时树状数组求的是差分数组的前缀和,实际上就是A中某个元素的值,所以,此时树状数组支持的是成段修改、单点查询。
因此可以发现,树状数组要么支持单点修改、成段查询,要么支持成段修改、单点查询。能不能够两个操作都成段呢?

成段修改成段查询

令源数组为A,差分数组为D,则:
∑ i = 1 n A i = D 1 + ( D 1 + D 2 ) + ( D 1 + D 2 + D 3 ) + ⋯ + ( D 1 + ⋯ + D n ) = n ⋅ ∑ i = 1 n D i − ∑ i = 1 n ( i − 1 ) ⋅ D i \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} i=1nAi=D1+(D1+D2)+(D1+D2+D3)++(D1++Dn)=ni=1nDii=1n(i1)Di
这个式子实际上是由两个前缀和构成的,因此可以建立两个树状数组,一个用来操作差分数组,另一个用来操作 ( i − 1 ) ⋅ D i (i-1)\cdot{D_i} (i1)Di数组。
因此使用2个树状数组即可完成对源数组的成段修改、成段求和操作。
POJ3468,很直白的题目,成段修改,成段求和:

/*
    成段修改,区间求和
*/
#include 
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;
}

二维树状数组

单点修改成段求和

在一维基础上,二维树状数组非常好理解且容易实现。只需分别按照行列坐标相加即可。例如:
树状数组与前缀和差分数组以及二维树状数组_第3张图片
C 46 = ( A 46 + A 45 ) + ( A 36 + A 35 ) + ( A 26 + A 25 ) + ( A 16 + A 15 ) C_{46}=(A_{46}+A_{45})+(A_{36}+A_{35})+(A_{26}+A_{25})+(A_{16}+A_{15}) C46=(A46+A45)+(A36+A35)+(A26+A25)+(A16+A15)
因此二维数组数组可以解决子矩阵和的问题。

//查询源数组[(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,令: D 11 = A 11 D i j = A i j + A i − 1 , j − 1 − A i − 1 , j − A i , j − 1 D_{11}=A_{11}\\D_{ij}=A_{ij}+A_{i-1,j-1}-A_{i-1,j}-A_{i,j-1} D11=A11Dij=Aij+Ai1,j1Ai1,jAi,j1
反过来有: A i j = ∑ u = 1 , v = 1 i , j D u , v A_{ij}=\sum_{u=1,v=1}^{i,j}D_{u,v} Aij=u=1,v=1i,jDu,v
即A是D的前缀和数组。
同样的,源数组上的单点查询会变为差分数组上的求和操作;而源数组上的成段修改操作,会变为差分数组上的4个单点操作。对差分数组建立树状数组即可支持相应的操作。
树状数组与前缀和差分数组以及二维树状数组_第4张图片
POJ2155在二维数组上要求支持成段修改、单点查询,正好使用差分数组实现。而且该数组是01的,只需做异或和即可。

/*
     N×N的矩阵,2种操作
     C x1 y1 x2 y2:子矩阵内的所有元素改变状态
     Q r y:问(r,y)位置上的元素值
*/
#include 
#include 

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 , j A i , j = ∑ i = 1 r ∑ j = 1 c ∑ u = 1 i ∑ v = 1 j D u , 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} i,jAi,j=i=1rj=1cu=1iv=1jDu,v
统计以后可以发现,对每一个 D u , v D_{u,v} Du,v在和式中一共出现了 ( r − u + 1 ) ⋅ ( c − v + 1 ) (r-u+1)\cdot{(c-v+1)} (ru+1)(cv+1)次。例如 D 1 , 1 D_{1,1} D1,1出现了 r × c r\times{c} r×c次,而 D r , c D_{r,c} Dr,c出现了1次。所以原式可以写成:
r ⋅ c ⋅ ∑ i = 1 r ∑ j = 1 c D i , j + ∑ i = 1 r ∑ j = 1 c D i , j ⋅ ( i − 1 ) ⋅ ( j − 1 ) − c ⋅ ∑ i = 1 r ∑ j = 1 c D i , j ⋅ ( i − 1 ) − r ⋅ ∑ i = 1 r ∑ j = 1 c D i , j ⋅ ( j − 1 ) 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)} rci=1rj=1cDi,j+i=1rj=1cDi,j(i1)(j1)ci=1rj=1cDi,j(i1)ri=1rj=1cDi,j(j1)
于是,可以使用4个二维树状数组来实现这个求和操作。因此可以使用4个树状数组来支持源数组的成段修改、成段求和操作。
洛谷4514是一个非常直白的成段修改成段求和的题目。

/*
     N×M的矩阵,2种操作成段修改成段更新
     Luogu提交要开启O2优化
*/
#include 
#include 
#include 
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。这样,查询树状数组所得到的和其实就是满足某些条件的值的数量。相当情况下,这样的处理需要使用到离散化。

逆序对

逆序对问题可以使用经典的分支策略实现,也就是归并排序的一个流程。设原始数据序列为 D D D,令数组A记录: A i A_i Ai表示D中值为i的元素的数量。则对D从后往前,对每一个 D i D_i Di,查询数组A中 [ 1 , D i − 1 ] [1,D_i-1] [1,Di1]中的和,即可得到D中以 D i D_i Di为首的逆序对数量。
洛谷1908是一个基本的逆序对问题,如果用树状数组或者线段树,需要离散化。

/*
     逆序对
*/
#include 
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记录: A i A_i Ai表示y坐标为i的点的数量。然后遍历点集,对每一个点 P i P_i Pi,查询数组A中 [ 1 , P i . y − 1 ] [1,P_i.y-1] [1,Pi.y1]的和,即可知道答案。同理也可以求出左上、右下、右上等各方的点的数量。当然,对于严格与不严格的情况,处理上有一定的不同,不过并不复杂。
POJ2481给定区间,要求对每一个区间求出其真父集,相当于求左上点。

/**
    给定N个区间[s,e]。
    如果a区间能够真包含b区间,则称a比b强壮。
    对每一个区间,问比其强壮的区间有多少个
*/
#include 
#include 
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;
}

你可能感兴趣的:(ACM数据结构)