【总结】树状数组(7.26)

update:二维树状数组的实现

一.概述

树状数组,是一个区间查询单点修改复杂度都为log(n)的数据结构。主要用于查询任意两点之间的所有元素之和。

树状数组的题多半要转化,利用差分数组,可以将区间修改变为单点修改单点查询变为区间查询(即前缀和)。只要满足单点修改,我们总能用树状数组来维护前缀和。

b i t [ i ] bit[i] bit[i]数组可以看作一个树形结构,它满足以下三个性质:

  1. 每个内部节点 b i t [ x ] bit[x] bit[x]保存以它为根的子树中所有叶节点的和
  2. 每个内部节点 b i t [ x ] bit[x] bit[x]的子节点个数等于 l o w b i t ( x ) lowbit(x) lowbit(x)的位数
  3. 除树根外,每个内部节点bit[x]的父节点是bit[x+lowbit(x)]
  4. 树的深度为 O ( l o g N ) O(logN) OlogN
  5. b i t [ x ] bit[x] bit[x]表示区间 [ x − l o w b i t ( x ) + 1 , x ] [x-lowbit(x)+1,x] [xlowbit(x)+1,x]

个人认为它比较数学化,利用了数的性质(尤其是二进制)。

二.例题

  1. 模拟类
  2. 计数类(偏数学)

二维树状数组:

https://blog.csdn.net/qq_35885746/article/details/89247993

1. 单点修改,区间查询

思路: b i t [ i ] [ j ] bit[i][j] bit[i][j]表示 ( i − l o w b i t ( i ) + 1 , j − l o w b i t ( j ) + 1 ) (i-lowbit(i)+1,j-lowbit(j)+1) (ilowbit(i)+1,jlowbit(j)+1) ( i , j ) (i,j) (i,j)的子矩阵和,sum函数显然很好写:

ll Sum(int x, int y) {
    
    
    ll tot = 0;
    for (int i = x; i; i -= i & -i) {
    
    
        for (int j = y; j; j -= j & -j) {
    
    
            tot += bit[i][j];
        }
    }
    return tot;
}

关键的是update函数。仔细思考:i,j的父节点分别是i+lowbit(i)-1,j+lowbit(j)-1,我们要更新的x,y必然都是i,j的祖先节点,所以用二重循环遍历祖先节点即可。

void update(int x, int y, int k) {
    
    
    for (int i = x; i <= n; i += i & -i) {
    
    
        for (int j = y; j <= m; j += j & -j) {
    
    
            bit[i][j] += k;
        }
    }
}

查询操作是O(1):

long long get() {
    
    
    printf("%lld\n", Sum(c,d) - Sum(c,b-1) - Sum(a-1,d) + Sum(a-1,b-1));
}

总时间复杂度是 O ( l o g n ∗ l o g n ∗ t ) O(logn*logn*t) Olognlognt,t是操作总数

2.区间修改,单点查询

思路:这里维护的数组值是差分数组的前缀和。
我们可以令差分数组d[i][j]=a[i][j]- a[i-1][j]-a[i][j-1]+a[i-1][j-1]。
然后神奇的发现: a [ i ] [ j ] = ∑ x = 1 i ∑ y = 1 j d [ x ] [ y ] a[i][j]=\sum_{x=1}^{i}\sum_{y=1}^{j}d[x][y] a[i][j]=x=1iy=1jd[x][y]
至于证明,显然成立(虽然这个式子比较难想)

long long Sum(int x, int y) {
    
    
    long long tot = 0;
    for (int i = x; i; i -= i & -i) {
    
    
        for (int j = y; j; j -= j & -j) {
    
    
            tot += bit[i][j];
        }
    }
    return tot;
}
long long get() {
    
    
    printf("%lld\n", Sum(x,y));
}

然后考虑d[i][j]数组的变化,发现一次区间修改相当于把四个点的d值进行修改。update函数把sum累加,正好就是点的变化。

void update(int x, int y, int k) {
    
    
    for (int i = x; i <= n; i += i & -i) {
    
    
        for (int j = y; j <= m; j += j & -j) {
    
    
            bit[i][j] += k;
        }
    }
}

3.区间修改,区间查询

思路:仍然维护d数组,修改函数同上。
下面讨论如何计算区间的和:
s u m [ x ] [ y ] = ∑ i = 1 x ∑ j = 1 y a [ i ] [ j ] = ∑ i = 1 x ∑ j = 1 y d [ i ] [ j ] ∗ ( x − i + 1 ) ∗ ( y − j + 1 ) = ∑ i = 1 x ∑ j = 1 y d [ i ] [ j ] ∗ ( x y + x + y + 1 ) − d [ i ] [ j ] ∗ i ∗ ( y + 1 ) − d [ i ] [ j ] ∗ j ∗ ( x + 1 ) + d [ i ] [ j ] ∗ i ∗ j sum[x][y]=\sum_{i=1}^{x}\sum_{j=1}^{y}a[i][j]=\sum_{i=1}^{x}\sum_{j=1}^{y}d[i][j]*(x-i+1)*(y-j+1)=\sum_{i=1}^{x}\sum_{j=1}^{y}d[i][j]*(xy+x+y+1)-d[i][j]*i*(y+1)-d[i][j]*j*(x+1)+d[i][j]*i*j sum[x][y]=i=1xj=1ya[i][j]=i=1xj=1yd[i][j](xi+1)(yj+1)=i=1xj=1yd[i][j](xy+x+y+1)d[i][j]i(y+1)d[i][j]j(x+1)+d[i][j]ij
那么我们要开四个树状数组,分别维护:

d [ i ] [ j ] , d [ i ] [ j ] ∗ i , d [ i ] [ j ] ∗ j , d [ i ] [ j ] ∗ i ∗ j d[i][j],d[i][j]*i,d[i][j]*j,d[i][j]*i*j d[i][j],d[i][j]i,d[i][j]j,d[i][j]ij

这样就可以解决上述问题了

void update(int x, int y, long long k) {
    
    
    for (int i = x; i <= n; i += i & -i) {
    
    
        for (int j = y; j <= m; j += j & -j) {
    
    
            bit1[i][j] += k, bit2[i][j] += 1LL * k * x, bit3[i][j] += 1LL * k * y,
                bit4[i][j] += 1LL * x * y * k;
        }
    }
}
long long Sum(int x, int y) {
    
    
    long long tot = 0;
    for (int i = x; i; i -= i & -i) {
    
    
        for (int j = y; j; j -= j & -j) {
    
    
            tot += bit4[i][j] - (x + 1) * bit3[i][j] - (y + 1) * bit2[i][j] + (x + 1) * (y + 1) * bit1[i][j];
        }
    }
    return tot;
}
long long get() {
    
    
    printf("%lld\n", Sum(c, d) - Sum(c, b - 1) - Sum(a - 1, d) + Sum(a - 1, b - 1));
}

猜你喜欢

转载自blog.csdn.net/cqbzlydd/article/details/107599806