树状数组详解

导入

前缀和 相信大家都知道,它可以用来求区间和。

可是前缀和不怎么灵活,如果要对某个点进行修改,那就需要更新前缀和数组的所有下标不小于修改位置的元素的值,算法复杂度最高可达 O ( n ) O(n) O(n),速度十分缓慢,如果碰到大量修改,会直接歇菜。

为了加快单点修改的速度,树状数组就诞生了!

什么是树状数组

树状数组或二叉索引树(英语:Binary Indexed Tree),又以其发明者命名为Fenwick树,最早由Peter M. Fenwick于1994年以A New Data Structure for Cumulative Frequency Tables为题发表在SOFTWARE PRACTICE AND EXPERIENCE。其初衷是解决数据压缩里的累积频率(Cumulative Frequency)的计算问题(当我没说),现多用于高效计算数列的前缀和, 区间和。

以上内容来自网络。

如果你看完了上一段,那么恭喜你!浪费了10秒钟。

树状数组支持以下两种操作,

  • 单点修改。
  • 区间和查询。

两种操作的时间复杂度都为 O ( l o g   n ) O(log\space n) O(log n)

树状数组的构造

树状数组的思想大概是这样的,我们可以为数组维护多个区间,在查询和修改时,对这几个包含该点或区间的区间进行修改和查询。

可这些区间也不能随便设置,不然在进行查询修改时,如果包含区间的区间过多,那也会导致速度缓慢。

好了,不扯这些废话了,先来看看它长什么样吧。

设一个长度为 n n n 的树状数组 c i c_i ci,那它的元素维护的区间是这样的。


其中 c i c_i ci 就等于 a a a 数组 [ i − l o w b i t ( i ) + 1 , i ] [i-lowbit(i)+1,i] [ilowbit(i)+1,i] 的区间和。

l o w b i t ( i ) lowbit(i) lowbit(i) 是什么呢? l o w b i t ( i ) lowbit(i) lowbit(i) 指的是 i i i 在二进制下最后一个的为 1 1 1 的哪一位所代表的值。比如当 i = 10 i=10 i=10,即 ( 1010 ) 2 (1010)_2 (1010)2 时,最后一个为 1 1 1 的二进制位为从右往左第二位,所以 l o w b i t ( i ) = ( 10 ) 2 = 2 lowbit(i)=(10)_2=2 lowbit(i)=(10)2=2

lowbit的实现方法

l o w b i t ( x ) lowbit(x) lowbit(x) 的实现方法如下:

int Lowbit(int x){
	return x&(-x);
	//十分简洁,算法复杂度为 O(1)
}

为什么是 x&(-x) 呢?

大家应该都知道二进制的原码、反码、补码。当 x x x 为负数时,他的二进制就是用补码来表示。即将 x x x 包括符号位取反后加 1 1 1

而将 x x x 变成负数后,二进制在运算时就会变成补码。因为 l o w b i t ( x ) lowbit(x) lowbit(x) 是最后一个为 1 1 1 的二进制位,所以它后面所有的二进制位都为 0 0 0,取反后自然为 1 1 1 l o w b i t ( x ) lowbit(x) lowbit(x) 的那一位则会变成 0 0 0,加 1 1 1 后, l o w b i t ( x ) lowbit(x) lowbit(x) 后面的二进制位会一直进位到 l o w b i t ( x ) lowbit(x) lowbit(x) 这一位,并全部变为 0 0 0。所以 l o w b i t ( x ) lowbit(x) lowbit(x) 的后面的二进制位进行按位与后都为 0 0 0。而 l o w b i t ( x ) lowbit(x) lowbit(x) 前面的二进制位因为取反,且加 1 1 1 时没有被影响的原因,按位与时都是 1&00&1,答案自然为 0 0 0。而只有 l o w b i t ( x ) lowbit(x) lowbit(x) 的那一位在变成补码时仍和原来一样,都为 1 1 1,按位与后就只剩它了。所以答案一定为 l o w b i t ( x ) lowbit(x) lowbit(x)

如果前面那一大坨文字还是令你有些困惑,那我们可以来举一个例子。

x x x 26 26 26,则二进制表达下为 ( 11010 ) 2 (11010)_2 (11010)2,很容易看出 l o w b i t ( x ) = ( 10 ) 2 = 2 lowbit(x)=(10)_2=2 lowbit(x)=(10)2=2。我们再用之前的方法验证一下, − x -x x 取反后为 ( 00101 ) 2 (00101)_2 (00101)2,加 1 1 1 后为 ( 00110 ) 2 (00110)_2 (00110)2。和 x x x 按位与后,结果为 11010&00110=2,结果仍为 2 2 2

如果你还是不懂的话,也可以这么写。

int Lowbit(int x){
	int cnt=0;
	while(!(x&1))
		x>>=1,cnt++;//统计lowbit后有多少0
	return 1<<cnt;//返回lowbit的值,即2的cnt次方
	//略微慢些,算法复杂度为 O(log n)
}

因为可能要遍历 x x x 的所有二进制位,所以时间复杂度为 O ( l o g   n ) O(log \space n) O(log n)。虽然慢了点,但也没什么大碍。

树状数组求区间和

方法

知道了树状数组的形成,那通过树状数组如何进行区间和查询呢?

设要求区间 [ l , r ] [l,r] [l,r] 之和,那我们可以先对他进行类似前缀和的转换。将它转化成区间 [ 1 , r ] [1,r] [1,r] 之和,减区间 [ 1 , l − 1 ] [1,l-1] [1,l1] 之和( [ 1 , 0 ] [1,0] [1,0] 的区间和为 0 0 0)。

这样我们只需要求前 x x x 个数的和就行了。

看了树状数组的构造的朋友们应该能看出, c i c_i ci 维护的区间的右端点为 i i i,所以我们在寻找前缀时,只需将 c x c_x cx 加上前 x − l o w b i t ( x ) x-lowbit(x) xlowbit(x) 个数的和(因为 c x c_x cx 只涵盖了区间 [ x − l o w b i t ( x ) + 1 , x ] [x-lowbit(x)+1,x] [xlowbit(x)+1,x]),一直到 x x x 0 0 0 为止,递归/递推即可。

代码如下:

int Sum(int x){
	if(x==0)
		return 0;//如果x=0,返回0并结束递归
	return c[x]+Sum(x-Lowbit(x));
}

算法复杂度分析

之前也说了,查询的算法复杂度为 O ( l o g   n ) O(log\space n) O(log n),接下来,我来解释一下是为什么。

每次递归的时间复杂度为 O ( 1 ) O(1) O(1),由于我们递归时,都是将 x x x 一直减去 l o w b i t ( x ) lowbit(x) lowbit(x) 0 0 0 为止。所以递归的次数为, x x x 在二进制表达下,数值为 1 1 1 的个数。考虑最坏的情况,即 x x x 所有二进制位都为 1 1 1,那时间复杂度为 x x x 在二进制下的位数,即 O ( l o g   n ) O(log \space n) O(log n)

树状数组单点修改

方法

树状数组的单点修改可能比求区间和更为难懂一些,不过大家只要认真阅读,仔细思考,相信也是能快速搞定的。

设我们要将数组的第 x x x 个数加上 k k k(如果是减去的话,就是加上 − k -k k)。

我们从 c x c_x cx 开始枚举所有包含 x x x 的区间(显然,因为 i i i 再小就不包含 x x x 了),因为如果区间不包含 x x x 则不会被影响,以右端点从小到大(或者说以 c i c_i ci i i i 小到大,因为 c i c_i ci 维护的区间的右端点为 i i i)修改每一个包含第 x x x 个数的区间,由于所有 c i c_i ci 两两枚举的区间中,要么没有公共的部分,要么一个完全包含在另一个里面,所以我们在判断是否包含时,只需看该区间的左端点是否不超过上一个被修改的 c i c_i ci 的左端点,即 c j − l o w b i t ( j ) ≤ c i − l o w b i t ( i ) c_j-lowbit(j) \le c_i-lowbit(i) cjlowbit(j)cilowbit(i) c j c_j cj 为最近一个包含 c i c_i ci 的数组。

我就直接说结果了,设上一个被修改的数组下标为 i i i,那下一个被修改的数组下标就是 i + l o w b i t ( i ) i+lowbit(i) i+lowbit(i),我们直接从 x x x 开始一直递归或递推修改到下标超过 n n n 为止即可。

先证明一下 c i + l o w b i t ( i ) c_{i+lowbit(i)} ci+lowbit(i) 维护的区间是包含 c i c_{i} ci 维护的区间的,即 i + l o w b i t ( i ) − l o w b i t ( i + l o w b i t ( i ) ) ≤ i − l o w b i t ( i ) i+lowbit(i)-lowbit(i+lowbit(i)) \le i-lowbit(i) i+lowbit(i)lowbit(i+lowbit(i))ilowbit(i)。设 j j j i + l o w b i t ( i ) i+lowbit(i) i+lowbit(i),我们将问题分为两类讨论。

一种是 l o w b i t ( i ) lowbit(i) lowbit(i) 那一位左边的一位没有 1 1 1

因为 l o w b i t ( i ) lowbit(i) lowbit(i) 那一位加了 1 1 1,所以肯定会发生进位,而由于左边没有紧挨着的 1 1 1,所以无法一直进位,所以 l o w b i t ( j ) lowbit(j) lowbit(j) 的值为 l o w b i t ( i ) × 2 lowbit(i) \times 2 lowbit(i)×2,而其它位不会发生变化,所以将 j − l o w b i t ( j ) j-lowbit(j) jlowbit(j) i − l o w b i t ( i ) i-lowbit(i) ilowbit(i) 减去后,剩下的位数都是相同的。即 j − l o w b i t ( j ) = i − l o w b i t ( i ) j-lowbit(j)=i-lowbit(i) jlowbit(j)=ilowbit(i)

举个例子,比如 i = 18 i=18 i=18 时,它的二进制为 10010 10010 10010,加上 l o w b i t ( 18 ) lowbit(18) lowbit(18) ( 10100 ) 2 = 20 (10100)_2=20 (10100)2=20 20 − l o w b i t ( 20 ) = 18 − l o w b i t ( 18 ) 20-lowbit(20)=18-lowbit(18) 20lowbit(20)=18lowbit(18),都为 ( 10000 ) 2 = 16 (10000)_2=16 (10000)2=16

还有一种是 l o w b i t ( i ) lowbit(i) lowbit(i) 那一位左边的一位有 1 1 1

如果 l o w b i t ( i ) lowbit(i) lowbit(i) 左边有挨着 1 1 1,那 i i i l o w b i t ( i ) lowbit(i) lowbit(i) 时,会一直进位到进的那一位为 0 0 0 为止, l o w b i t ( j ) lowbit(j) lowbit(j) 就成了 l o w b i t ( i ) lowbit(i) lowbit(i) 左边第一个为 0 0 0 的那一位。由于进位时 l o w b i t ( i ) lowbit(i) lowbit(i) 左边紧挨着的 1 1 1 都变成了 0 0 0,所以 j − l o w b i t ( j ) < i − l o w b i t ( i ) j-lowbit(j)<i-lowbit(i) jlowbit(j)<ilowbit(i)

举个例子,比如 i = 22 i=22 i=22 时,它的二进制为 10110 10110 10110,加上 l o w b i t ( 22 ) lowbit(22) lowbit(22) ( 11000 ) 2 = 24 (11000)_2=24 (11000)2=24。容易证明, 24 − l o w b i t ( 24 ) < 22 − l o w b i t ( 22 ) 24-lowbit(24)<22-lowbit(22) 24lowbit(24)<22lowbit(22)

时间复杂度分析

和查询操作一样,单点修改的时间复杂度也为 O ( l o g   n ) O(log\space n) O(log n)。接下来我解释一下是为什么。

考虑最坏的情况,即加上 l o w b i t ( i ) lowbit(i) lowbit(i) 后只进一位,那会一直进到二进制位数大于 n n n 为止,加 l o w b i t ( i ) lowbit(i) lowbit(i) 的操作数为 n n n 的二进制位数,即 O ( l o g   n ) O(log\space n) O(log n)

习题

P2068 统计和
P3374 【模板】树状数组 1
P3368 【模板】树状数组 2
P1908 逆序对
P1637 三元上升子序列

提示:数组数组的值不仅可以存数组的值,也可以存一个数出现的次数。如果内存不够,可以试试离散化哦。


好了,树状数组的知识点基本上就讲完了。如果对你有帮助的话,不妨就给我点一个赞吧,你们的支持是我创作的最大动力。如果该文章有表述错误或可以改进的地方,欢迎大家在评论区指出。

  • 14
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值