树状数组

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 单点更新与区间查询

  为了解决刚才提出的问题,需要设计以下两个函数:

  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]
  2. 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[nlowbit(n)]+A[nlowbit(n)+1]++A[n]=getSum(nlowbit(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(i1)求区间 [ 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章
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值