树状数组
树状数组目录
一. 问题的提出
有一个一维数组,长度为
n
n
n 。
你可以对这个数组进行两种操作:
- 修改: 对 1 1 1 ~ n n n 之间的一个元素 n u m [ k ] num[k] num[k] 增加 x x x。
- 求和: 求 i i i 到 j j j 的和。
常见的做法就是: 循环累加 循环累加 循环累加 和 前缀和 前缀和 前缀和
1. 循环累加
- 修改:用数组储存修改结果 O ( 1 ) O(1) O(1)
- 求和:用循环累加 i i i ~ j j j 的每个数 O ( j − i + 1 ) O(j - i + 1) O(j−i+1)
代码如下:
// 修改
num[k] += x;
// 求和
for (int k = i; k <= j; k++) cnt += num[i];
printf("%d\n", cnt);
2. 前缀和
- 修改:用数组储存修改结果,再求出前缀和 O ( n − k + 1 ) O(n - k + 1) O(n−k+1)
- 求和:直接运用前缀和算出结果 O ( 1 ) O(1) O(1)
代码如下:
// 修改
num[k] += v;
for (int i = k; i <= n; i++) pre[i] = pre[i - 1] + num[i];
// 求和
printf("%d\n", pre[j] - pre[i - 1]);
但是上面两种方法的时间复杂度都很大,在 数据较大 和 时间需求较快 的时候不便于我们的求解。
二. 概念
树状数组 —— Binary Indexed Tree(B.I.T) 也称作 Fenwick Tree : 它是一个
查询
查询
查询 和
修改
修改
修改 的时间复杂度都为
l
o
g
2
n
log_2n
log2n 的数据结构。它主要用于查询两点之间的所有元素之和。
A
A
A 表示 给定数组
C
C
C 表示 树状数组
C
1
=
A
1
C_1 = A_1
C1=A1
C
2
=
C
1
+
A
1
=
A
1
+
A
2
C_2 = C_1 + A_1 = A_1 + A_2
C2=C1+A1=A1+A2
C
3
=
A
3
C_3 = A_3
C3=A3
C
4
=
C
2
+
C
3
+
A
4
=
A
1
+
A
2
+
A
3
+
A
4
C_4 = C_2 + C_3 + A_4 = A_1 + A_2 + A_3 + A_4
C4=C2+C3+A4=A1+A2+A3+A4
C
5
=
A
5
C_5 = A_5
C5=A5
C
6
=
C
5
+
A
6
=
A
5
+
A
6
C_6 = C_5 + A_6 = A_5 + A_6
C6=C5+A6=A5+A6
C
7
=
A
7
C_7 = A_7
C7=A7
C
8
=
C
4
+
C
6
+
C
7
+
A
8
=
A
1
+
A
2
+
A
3
+
A
4
+
A
5
+
A
6
+
A
7
+
A
8
C_8 = C_4 + C_6 + C_7 + A_8 = A_1 + A_2 + A_3 + A_4 + A_5 + A_6 + A_7 + A_8
C8=C4+C6+C7+A8=A1+A2+A3+A4+A5+A6+A7+A8
这就是 树状数组 (它长得像树一样 ,所以称为 树状数组 )。
- " 如何求已知数组下标的树状数组 B i t [ i ] Bit[i] Bit[i] 的 子叶个数 ( l o w b i t ) 子叶个数(lowbit) 子叶个数(lowbit) (子叶个数: 树状数组 所含 给定数组 的个数,例如: C 1 C_1 C1 的子叶个数为 1 1 1 , C 3 C_3 C3 的子叶个数为 1 1 1 , C 4 C_4 C4 的子叶个数为 4 4 4 ······) " 。
- " 如何对树状数组进行 修改 修改 修改 " 。
- " 如何用树状数组求
前缀和
前缀和
前缀和 " 。
······
1. l o w b i t lowbit lowbit
(1). 什么是 l o w b i t lowbit lowbit ?
l o w b i t ( i ) lowbit(i) lowbit(i) 是将 i i i 转化成二进制数之后,只保留最低位的 1 1 1 及其后面的 0 0 0 ,舍去前面的所有数字,然后再转成十进制数,这个数也是树状数组中 i i i 号位的子叶个数。
(2). 怎么求 l o w b i t lowbit lowbit ?
l o w b i t ( 22 ) lowbit(22) lowbit(22) ,它的意思是将 22 22 22 转化成二进制数之后,得到 10110 10110 10110 ,保留最后一个 1 1 1 及其它后面的 0 0 0,并舍去前面的所有数字,得到 10 10 10,转化为十进制数为2,即 l o w b i t ( 22 ) = 2 lowbit(22)=2 lowbit(22)=2,所以 C [ 22 ] C[22] C[22] 的子叶个数为 2 2 2 。
我们可以在草稿纸上可以计算出一个数的
l
o
w
b
i
t
lowbit
lowbit ,但是在
C
+
+
C++
C++ 中怎么实现呢?
下面,兔兔会给大家教几种求
l
o
w
b
i
t
lowbit
lowbit 的方法:
-
我们先来熟悉一下位运算中的按位与 & :
0 0 0 & 0 = 0 0 = 0 0=0
0 0 0 & 1 = 0 1 = 0 1=0
1 1 1 & 0 = 0 0 = 0 0=0
1 1 1 & 1 = 1 1 = 1 1=1 -
求 l o w b i t lowbit lowbit 的 方法一:
原数为 x x x (十进制),先将原数转化成二进制,之后将它的最后一个 1 1 1 替换成 0 0 0 得到 x ′ x' x′ ,然后再用 x x x 减去 x ′ x' x′ (十进制相减),答案就是 l o w b i t ( x ) lowbit(x) lowbit(x) 的结果。参考代码如下:
int lowbit(int x)
{
return x - (x & (x - 1));
}
有些读者可能看不太懂。
我给大家解释一下:
x
x
x 的二进制可以看做
A
1
B
A1B
A1B (
A
A
A 是最后一个
1
1
1 之前的部分,
B
B
B 是最后一个
1
1
1 之后的
0
0
0 )
x
−
1
x - 1
x−1 的二进制可以看做
A
0
C
A0C
A0C (
C
C
C 是和
B
B
B 一样长的
1
1
1 )
x
x
x &
(
x
−
1
)
(x - 1)
(x−1) 的二进制就是
A
1
B
A1B
A1B &
A
0
C
A0C
A0C =
A
0
B
A0B
A0B
x
−
(
x
x - (x
x−(x &
(
x
−
1
)
)
(x - 1))
(x−1)) 的二进制就是
A
1
B
–
A
0
B
=
0
…
010
…
0
A1B – A0B = 0…010…0
A1B–A0B=0…010…0
就得到我们所求的
l
o
w
b
i
t
(
x
)
lowbit(x)
lowbit(x)了。
- 求
l
o
w
b
i
t
lowbit
lowbit 的 方法二:
原数为 x x x (十进制),先将原数转化成二进制之后,在和原数的相反数 − x -x −x 的二进制进行按位与,答案就是 l o w b i t ( x ) lowbit(x) lowbit(x) 的结果。参考代码如下:
int lowbit(int x)
{
return x & (-x);
}
例如:
l
o
w
b
i
t
(
22
)
lowbit(22)
lowbit(22)
22
22
22 的二进制原码
011010
011010
011010 ,正数的补码等于它的原码
011010
011010
011010
−
22
-22
−22 的二进制原码
111010
111010
111010 ,负数的补码等于它的原码取反加
1
1
1 ,为
100101
+
1
=
100110
100101 + 1 = 100110
100101+1=100110 (二进制)。
011010
011010
011010 &
100110
=
000010
100110 = 000010
100110=000010 正数转换成原码后依然是
000010
000010
000010
所以
l
o
w
b
i
t
(
22
)
=
2
lowbit(22)=2
lowbit(22)=2
证明:设这个数
x
x
x 的二进制为
0
A
1
B
0A1B
0A1B (
0
0
0 是
x
x
x 的符号位,
1
1
1 是
(
x
)
2
(x)_{2}
(x)2 的最后一个
1
1
1,
A
A
A 是最后一个
1
1
1 前面的数,
B
B
B 是最后一个
1
1
1 后面的数 (
000
⋅
⋅
⋅
000
000···000
000⋅⋅⋅000))
−
x
=
1
(
-x = 1(
−x=1(~
A
)
0
C
A)0C
A)0C (
1
1
1 是符号位,
C
C
C 是和
B
B
B一样长度的
1
1
1)
+
1
+ 1
+1
因为
C
C
C 全是
1
1
1,所以
C
+
1
C+1
C+1 要进位到
0
0
0,即
−
x
=
1
(
-x = 1(
−x=1(~
A
)
1
B
A)1B
A)1B。
所以
x
x
x &
(
−
x
)
=
0
A
1
B
(-x) = 0A1B
(−x)=0A1B &
1
(
1(
1(~
A
)
1
B
=
1
B
A)1B = 1B
A)1B=1B,
1
B
1B
1B 就是我们求的
l
o
w
b
i
t
(
x
)
lowbit(x)
lowbit(x)。
2. u p d a t e update update
(1). 什么是 u p d a t e update update ?
u p d a t e update update 就是用来更新树状数组的一个函数。
(2). 怎么用 l o w b i t lowbit lowbit 进行 u p d a t e update update ?
在上面的
l
o
w
b
i
t
lowbit
lowbit 中,我们可以发现一个规律 (兔兔也不知道怎么证明的,如果有知道的小伙伴,可以在博客下方评论哟~):对于任意一个下标为
i
i
i 的
A
A
A 数组
A
[
i
]
A[i]
A[i] ,包含它的树状数组
C
[
]
C[]
C[] 的下标就是
i
i
i 每次加上当前阶段的
l
o
w
b
i
t
(
i
)
lowbit(i)
lowbit(i)。
例如:上面的
A
3
A_3
A3,
C
3
C_3
C3 就含有
A
3
A_3
A3, 因为
l
o
w
b
i
t
(
3
)
lowbit(3)
lowbit(3) 是
1
1
1 ,所以
3
+
1
=
4
3 + 1 = 4
3+1=4 ;接着是
C
4
C_4
C4,所以
C
4
C_4
C4 也含有
A
3
A_3
A3,
l
o
w
b
i
t
(
4
)
lowbit(4)
lowbit(4) 是
4
4
4 ,所以
4
+
4
=
8
4 + 4 = 8
4+4=8 ;接着就是
C
8
C_8
C8, 所以
C
8
C_8
C8 也含有
A
3
A_3
A3 ,
l
o
w
b
i
t
(
8
)
lowbit(8)
lowbit(8) 是
8
8
8 ,所以
8
+
8
=
16
8 + 8 = 16
8+8=16 , 因为上面的数只有
8
8
8 个,所以当
i
i
i 超过了
8
8
8 就停止操作了。
代码实现如下:
void update(int k, int x) // x 为数组 a[k] 所需要增加的量
{
for (int i = k; i <= n; k++) Bit[i] += x;
}
例子如下:
#include<cstdio>
const int MAXN = 15;
int num[MAXN];
int Bit[MAXN];
int lowbit(int x)
{
return x & -x;
}
void update(int k, int x)
{
for (int i = k; i <= 8; i += lowbit(i)) Bit[i] += x;
}
int main()
{
for (int i = 1; i <= 8; i++)
{
num[i] = i;
update(i, num[i]); // num[i] 初值为 0 , 所以 num[i] = i 等于 num[i] += i
}
for (int i = 1; i <= 8; i++)
printf("%d ", Bit[i]);
return 0;
}
输出:
1 3 3 10 5 11 7 36
3. 有了 树状数组 怎么求 前缀和 ?
同样的,在上面 l o w b i t lowbit lowbit 可以发现一个规律。 P r e [ i ] Pre[i] Pre[i] 是前 i i i 个数的前缀和,而 P r e [ i ] Pre[i] Pre[i] 与 B i t [ ] Bit[] Bit[] 的关系,就如下面所示:
int lowbit(int x)
{
return x & (-x);
}
int Pre(int k)
{
int pre = 0;
for (int i = k; i > 0; i -= lowbit(i)) pre += Bit[i];
return pre;
}
有了这些算法,就可以解出之前的问题了,这样的算法也不会超时,这就是树状数组的优点。
未完结哦 未完结哦 未完结哦~