概述
- 树状数组(
Binary Indexed Tree,BIT,或称Fenwick Tree)支持区间查询、单点修改 一个区间的总信息由区间内所有元素共同贡献而得 然而一般的树状数组具有限制(与前缀和数组类似,因为朴素树状数组查询取件信息依赖于前缀和):- 元素间运算满足结合律
- 所有元素具有逆元,即满足消去律(可由运算结果及其中一个操作数得知另一个操作数)
- 满足上述要求的区间信息包括加和、乘积、异或等
- 一些特殊类型的树状数组可实现一些不满足上述要求的区间查询问题:
- 双树状数组求区间最值
- 扩展树状数组求不满足消去律的运算的区间信息
- 树状数组可求解的问题是线段树的子集,在后面可见其结构并非一棵完美的二叉树 但其价值在于能以更简单的代码、更小的空间/时间常数因子来完成相似任务
基本结构
为了实现最基本的树状数组的功能,需要考虑(接下来以求区间和为例):
- 数组的每个元素代表哪一个区间的区间信息(称为基本区间)
- 如何通过基本区间信息,来求任意范围的区间信息
- 修改单点值时,如何更新所有涉及该点的元素
设数组索引为(),为二进制最右边第一个的位置() 则管辖的范围为 例如,管辖、管辖、管辖、管辖 考虑的具体实现:实际就是
区间查询:获取任意范围的区间信息便很简单,由于前缀区间信息非常易于获取、且运算满足消去律 因此若设为区间的区间信息,则,其中
-为该运算的逆运算 获取前缀区间信息的方式:令迭代为直到退出循环、将途径的基本区间信息求和即可,是的单点修改:
- 先考虑其树形态,即不同元素区间之间的包含关系,可以发现: 若有,则、 由于,故此时管辖区间互不包含 且区间真包含于区间 其树形结构便是:对于一个结点,其直接子结点必须满足 对任意,所在结点的最近共同祖宗不是或
- 因此单点修改的过程类似获取前缀区间信息的逆过程:令迭代为直到退出循环、使途径的基本区间信息 因此也是的
建树:
- :变为次单点修改,其中
- :在的方法中,每次从叶子结点(原数组元素)上升到根结点,多花了; 由于该树中,父结点的编号一定比所有直接子结点的编号大 因此除了方法一,还可 因此在从小到大更新结点时,可顺便更新其直接父结点:
- :在输入时不进行建树,而是计算前缀和,输入完成后进行区间查询来建树 因此空间、时间复杂度的常数因子比方法二略大
原数组的区间修改与单点查询:差分运算也可使用树状数组,因为差分数组的前缀和即为原数组元素,对于这个问题,只需要多一步计算差分数组的步骤 即,如果要求是原数组的区间修改+单点查询,可转化为原数组的差分数组的:两点修改+求出区间和,这个要求和之前的分析一致 因为区间修改后,差分数组只有需要修改 此时使用方法二建树时,由于只使用一个数组,因此需记录来计算的值(令) 时间复杂度:仅在预处理时多了一遍,各种操作的复杂度均不变
原数组的区间修改与区间查询:沿用上述方法,用差分数组来实现的区间修改,对于区间查询,通过两个前缀和相减得到 可以对式子展开:如果原数组为,差分数组为,那么
即,维护两个树状数组,一个是其差分数组,另一个是带系数的差分数组(更新时乘上系数即可) 计算原数组的前缀和时,使第一个树状数组的前缀和乘再减去第二个树状数组的前缀和即可 时间复杂度:各种操作的复杂度的常数因子增大
朴素树状数组模板:
c++#define lbit(x) (x & (-x)) int a[100001], n, i, t; int query(int x) { // 查询前缀和 int ans = 0; while (x) { ans += a[x]; x -= lbit(x); } return ans; } int query(int x, int y) { // O(log n) 区间查询 return query(y) - query(x - 1); } void set_a(int idx, int val) { // O(log n) 单点修改 val = val - a[idx]; while (idx <= n) { a[idx] += val; idx += lbit(idx); } } int main() { cin >> n; // 使用方法二建树 for (i = 1; i <= n; ++i) { cin >> t; a[i] += t; if ((t = i + lbit(i)) <= n) { a[t] += a[i]; } } }
扩展树状数组
BIT应用
统计逆序对问题
- 即冒泡排序的最小交换次数[ NOIP 2013 提高组] 火柴排队