概念
Fenwick树是用于快速计算前缀和的数据结构。前缀和,就是一个数列的前N项和,是不是有点像级数的概念。
当然最笨的办法是按顺序相加,实时计算。优化一下就是缓存计算结果,这样能快速返回,用空间换时间。但是采取缓存方案时,修改的运算量比较大。比如修改数组的第0项,那么影响了前0项、前1项以及所有项的前缀和。
有没有均衡的方案?我们可以用二进制分解。比如15=8+4+2+1,可以这样计算前十五项的前缀和,定义一个缓存数组bit:
bit[8] = a[0] + a[1] +a[2] +a[3] +a[4]+ a[5]+ a[6]+ a[7]; //前8项的和
bit[12] = a[8] + a[9] + a[10] + a[11]; // 中间4项的和
bit[14] = a[12] + a[13]; //中间2项的和
bit[15] = a[14]; //最后一项的和
return bit[8] + bit[12] + bit[14] + bit[15]
那么前15项的前缀和,就是前八项前缀和加上第9项到12项的和。所以再回来看这个缓存数组bit。bit是bit indexed tree的缩写。缓存数组bit的第1、2、4、8项,也就是
2
n
2^n
2n项,必须是完整的前缀和。那剩下的就是两种:奇数项和偶数项。奇数项最好处理,因为奇数的分解最后一定剩下一个1,所以对于所有奇数项,
b
i
t
[
n
]
=
a
[
n
−
1
]
bit[n]=a[n-1]
bit[n]=a[n−1]。
最麻烦的是不是
2
n
2^n
2n的偶数项,比如6、10、12、14这几项。看上式中,为什么bit[12]等于四项的和,而bit[14]却只等于两项的和?
因为12=8+4,8存储在bit[8]中,所以bit[12]等于4项的和。而14=8+4+2,这个式子里的8和4由bit[8]和bit[12]存储了。所以bit[14]只需要存储两项就可以了。这实际上就是二进制最后一个1的大小。对于奇数项其实也是符合这个规则的,其实对于索引为
2
n
2^n
2n的项数也是适用的。
所以这个总结:假设bit数组的索引n的二进制形式,从右到左,第一个1代表的数值x,那么有:
b
i
t
[
n
]
=
∑
i
=
n
−
x
n
−
1
a
[
i
]
bit[n] = \sum_{i=n-x}^{n-1} a[i]
bit[n]=i=n−x∑n−1a[i]
例如,把n=12代进去,12的二进制形式为1100,最低的1代表了4。所以x=4,所以:
b
i
t
[
12
]
=
∑
i
=
12
−
4
12
−
1
a
[
i
]
=
∑
i
=
8
11
a
[
i
]
=
a
[
8
]
+
a
[
9
]
+
a
[
10
]
+
a
[
11
]
bit[12] = \sum_{i=12-4}^{12-1} a[i]= \sum_{i=8}^{11} a[i]=a[8]+a[9]+a[10]+a[11]
bit[12]=i=12−4∑12−1a[i]=i=8∑11a[i]=a[8]+a[9]+a[10]+a[11]
用一个图来表示这个公式:
在上面这个公式的基础上进行数据再合并。那么这种数据结构就叫做Fenwick树。其图如下,图中B代表bit数组,a代表原始数组:
修改算法
在上图可以看到,每个数据项的变化都会影响到本身及以后的节点。以a[1]为例子,会影响到b2、b4、b8,也就是影响到了bit数组的第2、4、8项。而a[4]的修改会影响到b5、b6和b8,也就是bit数组的第5、6、8项。将上图的箭头反向,可以形象地看到效果:
到底怎么计算呢?我们看1到2到4到8的过程,变成二进制很容易发现,是1变成10,再变成100,再变成1000。而其他的数字,比如
5
→
6
→
8
5\rarr6\rarr8
5→6→8,换成二进制是
101
→
110
→
1000
101\rarr110\rarr1000
101→110→1000。逆推是很难的,比顺推要难多了,但是我们耐心理完。因为前文有这个公式:
b
i
t
[
n
]
=
∑
i
=
n
−
x
n
−
1
a
[
i
]
bit[n] = \sum_{i=n-x}^{n-1} a[i]
bit[n]=i=n−x∑n−1a[i]
这个公式定义了我需要谁,所以我们再反过来想一想谁需要我。以bit[6]为例子6的二进制是110,最低位是10,所以包含了两个数,这两个数肯定是自己和小一点的数。其实6的二进制是110,保持100不变,加上自身的10和比10小的01,那么就是6和5。所以逆向思维过来,一个索引数被别人包含,就是这样计算,以5为例子,二进制位101,首先被自己包含。再找下一个尾部为0的数字,那就是110,再找下一个最接近自己的尾部至少两个0的数字,那就是1000(注意不能是1100,因为1100比1000大),然后再找下一个尾部至少4个0的数字,就是10000。其实更准确地说是最接近自己的尾部的0比自己多的数字。所以碰到了
2
n
2^n
2n之后,就是
2
n
+
1
2^{n+1}
2n+1了。逻辑明白了,那么计算的难题就来了。我们知道要得到下一个尾部0比自己多的数字,就需要找到自己最低位的1,然后在这个位置上加上一个1,就是下一个尾部的0比自己多的且比自己大的数了。以5=101,为例子,最低位的1,再加上1就是110,也就是6。其实就是抽出最低位的1,然后加上这个数就可以了。把这个增量定义为
δ
\delta
δ,那么就用位运算来找这个数吧。
首先,一个数的取反,把最低位的1变成了最高位的0,比如00111000变成了11000111,那么再加上一个1,这个最高位的0,恰好是同位置的1,也就是最低位的1,还是刚才的例子。
∼
00111000
=
11000111
∼
00111000
+
1
=
11001000
\sim 00111000 = 11000111\\ \sim 00111000 +1 = 11001000
∼00111000=11000111∼00111000+1=11001000
这个时候把两个数字00111000和11001000进行对比,因为前面的位刚好互反,原数字最低位的1,两个数字都是1,更低的位,原数字全部是0,这个数字也全部是0。所以做一个与运算,就可以把前面全部变成0,后面也全部变成0,原数字最低位的1还是1。算一下就是:
00111000
&
11001000
=
1000
00111000 ~ \& ~11001000 = 1000
00111000 & 11001000=1000
所以我们的增量
δ
\delta
δ:
δ
=
a
&
(
∼
a
+
1
)
\delta = a \& (\sim a +1)
δ=a&(∼a+1)
再恶补下整数编码的基础,计算机界定义所有位为1的有符号整数是-1,一个数和它的反码相加,得到的是全部为1的数,也就是-1。那么就有了下面的公式:
a
+
(
∼
a
)
=
−
1
∴
∼
a
=
−
1
−
a
∴
∼
a
+
1
=
−
a
∴
−
a
=
∼
a
+
1
a+(\sim a)=-1 \\ \therefore \sim a = -1 -a\\ \therefore \sim a +1 = -a\\ \therefore -a =\sim a +1\\
a+(∼a)=−1∴∼a=−1−a∴∼a+1=−a∴−a=∼a+1
所以我们的增量就是
a
&
−
a
a\& -a
a&−a。
求和算法
查询也就是求和了。求和需要用到bit数组。以前15项和为例子:
∵
15
=
8
+
4
+
2
+
1
=
2
3
+
2
2
+
2
1
+
2
0
∴
a
[
0
]
+
a
[
1
]
+
a
[
2
]
+
a
[
3
]
+
a
[
4
]
+
a
[
5
]
+
a
[
6
]
+
a
[
7
]
=
b
i
t
[
8
]
a
[
8
]
+
a
[
9
]
+
a
[
10
]
+
a
[
11
]
=
b
i
t
[
12
]
a
[
12
]
+
a
[
13
]
=
b
i
t
[
14
]
a
[
14
]
=
b
i
t
[
15
]
∴
s
u
m
(
15
)
=
b
i
t
[
8
]
+
b
i
t
[
12
]
+
b
i
t
[
14
]
+
b
i
t
[
15
]
\because 15= 8+4+2+1 = 2^3+2^2+2^1+2^0\\ \therefore a[0] + a[1] +a[2] +a[3] +a[4]+ a[5]+ a[6]+ a[7] = bit[8] \\ a[8]+a[9]+a[10]+a[11] = bit[12]\\ a[12]+a[13] = bit[14]\\ a[14]=bit[15]\\ \therefore sum(15)= bit[8] +bit[12]+ bit[14]+bit[15]
∵15=8+4+2+1=23+22+21+20∴a[0]+a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]=bit[8]a[8]+a[9]+a[10]+a[11]=bit[12]a[12]+a[13]=bit[14]a[14]=bit[15]∴sum(15)=bit[8]+bit[12]+bit[14]+bit[15]
这个算法呢?以15为例子,15的二进制形式为1111,二进制分解为1000,加上1000后面的1100,再加上1110,最后是1111。恰好对于了8,8后面的第4个,8后面的第6个,8后面的第7个。我们反过来,这样写:
s
u
m
(
15
)
=
b
i
t
[
15
]
+
b
i
t
[
14
]
+
b
i
t
[
12
]
+
b
i
t
[
8
]
sum(15)=bit[15] +bit[14]+bit[12]+bit[8]
sum(15)=bit[15]+bit[14]+bit[12]+bit[8]
其实就是把15的二进制形式1111不停地减去上面算出的
δ
\delta
δ。
代码实现
原理明白后代码就很短了。
public class FenWickTree {
private int[] values;
private int[] bit;
public FenWickTree(int length) {
values = new int[length];
bit = new int[length + 1];
}
public void setValues(int index, int value) {
values[index] = value;
index += 1;
while (index < bit.length) {
bit[index] += value;
index += index & -index;
}
}
public int getSum(int index) {
int sum = 0;
while (index > 0) {
sum += bit[index];
index -= index & -index;
}
return sum;
}
}
测试数据
public static void main(String[] args) {
final FenWickTree tree = new FenWickTree(10);
for (int i = 0; i < 10; i++) {
tree.updateValue(i, i + 1);
}
System.out.println(tree.getSum(10));
// 改成计算你n个1的和,注意因为是求和数组,所以传入的是增量
for (int i = 0; i < 10; i++) {
tree.updateValue(i, -i);
}
System.out.println(tree.getSum(10));
}
测试结果是55和10。