树状数组的证明

树状数组的证明

20220313115345

树状数组线段树 是解决区间问题常用的两种数据结构。其中线段树理解起来相对简单,而树状数组的代码实现则更简洁。

参考例题: 区域和检索-数组可修改,就是一个标准的树状数组题。更抽象的应用场景可以理解为:

  1. 假设存在数组 aia_i
  2. 存在操作 (ai,ai+1,ai+2,,ai+k)\bigodot(a_i, a_{i+1}, a_{i+2}, \cdots, a_{i+k}),并且

(ai,ai+1,,ai+k)=((ai,ai+1,,ai+j),(ai+j+1,ai+j+2,,ai+k))(1)\bigodot(a_i, a_{i+1}, \cdots, a_{i+k}) =\bigodot( \bigodot(a_i, a_{i+1}, \cdots, a_{i+j}), \bigodot(a_{i+j+1}, a_{i+j+2}, \cdots, a_{i+k}) )\tag{1}

(aj,(ak))=(aj,aK)(2)\bigodot(a_j, \bigodot(a_k)) = \bigodot(a_j, a_K) \tag{2}

  1. 树状数组可以 O(logn)O(\log{}n) 的时间复杂度求出 (a1,a2,a3,,ai)\bigodot(a_1, a_{2}, a_{3}, \cdots, a_{i})的值。
  2. 这里的 \bigodot 可以是 \sum, 也可以是 \prod,或者是任意其他满足上述等式的操作。

楼主从大一就已学习树状数组,但一直断断续续的用,断断续续的忘,每次使用时都要翻出模板代码来。

最近尝试手推了一下树状数组,记录于下。

假设原数组是 aia_i树状数组利用 aia_i 构建了新数组 bib_i

bi=i2k+1iaib_i = \bigodot_{i-2^k+1}^{i}a_i

i  mod  2k=0(m>ki  mod  2m=0)i \;\mathrm{mod}\; 2^k = 0 \land (\nexists{ }m > k \land i \;\mathrm{mod}\; 2^{m} = 0)

=\bigodot = \sum 为例,

b1=a1b2=a2+a1b3=a3b4=a4+a3+a2+a1b5=a5b6=a6+a5b7=a7b8=a8+a7+a6+a5+a4+a3+a2+a1 b_1 = a_1 \\ b_2 = a_2 + a_1 \\ b_3 = a_3 \\ b_4 = a_4 + a_3 + a_2 + a_1 \\ b_5 = a_5 \\ b_6 = a_6 + a_5 \\ b_7 = a_7 \\ b_8 = a_8 + a_7 + a_6 + a_5 + a_4 + a_3 + a_2 + a_1 \\

如下图,黑色为原数组 aia_i, 红色为数组 bib_i 20220312180813

假设目标为计算 i=1i=nai\bigodot_{i=1}^{i=n}a_i 利用公式 (1)(1),可得:

i=1i=nai=(i=1i=jai,i=j+1i=nai),j=2log2n=(bj,i=j+1i=nai)=(bj,(i=j+1i=kai,i=k+1i=nai),k=j+2log2(nj)=(bj,(bk,i=k+1i=nai))==(bj,(bk,(bl,))),j=2log2n,k=j+2log2(nj),l=k+2log2(nk),=(bj,bk,bl,))),j=2log2n,k=j+2log2(nj),l=k+2log2(nk),\begin{aligned} \bigodot_{i=1}^{i=n}a_i &= \bigodot(\bigodot_{i=1}^{i=j}a_i, \bigodot_{i=j+1}^{i=n}a_i), j=2^{\lfloor log_2n \rfloor}\\ &=\bigodot(b_j, \bigodot_{i=j+1}^{i=n}a_i)\\ &=\bigodot(b_j, \bigodot(\bigodot_{i=j+1}^{i=k}a_i, \bigodot_{i=k+1}^{i=n}a_i), k=j+2^{\lfloor log_2(n-j) \rfloor}\\ &=\bigodot(b_j, \bigodot(b_k, \bigodot_{i=k+1}^{i=n}a_i))\\ &={ }\vdots\\ &=\bigodot(b_j, \bigodot(b_k, \bigodot(b_l, \cdots))), j=2^{\lfloor log_2n \rfloor}, k=j+2^{\lfloor log_2(n-j) \rfloor}, l=k+2^{\lfloor log_2(n-k) \rfloor}, \cdots\\ &=\bigodot(b_j, b_k, b_l, \cdots))), j=2^{\lfloor log_2n \rfloor}, k=j+2^{\lfloor log_2(n-j) \rfloor}, l=k+2^{\lfloor log_2(n-k) \rfloor}, \cdots \end{aligned}

=\bigodot = \sum 为例,

i=1i=1ai=b1i=1i=2a2=b2i=1i=3a3=b3+b2i=1i=4a4=b4i=1i=5a5=b5i=1i=6a6=b6+b4i=1i=7a7=b7+b6+b4i=1i=8a8=b8 \sum_{i=1}^{i=1}a_i = b_1 \\ \sum_{i=1}^{i=2}a_2 = b_2 \\ \sum_{i=1}^{i=3}a_3 = b_3 + b_2 \\ \sum_{i=1}^{i=4}a_4 = b_4 \\ \sum_{i=1}^{i=5}a_5 = b_5 \\ \sum_{i=1}^{i=6}a_6 = b_6 + b_4 \\ \sum_{i=1}^{i=7}a_7 = b_7 + b_6 + b4 \\ \sum_{i=1}^{i=8}a_8 = b_8

所以求 i=1i=nai\bigodot_{i=1}^{i=n}a_i,实际是求

(bj,bk,bl,))),j=2log2n,k=j+2log2(nj),l=k+2log2(nk),\bigodot(b_j, b_k, b_l, \cdots))), j=2^{\lfloor log_2n \rfloor}, k=j+2^{\lfloor log_2(n-j) \rfloor}, l=k+2^{\lfloor log_2(n-k) \rfloor}, \cdots

对于 i,j,k,i, j, k, \cdots 很容易通过数学公式计算出,但从位运算的角度理解更合适。 以 n=58n=58 为例,二进制转换后

58=0b0011101058 = 0_b00111010

i=0b00100000j=0b00110000k=0b00111000l=0b00111010i = 0_b00100000 \\ j = 0_b00110000 \\ k = 0_b00111000 \\ l = 0_b00111010 \\

容易得到,l,k,j,i,l, k, j, i, \cdots从大到小相当于二进制消去最后的一个1.

二进制运算中,可以用 n(n&n)n-(n\&{-n}) 逐次迭代得到 k,j,i,k, j, i, \cdots。(其证明方式我已经找到了一个绝妙的方法,篇幅限制就不在这里展开了。)

树状数组不仅可以 O(logn)O(\log{}n) 的时间复杂度求出 (a1,a2,a3,,ai)\bigodot(a_1, a_{2}, a_{3}, \cdots, a_{i})的值。当原数组aia_i 更新时,仍可以 O(logn)O(\log{}n) 的时间复杂度更新 bib_i

假设 aia_i 更新,则只需要更新 bj1,bj2,bj3,b_{j_1}, b_{j_2}, b_{j_3}, \cdots的值,其中

j2k+1ijj-2^k+1 \leq i \leq j

j  mod  2k=0(m>kj  mod  2m=0)j \;\mathrm{mod}\; 2^k = 0 \land (\nexists{ }m > k \land j \;\mathrm{mod}\; 2^{m} = 0)

同样二进制运算的方式,用 i+(i&(i)i + (i\&(-i) 逐次迭代即可得到 jj 序列。

下面是一个 Kotlin 实现:

class FenwickTree<T, U, R>(val size: Int, private val default: U, val accumulateOp: (R, U) -> R, val updateOp: (Int, T, U) -> U) { val tree: MutableList<U> = ArrayList<U>(size).also { tree -> (0 until size).forEach { _ -> tree.add(default) } } fun lowBit(index: Int) = index and (-index) inline fun query(index: Int, default: R): R { var varIndex = index var result = default while (varIndex > 0) { result = accumulateOp(result, tree[varIndex]) varIndex -= lowBit(varIndex) } return result!! } inline fun update(index: Int, value: T) { var varIndex = index while (varIndex < size) { tree[varIndex] = updateOp(index, value, tree[varIndex]) varIndex += lowBit(varIndex) } } }