树状数组的奇妙运用

07 October 2019 | duanyll | Tags: OI 数据结构

树状数组是一种能够$O(\log n)$在线维护前缀和的数据结构, 其写法简单常数小… 不具体介绍了, 看看一些奇妙的操作吧.

人人都会的

  • 单点修改, 区间查询: 基本维护前缀和
  • 区间修改, 单点查询: 维护差分

区间修改, 区间查询

引入数组$d$, $d_i$表示区间$[i, n]$中需要加值的差分, 进行区间加法时, 就直接对$d$操作, 对$d_l$加上$x$, 对$d_{r+1}$减去$x$. 查询前缀和时, 设$sum_i$为区间$[1,i]$的元素和, 易得

\[\begin{aligned} sum_i &= \sum_{j=1}^i(a_j) + \sum_{j=1}^i\sum_{k=1}^j(d_k) \\ &= \sum_{j=1}^i(a_j) + \sum_{j=1}^i(d_j\times(i-j+1)) \\ &= \sum_{j=1}^i(a_j) + (i+1)\sum_{j=1}^i(d_j) + \sum_{j=1}^i(d_j*j) \end{aligned}\]

发现我们需要维护$a_i$, $d_i$, $d_i\times i$的前缀和. 代码可以这样写:

int suma[MAXN], sumd[MAXN], sumdi[MAXN];

void update(int* c, int pos, int x) {
    while (pos <= n) {
        c[pos] += x;
        pos += lowbit(pos);
    }
}

void sum(int* c, int pos) {
    int res = 0;
    while (pos >= 1) {
        res += c[pos];
        pos -= lowbit(pos);
    }
}

void add(int l, int r, int x) {
    update(sumd, l, x);
    update(sumd, r + 1, -x);
    update(sumdi, l, l * x);
    update(sumdi, r + 1, -(r + 1) * x);
}

int query(int l, int r) {
    return suma[r] + r * sum(sumd, r) + sum(sumdi, r)
           - (suma[l - 1] + (l - 1) * sum(sumd, l - 1) + sum(sumdi, l));
}

二维前缀和

首先, 由二维前缀和求矩阵$(x_1, y_1)\rightarrow(x_2,y_2)$的操作方法:

\[sum=sum[x_2][y_2]-sum[x_1-1][y_2]-sum[x_2][y_1-1]+sum[x_1-1][x_2-1]\]

用树状数组维护二维前缀和的方法:

void update(int x, int y, int val) {
    while (x <= n) {
        int t = y;
        while (t <= m) {
            c[x][t] += val;
            t += lowbit(t);
        }
        x += lowbit(x);
    }
}

int query(int x, int y) {
    int res = 0;
    while (x >= 1) {
        int t = y;
        while (t >= 1) {
            res += c[x][t]
            t -= lowbit(t);
        }
        x -= lowbit(x);
    }
    return res;
}

因此不难做到二维单点修改, 矩阵查询.

二维矩阵修改, 矩阵查询

首先说明一下二维差分怎么写. 由二维前缀和的形式, 定义

\[d[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1]\]

因此

\[a[i][j] = \sum_{n=1}^i\sum_{m=1}^j(d[n][m])\]

即可用二维树状数组维护差分.

比如给一个$5\times5$的矩阵中间$3\times3$的部分加上$x$, 可以这么修改差分数组:

 0  0  0  0  0
 0 +x  0  0 -x
 0  0  0  0  0
 0  0  0  0  0
 0 -x  0  0 +x

所以矩阵修改操作的写法:

void add(int x1, int y1, int x2, int y2, int x) {
    update(x1, y1, x);
    update(x1, y2 + 1, -x);
    update(x2 + 1, y1, -x);
    update(x2 + 1, y2 + 1, x);
} 

为了实现二维区间修改和查询的操作, 我们可以类比一维的情况, 用差分来表示前缀和.

\[\begin{aligned} sum[x][y] &= \sum_{i=1}^x\sum_{j=1}^y(a[i][j]) \\ &= \sum_{i=1}^x\sum_{j=1}^y\sum_{n=1}^i\sum_{m=1}^j(d[n][m]) \end{aligned}\]

根据前面的套路, 我们统计每一个$d[i][j]$的出现次数, 可以展开两层$\Sigma$: \(\begin{aligned} sum[x][y] =& \sum_{i=1}^x\sum_{j=1}^yd[i][j] * (x-i+1) * (y-j+1) \\ =& (x+1)*(y+1)\sum_{i=1}^x\sum_{j=1}^yd[i][j] \\ & -(y+1)\sum_{i=1}^x\sum_{j=1}^yd[i][j]*i-(x+1)\sum_{i=1}^x\sum_{j=1}^yd[i][j]*j \\ & +\sum_{i=1}^x\sum_{j=1}^yd[i][j]*i*j \end{aligned}\)

也就是说, 我们需要维护

  • $\sum_{i=1}^x\sum_{j=1}^yd[i][j]$
  • $\sum_{i=1}^x\sum_{j=1}^yd[i][j]*i$
  • $\sum_{i=1}^x\sum_{j=1}^yd[i][j]*j$
  • $\sum_{i=1}^x\sum_{j=1}^yd[i][j]ij$

开四个树状数组即可. 这样用树状数组写, 常数会比线段树小一些.