树状数组及其各种扩展

树状数组及其各种扩展

什么是树状数组

一种简单的区间数据结构, 可以维护简单修改的数组.

树状数组长什么样

\(\displaystyle val_{x} = \sum a_{i}, i \in (x - \operatorname{lowbit}(x), x]\)

注意是左开右闭区间, \(\displaystyle \LARGE i \in (x - \operatorname{lowbit}(x), x]\)

单点修

现在要给 \(a_{3} := a_{3} + 1\).

首先, 观察关于 \(a_{3}\) 的结点有哪些.

可以看出, \(val_{3}, val_{4}, val_{8}\)\(a_{3}\) 有关.

那么有什么东西可以从 \(3\)\(4\) 再到 \(8\) 呢?

观察二进制.

\(0011 = 3 \newline 0100 = 4 \newline 1000 = 8\)

可以看到, 上面的数加上最右边的\(1\)就是下面的数.

那怎么获取到最右边的\(1\)呢?

\(\operatorname{lowbit}(x) = x \& -x\).

为什么这样就能获得最右边的\(1\)呢?

首先, 负数在C语言里用补码存储, 可以自己尝试, \(\forall x \in \Z\)\(-x = \sim x + 1\).

那么, 看 \(3\)\(-3\) 按位与.

\(0011 \newline 1101\)

可以发现, 这时候就是取到了 \(3\) 最右边的 \(1\).

证明.

\(x\) 取反后所有的 \(1\) 变成 \(0\), \(0\) 变成 \(1\).

\(0011 \newline 1100\)

加一后最后面的 \(1\) 全部进位, 一直到第一个原来的 \(1\).

\(1100 \newline 1101\)

然后按位与 \(x\).

\(0011 \newline 1101\)

就是 \(\operatorname{lowbit}(x)\).

\(3 + \operatorname{lowbit}(3) = 4\)

\(4 + \operatorname{lowbit}(4) = 8\)

所以最后的单点修代码就写完了.

void chg(int x, ll k) {
  f1 (i, x, N, i & -i) // for (int i(x); i <= N; i += i & -i)
    val[i] += k;
}

区间查

查询 \(a\)\(1\)\(7\) 的和.

再次观察树状数组, 哪些结点加起来正好是 \(1\)\(7\).

很显然 \(val_{4}\), \(val_{6}\), \(val_{7}\) 加起来是 \(a\)\(1\)\(7\).

可以想到, \(7 - \operatorname{lowbit}(7) = 6\), \(6 - \operatorname{lowbit}(6) = 4\), \(4 - \operatorname{lowbit}(4) = 0\).

那么区间查的代码就写出来了.

ll qry(int x) { ll s(0LL);
  f2 (i, x, 1, i & -i) // for (int i(x); i >= 1; i -= i & -i)
    s += val[i];
  return s;
}

什么? 查 \(x\)\(y\), 不一定是 \(1\)\(x\)?

查了 \(1\)\(y\) 再扣掉 \(1\)\(x - 1\) 不就好了? 前缀和不是基本功吗?

方法1, 查 \(1\)\(y\) 在扣掉 \(1\)\(x - 1\).

ll qry(int x, int y) {
  return qry(y) - qry(x - 1);
}

方法2, 直接在查询时扣掉.

ll qry(int x, int y) { ll s(0LL);
  f2 (i, y, 1, i & -i)
    s += val[i];
  f2 (i, x - 1, 1, i & -i)
    s -= val[i];
  return s;
}

二维树状数组

单点修改, 矩阵求和, Defad觉得只需要说一下二维前缀和.

\(val_{i, j}\)\(a_{1, 1}\)\(a_{i, j}\) 的和.

观察二维前缀和, 如何求第二行第二个矩阵的和.

扣掉上面和左边, 加上左上即可.

void chg(int x, int y, ll k) {
  f1 (i, x, N, i & -i)
    f2 (j, y, M, j & -j)
      val[i][j] += k;
}
ll qry(int x, int y) { ll s(0LL);
  f2 (i, x, 1, i & -i)
    f2 (j, y, 1, j & -j)
      s += val[i][j];
  return s;
}

ll qry(int x_1, int y_1, int x_2, int y_2) {
  return qry(x_2, y_2)
        - qry(x_2, y_1 - 1)
        - qry(x_1 - 1, y_2)
        + qry(x_1 - 1, y_1 - 1);
}

扩展树状数组, 区间修区间查

推式子网上有很多, Defad不太喜欢在板子题上推式子, 所以这里直接用了.

\(\displaystyle val0_{x} = \sum_{i = 1}^{x} a_{i}\)

\(\displaystyle val1_{x} = \sum_{i = 1}^{x} a_{i} * i\)

修改满足差分性质, 在 \(x\)\(y + 1\) 处修改即可.

这里有一个可以偷懒的地方, 差分修改如果 \(y = N\) 是不能在 \(y + 1\) 修改的 (容易RE), 但是for (int i(y + 1); i <= N; i += i & -i)\(y = N\) 时甚至进不了for循环, 也就不会有任何问题.

void chg(int x, ll k) {
  f1 (i, x, N, i & -i) {
    val[0][i] += k;
    val[1][i] += k * x;
  }
}

void chg(int x, int y, ll k) {
  chg(x, k); chg(y + 1, -k);
}

查询就是 \(val0_{i} * (x + 1) - val1_{i}\) 即可.

\(x\)\(y\)还是查\(1\)\(y\)在扣掉\(1\)\(x - 1\).

ll qry(int x) { ll s(0LL);
  f2 (i, x, 1, i & -i) {
    s += val[0][i] * (x + 1) - val[1][i];
  }
  return s;
}

ll qry(int x, int y) { ll s(0LL);
  f2 (i, y, 1, i & -i) {
    s += val[0][i] * (y + 1) - val[1][i];
  }
  f2 (i, x - 1, 1, i & -i) {
    s -= val[0][i] * x - val[1][i];
  }
  return s;
}

扩展树状数组的二维版本, 矩阵修矩阵查

那么我们看一下二维差分的矩阵修改.

可以尝试出左上角 \(+ k\), 右上角的右边 \(- k\), 左下角的下面 \(- k\), 右下角的右下 \(+ k\)就可以实现矩阵加.

void chg(int x, int y, ll k) {
  f1 (i, x, N, i & -i) {
    f1 (j, y, M, j & -j) {
      val[0][i][j] += k;
      val[1][i][j] += k * x;
      val[2][i][j] += k * y;
      val[3][i][j] += k * x * y;
    }
  }
}

void chg(int x1, int y1, int x2, int y2, ll k) {
  chg(x1, y1, k); chg(x2 + 1, y1, -k);
  chg(x1, y2 + 1, -k); chg(x2 + 1, y2 + 1, k);
}

矩阵和还是一样的二维前缀和, 就是给一下这里怎么求二维前缀和.

ll qry(int x, int y) { ll s(0LL);
  f2 (i, x, 1, i & -i) {
    f2 (j, y, 1, j & -j) {
      s += val[0][i][j] * (x + 1) * (y + 1);
      s -= val[1][i][j] * (y + 1);
      s -= val[2][i][j] * (x + 1);
      s += val[3][i][j];
    }
  }
  return s;
}

ll qry(int x1, int y1, int x2, int y2) {
  return qry(x2, y2) - qry(x2, y1 - 1) - qry(x1 - 1, y2) + qry(x1 - 1, y1 - 1);
}

树状数组和线段树

Defad个人认为树状数组和线段树关系不大.

对比树状数组和线段树.

虽然树状数组是长得像删掉所有右儿子的线段树, 但树状数组和线段树的思想是完全不同的.

树状数组每个结点管长度为 \(\operatorname{lowbit}(x)\) 的区间, 显然是倍增.

线段树是每个结点分给左儿子一半, 分给右儿子一半, 很明显是分治.

所以Defad认为树状数组和线段树是关系不大的并且本质不同, 可能发明树状数组受了线段树的启发但是思想是不同的.

然后看复杂度.

树状数组的时间复杂度 (本节讨论区间操作) 就是常数极小的 \(2\)\(\log N\).

线段树的时间复杂度是常数极大 (Defad还没学会ZKW线段树, 不过非递归线段树常数也挺大) 的 \(\log N\).

树状数组的空间复杂度是 \(2\)\(N\) .

线段树的空间复杂度是 \(4\)\(N\), 还有懒标记就是 \(8\)\(N\).

所以说树状数组是卡时空利器.

习题

树状数组2

VJudge LuoGu

差分之后单点加区间查.

或者直接区间修单点查.

上帝造题的七分钟

VJudge LuoGu

经典矩阵加求矩阵和.

posted @ 2024-11-03 22:42  指针神教教主Defad  阅读(12)  评论(0编辑  收藏  举报