树状数组
线段树问题集合包含数树状数组,这意味着树状数组能解决的问题线段树往往也能解决。
虽然用法上,线段树比树状数组丰富很多,但是树状数组也有它的优势:1. 代码短;2. 运行效率很高(相对于线段树,常数非常小,大部分情况下能差 10 倍左右,如果是简单问题的话,常数会差的稍微少一些。)
方法选择上,如果能用树状数组来做,就不要用线段树。
树状数组
树状数组的作用
可以动态、快速地(O(log n))求前缀和,我们一般情况下就是用树状数组来求前缀和的 ~
动态、快速地求前缀和:设原数组为 a,其对应的前缀和数组为 s,传统求前缀和的方法求出前缀和后不能改变原数组的某个元素 a[i],否则 s[i] 就不再是 a[1] ~ a[i] 的和。但是若使用树状数组来构造前缀和,则当 a[i] 更新时,其前缀和 s[i] 会自动地被更新,而且效率是 O(log n) 。
树状数组支持的两种操作效率都是 O(log n)
- 单点修改(修改原数组 a)
- 区间查询(动态查询)
利用上述操作再配合差分思想,可以推出其它操作:
- 区间修改
- 单点查询
- 区间查询
树状数组的原理
树状数组算法中,原数组下标一定要从 1 开始,这点与前缀和算法一样。
**树状数组也是一个 一维数组,长度与原数组一样长 。**后续,我们只是在逻辑上画了很多层,但是物理结构依旧是一维的。
设树状数组为 c,则树状数组所有奇数序号都存储原数组 a 对应序列的值。
这一层被称为 第 0 层,即 2 0 2^{0} 20 层。
第 k 层的元素定义:树状数组 c 序号中凡是能被 2 k 2^{k} 2k整除,但是不能被 2 k + 1 2^{k+1} 2k+1整除的序号值(本质是看序号的二进制表示的数,末尾有几个 0,末尾有 k 个 0 就是 第 k 层),都归为第 k 层。
在第 k 层中,假设数组数组 c 的任意序号值为 c[i],则 c[i] 的值必定包含 a[i],但是其它部分的值必须根据 c[i] 所在的层数累加 c,累加 c 的元素个数 为 k。如下图:
比如:
c[2] = a[2] + c[1]; // 第 1 层
c[4] = a[4] + c[3] + c[2]; // 第 2 层
c[6] = a[6] + c[5]; // 第 1 层
c[8] = a[8] + c[7] + c[6] + c[4]; // 第 3 层
c[10] = a[10] + c[9]; // 第 1 层
c[12] = a[12] + c[11] + c[10]; // 第 2 层
c[14] = a[14] + c[13]; // 第 1 层
c[16] = a[16] + c[15] + c[14] + c[12] + c[8]; // 第 4 层
根据这个定义,我们可以发现,数树状数组 c 中存的数,其实是一段数据的和。c[i] 表示的是原数组 a 中,区间 ( i − 2 k , i ] ( i-2^{k}, i ] (i−2k,i] 所有元素的和。
根据上文,我们知道 k 表示 序列号的二进制表示右边共有 k 个 0,那么我们可以使用 lowbit运算
来计算
2
k
2^{k}
2k 。
lowbit(i) = i & -i; // 求得 2^k 的值,其中 k 表示 数字 i 的二进制表示末尾有 k 个 0
int lowbit(int i) {
return i & -i;
}
因此,上述区间表示为 ( i − l o w b i t ( i ) , i ] ( i - lowbit(i), i ] (i−lowbit(i),i]。
根据上述得到的结果,我们再来看看树状数组的两个最基本的操作如何实现。
-
求 1 ~ i 的前缀和
因为 c[i] 表示原数组 a 区间范围 ( i − l o w b i t ( i ) , i ] (i - lowbit(i), i] (i−lowbit(i),i] 的和,所以需要使用递归函数,每次累加 c[xi],其中 xi-1 = xi - lowbit(xi)
/* 求[1, x] 区间的和 */ int sum(int x) { int result = 0; for (int i = x; i > 0; i -= lowbit(i)) { // 对于二进制数字n,最多logn 个0,所以时间复杂度 O(log n) result += c[i]; } return result; }
-
给某个位置加上一个数 v,并更新前缀和。
我们发现,当更新一个位置 i 时,实际上影响的 c 元素是( 设 th = i, 则 th+1= th + lowbit(th) )所有合法范围的 th。
void add(int x, int v) { a[x] += v; // 更新原数组 //* i 每次加lowbit(i)意味着末尾多了一个0,对于数字n,最多有log n 个0, //* 所以为 O(log n) for (int i = x; i <= n; i += lowbit(i)) c[i] += v; // 更新树状数组 }
初始化树状数组
void initializeC(int x) {
int limit = x - lowbit(x);
for (int i = x; i > limit; --i ) { // c[x] 表示 a 数组区间 (x-lowbit(x), x] 范围的所有元素和
c[x] += y[i];
}
}
或者直接使用 add 函数也行
void add(int x, int v) {
a[x] += v;
for (int i = x; i <= n; i += lowbit(i)) {
c[i] += v;
}
}
int main()
{
for (int i = 1; i <= n; ++i) {
scanf("%d", &a[i]);
add(i, a[i]);
}
}