数据结构----树状数组

重温了一遍树状数组,已经忘得差不多了。。。


第01讲 什么是树状数组?

树状数组用来求区间元素和,求一次区间元素和的时间效率为O(logn)。

有些同学会觉得很奇怪。用一个数组S[i]保存序列A[]的前i个元素和,那么求区间i,j的元素和不就为S[j]-S[i-1],那么时间效率为O(1),岂不是更快?

但是,如果题目的A[]会改变呢?例如:

我们来定义下列问题:我们有n个盒子。可能的操作为

1.向盒子k添加石块

2.查询从盒子i到盒子j总的石块数

自然的解法带有对操作1为O(1)而对操作2为O(n)的时间复杂度。但是用树状数组,对操作1和2的时间复杂度都为O(logn)。


第02讲 图解树状数组C[]

现在来说明下树状数组是什么东西?假设序列为A[1]~A[8]


 

网络上面都有这个图,但是我将这个图做了2点改进。

(1)图中有一棵满二叉树,满二叉树的每一个结点对应A[]中的一个元素。

(2)C[i]为A[i]对应的那一列的最高的节点。

现在告诉你:序列C[]就是树状数组。

那么C[]如何求得?

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[5]=A[5];

C[6]=A[5]+A[6];

C[7]=A[7];

C[8]= A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];

以上只是枚举了所有的情况,那么推广到一般情况,得到一个C[i]的抽象定义:

因为A[]中的每个元素对应满二叉树的每个叶子,所以我们干脆把A[]中的每个元素当成叶子,那么:C[i]=C[i]的所有叶子的和。

现在不得不引出关于二进制的一个规律:

先仔细看下图:


将十进制化成二进制,然后观察这些二进制数最右边1的位置:

1 --> 00000001

2 --> 00000010

3 --> 00000011

4 --> 00000100

5 --> 00000101

6 --> 00000110

7 --> 00000111

8 --> 00001000

1的位置其实从我画的满二叉树中就可以看出来。但是这与C[]有什么关系呢?

接下来的这部分内容很重要:

在满二叉树中,

以1结尾的那些结点(C[1],C[3],C[5],C[7]),其叶子数有1个,所以这些结点C[i]代表区间范围为1的元素和;

以10结尾的那些结点(C[2],C[6]),其叶子数为2个,所以这些结点C[i]代表区间范围为2的元素和;

以100结尾的那些结点(C[4]),其叶子数为4个,所以这些结点C[i]代表区间范围为4的元素和;

以1000结尾的那些结点(C[8]),其叶子数为8个,所以这些结点C[i]代表区间范围为8的元素和。

扩展到一般情况:

i的二进制中的从右往左数有连续的x个“0”,那么拥有2^x个叶子,为序列A[]中的第i-2^x+1到第i个元素的和。

终于,我们得到了一个C[i]的具体定义:

C[i]=A[i-2^x+1]+…+A[i],其中x为i的二进制中的从右往左数有连续“0”的个数。


第03讲 利用树状数组求前i个元素的和S[i]

理解了C[i]后,前i个元素的和S[i]就很容易实现。

从C[i]的定义出发:

C[i]=A[i-2^x+1]+…+A[i],其中x为i的二进制中的从右往左数有连续“0”的个数。

我们可以知道:C[i]是肯定包括A[i]的,那么:

S[i]=C[i]+C[i-2^x]+…

也许上面这个公式太抽象了,因为有省略号,我们拿一个具体的实例来看:

S[7]=C[7]+C[6]+C[4]

因为C[7]=A[7],C[6]=A[6]+A[5],C[4]=A[4]+A[3]+A[2]+A[1],所以S[7]=C[7]+C[6]+C[4]

(1)i=7,求得x=0,那么我们求得了A[7];

(2)i=i-2^x=6,求得x=1,那么求得了A[6]+A[5];

(3)i=i-2^x=4,求得x=2,那么求得了A[4]+A[3]+A[2]+A[1]。

讲到这里其实有点难度,因为S[i]的求法,如果要讲清楚,那么得写太多的东西了。所以不理解的同学,再反复多看几遍。

从(1)(2)(3)这3步可以知道,求S[i]就是一个累加的过程,如果将2^x求出来了,那么这个过程用C++实现就没什么难度。

现在直接告诉你结论:2^x=i&(-i)

证明:设A’为A的二进制反码,i的二进制表示成A1B,其中A不管,B为全0序列。那么-i=A’0B’+1。由于B为全0序列,那么B’就是全1序列,所以-i=A’1B,所以:

i&(-i)= A1B& A’1B=1B,即2^x的值。

所以根据(1)(2)(3)的过程我们可以写出如下的函数:

int Sum(int i) //返回前i个元素和

{

       int s=0;

       while(i>0)

       {

              s+=C[i];

              i-=i&(-i);

       }

       return s;

}


第04讲 更新C[]          

正如第01讲提到的小石块问题,如果数组A[i]被更新了怎么办?那么如何改动C[]?

如果改动C[]也需要O(n)的时间复杂度,那么树状数组就没有任何优势。所以树状数组在改动C[]上面的时间效率为O(logn),为什么呢?

因为改动A[i]只需要改动部分的C[]。这一点从第02讲的图中就可以看出来:


如上图:

假如A[3]=3,接着A[3]+=1,那么哪些C[]需要改变呢?

答案从图中就可以得出:C[3],C[4],C[8]。因为这些值和A[3]是有联系的,他们用树的关系描述就是:C[3],C[4],C[8]是A[3]的祖先。

那么怎么知道那些C[]需要变化呢?

我们来看“A”这个结点。这个“A”结点非常的重要,因为他体现了一个关系:A的叶子数为C[3]的2倍。因为“A”的左子树和右子树的叶子数是相同的。 因为2^x代表的就是叶子数,所以C[3]的父亲是A,A的父亲是C[i+2^0],即C[3]改变,那么C[3+2^0]也改变。

我们再来看看“B”这个结点。B结点的叶子数为2倍的C[6]的叶子数。所以B和C[6+2^1]在同一列,所以C[6]改变,C[6+2^1]也改变。

推广到一般情况就是:

如果A[i]发生改变,那么C[i]发生改变,C[i]的父亲C[i+2^x]也发生改变。

这一行的迭代过程,我们可以写出当A[i]发生改变时,C[]的更新函数为:

void Update(int i,int value)  //A[i]的改变值为value

{

       while(i<=n)

       {

              C[i]+=value;

              i+=i&(-i);

       }

}

注意点:

(1)每个节点会有几个孩子?
lowbit(x)表示x&(-x)

(2)每个节点直接父亲节点是哪个节点?
x+lowbit(x)为x的直接父亲编号

(3)二维写法

void update(int X,int Y,int val){
	int x,y; 		//注意要令x,y为新的值
	for (x=X; x<=MN; x+=lowbit(x))
		for (y=Y; y<=MN; y+=lowbit(y))
			C[x][y]+=val;
}

int getsum(int X,int Y){
	int x,y,sum=0;
	for (x=X; x>0; x-=lowbit(x))
		for (y=Y; y>0; y-=lowbit(y))
			sum+=C[x][y];
	return sum;
}

参考例题:

POJ 2299

POJ 2352

POJ 2481
POJ 3067
POJ 1195
POJ 2029
POJ 2182
HDU 2852

参考链接:

树状数组详解(转)


搞懂树状数组




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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值