引入:
树状数组或二叉索引树(英语:Binary Indexed Tree),又以其发明者命名为Fenwick树,最早由Peter M. Fenwick于1994年以A New Data Structure for Cumulative Frequency Tables为题发表在SOFTWARE PRACTICE AND EXPERIENCE。其初衷是解决数据压缩里的累积频率(Cumulative Frequency)的计算问题,现多用于高效计算数列的前缀和, 区间和。——百度
说人话:树状数组是一种支持 单点修改 和 区间查询 的,代码量小的数据结构。
什么是单点修改?答:给定x,y,让a[x]自增y
什么是区间查询?答:给定l,r,求a[l....r]的合
类似地,还有:「区间修改」、「单点查询」。
原理:
树状数组能快速求解信息的原因:我们总能将一段前缀[1,n] 拆成 不多于logn段区间,使得这logn段区间的信息是 已知的。
举个例子:我们要求a[1.....7]的前缀合怎么办?
最暴力的方法就是计算a[1]+a[2]+...+a[7],但是这要算7次,有没有更好的方法?——当然有(于是树状数组闪亮登场。。。可能还有线段树)
如果我告诉你三个数X,Y,Z,且X=a[1....4]的合,Y=a[5....6]的合,Z=a[7...7]的合(其实就是a[7])
此时你会怎么算?你一定会回答:X+Y+Z(因为这样只需算3次)
小结:我们只需合并这logn 段区间的信息,就可以得到答案。相比于原来直接合并n个信息,效率有了很大的提高。
下面这张图展示了树状数组的工作原理:
我们首先引入二叉树,叶子节点代表A[1]~A[8]。
现在变形一下:
现在加上上面讲的原理再理解树状数组的操作原理:
c数组表示就是用来储存原始数组a某段区间的和的,也就是说,这些区间的信息是已知的,我们的目标就是把查询前缀拆成这些小区间。
例如,从图中可以看出:
- c[1]表示a[1...1]的合
- c[2]表示a[1...2]的合
- c[3]表示a[3...3]的合
- c[4]表示a[1...4]的合
- c[5]表示a[5...5]的合
- c[6]表示a[5...6]的合
- c[7]表示a[7...7]的合
- c[8]表示a[1...8]的合
- c[x]表示a[M...x]的合(其中M,即左端点我们还不知道,但是x,即右端点我们知道就是x)
不难发现,c[x]管辖的一定是一段右边界是x的区间的信息。我们先不关心左边界,先来感受一下树状数组是如何查询的。
过程:从 c[7]开始往前跳,发现 c[7]只管辖 a[7] 这个元素;然后找c[6],c[6]发现c[6] 管辖的是 a[5...6],然后跳到 c[4],发现 c[4] 管辖的是 a[1...4] 这些元素,然后再试图跳到 c[0],但事实上 c[0]不存在,不跳了。
我们刚刚找到的c是c[7],c[6],c[4],其实c[7]+c[6]+c[4]就是a[1...7]的合啦~~~QAQ
举例:计算a[4...7]的合
其实求a[4...7],其实就是查询a[1...7]的合与a[1...3]的合,最终将两个结果作差。
管辖区间:
那么问题来了,c[x](x>=1)管辖的区间到底往左延伸多少?也就是说,区间长度是多少?
树状数组中,规定 c[x]管辖的区间长度为 ,其中:
- 设二进制最低位为第 0 位,则 k恰好为 x二进制表示中,最低位的1 所在的二进制位数;
- ,即c[x]的区间长度恰好为x在二进制表示中,最低位的
1
以及后面所有0
组成的数。
举个栗子:c[88]所管辖的区间
因为88的十进制为88,二进制为01011000,其二进制最低位的 1
以及后面的 0
组成的二进制是 1000
,所以c[88]所管辖的区间长度为二进制数1000转为十进制数的结果,即8。
因此c[88]管辖a[81...88]的合。
我们记 x 二进制最低位 1
以及后面的 0
组成的数为 lowbit(x)。
那么 c[x]管辖的区间就是 a[x-lowbit(x)+1,x];
这里注意:lowbit指的不是最低位 1
所在的位数 k,而是这个 1
和后面所有 0
组成的。
怎么计算 lowbit
?根据位运算知识,可以得到 lowbit(x) = x & -x
。
lowbit的原理:
将 x
的二进制所有位全部取反,再加 1,就可以得到 -x
的二进制编码。例如, 的二进制编码是 110
,全部取反后得到 001
,加 1
得到 010
。
设原先 x
的二进制编码是 (...)10...00
,全部取反后得到 [...]01...11
,加 1
后得到 [...]10...00
,也就是 -x
的二进制编码了。这里 x
二进制表示中第一个 1
是 x
最低位的 1
。
(...)
和 [...]
中省略号的每一位分别相反,所以 x & -x = (...)10...00 & [...]10...00 = 10...00
,得到的结果就是 lowbit
。
int lowbit(int x){
return x&-x;
}
区间查询:
实现时,我们不一定要先把 c都跳出来然后一起合并,可以边跳边合并。
int get_sum(int x){// a[1]..a[x]的和
int ans=0;
while(x>0){
ans+=c[x];
x-=lowbit(x);
}
return ans;
}
单点修改:
现在来考虑如何单点修改 a[x]。
当我们修改A数组中某个值时,应当如何更新C数组呢?回想一下,区间查询的过程,再看一下上文中列出的过程。这里声明一下:单点更新实际上是不修改A数组的,而是修改树状数组C,向上更新区间长度为lowbit(i)所代表的节点的值。
当在A[1]加上值k,即更新A[1]时,需要向上更新C[1],C[2],C[4],C[8],这个时候只需将这4个节点每个节点的值加上k即可。
下面以维护区间和,单点加为例给出实现。
void add(int x,int k){
while(x<=n){// 不能越界
c[x]+=k;
x+=lowbit(x);
}
}
总结:
树状数组的重点就是利用二进制的变化,动态地更新树状数组。
树状数组的每一个节点并不是代表原数组的值,而是包含了原数组多个节点的值。
所以在更新a[1]时需要将所有包含a[1]的c[i]都加上k这也就利用到了二进制的神奇之处。
如果是更新a[i]的值,则每一次对c[i] 中的 i 向上更新,即每次i+=lowbit(i),这样就能c[i] 以及c[i] 的所有父节点都加上k。
反之求区间和也是和更新节点值差不多,只不过每次 i-=lowbit(i)。
树状数组&&线段树
区别:
1.两者在复杂度上同级, 但是树状数组的常数明显优于线段树, 其编程复杂度也远小于线段树.
2.树状数组的作用被线段树完全涵盖, 凡是可以使用树状数组解决的问题, 使用线段树一定可以解决, 但是线段树能够解决的问题树状数组未必能够解决.
3.树状数组的突出特点是其编程的极端简洁性, 使用lowbit技术可以在很短的几步操作中完成树状数组的核心操作,其代码效率远高于线段树。
最后一点BB:
本篇文章有借鉴oi.wiki和https://blog.csdn.net/ls2868916989/article/details/119268741
十分感谢!
若想继续提高有关树状数组的知识,详见树状数组提高篇
东方欲晓,莫道君行早。踏遍青山人未老,风景这边独好。加油啊!!!,拜拜~~~QAQ