下面我们介绍一种很酷的操作,叫做 lowbit
,它可以高效地计算
2
k
2^k
2k,即我们要证明:
l o w b i t ( i ) = 2 k {\rm lowbit}(i) = 2^k lowbit(i)=2k
其中 k k k 是将 i i i 表示成二进制以后,从右向左数,遇到 1 1 1 则停止时,数出的 0 0 0 的个数。
通过 lowbit
高效计算
2
k
2^k
2k
lowbit(i) = i & (-i)
理解这行伪代码需要一些二进制和位运算的知识作为铺垫。首先,我们知道负数的二进制表示为:相应正数的二进制表示的反码 + 1
。
例 8
计算 − 6 -6 −6 的二进制表示。
分析: 6 6 6 的二进制表示为 0000 0110 0000\;0110 00000110,先表示成反码,即“ 0 0 0 变 1 1 1, 1 1 1 变 0 0 0”,得 1111 1001 1111\;1001 11111001,再加 1 1 1,得 1111 1010 1111\;1010 11111010。
例 9
当
i = 6
时,计算 l o w b i t ( i ) {\rm lowbit}(i) lowbit(i) 。
分析:
- 由例 7 及「与」运算的定义,把它们按照数位对齐上下写好:
0000 0110
1111 1010
0000 0010
- 上下同时为
1
1
1 才写
1
1
1,否则写
0
0
0,最后得到
0000 0010
,这个二进制数表示成十进制数就是 2 2 2。建议大家多在稿纸上写几个具体的例子来计算 l o w b i t {\rm lowbit} lowbit,进而理解为什么 l o w b i t ( i ) = 2 k {\rm lowbit}(i)=2^k lowbit(i)=2k; - 下面我给出一个我的直观解释:如果我们直接将一个整数「位取反」,再与原来的数做「与」运算,一定得到 0 0 0。巧就巧在,负数的二进制表示上,除了要求对「按位取反」以外,还要「加」 1 1 1,在「加」 1 1 1 的过程中产生的进位数即是「将 i i i 表示成二进制以后,从右向左数,遇到 1 1 1 停止时数出 0 0 0 的个数」。
那么我们知道了 l o w b i t {\rm lowbit} lowbit 以后,又有什么用呢?由于位运算是十分高效的,它能帮助我们在树状数组中高效计算「从子结点到父结点」(即对应「单点更新」操作),高效计算「前缀和由预处理数组的那些元素表示」(即对应「前缀和查询操作」)。
体会 lowbit
的作用
1、「单点更新」操作:从子结点到父结点
例 10
修改 A [ 3 ] A[3] A[3], 分析对数组 C C C 产生的变化。
分析:
- 从图中我们可以看出 A [ 3 ] A[3] A[3] 的父结点以及祖先结点依次是 C [ 3 ] C[3] C[3]、 C [ 4 ] C[4] C[4]、 C [ 8 ] C[8] C[8] ,所以修改了 A [ 3 ] A[3] A[3] 以后 C [ 3 ] C[3] C[3]、 C [ 4 ] C[4] C[4]、 C [ 8 ] C[8] C[8] 的值也要修改;
- 先看 C [ 3 ] C[3] C[3] , l o w b i t ( 3 ) = 1 {\rm lowbit}(3) = 1 lowbit(3)=1, 3 + l o w b i t ( 3 ) = 4 3 + {\rm lowbit}(3) = 4 3+lowbit(3)=4 就是 C [ 3 ] C[3] C[3] 的父亲结点 C [ 4 ] C[4] C[4] 的下标值;
- 再看 C [ 4 ] C[4] C[4] , l o w b i t ( 4 ) = 4 {\rm lowbit}(4) = 4 lowbit(4)=4, 4 + l o w b i t ( 4 ) = 8 4 + {\rm lowbit}(4) = 8 4+lowbit(4)=8 就是 C [ 4 ] C[4] C[4] 的父亲结点 C [ 8 ] C[8] C[8] 的下标值;
- 从图中,也可以验证:红色结点的下标值 + 右下角蓝色圆形结点的值 = 红色结点的双亲结点的下标值。
下面试图解释这个现象(个人理解):
- 3 3 3 即 0011 0011 0011,从右向左,遇到 0 0 0 放过,遇到 1 1 1 为止,给这个数位加 1 1 1,这个操作就相当于加上了一个 2 k 2^k 2k 的二进制数,即一个 l o w b i t {\rm lowbit} lowbit 值,有意思的事情就发生在此时,马上就发发生了进位,得到 0100 0100 0100,即 4 4 4 的二进制表示;
- 接下来处理 0100 0100 0100,从右向左,从右向左,遇到 0 0 0 放过,遇到 1 1 1 为止,给这个数位加 1 1 1,同样地,这个操作就相当于加上了一个 2 k 2^k 2k 的二进制数,即一个 l o w b i t {\rm lowbit} lowbit 值,可以看到,马上就发发生了进位,得到 1000 1000 1000,即 8 8 8 的二进制表示;
- 从上面的叙述中,你可以发现,我们又在做「从右边到左边数,遇到
1
1
1 之前数出
0
0
0 的个数」这件事情了,
由此我们可以总结出规律:从已知子结点的索引 i i i ,则结点 i i i 的父结点的索引 p a r e n t {\rm parent} parent 的计算公式为:
p a r e n t ( i ) = i + l o w b i t ( i ) {\rm parent}(i) = i + {\rm lowbit}(i) parent(i)=i+lowbit(i)
还需要说明的是,这不是巧合和循环论证,这正是因为对「从右边到左边数出 0 0 0 的个数,遇到 1 1 1 停止这件事情」的定义,使用 l o w b i t {\rm lowbit} lowbit 可以快速计算这件事成立,才会有的。
分析到这里「单点更新」的代码就可以马上写出来了。
Java 代码:
/**
* 单点更新
*
* @param i 原始数组索引 i
* @param delta 变化值 = 更新以后的值 - 原始值
*/
public void update(int i, int delta) {
// 从下到上更新,注意,预处理数组,比原始数组的 len 大 1,故 预处理索引的最大值为 len
while (i <= len) {
tree[i] += delta;
i += lowbit(i);
}
}
public static int lowbit(int x) {
return x & (-x);
}
2、「前缀和查询」操作:计算前缀和由预处理数组的那些元素表示
还是上面那张图。
例 11
求出「前缀和(6)」。
- 由图可以看出前缀和(6) = C [ 6 ] C[6] C[6] + C [ 4 ] C[4] C[4];
- 先看 C [ 6 ] C[6] C[6] , l o w b i t ( 6 ) = 2 {\rm lowbit}(6) = 2 lowbit(6)=2, 6 − l o w b i t ( 6 ) = 4 6 - {\rm lowbit}(6) = 4 6−lowbit(6)=4 正好是 C [ 6 ] C[6] C[6] 的上一个非叶子结点 C [ 4 ] C[4] C[4] 的下标值。这里给出我的一个直观解释,如果下标表示高度,那么上一个非叶子结点,其实就是从右边向左边画一条水平线,遇到的墙的下标。只要这个值大于 0 0 0,都能正确求出来。
例 12
求出「前缀和(5)」。
- 再看 C [ 5 ] C[5] C[5] , l o w b i t ( 5 ) = 1 {\rm lowbit}(5) = 1 lowbit(5)=1, 5 − l o w b i t ( 6 ) = 4 5 - {\rm lowbit}(6) = 4 5−lowbit(6)=4 正好是 C [ 5 ] C[5] C[5] 的上一个非叶子结点 C [ 4 ] C[4] C[4] 的下标值,故「前缀和(5)」 = C [ 5 ] C[5] C[5] + C [ 4 ] C[4] C[4]。
例 13
求出「前缀和(7)」。
- 再看 C [ 7 ] C[7] C[7] , l o w b i t ( 7 ) = 1 {\rm lowbit}(7) = 1 lowbit(7)=1, 7 − l o w b i t ( 7 ) = 6 7 -{\rm lowbit}(7) = 6 7−lowbit(7)=6 正好是 C [ 7 ] C[7] C[7] 的上一个非叶子结点 C [ 6 ] C[6] C[6] 的下标值,再由例 9 的分析,「前缀和(7)」 = C [ 7 ] C[7] C[7] + C [ 6 ] C[6] C[6] + C [ 4 ] C[4] C[4]。
例 14
求出「前缀和(8)」。
- 再看 C [ 8 ] C[8] C[8] , l o w b i t ( 8 ) = 8 {\rm lowbit}(8) = 8 lowbit(8)=8, 8 − l o w b i t ( 8 ) = 0 8 - {\rm lowbit}(8) = 0 8−lowbit(8)=0 , 0 0 0 表示没有,从图上也可以看出从右边向左边画一条水平线,不会遇到的墙,故「前缀和(8)」 = C [ 8 ] C[8] C[8] 。
经过以上的分析,求前缀和的代码也可以写出来了。
Java 代码:
/**
* 查询前缀和
*
* @param i 前缀的最大索引,即查询区间 [0, i] 的所有元素之和
*/
public int query(int i) {
// 从右到左查询
int sum = 0;
while (i > 0) {
sum += tree[i];
i -= lowbit(i);
}
return sum;
}
可以看出「单点更新」和「前缀和查询操作」的代码量其实是很少的。
3、树状数组的初始化
- 这里要说明的是,初始化前缀和数组应该交给调用者来决定;
- 下面是一种初始化的方式。树状数组的初始化可以通过「单点更新」来实现,因为「最最开始」的时候,数组的每个元素的值都为 0 0 0,每个都对应地加上原始数组的值,就完成了预处理数组 C C C 的创建;
- 这里要特别注意,
update
操作的第 2 2 2 个索引值是一个变化值,而不是变化以后的值。因为我们的操作是逐层上报,汇报变更值会让我们的操作更加简单,这一点请大家反复体会。
Java 代码:
public FenwickTree(int[] nums) {
this.len = nums.length + 1;
tree = new int[this.len + 1];
for (int i = 1; i <= len; i++) {
update(i, nums[i]);
}
}
基于以上所述,树状数组的完整代码已经可以写出来了。
Java 代码:
public class FenwickTree {
/**
* 预处理数组
*/
private int[] tree;
private int len;
public FenwickTree(int n) {
this.len = n;
tree = new int[n + 1];
}
/**
* 单点更新
*
* @param i 原始数组索引 i
* @param delta 变化值 = 更新以后的值 - 原始值
*/
public void update(int i, int delta) {
// 从下到上更新,注意,预处理数组,比原始数组的 len 大 1,故 预处理索引的最大值为 len
while (i <= len) {
tree[i] += delta;
i += lowbit(i);
}
}
/**
* 查询前缀和
*
* @param i 前缀的最大索引,即查询区间 [0, i] 的所有元素之和
*/
public int query(int i) {
// 从右到左查询
int sum = 0;
while (i > 0) {
sum += tree[i];
i -= lowbit(i);
}
return sum;
}
public static int lowbit(int x) {
return x & (-x);
}
}
Python 代码:
class FenwickTree:
def __init__(self, n):
self.size = n
self.tree = [0 for _ in range(n + 1)]
def __lowbit(self, index):
return index & (-index)
# 单点更新:从下到上,最多到 size,可以取等
def update(self, index, delta):
while index <= self.size:
self.tree[index] += delta
index += self.__lowbit(index)
# 区间查询:从上到下,最少到 1,可以取等
def query(self, index):
res = 0
while index > 0:
res += self.tree[index]
index -= self.__lowbit(index)
return res