树状数组详解

转载自:https://lornd.top/index.php/archives/3/

为什么要使用树状数组

事实上,树状数组的所有操作均可以使用线段树完成。然而,打个比方,你可以用高精度写完只要你算 \(A + B\) 且结果比较小的题目,但是你不会去写高精度,因为那太麻烦了,而且时间和空间效率都不是很高。而树状数组相对于线段树的优势也就是在时间和空间的优势上,掌握了树状数组与没掌握,在做一些题目时就是 \(AC\)\(TLE\) 的区别了。

树状数组的原理

以单点修改求区间和为例,区间树状数组事实上可以看成是一种特殊的前缀和数组,它的过程是这样的(假设原数组为 \([1,2,3,4,5,6,7,8,9]\) ):

  1. 将数组中的偶数位分离出来 ( \([2,4,6,8]\) )
  2. 每个偶数位的数加上与它相邻的左边一位的数,得到一个新的序列 ( \([3,7,11,15][2,4,6,8]\)
  3. 对新的序列重新进行步骤一至步骤二,直到不能进行操作为止 ( \([3,7,11,15][2,4,6,8]\to [10,26][4,8]\to [34][8]\)
  4. 将原来的奇数位混插回去,得到树状数组 ( \([1,3,3,10,5,11,7,36,9]\)

注:\([a,b][c,d]\) 表示序列中有两个元素 \(a,b\) ,他们在原数组中对应的位置是 \(c,d\)

如果记原数组为 \(A\) ,树状数组为 \(C\) ,从上面的过程我们可以看出(为了方便表述,我们将下标表示为二进制形式):

\(C_{0001} = A_{0001}\ (C_1 = A_{1})\)

\(C_{0010} = A_{0001} + A_{0010}\ (C_{2} = A_1 + A_2)\)

\(C_{0011} = A_{0011}\ (C_{3} = A_3)\)

\(C_{0100} = A_{0001} + A_{0010} + A_{0011} + A_{0100}\ (C_{4} = \sum\limits_{i = 1}^4 A_i)\)

\(C_{0101} = A_{0101}\ (C_{5} = A_5)\)

\(C_{0110} = A_{0110} + A_{0101}\ (C_{6} = A_5 + A_6)\)

\(C_{0111} = A_{0111}\ (C_7 = A_7)\)

\(C_{1000} = C_{0001} + C_{0010} + C_{0011} + C_{0100} + C_{0101} + C_{0110} + C_{0111} + C_{1000}\ (C_8 = \sum\limits_{i = 1}^8 A_i)\)

\(C_{1001} = A_{1001}\ (C_9 = A_9)\)

我们可以发现,\(C_i = \sum\limits_{i = l}^r A_i\),其中 \(r = i\)\(l\)\(i\) 去掉最低位的 \(1\) 再在最低位补 \(1\) 后得到的数,即 \(l = i - (i\ \&\ (i\ \hat\ \ (i - 1))) + 1\) ,我们给了 \(i\ \&\ (i\ \hat\ \ (i - 1))\) 另外一个名称,即 \(lowbit(i)\) ,它还可以表示为 \(i\ \&\ (-i)\) ,至于为什么,就和计算机中负整数补码有关了,网络上有很多相关资料,在此不做赘述。

再放一张图以帮助理解,蓝色为 \(A\) ,橙色为 \(C\)

树状数组

所以我们现在就理清了树状数组和原数组的关系:\(C_i = \sum\limits_{j = i- lowbit(i) + 1}^i A_j\)

我们还可以发现 \(lowbit(i)\) 表示的就是 \(i\) 的二进制表示只保留最低位的 \(1\) 得到的数。

树状数组的单点修改

我们考虑将原数组的某个位置 \(place\) 的元素加上了 \(key\) 。那么它会影响哪几个 \(C_i\) 的值呢?

为了表述方便,我们计 \(p(i)\) 表示 \(i\) 的第一个 \(1\) 的位置,容易发现 \(lowbit(i) = 2 ^ {p(i)}\),再计 \(m\in[1, p(place)), n\in (p(place), + \infty)\)

根据我们的发现,如果它改变了 \(C_i\) 的值,那么一定有 \(i - lowbit(i) + 1 \le place\) ,而且 \(i \ge place\) 。由此我们可以得到以下结论:

  1. \(i\) 在第 \(m\) 位上不能有 \(1\) ,因为 \(i \ge place\) ,所以 \(i\) 在第 \(p(place)\)\(n\) 位上一定有 \(1\) ,当 \(i\) 在第 \(m\) 位上有 \(1\) 时,一定有 \(i - lowbit(i) + 1 > place\) ,所以结论 \(1\) 成立。

  2. \(i\)\(1\) ,则对于第 \(p(i)\) 位左边的某一位 \(j\) 一定有 \(i[j] \le place[j]\) 。否则就会出现 \(i - lowbit(i) + 1 > place\) 的情况。

在这两个条件的限制下,我们如何找到所有的 \(i\) 呢?事实上很简单,只需要不断把 \(place\) 的最低位的 \(1\) 不断左移即可,在这个过程中遍历到的所有的数都是我们要找的 \(i\)

怎么左移?不断让 \(place\) 加上 \(lowbit(place)\) 即可,所以,修改代码如下:

void modify(int place, int key) {
    for (; place <= n; place += lowbit(x)) {
        C[place] += key;
    }
}

树状数组的区间查询

我们先考虑简单的情况:求 \([1, i]\) 的区间和。

从后往前考虑,首先发现 \(C[i]\) 表示了 \(\sum\limits_{j = i - lowbit(i) + 1} ^{i} A_j\) ,我们把这个记录下来之后,只需要得到 \([1, i - lowbit(i)]\) 的区间和即可,这样就形成了一个类似于递归的过程,终点就是 \(lowbit(i) = i\) ,将这个过程中的所有的 \(C[i]\) 加起来,就是答案了。代码如下:

int query(int place) {
    int ans = 0;
    for (; place; place -= lowbit(place)) {
        ans += C[place];
    }
    return ans;
}

那现在如果是区间 \([l, r]\) 怎么办?根据前缀和思想,答案就是 \(query(r) - query(l - 1)\)

到此为止,树状数组的基本操作就介绍完毕了,如有疑问,欢迎在评论区提出。

posted @ 2019-07-15 13:32  lornd  阅读(278)  评论(0编辑  收藏  举报