树状数组(Binary Indexed Tree)

树状数组(Binary Indexed Tree),又叫做Fenwick Tree,它能够高效地获取数组中连续N个数的和。

传统数组(共n个元素)的元素修改和连续元素求和的复杂度分别为O(1)和O(N)。树状数组通过将线性结构转换成
逻辑上的树状结构(线性结构只能逐个扫描元素,而树状结构可以实现跳跃式扫描),使得修改和求和复杂度均为O(lgN),大大提高了整体效率。

对于给定的数组A,我们创建一个等大的数组BIT[N],并在逻辑上把BIT[N]构造成数组A对应的求和树。

我们这样构造BIT数组,对于下标为2的幂减1的(因为下标从0开始),用BIT[2k-1]来存储数组A[0…2k-1]的前2k项的和。对于中间的下标,以二分的形式来求中间段的和。

即:给定序列(数列)A,我们设一个数组BIT[N]满足

BIT[i-1] = A[i – 
2
k+1-1] + … + A[i-1]

其中,k为i在二进制下末尾0的个数,i∈[1, N]。则我们称BIT为树状数组。

用通俗的话说,就是BIT[i]表示从A[i]开始,包括A[i]本身,向左连数2k个数求和。

《树状数组(Binary Indexed Tree)》

下面的问题是,给定i,如何求2k?

2k的意思就是该二进制数从低位向右到第一次遇到1为止,所截得的位数。也就是i所对应的二进制数末尾连续0的个数。

这正好可以利用补码,负数的原码与补码进行位与。原因:参考补码的定义。

答案很简单:
2
= i&(i^(i-1)) ,也就是i&(-i)

有了以上定义,下面我们来看看怎么初始化BIT数组。有些比较憨厚的同学会说,直接按照定义进行初始化就OK了。是的,直接按照定义进行初始化,结果是正确的,但是,会存在重复计算的问题。当我们计算BIT[7]时,如果按照定义进行计算,则BIT[7] = A[7] + A[6] + A[5] + A[4] + A[3] + A[2] + A[1] + A[0],而实际上,参照上图,只需利用前面已计算过的值,即BIT[7] = A[7] + BIT[6] + BIT[5] + BIT[3]即可。也就是说,可以减少一半的计算量。当N很大时,减少的计算量是比较可观的。

//类似动规,使用先前已计算过的值
//避免了无谓的重复计算
void initBIT(int* BIT, int N)
{
	for(int index = 1; index <= N; index++)
	{
		int lowbits = index & (-index) ;
		int sub = index-1;
		BIT[sub] = A[sub];
		for(int offset = 1; offset < lowbits; offset<<=1)
			BIT[sub] += BIT[sub-offset];
	}
}

当我们修改A[i]的值时,可以从BIT[i]往根节点一路上溯,调整这条路径上的所有BIT[]即可,这个操作的复杂度在最坏情况下就是树的高度即O(logn)。

《树状数组(Binary Indexed Tree)》

用通俗的话来说,就是对BIT中某一元素BIT[k]加上num之后,后面的所有元素,凡是求和范围包含BIT[k]的,都应该加上num。从程序的实现角度来看,就是修改BIT[k]之后,从后面最近的第一个求和范围包含BIT[k]的元素开始,对所有包含BIT[k]的元素,依次加上num.

那么怎么求BIT[k]后面哪些元素的求和范围包含BIT[k]呢?

我们记fakeIndex = index + 1,即从逻辑上把数组下标从1开始计数。

对于fakeIndex为奇数的,下一个包含它的元素一定是BIT[fakeIndex+1],如果fakeIndex为偶数,则由定义可知,下一个包含它的元素,那个元素的包含范围一定至少是当前元素包含范围的两倍。也就是说,只要把fakeIndex加上lowbit(fakeIndex)即可。即:nextIndex = fakeIndex + (fakeIndex & -fakeIndex);

//对原数组A[index]进行加num操作: A[index] += num
//index∈[0, N-1]
void plus(int index, int num)
{
	if(0 == num)
		return;
	A[index] += num;
	//fakeIndex为数组第一个元素的下标从1开始计数
	int fakeIndex = index + 1;
	while(fakeIndex <= N)
	{
		fakeIndex += fakeIndex & (-fakeIndex);
		BIT[fakeIndex-1] += num;
	}
}

//对原数组进行赋值操作: A[index] = num
//index∈[0, N-1]
void setValue(int index, int num)
{
	A[index] = num;
	plus(index, num - A[index]);
}

下面开始进入 树状数组的实用阶段:

效用一:求数组前n项和,时间复杂度最坏情况下为log(N). 其实,求和的次数就是n用二进制表示时,该二进制数中1的个数。所以最坏情况下时间复杂度为log(N).最好的时候为O(1)。

int getSum(int index)
{
	int fakeIndex = index + 1;
	int lowbits = 0;
	int sum = 0;
	while(fakeIndex)
	{
		sum += BIT[fakeIndex-1];
		lowbits = (fakeIndex) & (-fakeIndex);
		fakeIndex -= lowbits;
	}
	return sum;
}

效用二:求指定区间上的数组所有项的和 [leftIndex, rightIndex]

int getIntervalSum(int leftIndex, int rightIndex)
{
	int rightSum = getSum(rightIndex);
	int leftSum = getSum(leftIndex-1);
	return rightSum - leftSum;
}

树状数组BIT的基础知识已介绍完毕。

点赞