Fenwick Tree
树状数组(Binary Indexed Tree,又称 Fenwick Tree)是一种基于数组实现的数据结构,用于高效地动态维护前缀和。
树状数组可以在 O ( log n ) O(\log n) O(logn) 的时间复杂度内进行单点修改和前缀求和操作,适用于一些仅需要维护前缀和的问题,例如区间求和、区间平均数、区间中位数等。
一、问题描述
对于给定数组 a 1 , a 2 , . . . , a [ n ] a_{1},a_{2},...,a[n] a1,a2,...,a[n],当需要频繁询问某一区间 a i , a i + 1 , . . . , a [ j ] a_{i},a_{i+1},...,a[j] ai,ai+1,...,a[j] (其中 1 < = i < j < = n 1<=i<j<=n 1<=i<j<=n),且存在修改 a i a_{i} ai 元素值的情况时,如何设计一种高效的查询和修改算法?
一种直观的做法是暴力法,即每次使用 O ( 1 ) O(1) O(1) 的时间复杂度进行单点修改,但是查询的过程中由于需要遍历区间中的每个元素,最坏情况需要 O ( n 2 ) O(n^{2}) O(n2) 的时间(这里是假设操作的规模是 O ( n ) O(n) O(n) )。为了解决这种查询效率低下的情况,一种思路是使用树状数组。
二、解决思想
在树状数组 d d d 中,每个数组位置所存储的值并不仅仅是当前元素的值,而是一定区间内元素的累加值,具体存储规则如下图左图示所示。
左图示中,元素下标对应的矩阵表示该位置存储的值为矩阵覆盖区域对应的原数组元素之和
,比如下标为
1
1
1 处存储原数组下标为1的元素值,下标为4处存储原数组下标为
1
1
1 至
4
4
4 的元素值之和。
树状数组的存储图中反映的存储规律为:
编号为 x x x 的节点所管辖的区间为 2 k 2^{k} 2k(其中 k k k 为 x x x 二进制末尾 0 的个数)个元素。
比如
d
[
6
]
=
a
[
5
]
+
a
[
6
]
d[6] = a[5] + a[6]
d[6]=a[5]+a[6],因为位置 6 对应的二进制为 110
,末尾的 0 个数为 1,因此需要存储
2
1
=
2
2^{1} = 2
21=2 个元素,其他同理。
有了上述的存储规则后,如何进行区间查询和元素修改操作呢?为了便于对操作的描述,可将左侧图示进行旋转获得右侧图示(树的深度为
l
o
g
2
n
+
1
log_{2}{n+1}
log2n+1),并用直线连接表示前缀累加节点之间的关联关系。另外,为了便于后续对区间查询和元素修改操作的描述,这里引入 lowbit
操作:
public int lowbit(int x) {
return x & (-x);
}
即所谓的 lowbit
操作就是为了获取当前变量
x
x
x 二进制中最后一个 1 的位置。这个操作可以先记住,后续的操作介绍中能帮助进一步理解。
(1)区间查询
在右侧图示的树结构中,若查询某个节点的前缀和,则需要从这个点向左上找到上一个节点,并加上其节点的值,以此不断向左上查找节点值并累加。比如,查询 节点 7
的前缀和,需要从 节点7
出发, 找到 节点 6
进行累加操作,再找到 节点 4
进行累加操作(可以结合图中矩阵的覆盖面积进行理解)。那么这一遍历过程是如何实现的呢?这需要我们先将节点编号转成二进制的形式:
- 7 ---- 0111
- 6 ---- 0110
- 4 ---- 0100
可以发现一个普遍的规律:下一次遍历的位置
x
′
x'
x′ 是当前位置
x
x
x 在二进制位上抹去最后一个
1
1
1。因此,这时就可以使用到我们之前提到的 lowbit
操作了,即对于下一次遍历的位置
x
′
x'
x′ 有:
x
′
=
x
−
l
o
w
b
i
t
(
x
)
x' = x - lowbit(x)
x′=x−lowbit(x)。
(2)元素修改
元素修改可以看作是区间查询的逆过程,即在右侧图示的树结构中,若修改其中的某一节点,则需要一层一层向上找到父节点(向右上方,与区间查询正好相反),并按照需要对每个父节点进行相同的修改处理。比如,对 节点 3
进行加
k
k
k 操作,则需要依次遍历 节点 3
、节点 4
、节点 8
并在遍历的过程中进行加
k
k
k 操作。在遍历下一个节点的过程中,只需要使用与区间查询类似的逆向过程即可,即对当前
x
x
x 位置节点,下一遍历节点
x
′
=
x
+
l
o
w
b
i
t
(
x
)
x' = x+lowbit(x)
x′=x+lowbit(x)。
三、代码实现
class FenwickTree {
private int[] tree;
public FenwickTree(int n) {
tree = new int[n+1];
}
public int lowbit(int x) {
return x & (-x);
}
public void add(int i, int val) {
while(i < tree.length) {
tree[i] += val;
i += lowbit(i);
}
}
public int query(int i) {
int res = 0;
while(i > 0) {
res += tree[i];
i -= lowbit(i);
}
}
}