树状数组原理解析

我们知道,对长度为n的数组,如果我们要改变其中某个值,则时间复杂度为O(1)。如果要求出S[m]=a[1]+a[2]+.....+a[m],则需要O(m)的时间复杂度。若我们一边修改数组的值,一边要求求出其部分和S[m],使用一般的方法,时间复杂度是O(m*n)的,若m和查找次数n很大,那么该算法将不可取。

为了解决这个问题,出现了树状数组这一数据结构。它可以以O(log n)的时间复杂度修改数组中的值,同时以O(log n)的时间复杂度求部分和。这里的log是以2为底的。


1、基本结构

树状数组可以简单地用下面的图理解:

C[1]=A[1]

C[2]=A[1]+A[2]

C[3]=A[3]

C[4]=A[1]+A[2]+A[3]+A[4]

……

可以看到,C[1]“管辖”一个元素,而C[2],C[6]“管辖”2个元素,C[8]“管辖”8个元素。那么C[N]“管辖”多少个元素呢?在树状数组中,这是个重要的概念。我们规定,把N转换为二进制,最右侧0的个数为k,则它管辖的元素个数就是2^k个。如2的二进制位10,2^1=2。4的二进制为100,2^2=4。定义函数lowbit(N)=2^k。那么C[N]管辖了lowbit[N]个元素。根据这样的定义,你就可以自己画出更大范围的树状数组了。


2、树状数组的求和过程及其原理

假设C数组已经初始化好了。如何利用它来求和呢?

举个例子。

我们要求S[6]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]

先令S=C[6]=A[5]+A[6],注意到lowbit(6)=2,现在的S中只有两个数的和。所以我们还求6-lowbit(6)=4个数的和。这时找到C[4],S=S+C[4],lowbit(4)=4。这时我们已经求了2+4=6个数的和了,所以S就是答案。

可能上面的过程表达的还是不太清楚。我们用一个程序段来更好地表达这个过程:

int sum(int k){
	int s=0;
	for (int i=k;i>=1;i-=lowbit(i))
		s+=c[i];
	return s;
}

在上述程序段中,我们要求S[k],先让i=k;s+=c[i],之后每次让i=i-lowbit(i),s+=c[i],直到到最开头做完为止。

为什么可以这样做?且为什么这样做的复杂度是log级别的?下面再深入解释下原理:

注意观察,其实每次每次让i=i-lowbit(i),都减去了i二进制中最右边的一个1!而i的二进制中最多有log i个1,因此时间复杂度是log级的。

这样做的正确性在于:把求S[k]转换成求几段和的累加,而“分段”是以k的二进制中的1来决定的。

听起来有点拗口,我们还是用一个具体的例子解释。

比如求S[105],105的二进制是1101001。首先S+=C[105],注意到lowbit(105)=1,这样就减去了1101001中最右边的1。1101001-1=1101000。1101000是104的二进制。下面S+=C[104],lowbit(104)=8,104-8=96。96的二进制是1100000。这就减去了右边第二个1。下面S+=C[96],lowbit(96)=32,96-32=64.64的二进制是1000000,这就减去了右边第三个1。最后S+=C[32]结束。

整个求和过程i的变化就是1101001->1101000->1100000->1000000->0.


3、树状数组的初始化

初始化C数组有两种方法。

一种是利用sum函数,C[N]=A[N]+Sum(N-1)-Sum(N-lowbit(N)),这很好理解。

还有一种是利用以下的程序段:

void init(){
	for (int i=1;i<=n;i++){
		c[i]=a[i];
		for(int j=i-1;j>i-lowbit(i);j-=lowbit(j))
			c[i]+=c[j];//注意,这里是加c[j]而不是a[j]
	}
}

这个程序段和上面的思路类似,读者可自行分析其正确性。


4、树状数组对值的修改。

若修改数组中的一个值,则C数组需要做相应修改。我们每次往上找父节点就行了,时间复杂度也是log级的。

参考程序段:

void modify(int x,int v){
	for (int i=x;i<=n;i+=lowbit(i))
		c[i]+=v;
}

5、其他

注意写树状数组的时候,最好把数组下标定为1..N,而不是0..N-1,这样有利于编程方便!


6、参考程序

#include<cstdlib>
#include<cstdio>

int n,a[10001],c[10001],m,t,x,y,v;

int lowbit(int x){
	return x & (-x);
}

int sum(int k){
	int s=0;
	for (int i=k;i>=1;i-=lowbit(i))
		s+=c[i];
	return s;
}

void modify(int x,int v){
	for (int i=x;i<=n;i+=lowbit(i))
		c[i]+=v;
}

void init(){
	for (int i=1;i<=n;i++){
		c[i]=a[i];
		for(int j=i-1;j>i-lowbit(i);j-=lowbit(j))
			c[i]+=c[j];//注意,这里是加c[j]而不是a[j]
	}
}

int main(){
	scanf("%d%d",&n,&m);
	a[0]=0;c[0]=0;
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	init();

	for (int i=1;i<=m;i++){
		scanf("%d",&t);
		if (t==1){
			scanf("%d%d",&x,&v);
			modify(x,v);
		}else{
			scanf("%d%d",&x,&y);
			printf("%d\n",sum(y)-sum(x-1));
		}
	}

	return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值