树状数组 Binary Indexed Tree 学习笔记
看了不少树状数组的资料,讲解大多十分清楚,但是都没有明确的推导过程,所以自己尝试不那么严谨地推导一下
一个长度为n的数组C,需要能修改任意位置的数,并求前k个数的和(或者连续k个数的和)
- 如果直接累加,则修改时间复杂度O(1),求和时间复杂度O(n)
- 如果使用辅助数组存储前k个数的和,则修改时需要更新从当前位置到数组末尾的所有数,时间复杂度O(n),求和时间复杂度O(1)
考虑平衡修改复杂度和求和复杂度,除非存储前k个数的和,否则求和复杂度不可能降为O(1),所以考虑想办法降为O(log(n))
- 如果生成辅助数组D,将前k个数求和化作k的二进制形式每个1代表的数在辅助数组D中的值的和,则求和复杂度可以降为O(log(n))
考虑如何实现上述方案,即求辅助数组D
- 令数组C=(C0,C1,...,Cn-1),辅助数组D=(D0,D1,...,Dn),D0=0
- 在k>0的情况下,设i=f(k)为k的二进制表示的从右数第一个1的位置(从0开始计算),则D[k]其实是包含从C[k-1]到C[k-2i],共2i个数的和。如对于k=5,5的二进制形式是101,则i=0,D[5]包含C[4]到C[4],即D[5]=C[4];对于k=6,6的二进制形式是110,则i=1,D[6]包含C[5]到C[4],即D[6]=C[5]+C[4]。
- 由于k二进制表示从右数第i位为1,第i位右边的位置都是0,可知D[k]其实是对满足如下条件的所有C[j]求和:j二进制形式低i位从000...000、000...001、000...010一直遍历到111...111,第i位为0,高位同k相同。如对于k=88,二进制形式为10110002,D[k]需要包含C[10100002]、C[10100012]、C[10100102]、...、C[10101112]。
- 这样如果修改C[j],同时需要依次修改满足以下条件的k:
- 从右向左寻找j二进制形式中的0,设找到0的位置为i,将位置i从0置为1,并将这位的右边全部置0,可以得到k。k的i以上的高位与j相同,k的第i位为1,j的第i位为0,k的i以下的位置全部为0,根据上面定义可知D[k]一定包含C[j]
- 对于j二进制形式中为1的位置,设此位置为i'。对于所有的f(k)=i',根据上面定义,D[k]包含的所有C[j']的下标j'第i'位都是0,因此D[k]必然不包含C[j],所以无法找到k,满足f(k)=i'并且D[k]包含C[j]
- 将第1步求得的k赋值给j,重复迭代上述步骤,直至j超过最大限定值n
- 综合1、2、3可知修改时间复杂度也为O(log(n))
上面对树状数组的设计思路进行了简单的推理,为了推理方便,假设的数组C下标从0~n-1,而辅助数组D的下标从0~n。大部分文献从使用角度出发,并不是这样定义辅助数组的,由上面定义可知在k>0时D[k]一定包含C[k-1],所以可以设Ei+1=Ci,得到新数组E=(E1,E2,...,En)代替数组C,此时数组E与辅助数组D一一对应
在数组修改(更新)的过程中,由于i+1等同于将i的最低0位置为1,并将此位右边全部置0,又由于最低的0位右边的位对于D[k]的查找没有影响,因此将数组C替换为数组E,修改E[j]的过程改为:先修改D[j],然后从j二进制形式最低的非0位开始,从右向左寻找j二进制形式中的0,找到则将0置为1,并将这位的右边全部置0,得到k,修改D[k]
参考文献:
https://zh.wikipedia.org/wiki/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84