🌲树状数组
🌿 为什么要有树状数组
考虑以下情形
现有一个数组
A[n]
,讨论以下两种操作的复杂度
- 计算前缀和
A.sum(1, m)
- 修改任意一个元素
A[i]
可以有的解法:
- 第一种:对于原始数组,求和操作的复杂度是 O ( n ) O(n) O(n),修改操作的复杂度是 O ( 1 ) O(1) O(1)。
- 第二种:可以构建一个辅助数组
sum[n]
,表示前缀和。这样,求和操作的复杂度就变成了 O ( 1 ) O(1) O(1),而修改操作为 O ( n ) O(n) O(n)。 - 第三种:有没有一种数据结构可以折中一下呢?树状数组:它的求和复杂度为 O ( l o g n ) O(logn) O(logn),修改复杂度也是 O ( l o g n ) O(logn) O(logn)。
🌿 树状数组的结构
在原数组A[n]
的基础上构建一个辅助数组,记作C[n]
。注意:下标从1开始。
🍃 lowBit
- 它表示:一个数用二进制表示时,最低位的1所表示的值。
- 另一重含义:它表示一个数的最大因数,这个因数必须是2的k次幂。
- 计算
lowBit
的方法:lowBit = i&(-i)
。原理:补码,取反加一。 - 比如:
- 24:二进制表示为
11000
,lowBit = 8
。 - 12:二进制表示为
1100
,lowBit = 4
。
- 24:二进制表示为
🍃 C[i]
的含义
C[i]
表示:数组A[n]
中,包括A[i]
在内的前方lowBit
个元素的总和。
C [ i ] = ∑ t = 0 l o w B i t A [ i − t ] C[i] = \sum_{t=0}^{lowBit}A[i-t] C[i]=t=0∑lowBitA[i−t]
C[i]
还可以使用数组C[n]
中的其他元素来表示:- 对于
i
来说: 2 k = l o w B i t 2^k = lowBit 2k=lowBit;i
的二进制表示中,末尾有k
个0;C[i]
表示A[n]
中 2 k 2^k 2k个元素的和; - 当
t
<
k
t<k
t<k时,对于
i
−
2
t
i-2^t
i−2t,它的末尾有
t
个0;那么 C [ i − 2 t ] C[i-2^t] C[i−2t]表示A[n]
中 2 t 2^t 2t个元素的和; -
2
k
2^k
2k可以写成
2
k
=
1
+
∑
t
=
0
k
−
1
2
t
2^k=1+\sum_{t=0}^{k-1}2^t
2k=1+∑t=0k−12t,所以
C[i]
可以用其他的C[n]
表示为:
- 对于
C [ i ] = A [ i ] + ∑ t = 0 k − 1 C [ i − 2 t ] , 其 中 2 k = l o w B i t C[i] = A[i] + \sum_{t=0}^{k - 1}C[i-2^t],其中2^k=lowBit C[i]=A[i]+t=0∑k−1C[i−2t],其中2k=lowBit
🍃 父子结点
C[i]
的父节点是C[i+lowBit]
。C[i]
的子节点共有k + 1
个,其中 2 k = l o w B i t 2^k = lowBit 2k=lowBit。
🌿 树状数组的操作
🍃 构造
通过C[i]
的子节点来构造C[i]
,复杂度是
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)。
void Init(vector<int> A, vector<int> C) {
for (int i = 1; i < A.size(); ++i) {
int lowBit = i & (-i);
C[i] = A[i];
for (int pf = 1; pf < lowBit; pf <<= 1) {
C[i] += C[i - pf];
}
}
}
🍃 前缀和
复杂度为 O ( l o g n ) O(logn) O(logn)。
// 计算前缀和,闭区间A[1, m]
int PrefixSum(vector<int> C, int m) {
int sum = 0;
while (m > 0) {
sum += C[m];
m -= (m & (-m));
}
return sum;
}
🍃 区间和
// 计算区间和,闭区间C[a, b]
int IntervalSum(vector<int> C, int a, int b) {
return PrefixSum(C, b) - PrefixSum(C, a - 1);
}
🍃 查询
直接查询A[n]
即可,所以复杂度为
O
(
1
)
O(1)
O(1)。
🍃 修改
找到所有的祖先结点即可,复杂度为 O ( l o g n ) O(logn) O(logn)。
void Update(vector<int>& A, vector<int>& C, int index, int value) {
int delta = value - A[index];
A[index] = value;
// 受影响的结点都增加delta即可
while (index < c.size()) {
c[index] += delta;
index += (i & (-i)); // 定位到父节点
}
}
🌳 拓宽视野
🌾 差分树状数组
优点是:修改区间,查询点
在A[i]
的基础上构建差分数组D[i] = A[i] - A[i - 1]
。然后在差分数组的基础上构建树状数组C[i]
,这样可以实现区间更新和单点查询了。