树状数组BIT

树状数组

一、Lowbit运算

l o w b i t ( x ) = x & ( − x ) lowbit(x)=x\&(-x) lowbit(x)=x&(x)

  正数在计算机中一般采用的是补码存储,并且把一个补码表示的整数 x x x 变成其相反数的过程相当于把 x x x 的二进制每一位都取反,然后末尾加一。等价于直接把 x x x 的二进制最右边的 1 1 1 左边的每一位都取反

x = ( 0000001101001100 ) 2 x=(0000001101001100)_2 x=(0000001101001100)2

x x x 最右边的 1 1 1 是在 2 号位,因此把它左边的所有位全部取反,于是有 − x -x x

− x = ( 1111110010110100 ) 2 -x=(1111110010110100)_2 x=(1111110010110100)2

  通过它可以很容易推得 lowbit(x)=x&(-x)就是取 x x x 的二进制最右边的 1 1 1 和它右边所有的 0 0 0,因此它一定是 2 2 2 的幂次,即 1 、 2 、 4 、 8 1、2、4、8 1248等。

  例如对上面的例子来说, x & ( − x ) = ( 0000000000000100 ) 2 = 4 x\&(-x)=(0000000000000100)_2=4 x&(x)=(0000000000000100)2=4;而对 x = 6 = ( 110 ) 2 x=6=(110)_2 x=6=(110)2来说, x & ( − x ) = ( 010 ) 2 = 2 x\&(-x)=(010)_2=2 x&(x)=(010)2=2。显然, l o w b i t ( x ) lowbit(x) lowbit(x)也可以理解为能整除 x x x的最大2的幂次


图1-1 x&(-x)示意图

二、树状数组的应用

2.1 提出背景

  给出一个整数序列 A A A,元素个数为 N ( N ≤ 1 0 5 ) N(N≤10^5) N(N105),接下来查询 K K K ( K ≤ 1 0 5 ) (K≤10^5) (K105),每次查询将给出一个正整数 x ( x ≤ N ) x(x≤N) x(xN),求前 x x x 个整数之和。

  例如对 5 5 5 个整数 2 、 4 、 1 、 5 、 3 2、4、1、5、3 24153 来说,

  • 如果查询前 3 3 3 个整数之和,就需要输出 7 7 7

  • 如果查询前 5 5 5 个整数之和,就需要输出 15 15 15

  对于这个问题,一般的做法是开一个 s u m sum sum数组,其中 s u m [ i ] sum[i] sum[i] 表示前 i i i 个整数之和(数组下标从1开始),这样 s u m sum sum 数组就可以在输入 N N N 个整数时就预处理出来。接着每次查询前 x x x 个整数之和时,输出 s u m [ x ] sum[x] sum[x] 即可。显然每次查询的复杂度是 O ( 1 ) O(1) O(1) ,因此查询的总复杂度是 O ( k ) O(k) O(k)

  假设在查询过程中可能随时给第 x x x 个整数加上一个整数 v v v ,要求在查询中能实时输出前 x x x 个整数之和(更新操作和查询操作的次数总和为 K K K 次)。

  例如同样对整数 2 、 4 、 1 、 5 、 3 2、4、1、5、3 24153 来说,

  • 一开始查询前 3 3 3 个整数之和,将输出 7 7 7

  • 接着把第 2 2 2 个整数增加 3 3 3 ,此时序列会变成 2 、 7 、 1 、 5 、 3 2、7、1、5、3 27153

  • 之后又进行一次查询,要求查询前 4 4 4 个整数之和,此时应当输出 15 15 15 而不是 12 12 12

  对于升级后的问题,如果还是之前的做法,虽然单词查询的时间复杂度仍然是 O ( 1 ) O(1) O(1) ,但在进行更新时却需要给 s u m [ x ] 、 s u m [ x + 1 ] 、 ⋅ ⋅ ⋅ 、 s u m [ N ] sum[x]、sum[x+1]、···、sum[N] sum[x]sum[x+1]sum[N] 都加上整数 v v v ,这样就会使得单次更新的时间复杂度为 O ( N ) O(N) O(N) ,那么如果 K K K 此操作中大部分都是更新操作,操作的总复杂度就会是 O ( K N ) O(KN) O(KN) ,显然无法承受。

  如果不设置 s u m sum sum 数组,直接对原数组进行更新和查询,显然,虽然单次更新的时间复杂度变为了 O ( 1 ) O(1) O(1) ,但是单次查询的时间复杂度却变为了 O ( N ) O(N) O(N)

  下面就来讲述 树状数组 是如何解决这个问题的。

2.2 树状数组

  树状数组(Binary Indexed Tree,BIT)。它仍然是一个数组,并且与 s u m sum sum 数组类似,是用来记录和的数组,只不过它存放的不是前 i i i 个整数之和,而是 i i i 号位之前(含 i i i 号位,下同) l o w b i t ( i ) lowbit(i) lowbit(i) 个整数之和。如图 2-1 所示,数组 A A A 是原始数组,有 A [ 1 ] ~ A [ 16 ] A[1]~A[16] A[1]A[16] 16 16 16 个元素;数组 C C C 是树状数组,其中 C [ i ] C[i] C[i] 存放数组 A A A i i i 号位之前 l o w b i t ( i ) lowbit(i) lowbit(i) 个元素之和,可以结合图 2-2 理解,(为了能够讲的更清楚,我会尽可能减少二进制的出现)。显然 C [ i ] C[i] C[i]的覆盖长度是 l o w b i t ( i ) lowbit(i) lowbit(i)(也可以理解成管辖范围),它是 2 2 2 的幂次,即 1 、 2 、 4 、 8 1、2、4、8 1248 等。

  需要注意的是,树状数组仍旧是一个平坦的数组,画成树形是为了让存储的元素更容易观察。


图2-1 树状数组定义图

  如果还没能理解树状数组的定义,下面给出了 C [ 1 ] ~ C [ 16 ] C[1]~C[16] C[1]C[16] 的定义,结合图 2-1 进行理解,已经理解的可以跳过。


图2-2 树状数组定义辅助理解图

C C C数组 A A A数组长度
C [ 1 ] = C[1]= C[1]= A [ 1 ] A[1] A[1] l o w b i t ( 1 ) = 1 lowbit(1)=1 lowbit(1)=1
C [ 2 ] = C[2]= C[2]= A [ 1 ] + A [ 2 ] A[1]+A[2] A[1]+A[2] l o w b i t ( 2 ) = 2 lowbit(2)=2 lowbit(2)=2
C [ 3 ] = C[3]= C[3]= A [ 3 ] A[3] A[3] l o w b i t ( 3 ) = 1 lowbit(3)=1 lowbit(3)=1
C [ 4 ] = C[4]= C[4]= A [ 1 ] + A [ 2 ] + A [ 3 ] + A [ 4 ] A[1]+A[2]+A[3]+A[4] A[1]+A[2]+A[3]+A[4] l o w b i t ( 4 ) = 4 lowbit(4)=4 lowbit(4)=4
C [ 5 ] = C[5]= C[5]= A [ 5 ] A[5] A[5] l o w b i t ( 5 ) = 1 lowbit(5)=1 lowbit(5)=1
C [ 6 ] = C[6]= C[6]= A [ 5 ] + A [ 6 ] A[5]+A[6] A[5]+A[6] l o w b i t ( 6 ) = 2 lowbit(6)=2 lowbit(6)=2
C [ 7 ] = C[7]= C[7]= A [ 7 ] A[7] A[7] l o w b i t ( 7 ) = 1 lowbit(7)=1 lowbit(7)=1
C [ 8 ] = C[8]= C[8]= A [ 1 ] + A [ 2 ] + A [ 3 ] + A [ 4 ] + A [ 5 ] + A [ 6 ] + A [ 7 ] + A [ 8 ] A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8] A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8] l o w b i t ( 8 ) = 8 lowbit(8)=8 lowbit(8)=8

  强调,树状数组的定义非常重要,特别是“ C [ i ] C[i] C[i]的覆盖长度是 l o w b i t ( i ) lowbit(i) lowbit(i)”这点;另外,树状数组的下标必须从 1 1 1 开始,务必注意⚠️

  接着我们就要去解决之前提出的问题:

  1. 涉及函数getSum(x),返回 x x x 个数之和` A [ 1 ] + ⋅ ⋅ ⋅ + A [ x ] A[1]+···+A[x] A[1]++A[x]
  2. 涉及函数update(x,v),实现将第 x x x 个数上加上一个数 v v v 的功能,即 A [ x ] + = v A[x]+=v A[x]+=v
2.2.1 getSum函数

  对于第一个问题,假设想要查询 A [ 1 ] + ⋅ ⋅ ⋅ + A [ 14 ] A[1]+···+A[14] A[1]++A[14] ,那么从树状数组的定义出发,从图 2-2 很容易发现 A [ 1 ] + ⋅ ⋅ ⋅ + A [ 14 ] = C [ 8 ] + C [ 12 ] + C [ 14 ] A[1]+···+A[14]=C[8]+C[12]+C[14] A[1]++A[14]=C[8]+C[12]+C[14]。又比如查询 A [ 1 ] + ⋅ ⋅ ⋅ + A [ 11 ] A[1]+···+A[11] A[1]++A[11],同样可以从图中得到 A [ 1 ] + ⋅ ⋅ ⋅ + A [ 11 ] = C [ 8 ] + C [ 10 ] + C [ 11 ] A[1]+···+A[11]=C[8]+C[10]+C[11] A[1]++A[11]=C[8]+C[10]+C[11]


图2-2 树状数组定义辅助理解图

  记 s u m ( 1 , x ) = A [ 1 ] + ⋅ ⋅ ⋅ + A [ x ] sum(1,x)=A[1]+···+A[x] sum(1,x)=A[1]++A[x],由于 C [ x ] C[x] C[x]的覆盖长度是 l o w b i t ( x ) lowbit(x) lowbit(x),因此

C [ x ] = A [ x − l o w b i t ( x ) + 1 ] + ⋅ ⋅ ⋅ + A [ x ] \begin{aligned} C[x]=A[x-lowbit(x)+1]+···+A[x] \end{aligned} C[x]=A[xlowbit(x)+1]++A[x]

  于是我们可以马上得到
s u m ( 1 , x ) = A [ 1 ] + ⋅ ⋅ ⋅ + A [ x ] = A [ 1 ] + ⋅ ⋅ ⋅ + A [ x − l o w b i t ( x ) ] + A [ x − l o w b i t ( x ) + 1 ] + ⋅ ⋅ ⋅ + A [ x ] = s u m ( 1 , x − l o w b i t ( x ) ) + C [ x ] \begin{aligned} sum(1,x) &=A[1]+···+A[x] \\ &=A[1]+···+A[x-lowbit(x)]+A[x-lowbit(x)+1]+···+A[x] \\ &=sum(1,x-lowbit(x))+C[x] \\ \end{aligned} sum(1,x)=A[1]++A[x]=A[1]++A[xlowbit(x)]+A[xlowbit(x)+1]++A[x]=sum(1,xlowbit(x))+C[x]
  这样就把 s u m ( 1 , x ) sum(1,x) sum(1,x) 转化为 s u m ( 1 , x − l o w b i t ( x ) ) sum(1,x-lowbit(x)) sum(1,xlowbit(x)) 了,示意图如下


图2-3 树状数组求和示意图

  接着就很容易写出getsum函数了:

int getSum(int x){
    int sum=0;
    for(int i=x;i>0;i-=lowbit(i)){//注意⚠️是i>0,而不是i>=0
        sum+=c[i];//累计C[i],然后把问题缩小为sum(1,i-lowbit(i))
    }
    return sum;//返回和
}

getSum时间复杂度分析

  前面已经讲过 l o w b i t ( i ) lowbit(i) lowbit(i)函数,即定位 i i i 的二进制中最右边的 1 1 1 ,因此 i = i − l o w b i t ( i ) i=i-lowbit(i) i=ilowbit(i) 事实上是不断把 i i i 的二进制中最右边的 1 1 1 置为 0 0 0 的过程。所以 getSum函数的for循环执行次数为 x x x 的二进制中 1 1 1 的个数,也就是说,getSum函数的时间复杂度为 O ( l o g N ) O(logN) O(logN)

  从另一个角度理解,结合图 2-1 和图 2-2 就会发现,getSum函数实际上是在沿着一条不断左上的路径行进(可以想一下getSum(14)getSum(11)的过程),如图 2-4 所示。(再次强调,不要过深陷入图中的二进制,因为这与理解getSum函数没有关系)。由于“树🌲”高是 O ( l o g N ) O(logN) O(logN) 级别,因此可以同样得到getSum函数的时间复杂度就是 O ( l o g N ) O(logN) O(logN)。另外,如果要求数组下标在区间 [ x , y ] [x,y] [x,y]内的数之和,即 A [ x ] + A [ x + 1 ] + ⋅ ⋅ ⋅ + A [ y ] A[x]+A[x+1]+···+A[y] A[x]+A[x+1]++A[y],可以转换成getSum(y)-getSum(x-1)来解决,这是一个很重要的技巧


图2-4 getSum函数二进制路径图

2.2.2 update函数

  接着来看第二个问题,即如何设计函数update(x,v),实现将第 x x x 个数加上一个数 v v v 的功能。

  继续看图 2-1,来看两个例子。加入要让 A [ 6 ] A[6] A[6] 加上一个数 v v v,那么就要寻找树状数组 C C C 中能覆盖了 A [ 6 ] A[6] A[6] 的元素,让他们都加上 v v v。也就是说,如果要让 A [ 6 ] A[6] A[6] 加上 v v v ,实际上就是要让 C [ 6 ] 、 C [ 8 ] 、 C [ 16 ] C[6]、C[8]、C[16] C[6]C[8]C[16] 都加上 v v v。同样,如果要将 A [ 9 ] A[9] A[9] 加上一个数 v v v ,实际上就是要让 C [ 9 ] 、 C [ 12 ] 、 C [ 16 ] C[9]、C[12]、C[16] C[9]C[12]C[16] 都加上 v v v 。那么问题就转变为——在给 A [ x ] A[x] A[x] 加上 v v v 时,怎样去寻找树状数组中的对应项。


图2-1 树状数组定义图

  要让 A [ x ] A[x] A[x] 加上 v v v ,就是要寻找树状数组 C C C 中能覆盖 A [ x ] A[x] A[x] 的元素,让他们都加上 v v v 。从图 2-1 中直观的看,只需要总是离当前“矩形” C [ x ] C[x] C[x] 最近的“矩形” C [ y ] C[y] C[y] ,使得 C [ y ] C[y] C[y] 能够覆盖 C [ x ] C[x] C[x] 即可。

  那么要怎么找呢?首先,可以得到一个显然的结论: l o w b i t ( y ) lowbit(y) lowbit(y) 必须大于 l o w b i t ( x ) lowbit(x) lowbit(x) (不然没办法覆盖呀)。于是问题等价于求一个尽可能小的整数 a a a 使得 l o w b i t ( x + a ) > l o w b i t ( x ) lowbit(x+a)>lowbit(x) lowbit(x+a)>lowbit(x)。显然,由于 l o w b i t ( x ) lowbit(x) lowbit(x) 是取 x x x 的二进制最右边的 1 1 1 的位置,因此如果 l o w b i t ( a ) < l o w b i t ( x ) lowbit(a)<lowbit(x) lowbit(a)<lowbit(x) l o w b i t ( x + a ) lowbit(x+a) lowbit(x+a) 就会小于 l o w b i t ( x ) lowbit(x) lowbit(x)。为此 l o w b i t ( a ) lowbit(a) lowbit(a) 必须不小于 l o w b i t ( x ) lowbit(x) lowbit(x)。接着发现,当 a a a l o w b i t ( x ) lowbit(x) lowbit(x) 时,由于 x x x a a a 的二进制最右边的 1 1 1 的位置相同,因此 x + a x+a x+a 会在这个 1 1 1 的位置上产生进位,使得进位过程中的所有连续的 1 1 1 变成 0 0 0,直到把它们左边第一个 0 0 0 置为 1 1 1 时结束。于是 l o w b i t ( x + a ) > l o w b i t ( x ) lowbit(x+a)>lowbit(x) lowbit(x+a)>lowbit(x) 显然成立,最小的 a a a 就是 l o w b i t ( x ) lowbit(x) lowbit(x)

  于是update函数的做法就很明确了,只要让 x x x 不断加上 l o w b i t ( x ) lowbit(x) lowbit(x),并让每步的 C [ x ] C[x] C[x] 都加上 v v v ,直到 x x x 超过给定的数据范围为止(因为在不给定数据范围的情况下,更新操作是无上限的)。

update函数:

void update(int x,int v){
    for(int i=x;i<=N;i+=lowbit(i)){ //注意 i 必须能取到 N
        c[i]+=v;    //让C[i]加上v,然后让C[i+lowbit(i)]加上 v
    }
}

update函数时间复杂度分析

  显然,这个过程是从右到左不断定位 x x x 的二进制最右边的 1 1 1 左边的 0 0 0 的过程,因此 ,update函数的时间复杂度为 O ( l o g N ) O(logN) O(logN)

  同样,从另一个角度理解,结合图 2-1 和图 2-2 会发现,update函数的过程实际上是在沿着一条不断右上的路径行进,如图 2-5 所示。于是由于“树🌲”高是 O ( l o g N ) O(logN) O(logN) 级别,因此可以同样得到 update函数的时间复杂度就是 O ( l o g N ) O(logN) O(logN)


图2-5 update函数二进制路径图
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值