背景:
树状数组(Binary Index Tree)是一种处理区间问题较为容易实现的数据结构,可以处理单点更新,区间更新,单点查询,区间查询等问题,是一种处理在线问题的利器,其查询跟修改的时间复杂度均为O(logn)。
引入:
在算法竞赛中经常会遇到这样一类问题:给定一个数列,让你求出这个序列的某个数及其之前的数的和。通常最暴力的做法,求x位置及其之前的数的和,就是从第一个数开始依次扫到x位置,这样对于一次的查询当然没什么大问题,那么对于多次的查询,复杂度就是O(query * position),我们知道1s能跑1e9左右的数据,所以对于多次查询会TLE。那么,优化一下,我们可以通过预处理前缀和来解决这个问题,也就是设置一个pre数组,数组的下标就是代表这个位置以及之前位置的数字的和,这样我们可以在O(n)时间内预处理出数列的每一段的和,可以在O(n)时间内查询出这个第一个数到该位置的数的和,当然也可以查询一段区间的和,比如查询区间[x , y]的和,我们知道,pre[x]代表的就是[1 , x]的和,pre[y]代表的就是[1 , y]的数列和,那么我们用[1 , y]这段数列的和减去[1 , x - 1]这段数列的和,那么就是[x , y]这段数列的和了。这是一个好的trick对于没有修改操作的问题来说。
那么,如果问题变成要对某个位置的值进行修改,要是还用预处理的方法,显然,每次处理的时间为O(n - x),那么如果处理的次数比较多,显然还是并不好的,这时候,我们就需要一个有力的数据结构来维护我们的数列了。所以先来看看树状数组长啥样:
正题:
BIT就是一种简单易用的利器,在讲BIT之前,先介绍BIT的重要的一个位运算,就是lowbit(x)运算,lowbit(x) = x & (-x)
在计算机组成原理中,整数在计算机的存储一般都是补码的形式,也就是说,把一个整数x变成补码,(-x),就是把x的二进制形式每一位都取反(就是0变成1,1变成0),也就是相当于对x的二进制的最右边的1的左边的所有位取反,对于x = 来说, 他的最右边的1为从右往左第二个数,所以,-x = 。然后在讲一下,一些位运算:
按位与(&):两位都为1才是1,否则就是0
按位或(|):其中一个为1就是1,否则为0
按位异或(^):任何与0异或都是本身,1^1 = 0
所以对上面的x进行lowbit运算结果为:
x 1001011101 100
-x 0110100010 100
x & (-x) 0000000000 100
然后我们再来讲一下BIT,对于原来的数列我们记为A[i] , BIT存储的数列我们记为BIT[i],那么BIT就是一个类似前缀和的东西,而不是一个跟线段树一样的二叉树(关于线段树,之后会更),每个BIT[i]记录的是一个块的数据,先画个图see see:
图1(接下来的数组块依次类推啊,人懒懒得画):
那么我们可以看出来,每个BIT[i]覆盖的范围,就是lowbit(i),也可以理解成占领管理的范围,大概更加形象。从图1我们可以看出来,每一个块覆盖的范围就是这个lowbit(i)的长度,可以看出来其管理的地界:
B[1] = A[1]......................................................................................................................lowbit(1) = 1
B[2] = A[1] + A[2] ...........................................................................................................lowbit(2) = 2
B[3] = A[3] .....................................................................................................................lowbit(3) = 1
B[4] = A[1] + A[2] + A[3] + A[4]......................................................................................lowbit(4) = 4
......................依次类推,懒得写了...........(注意啊,数组的下标一定是从1开始的呀,不然会GG)
所以,我们就可以对BIT进行修改查询操作啦,首先介绍的是:
单点修改,区间查询:
修改很简单,就是把管辖这一位置的所有块都进行更新,比如更新位置x,那么就更新管理他的所有块,也就是从x块开始更新到n,每次间隔为lowbit(x -> n - 1),每次更新都跟走楼梯一样,看上面的图就知道啦,看看代码:
void Update(int pos , int value) { while(pos <= n) { tree[pos] = tree[pos] + value; pos += lowbit(pos); } }
每次都更新能够管到这个位置的区块,然后我们再来看看求[1 , x]的区间和?也就是从x块开始记录和,每次x往左走的步数就是lowbit(x -> 1),因为加过的区块也不会再加,所以这相当于一个下楼梯的过程,也来看一下代码:
int Query(int pos) { int sum = 0; while(pos > 0) { sum += tree[pos]; x -= lowbit(pos); } return sum; }
因为BIT存储的是前缀和的形式,所以,求[x , y]的区间和也就是Query(y) - Query(x)。