更好的阅读体验
定义
首先参考维基百科的定义:
A Fenwick tree or binary indexed tree is a data structure that can efficiently update elements and calculate prefix sums in a table of numbers.
翻译过来就是:
Fenwick树,或者说二叉索引树是一种能高效对一个数组,更新并计算前缀和的数据结构。
所以根据定义来说,这个数据结构一定支持两种高效操作:
- 更新操作,例如
update(index, value)
,将数组中index
位置的值改成value
。 - 区间查询,例如
preSum(index)
,查询数组中下标从 1 1 1 到index
的前缀和。
朴素想法
我们单纯的考虑这个问题,有三种朴素做法
- 开数组维护上述结构,每次更新的时候直接更新值,每次查询的时候扫描整个数组。这样做的话,时间复杂度【更新 O ( 1 ) O(1) O(1),查询 O ( n ) O(n) O(n) 】,空间复杂度 O ( n ) O(n) O(n) 。
- 开数组维护前缀和。每次更新的时候更新每个后缀,查询的时候直接返回。这样做的话,时间复杂度【更新 O ( n ) O(n) O(n),查询 O ( 1 ) O(1) O(1) 】,空间复杂度 O ( n ) O(n) O(n) 。
- 普通的二叉树来维护。这样做的话,时间复杂度【更新 O ( l o g n ) O(log\ n) O(log n) ,查询 O ( l o g n ) O(log\ n) O(log n)】,空间复杂度 O ( n ) O(n) O(n) 。
在最终的过程中,我们可以根据读写量来灵活的选择,读少写多采用方案 1 1 1 ,读多写少采用方案 2 2 2 ,如果读写均衡采用方案 3 3 3 。
痛点
- 对于上述方案 1 1 1 和 2 2 2 ,两个操作性能并不均衡,在比赛中,并不知道哪个更多,哪个更少,无法预估。
- 上述方案 3 3 3 ,代码量大,而且常数不小。
- 是否存在一个数据结构,性能均衡,而且比较容易打出来呢?
应运而生
于是就有了树状数组这个结构。
让我们先忽略这个概念,先考虑一下线段树怎么实现的。
从上图我们可以看到,我们对于每个区间维护了一个属性,然后对于区间和这种操作,这个属性就是区间的值,然后我们统计区间和,只需要找到一个个最大的区间,然后将他们合并即可。
简单点说,线段树是通过将区间拆分然后重组来保证了性能。其中拆分后打lazy标记保证了每次更新复杂度是 O(n)
的,重组保证了,每次求区间属性的时候复杂度是 O(n)
。
基于上述想法,我们可以换一种枚举节点/区间的方式来实现这个过程。
其中有一半的节点和源节点保持一致,长度为 1 1 1,四分之一的节点和二叉树的叶节点的父节点一致,长度为 2 2 2,以此类推。。
可能这样看不直观,我们换一个大点的图:
如图,不妨设原数组为 a a a,需要生成的数组为 t t t,一共有 n n n 个数字,下标从 1 1 1 开始 。我们按照上述的方式生成一个数组。
如何生成这个数组
简单点说就是如何统计 t i t_i ti 。
我们可以看到对于第 i i i 个位置的 t i t_i ti 管理了多长的区间呢?
我们来看几个例子:
- t 1 t_1 t1 管理了长度为 1 1 1 的区间 [ 1 , 1 ] [1,1] [1,1] ,其中 1 1 1 的二进制表示为 00001 00001 00001 。
- t 4 t_4 t4 管理了长度为 4 4 4 的区间 [ 1 , 4 ] [1,4] [1,4] ,其中 4 4 4 的二进制表示为 00100 00100 00100 。
- t 14 t_{14} t14 管理了长度为 2 2 2 的区间 [ 13 , 14 ] [13,14] [13,14] ,其中 14 14 14 的二进制表示为 01110 01110 01110 。
- t 16 t_{16} t16 管理了长度为 16 16 16 的区间 [ 1 , 16 ] [1,16] [1,16] ,其中 16 16 16 的二进制表示为 10000 10000 10000 。
目前看不出来啥规律是吧,那我们定义一个函数 lowbit(x)
表示
x
x
x 在二进制表示下最低位对应的数字。
现在发现了吗?
是的,
t
x
t_x
tx 管理了 lowbit(x)
长度,实际为
[
x
−
l
o
w
b
i
t
(
x
)
+
1
,
x
]
[x-lowbit(x)+1, x]
[x−lowbit(x)+1,x] 这个区间。也即
t
x
=
∑
i
=
x
−
l
o
w
b
i
t
(
x
)
+
1
x
a
i
t_x = \sum_{i=x-lowbit(x)+1}^{x} a_i
tx=∑i=x−lowbit(x)+1xai
如何更新
更新一般有两种,update
和add
,二者本质上并无不同,我们仅讨论add
。
还是老样子,我们先看一下如果需要更新 a 4 a_4 a4 会影响那些值呢?
如图,可以看到需要更新 t 4 , t 8 , t 16 t_4, t_8, t_{16} t4,t8,t16 这三个值。
同理,如果更新 t 3 t_3 t3 需要更新 t 3 , t 4 , t 8 , t 16 t_3,t_4,t_8,t_{16} t3,t4,t8,t16 这四个值。
从中我们可以简单的发现,如果更新 t 1 t_1 t1 需要同时更新 t 2 t_2 t2 ,如果更新 t 0 t_0 t0 同时需要更新 t 1 t_1 t1 那么也必然需要同时更新 t 2 t_2 t2 。这个很好理解,我们把上述的图从下往上看,显然改了底层,上层需要更新,上层的上层显然也需要更新。
基于此,我们只需要找到 t i t_i ti 对应的上层,然后递归这个过程就可以完成完整的更新逻辑了。
那么如何找到 t i t_i ti 对应的上层呢?
先说结论 t i t_i ti 的上层就是 t i + l o w b i t ( i ) t_{i+lowbit(i)} ti+lowbit(i) 。
证明
虽然这个结论很巧妙,但是证明起来确实让我懵了几秒。如果对证明过程不感兴趣可以略过此部分
还记得刚刚提到的吗? t i t_i ti 是处理了区间 [ i − l o w b i t ( i ) + 1 , i ] [i-lowbit(i)+1, i] [i−lowbit(i)+1,i] 。
也就是我要找到一个最小的 j j j 满足 [ j − l o w b i t ( j ) + 1 , j ] [j-lowbit(j)+1, j] [j−lowbit(j)+1,j] 能覆盖上述区间,也即,找到最小的 j j j 满足 j − l o w b i t ( j ) ≤ i − l o w b i t ( i ) j-lowbit(j) \le i-lowbit(i) j−lowbit(j)≤i−lowbit(i) 并且 i ≤ j i \le j i≤j ,二者不同时取等号。
所以通过上述两个式子能得到 l o w b i t ( j ) > l o w b i t ( i ) lowbit(j) \gt lowbit(i) lowbit(j)>lowbit(i) 。
也即原条件转换成了,找到最小的 j j j 满足 $lowbit(j) \gt lowbit(i) $ 且 i ≤ j i \le j i≤j 。
所以我们不妨将 i i i 和 j j j 都在二进制表示下右移 l o w b i t ( i ) lowbit(i) lowbit(i) 位,原问题不变。
那么我们有新的 l o w b i t ( i ′ ) = 1 lowbit(i^{'}) = 1 lowbit(i′)=1,那么显然有 j ′ = i ′ + 1 j^{'} = i^{'}+1 j′=i′+1 是最小的 j j j ,并且,因为 l o w b i t ( i ′ ) = 1 lowbit(i^{'}) = 1 lowbit(i′)=1 ,所以 l o w b i t ( j ′ ) = l o w b i t ( i ′ + 1 ) lowbit(j^{'}) = lowbit(i^{'} + 1) lowbit(j′)=lowbit(i′+1) 显然至少为 2 2 2 ,原问题得证。
一个简单的示例
我们以上图中更新 a 3 a_3 a3 为例,假设更新操作为更新 a 3 = 5 a_3 = 5 a3=5 需要进行哪些操作。
(这里本来是个gif,但是csdn转存有点问题,所以导致成了图片,建议看原链接
从上图中我们可以看出:
- 我们先更新 t 3 t_3 t3 。
- 然后找 t 3 t_3 t3 的上级,也即 t 3 + l o w b i t ( 3 ) = t 4 t_{3+lowbit(3)} = t_{4} t3+lowbit(3)=t4 ,然后更新 t 4 t_4 t4 。
- 然后找 t 4 t_4 t4 的上级,也即 t 4 + l o w b i t ( 4 ) = t 8 t_{4+lowbit(4)} = t_8 t4+lowbit(4)=t8 ,然后更新 t 8 t_8 t8 。
- 依次类推
- 。。。
可以看出这是一个可以递归调用的过程。
我们简单看下代码:
void update(int index, int add) {
while(index<n) {
t[index]+=add;
index += lowbit(index);
}
}
和上述流程一致。
如何查询
以前缀和为例,我们看看如何求前缀和。
我们假设查询长度为 i i i 的前缀和,也即查询 [ 1 , i ] [1,i] [1,i] 的区间和。
首先,每个 t i t_i ti 管理的区间是 [ i − l o w b i t ( i ) + 1 , i ] [i-lowbit(i)+1, i] [i−lowbit(i)+1,i] ,也即,它本身管理的是某个前缀和的后缀和,一个朴素自然的想法就是我们每次选择一个 t i t_i ti 统计这部分后缀和,然后就变成了一个求 [ 1 , i − l o w b i t ( i ) ] [1,i-lowbit(i)] [1,i−lowbit(i)] 的区间和这个子问题,然后我们递归这个过程就可以求出前缀和。
还是老样子,我们来看个例子(以求 [ 1 , 11 ] [1,11] [1,11] 的区间和为例:
(这里本来是个gif,但是csdn转存有点问题,所以导致成了图片,建议看原链接
- 我们先统计 t 11 t_{11} t11 的贡献,然后发现 t 11 t_{11} t11 管理了区间 [ 11 , 11 ] [11,11] [11,11] ,所以问题转换为子问题:统计区间 [ 1 , 10 ] [1,10] [1,10] 的区间和。
- 然后统计 t 10 t_{10} t10 的贡献,它管理了区间 [ 9 , 10 ] [9,10] [9,10] ,所以问题转换为子问题:统计区间 [ 1 , 8 ] [1,8] [1,8] 的贡献。
- 以此类推。。
我们可以发现,这个也显然是一个可以递归的过程,来简单看下代码:
int getPrefix(int index) {
int sum = 0;
while(index) {
sum+=t[index];
index -= lowbit(index);
}
}
然后这里还剩余一个额外的证明:如何证明这个复杂度是 O ( l o g n ) O(log n) O(logn) 的。
一个直观的想法是:将
i
i
i 在二进制下表示,我每次减少了lowbit(i)
,也即减少了一个二进制位。对于一个自然数来说,最多有多少个二进制表示下的
1
1
1 呢?显然至多只有
l
o
g
2
(
n
)
log_2(n)
log2(n) 个。所以复杂度可以保证。
至此,树状数组所有的东西就讲解完毕了。