树状数组
树状数组(二元索引树 / 二元下标树 / Binary Indexed Tree, BIT / Fenwick Tree)
引入
树状数组是一种支持 单点修改 和 区间查询 的,代码量小的数据结构。
注意
事实上,树状数组能解决的问题是线段树能解决的问题的子集:树状数组能做的,线段树一定能做;线段树能做的,树状数组不一定可以。然而,树状数组的代码要远比线段树短,时间效率常数也更小,因此仍有学习价值。
有时,在差分数组和辅助数组的帮助下,树状数组还可解决更强的 区间加单点值 和 区间加区间和 问题。
定义
对于大小为 n n n 的序列 n u m s nums nums ,最基本的树状数组以 O ( l o g n ) O(logn) O(logn) 时间复杂度同时支持如下两种操作。
- 更新 n u m s nums nums 中单个元素的值,即单点修改。
- 求 n u m s nums nums 任意区间的元素值之和,即区间查询。
无论使用普通数组还是利用前缀和数组,对于上述两种操作,均有一种的时间复杂度为 O ( n ) O(n) O(n)。而树状数组通过维护一个与 n u m s nums nums 等大的,在逻辑上为树状结构 (一棵或多棵多叉树) t r e e [ ] tree[] tree[] ,使得两种操作的时间复杂度均为 O ( l o g n ) O(logn) O(logn)。
序列操作 | 数组 | 前缀和 | 树状数组 |
---|---|---|---|
单点修改 | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) | O ( l o g n ) O(logn) O(logn) |
区间查询 | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | O ( l o g n ) O(logn) O(logn) |
相关操作:
操作 | 定义 |
---|---|
单点修改 (Point Update, PU) | 修改 n u m s nums nums 的单个元素 |
单点查询 (Point Query, PQ) | 查询 n u m s nums nums 的单个元素 |
区间修改 (Range Update, RU) | 修改 n u m s nums nums 的某个区间(区间元素都加上同一个数) |
区间查询 (Range Query, RQ) | 求 n u m s nums nums 的某个区间的区间和 |
操作
PURQ BIT(单改区查):
最基本的树状数组支持「单点修改 (PU)」和「区间查询 (RQ)」,即 PURQ BIT。
区间
树状数组这一数据结构,对输入数组 n u m s nums nums 划分为多个子区间,使得对任意的 [ 0 , k ] [0, k] [0,k] 前缀区间,都可以由划分结果的若干个连续的子区间构成,这些子区间的区间和相加即可得到 前缀区间和 ,对于任意区间 [ l , r ] [l,r] [l,r] ,将右界 [ 0 , r ] [0,r] [0,r] 前缀区间的区间和减去左界前一位的前缀区间 [ 0 , l − 1 ] [0,l-1] [0,l−1] 的区间和,即为 [ l , r ] [l,r] [l,r] 区间和。
这就是树状数组能快速求解信息的原因:我们总能将一段前缀 [ 1 , n ] [1, n] [1,n] 拆成 不多于 l o g n logn logn 段区间,使得这 l o g n logn logn 段区间的信息是 已知的。
于是,我们只需合并这 l o g n logn logn 段区间的信息,就可以得到答案。相比于原来直接 n n n 合并个信息,效率有了很大的提高。
下图展示了树状数组的工作原理:
最下面的八个方块代表原始数据数组 a a a 。上面参差不齐的方块(与最上面的八个方块是同一个数组)代表数组的上级—— c c c 数组。
c c c 数组就是用来储存原始数组 a a a 某段区间的和的,也就是说,这些区间的信息是已知的,我们的目标就是把查询前缀拆成这些小区间。
不难发现 c [ x ] c[x] c[x] 管辖的一定是一段右边界是 x x x 的区间总信息。无论怎么划分,一定有且只有一个以 x x x 为右界的子区间。
举例:计算 a [ 1...7 ] a[1...7] a[1...7] 的和。
过程:从 c 7 c_7 c7 开始往前跳,发现 c 7 c_7 c7 只管辖 a 7 a_7 a7 这个元素;然后找 c 6 c_6 c6 ,发现 c 6 c_6 c6 管辖的是 a [ 1...7 ] a[1...7] a[1...7] ,然后跳到 c 4 c_4 c4 ,发现 c 4 c_4 c4 管辖的是 a [ 1...4 ] a[1...4] a[1...4]这些元素,然后再试图跳到 c 0 c_0 c0 ,但事实上 c 0 c_0 c0 不存在,不跳了。
我们刚刚找到 c c c 是 c 7 , c 6 , c 4 c_7,c_6,c_4 c7,c6,c4 ,事实上这就是 a [ 1...7 ] a[1...7] a[1...7] 拆分出的三个小区间,合并得到的答案是 c 7 + c 6 + c 4 c_7+c_6+c_4 c7+c6+c4 。
举例:计算 a [ 4...7 ] a[4...7] a[4...7] 的和。
区间划分
问题: c [ x ] ( x ≥ 1 ) c[x](x≥1) c[x](x≥1) 管辖的区间向左延伸多少?也就是说,区间长度是多少?
树状数组中,规定 c [ x ] c[x] c[x] 管辖的区间长度为 2 k 2^k 2k ,其中:
- 设二进制最低位为第 0 0 0 位, 则 k k k 恰好为 x x x 二进制表示中,最低位的 1 1 1 所在的二进制位数;
- 2 k 2^k 2k ( c [ x ] c[x] c[x] 的管辖区间长度)恰好为 x x x 二进制表示中,最低位的 1 1 1 以及后面所有 0 0 0 组成的数。
举例:
c 88 c_{88} c88 管辖的是哪个区间?
因为 8 8 ( 10 ) = 0101100 0 ( 2 ) 88_{(10)} = 01011000_{(2)} 88(10)=01011000(2),其二进制最低位的 1 以及后面的 0 组成的二进制是 1000 ,即 8 ,所以 c 88 c_{88} c88 管辖 8 个 a a a 数组的元素。因此 c 88 c_{88} c88 代表 a [ 81...88 ] a[81...88] a[81...88] 的区间信息。
我们记 x x x 二进制最低位 1 以及后面的 0 组成的数为 l o w b i t ( x ) lowbit(x) lowbit(x) ,那么 c [ x ] c[x] c[x] 管辖的区间就是 [ x − l o w b i t ( x ) + 1 , x ] [x-lowbit(x) + 1,x] [x−lowbit(x)+1,x] 。
根据位运算知识,可以得到 lowbit(x) = x & -x
。
int lowbit(int x) {
// x 的二进制中,最低位的 1 以及后面所有 0 组成的数。
// lowbit(0b01011000) == 0b00001000
// ~~~~^~~~
// lowbit(0b01110010) == 0b00000010
// ~~~~~~^~
return x & -x;
}
区间查询
只需将有关 l . . . r l...r l...r 的区间查询转化为 1... r 1...r 1...r 和 1... l − 1 1...l-1 1...l−1 的前缀查询再差分。
查询 a [ 1... x ] a[1...x] a[1...x] 的过程:
- 从 c [ x ] c[x] c[x] 开始往前跳,有 c [ x ] c[x] c[x] 管辖 a [ x − l o w b i t ( x ) + 1... x ] a[x-lowbit(x)+1...x] a[x−lowbit(x)+1...x] ;
- 令 x ← x − b o w b i t ( x ) x←x-bowbit(x) x←x−bowbit(x),如果 x = 0 x=0 x=0 说明已经跳到尽头了,终止循环;否则回到第一步。
- 将跳到的 c c c 合并。
实现时,我们不一定要先把 c c c 都跳出来然后一起合并,可以边跳边合并。
int getsum(int x) { // a[1]..a[x]的和
int ans = 0;
while (x > 0) {
ans = ans + c[x]; // 边跳边合并。
x = x - lowbit(x);
}
return ans;
}
单点修改
树状数组的一些基本性质:
- 性质1:对于 x ≤ y x≤y x≤y ,要么有 c [ x ] c[x] c[x] 和 c [ y ] c[y] c[y] 不交,要么有 c [ x ] c[x] c[x] 包含于 c [ y ] c[y] c[y] 。
- 性质2:在 c [ x ] c[x] c[x] 真包含于 c [ x + l o w b i t ( x ) ] c[x+lowbit(x)] c[x+lowbit(x)]
- 性质3:对于任意 x < y < x + l o w b i t ( x ) x<y<x+lowbit(x) x<y<x+lowbit(x) ,有 c [ x ] c[x] c[x] 和 c [ y ] c[y] c[y] 不交。
只需遍历并修改管辖了 a [ x ] a[x] a[x] 的所有 c [ y ] c[y] c[y] ,因为其他的 c c c 显然没有发生变化。
管辖 a [ x ] a[x] a[x] 的 c [ y ] c[y] c[y] 一定包含 c [ x ] c[x] c[x] (根据性质1),所以 y 在树状数组树形态上是 x 的祖先。因此我们从 x 开始不断跳父亲,直到跳得超过了原数组长度为止。
设 n n n 表示 a a a 的大小,单点修改 a [ x ] a[x] a[x] 的过程:
- 初始化令 x " = x x^" = x x"=x 。
- 修改 c [ x " ] c[x"] c[x"] 。
- 令 x " ← x " + l o w b i t ( x " ) x" ← x" + lowbit(x^") x"←x"+lowbit(x") ,如果 x " > n x" > n x">n 说明已经跳到尽头了,终止循环;否则回到第二步。
下面以维护区间和,单点加为例给出实现。
void add(int x, int k) {
while (x <= n) { // 不能越界
c[x] = c[x] + k;
x = x + lowbit(x);
}
}
**区间信息和单点修改的种类,共同决定 c [ x " ] c[x"] c[x"] 的修改方式。**下面给几个例子:
- 若 c [ x " ] c[x"] c[x"] 维护区间和,修改种类是将 a [ x ] a[x] a[x] 加上 p p p ,则修改方式则是将所有 c [ x " ] c[x"] c[x"] 也加上 p p p 。
- 若 c [ x " ] c[x"] c[x"] 维护区间积,修改种类是将 a [ x ] a[x] a[x] 乘上 p p p ,则修改方式则是将所有 c [ x " ] c[x"] c[x"] 也乘上 p p p 。
然而,单点修改的自由性使得修改的种类和维护的信息不一定是同种运算。
例如: c [ x " ] c[x"] c[x"] 维护区间和,修改种类是将 a [ x ] a[x] a[x] 赋值为 p p p ,可以考虑转化为将 a [ x " ] a[x"] a[x"] 加上 p − a [ x ] p-a[x] p−a[x] 。如果是将 a [ x ] a[x] a[x] 乘上 p p p ,就考虑转化 a [ x ] a[x] a[x] 加上 a [ x ] × p − a [ x ] a[x]×p-a[x] a[x]×p−a[x] 。
建树
Θ ( n l o g n ) \Theta(nlogn) Θ(nlogn)
可以直接转化为 n n n 次单点修改
比如给定序列 a = ( 5 , 1 , 4 ) a=(5,1,4) a=(5,1,4) 要求建树,直接看作对 a [ 0 ] a[0] a[0] 加 5 ,对 a [ 1 ] a[1] a[1] 加 1 ,对 a [ 4 ] a[4] a[4] 加 4。
Tricks Θ ( n ) \Theta(n) Θ(n)
方法一:
每一个节点的值是由所有与自己直接相连的儿子的值求和得到的。因此可以倒着考虑贡献,即每次确定完儿子的值后,用自己的值更新自己的直接父亲。
// Θ(n) 建树
void init() {
for (int i = 1; i <= n; ++i) {
t[i] += a[i];
int j = i + lowbit(i);
if (j <= n) t[j] += t[i];
}
}
方法二:
前面讲的 c [ i ] c[i] c[i] 表示的区间是 [ i − l o w b i t ( i ) + 1 , i ] [i-lowbit(i)+1,i] [i−lowbit(i)+1,i],那么我们可以先预处理一个 s u m sum sum 前缀和数组,在计算 c c c 数组。
// Θ(n) 建树
void init() {
for (int i = 1; i <= n; ++i) {
t[i] = sum[i] - sum[i - lowbit(i)];
}
}
模板示例:
// 单点修改区间查询
class PURQBIT {
int[] nums, tree; // nums 为输入数组,tree 为对应 nums 的区间和树状数组
int n; // nums大小
public PURQBIT(int[] nums){
this.nums = nums;
this.n = nums.length;
this.tree = new int[n];
for(int i = 0; i < n; i++){
add(i, nums[i]);
}
}
// 单点修改: 令 nums[k] = x
public void update (int k, int x){
add(k, x - nums[k]);
nums[k] = x; // 更新 nums[k] 为 x
}
// 单点修改: 令 nums[k] += x
public void add(int k, int x){
for(int i = k + 1; i <= n; i += lowbit(i)){
tree[i - 1] += x; // 包含第 k 项的区间都加上 x
}
}
// 区间查询 (区间求和): 求 nums[l] 到 nums[r] 之和
public int sum(int l, int r){
return preSum(r) - preSum(l - 1);
}
// 求前缀和: 求 nums[0] 到 nums[k] 的区间和 (前 k+1 项和)
private int preSum(int k){
int ans = 0;
for(int i = k + 1; i > 0; i -= lowbit(i)){
ans += tree[i - 1];
}
return ans;
}
private int lowbit(int i){
return i & -i;
}
}