树状数组总结

树状数组



一. 问题的提出

有一个一维数组,长度为 n n n
你可以对这个数组进行两种操作:

  1. 修改: 1 1 1 ~ n n n 之间的一个元素 n u m [ k ] num[k] num[k] 增加 x x x
  2. 求和: 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(ji+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(nk+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

这就是 树状数组 (它长得像树一样 ,所以称为 树状数组 )。


  1. " 如何求已知数组下标的树状数组 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 ······) " 。
  2. " 如何对树状数组进行 修改 修改 修改 " 。
  3. " 如何用树状数组求 前缀和 前缀和 前缀和 " 。
    ······

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 x1 的二进制可以看做 A 0 C A0C A0C ( C C C 是和 B B B 一样长的 1 1 1 )
x x x & ( x − 1 ) (x - 1) (x1) 的二进制就是 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)) (x1)) 的二进制就是 A 1 B – A 0 B = 0 … 010 … 0 A1B – A0B = 0…010…0 A1BA0B=00100
就得到我们所求的 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;		
}

有了这些算法,就可以解出之前的问题了,这样的算法也不会超时,这就是树状数组的优点。


未完结哦 未完结哦 未完结哦~

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值