1 lowbit运算
l o w b i t lowbit lowbit运算: l o w b i t ( x ) = x & ( − x ) lowbit(x) = x\&(-x) lowbit(x)=x&(−x),它的作用是取 x x x的二进制最右边的1和它右边所有0。
整数在计算机中采用补码存储, x x x变为 − x -x −x的过程就是按位取反后末位加1。二者与操作后就得到了 x x x最右边的1和它右边所有0。 l o w b i t ( x ) lowbit(x) lowbit(x)也可以理解为能整除 x x x的最大2的幂次。
2 问题
给定一个包含 N N N个元素的数组 A A A,求它的区间 [ i , j ] [i, j] [i,j]元素之和。最自然的想法是建一个前缀和的数组,然后做减法就可以求出任意区间和,时间复杂度 O ( 1 ) O(1) O(1),空间复杂度 O ( n ) O(n) O(n)。
但是,如果对数组 A A A进行更新,那我们就不得不再更新前缀和,就很麻烦。
树状数组可以很好地解决这个问题,它可以使得查询和更新的时间复杂度都变成 O ( l o g n ) O(logn) O(logn)。
3 树状数组(Binary Indexed Tree, BIT)
树状数组与前缀和数组类似,是一个用来记录和的数组,不过它存的是在
i
i
i号位之前(包含
i
i
i号位)的
l
o
w
b
i
t
(
i
)
lowbit(i)
lowbit(i)个整数之和。另外,树状数组的下标必须从1开始。如下图
C
C
C数组,
C
[
i
]
C[i]
C[i]的覆盖范围是
l
o
w
b
i
t
(
i
)
lowbit(i)
lowbit(i)。
再结合下图就很容易理解了
3.1 单点更新与区间查询
为了解决刚才提出的问题,需要设计以下两个函数:
- g e t S u m ( n ) getSum(n) getSum(n):返回前 n n n个数的和 A [ 1 ] + ⋯ + A [ n ] A[1] + \cdots +A[n] A[1]+⋯+A[n]
- u p d a t e ( n , v ) update(n, v) update(n,v):实现将第 n n n个数加上一个数 v v v的功能,即 A [ n ] + = v A[n] += v A[n]+=v
先解决第一个函数,看几个例子:
A
[
1
]
+
⋯
+
A
[
14
]
=
C
[
8
]
+
C
[
12
]
+
C
[
14
]
A
[
1
]
+
⋯
+
A
[
11
]
=
C
[
8
]
+
C
[
10
]
+
C
[
11
]
A[1] + \cdots + A[14] = C[8] + C[12] + C[14]\\ A[1] + \cdots + A[11] = C[8] + C[10] + C[11]\\
A[1]+⋯+A[14]=C[8]+C[12]+C[14]A[1]+⋯+A[11]=C[8]+C[10]+C[11]
类似地,数组
A
A
A的前
n
n
n项和都可以由树状数组中的一些项的求和来表达,我们可以通过
l
o
w
b
i
t
lowbit
lowbit操作来找出这些项:
g
e
t
S
u
m
(
n
)
=
A
[
1
]
+
⋯
+
A
[
n
]
=
A
[
1
]
+
⋯
+
A
[
n
−
l
o
w
b
i
t
(
n
)
]
+
A
[
n
−
l
o
w
b
i
t
(
n
)
+
1
]
+
⋯
+
A
[
n
]
=
g
e
t
S
u
m
(
n
−
l
o
w
b
i
t
(
n
)
)
+
C
(
n
)
\begin{aligned} getSum(n) &= A[1] + \cdots + A[n] \\ &= A[1] + \cdots + A[n - lowbit(n)] + A[n - lowbit(n) + 1] + \cdots + A[n]\\ &= getSum(n - lowbit(n)) + C(n) \end{aligned}
getSum(n)=A[1]+⋯+A[n]=A[1]+⋯+A[n−lowbit(n)]+A[n−lowbit(n)+1]+⋯+A[n]=getSum(n−lowbit(n))+C(n)
这样就可以写出
g
e
t
S
u
m
(
n
)
getSum(n)
getSum(n)了:
//getSum返回前n个整数之和
int getSum(int n){
int sum = 0;
for (int i = n; i > 0; i -= lowbit(x)){
sum += C[i];
}
return sum;
}
这样我们就可以通过
g
e
t
S
u
m
(
j
)
−
g
e
t
S
u
m
(
i
−
1
)
getSum(j) - getSum(i-1)
getSum(j)−getSum(i−1)求区间
[
i
,
j
]
[i, j]
[i,j]的和。这个过程就像不断向左上爬,如下图
然后看第二个函数,更新
A
A
A中的一个值后如何对树状数组进行更新。还是先看例子,如果更新
A
[
6
]
A[6]
A[6],我们要去找
C
C
C中哪些项包含了
A
[
6
]
A[6]
A[6],我们看上图,可以看到包含
A
[
6
]
A[6]
A[6]的有
C
[
6
]
,
C
[
8
]
,
C
[
16
]
C[6],C[8],C[16]
C[6],C[8],C[16],放在上图中,就是寻找包含
A
[
6
]
A[6]
A[6]的那些矩形。
实际上这就是刚刚求和的逆操作,每次加上一个
l
o
w
b
i
t
lowbit
lowbit就可以得到下一个位置:
C [ 6 ] : 110 C [ 8 ] : 1000 = 6 + l o w b i t ( 6 ) = 110 + 10 C [ 16 ] : 10000 = 8 + l o w b i t ( 8 ) = 1000 + 1000 \begin{aligned} & C[6]: 110\\ & C[8]: 1000 = 6 + lowbit(6) = 110 + 10\\ & C[16]: 10000 = 8 + lowbit(8) = 1000 + 1000 \end{aligned} C[6]:110C[8]:1000=6+lowbit(6)=110+10C[16]:10000=8+lowbit(8)=1000+1000
这样就可以写出 u p d a t e ( n , v ) update(n, v) update(n,v)了:
void update(int n, int v){
for(int i = n; i <= n; i += lowbit(i)){
C[i] += v;
}
}
这个过程就像不断向右上爬,如下图
练习题
4 参考资料
- 《算法笔记》第13章