树状知识详解(转载)

转自大佬博客

树状数组基础

树状数组是一个查询和修改复杂度都为log(n)的数据结构。主要用于数组的单点修改&&区间求和.

另外一个拥有类似功能的是线段树.

具体区别和联系如下:

1.两者在复杂度上同级, 但是树状数组的常数明显优于线段树, 其编程复杂度也远小于线段树.

2.树状数组的作用被线段树完全涵盖, 凡是可以使用树状数组解决的问题, 使用线段树一定可以解决, 但是线段树能够解决的问题树状数组未必能够解决.

3.树状数组的突出特点是其编程的极端简洁性, 使用lowbit技术可以在很短的几步操作中完成树状数组的核心操作,其代码效率远高于线段树。

上面出现了一个新名词:lowbit.其实lowbit(x)就是求x最低位的1;

下面加图进行解释

对于一般的二叉树,我们是这样画的

把位置稍微移动一下,便是树状数组的画法

上图其实是求和之后的数组,原数组和求和数组的对照关系如下,其中a数组是原数组,c数组是求和后的数组:

C[i]代表 子树的叶子结点的权值之和

如图可以知道

C[1]=A[1];

C[2]=A[1]+A[2];

C[3]=A[3];

C[4]=A[1]+A[2]+A[3]+A[4];

C[5]=A[5];

C[6]=A[5]+A[6];

C[7]=A[7];

C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];

再将其转化为二进制看一下:

        C[1] = C[0001] = A[1];

        C[2] = C[0010] = A[1]+A[2];

        C[3] = C[0011] = A[3];

        C[4] = C[0100] = A[1]+A[2]+A[3]+A[4];

        C[5] = C[0101] = A[5];

        C[6] = C[0110] = A[5]+A[6];

        C[7] = C[0111] = A[7];

        C[8] = C[1000] = A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];

对照式子可以发现  C[i]=A[i-2^k+1]+A[i-2^k+2]+......A[i]; (k为i的二进制中从最低位到高位连续零的长度)例如i=8(1000)时,k=3;

C[8] = A[8-2^3+1]+A[8-2^3+2]+......+A[8]

即为上面列出的式子

现在我们返回到lowbit中来

其实不难看出lowbit(i)便是上面的2^k

因为2^k后面一定有k个0

比如说2^5==>100000

正好是i最低位的1加上后缀0所得的值

开篇就说了,lowbit(x)是取出x的最低位1;具体操作为

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

极致简短!!!!现在我们来理解一下这行代码:

我们知道,对于一个数的负数就等于对这个数取反+1

以二进制数11010为例:11010的补码为00101,加1后为00110,两者相与便是最低位的1

其实很好理解,补码和原码必然相反,所以原码有0的部位补码全是1,补码再+1之后由于进位那么最末尾的1和原码

最右边的1一定是同一个位置(当遇到第一个1的时候补码此位为0,由于前面会进一位,所以此位会变为1)

所以我们只需要进行a&(-a)就可以取出最低位的1了

会了lowbit,我们就可以进行区间查询和单点更新了!!!

--------------------------------------------------------------------------------------------

单点更新:

继续看开始给出的图

此时如果我们要更改A[1]

则有以下需要进行同步更新

1(001)        C[1]+=A[1]

lowbit(1)=001 1+lowbit(1)=2(010)     C[2]+=A[1]

lowbit(2)=010 2+lowbit(2)=4(100)     C[4]+=A[1]

lowbit(4)=100 4+lowbit(4)=8(1000)   C[8]+=A[1]

换成代码就是:

void update(int x,int y,int n)
{

      for(int i=x;i<=n;i+=lowbit(i))    //x为更新的位置,y为更新后的数,n为数组最大值

      c[i] += y;

}

--------------------------------------------------------------------------------------------

区间查询:

举个例子 i=5

C[4]=A[1]+A[2]+A[3]+A[4]; 

C[5]=A[5];

可以推出:   sum(i = 5)  ==> C[4]+C[5];

序号写为二进制: sum(101)=C[(100)]+C[(101)];

第一次101,减去最低位的1就是100;

其实也就是单点更新的逆操作

代码如下:

int getsum(int x)
{

    int ans = 0;

    for(int i=x;i;i-=lowbit(i))

    ans += c[i];

    return ans;

}


高级操作

该部分内容转自胡小兔的OI博

区间修改+单点查询

通过“差分”(就是记录数组中每个元素与前一个元素的差),可以把这个问题转化为问题1。

查询

设原数组为a[i], 设数组d[i]=a[i]-a[i-1](a[0]=0),则a[i]=\sum_{j=1}^{i}d[j],可以通过求d[i]的前缀和查询。

修改

当给区间[l,r]加上x的时候,a[l]与前一个元素 a[l-1]的差增加了x,a[r+1]与 a[r]的差减少了x。根据d[i]数组的定义,只需给a[l]加上 x, 给 a[r+1]减去x即可

void add(int p, int x)
{                 //这个函数用来在树状数组中直接修改

    while(p <= n) sum[p] += x, p += p & -p;

}

void range_add(int l, int r, int x)
{                 //给区间[l, r]加上x

       add(l, x), add(r + 1, -x);

}
int ask(int p)
{                 //单点查询

    int res = 0;

    while(p)
         res += sum[p], p -= p & -p;

    return res;

}

区间修改+区间查询

这是最常用的部分,也是用线段树写着最麻烦的部分——但是现在我们有了树状数组!

怎么求呢?我们基于问题2的“差分”思路,考虑一下如何在问题2构建的树状数组中求前缀和:

位置p的前缀和 =\sum_{i=1}^{p}a[i]=\sum_{i=1}^{p}\sum_{j=1}^{i}d[j]

在等式最右侧的式子\sum_{i=1}^{p}\sum_{j=1}^{i}d[j]中,d[1]被用了p次,d[2]被用了p-1次……那么我们可以写出:

位置p的前缀和 =\sum_{i=1}^{p}\sum_{j=1}^{i}d[j]=\sum_{i=1}^{p}d[i]*(p-i+1)=(p+1)*\sum_{i=1}^{p}d[i]-\sum_{i=1}^{p}d[i]*i

那么我们可以维护两个数组的前缀和:
一个数组是 sum1[i]=d[i]
另一个数组是 sum2[i]=d[i]*i

查询

位置p的前缀和即:(p+1)*sum1数组中p的前缀和 - sum2数组中p的前缀和。

区间[l, r]的和即:位置r的前缀和 - 位置l的前缀和。

修改

对于sum1数组的修改同问题2中对d数组的修改。

对于sum2数组的修改也类似,我们给 sum2[l] 加上 l * x,给 sum2[r + 1] 减去 (r + 1) * x。

 
void add(ll p, ll x)
{

    for(int i = p; i <= n; i += i & -i)

    sum1[i] += x, sum2[i] += x * p;

}

void range_add(ll l, ll r, ll x)
{

    add(l, x), add(r + 1, -x);

}

ll ask(ll p)
{

    ll res = 0;

    for(int i = p; i; i -= i & -i)

        res += (p + 1) * sum1[i] - sum2[i];

    return res;

}

ll range_ask(ll l, ll r)
{

    return ask(r) - ask(l - 1);

}

用这个做区间修改区间求和的题,无论是时间上还是空间上都比带lazy标记的线段树要优。

二维树状数组

我们已经学会了对于序列的常用操作,那么我们不由得想到(谁会想到啊喂)……能不能把类似的操作应用到矩阵上呢?这时候我们就要写二维树状数组了!

在一维树状数组中,tree[x](树状数组中的那个“数组”)记录的是右端点为x、长度为lowbit(x)的区间的区间和。
那么在二维树状数组中,可以类似地定义tree[x][y]记录的是右下角为(x, y),高为lowbit(x), 宽为 lowbit(y)的区间的区间和。

单点修改+区间查询

void add(int x, int y, int z)
{ 
            //将点(x, y)加上z

    int memo_y = y;

    while(x <= n)
    {

        y = memo_y;

        while(y <= n)

            tree[x][y] += z, y += y & -y;

            x += x & -x;

    }

}

void ask(int x, int y)
{            //求左上角为(1,1)右下角为(x,y) 的矩阵和

    int res = 0, memo_y = y;

    while(x)
    {

        y = memo_y;

        while(y)

            res += tree[x][y], y -= y & -y;

        x -= x & -x;

    }

}

区间修改 + 单点查询

我们对于一维数组进行差分,是为了使差分数组前缀和等于原数组对应位置的元素。

那么如何对二维数组进行差分呢?可以针对二维前缀和的求法来设计方案。

二维前缀和:

sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+a[i][j]

那么我们可以令差分数组d[i][j]表示a[i][j]与 a[i-1][j]+a[i][j-1]-a[i-1][j-1]的差。

例如下面这个矩阵

 
  1. 1 4 8

  2. 6 7 2

  3. 3 9 5

对应的差分数组就是

 
  1. 1 3 4

  2. 5 -2 -9

  3. -3 5 1

当我们想要将一个矩阵加上x时,怎么做呢?
下面是给最中间的3*3矩阵加上x时,差分数组的变化:

 
  1. 0 0 0 0 0

  2. 0 +x 0 0 -x

  3. 0 0 0 0 0

  4. 0 0 0 0 0

  5. 0 -x 0 0 +x

这样给修改差分,造成的效果就是:

 
  1. 0 0 0 0 0

  2. 0 x x x 0

  3. 0 x x x 0

  4. 0 x x x 0

  5. 0 0 0 0 0

那么我们开始写代码吧!

void add(int x, int y, int z)
{

    int memo_y = y;

    while(x <= n)
    {

        y = memo_y;

        while(y <= n)

            tree[x][y] += z, y += y & -y;

        x += x & -x;

    }

}

void range_add(int xa, int ya, int xb, int yb, int z)
{

    add(xa, ya, z);

    add(xa, yb + 1, -z);

    add(xb + 1, ya, -z);

    add(xb + 1, yb + 1, z);

}

void ask(int x, int y)
{

    int res = 0, memo_y = y;

    while(x)
    {

        y = memo_y;

        while(y)

            res += tree[x][y], y -= y & -y;

        x -= x & -x;

    }

}

区间修改 + 区间查询

类比之前一维数组的区间修改区间查询,下面这个式子表示的是点(x, y)的二维前缀和:

\sum_{i=1}^{x}\sum_{j=1}^{y}\sum_{k=1}^{i}\sum_{h=1}^{j}d[h][k]

(d[h][k]为点(h, k)对应的“二维差分”(同上题))

这个式子炒鸡复杂(O(n^4) 复杂度!),但利用树状数组,我们可以把它优化到O(\log_2 n)

首先,类比一维数组,统计一下每个d[h][k]出现过多少次。d[1][1]出现了x*y次,d[1][2]出现了x*(y-1)次……d[h][k]出现了(x-h+1)*(y-k+1) 次。

那么这个式子就可以写成:

\sum_{i=1}^{x}\sum_{j=1}^{y}d[i][j]*(x+1-i)*(y+1-j)

把这个式子展开,就得到:

(x+1)*(y+1)*\sum_{i=1}^{x}\sum_{j=1}^{y}d[i][j]-(y+1)*\sum_{i=1}^{x}\sum_{j=1}^{y}d[i][j]*i-(x+1)*\sum_{i=1}^{x}\sum_{j=1}^{y}d[i][j]*j+\sum_{i=1}^{x}\sum_{j=1}^{y}d[i][j]*i*j

那么我们要开四个树状数组,分别维护:

d[i][j],d[i][j]*i,d[i][j]*j,d[i][j]*i*j

这样就完成了!

#include <cstdio>

#include <cmath>

#include <cstring>

#include <algorithm>

#include <iostream>

using namespace std;

typedef long long ll;

ll read()
{

    char c;
    bool op = 0;

    while((c = getchar()) < '0' || c > '9')

        if(c == '-') op = 1;

    ll res = c - '0';

    while((c = getchar()) >= '0' && c <= '9')

        res = res * 10 + c - '0';

    return op ? -res : res;

}

const int N = 205;

ll n, m, Q;

ll t1[N][N], t2[N][N], t3[N][N], t4[N][N];

void add(ll x, ll y, ll z)
{

    for(int X = x; X <= n; X += X & -X)

        for(int Y = y; Y <= m; Y += Y & -Y)
        {

            t1[X][Y] += z;

            t2[X][Y] += z * x;

            t3[X][Y] += z * y;

            t4[X][Y] += z * x * y;

        }

}

void range_add(ll xa, ll ya, ll xb, ll yb, ll z)  //(xa, ya) 到 (xb, yb) 的矩形
{

    add(xa, ya, z);

    add(xa, yb + 1, -z);

    add(xb + 1, ya, -z);

    add(xb + 1, yb + 1, z);

}

ll ask(ll x, ll y)
{

    ll res = 0;

    for(int i = x; i; i -= i & -i)

        for(int j = y; j; j -= j & -j)

            res += (x + 1) * (y + 1) * t1[i][j]

                   - (y + 1) * t2[i][j]

                   - (x + 1) * t3[i][j]

                   + t4[i][j];

    return res;

}

ll range_ask(ll xa, ll ya, ll xb, ll yb)
{

    return ask(xb, yb) - ask(xb, ya - 1) - ask(xa - 1, yb) + ask(xa - 1, ya - 1);

}

int main()
{

    n = read(), m = read(), Q = read();

    for(int i = 1; i <= n; i++)
    {

        for(int j = 1; j <= m; j++)
        {

            ll z = read();

            range_add(i, j, i, j, z);

        }

    }

    while(Q--)
    {

        ll ya = read(), xa = read(), yb = read(), xb = read(), z = read(), a = read();

        if(range_ask(xa, ya, xb, yb) < z * (xb - xa + 1) * (yb - ya + 1))

            range_add(xa, ya, xb, yb, a);

    }

    for(int i = 1; i <= n; i++)
    {

        for(int j = 1; j <= m; j++)

            printf("%lld ", range_ask(i, j, i, j));

        putchar('\n');

    }

    return 0;

}

猜你喜欢

转载自blog.csdn.net/JKdd123456/article/details/81305465