码迷,mamicode.com
首页 > 编程语言 > 详细

树状数组

时间:2017-04-26 23:02:33      阅读:286      评论:0      收藏:0      [点我收藏+]

标签:快速   alt   防止   i+1   replace   题解   下标   fine   概述   

树状数组

                  转自:      By 岩之痕  http://blog.csdn.net/zearot/article/details/45028723

一、概述

树状数组是一种 用数组进行存储的 自下而上进行操作的  多叉树。

以下叙述中,A为原数组,C为树状数组所用的数组,B为特殊情况需要用到的辅助数组。

最基本的应用就是维护一个支持两种操作的数列:1.让A[i]加上某数X     2.求一个区间A[L] + A[L+1] + ... + A[R] 的和。

树状数组可以做到两个操作的时间复杂度都是O(log2(n)),而且一般比线段树快。

树状数组下标从1开始。

 

二、建树   (图中以n=11的数组为例进行建树)

令C数组的每个元素与A数组相等,这时的C称为第一行,见下图:(操作11次)

技术分享

然后把第一行的C中,奇数项不动,偶数项每个加上自己的前一项。即C[2]+=C[1],C[4]+=C[3], C[6]+=C[5]......(操作5次)见下图:

技术分享

上图第一行就是最终的C数组的值了,这时对第二行进行同样的操作,也就是偶数项都加上自己的前一项,C[4]+=C[2],C[8]+=C[6],.....(操作2次)见下图:

技术分享

上图第二行剩下的数也是最终的C的值了,然后对第三行进行同样的操作:C[8]+=C[4]...   (操作1次)下图是最终形成的数组,明显看出这是一颗无根的多叉树。

技术分享

                                     图一:n=11的建好之后的树状数组

建树代码:

 

 
void Build(int n){//运行函数之前C数组的每个数都等于A数组的每个数
    int X=2,HalfX=1;
    while(X <= n){
    	for(int i=X;i<=n;i+=X){
    		C[i]+=C[i-HalfX];
    	}
    	X <<= 1;HalfX <<= 1;
    }
}

这样理论上操作次数是  2*n - count_bits(n)   ,  其中count_bits(n)返回n的二进制表示中1的个数。

 

对于n=11 的情况   2*11 - count_bits(11) = 19 次。 若不保留A数组,直接将A数组变成C数组,那么操作次数就是 n - count_bits(n) (少了第一次的n个赋值).

总之是O(n)的建树。

但是一般更简单的写法是先清空C数组,然后调用n次Add(i,A[i])函数,这样时间复杂度是O(n*log2(n)) 。

很奇怪的是,在用HDU1166(点修改区间查询)这道题进行测试的时候,第一种方法反而比第二种慢,没想通。

不过,这么建树还是有好处的,比如在 做动态区间第k大的时候,用树状数组套可持久化线段树这种方法的话,

这么建树的空间消耗是明显小于调用n次Add的。又或者,如果维护的性质的“加法”很耗时间的话,也可以考虑直接建树。

 

三、Add 函数(修改某一项)

首先来理解如何对上述数组进行修改(下图中沿着斜线向上走,经过的节点都要修改)。

再来看一下上面最后一张图:

技术分享

注意到:

第  1   行C的下标为1*(1,3,5,7,...)   1的父节点是2。3的父节点是4。5的父节点是6。7的父节点是8。9的父节点是10.      每个C存了1个A中的数

第  2   行C的下标为2*(1,3,5,7,..)    下标除掉2以后,1的父节点是2。3的父节点是4。                                                             每个C存了2个A中的数

第  3   行C的下标为4*(1,3,5,7...)    下标除掉4以后,1的父节点是2。                                                                                  每个C存了4个A中的数

第k+1行C的下标为2^k*(1,3,5,7..) 下标除掉2^k以后,1的父节点是2。3的父节点是4等等                                                      每个C存了2^k个A中的数

现在问题就是拿到一个下标 x ,怎么去找 x 的父节点的问题(因为更新完x,也必须去更新x的父节点)

假设 x =  (2^k) *  某奇数 ,则根据上述规律, x 的父节点下标其实是: (某奇数+1)* (2^k) = 某奇数*(2^k) + (2^k) = x+ (2^k) 

于是只要通过 x 快速找到这个 (2^k) 就可以找到 x 的父节点了,而树状数组中很重要的函数lowbit 刚好就可以求出 (2^k) 

即,若 x =  (2^k) *  某奇数  则 lowbit(x) = 2^k    (关于这部分内容去看第五部分:lowbit 函数)

于是就不难看懂如下Add代码:

 

 
void Add(int x,int c){
	if(!x) return;//防止死循环
	while(x <= n){
		A[x]+=c;
		x+=lowbit(x);
	}
}


四:Sum函数(求前x项的和)

 

Sum中x节点存的数的个数是lowbit(x),

C[x]=A[x-lowbit(x)+1] + A[x-lowbit(x)+2] +..+A[x-1]+A[x]

所以结果加上了C[x]之后,紧接着是要去找A[x-lowbit(x)]的值,于是不难看懂如下Sum代码:

 

 
int Sum(int x){//前x项的和
    int ANS=0;
   	while(x>0){
   		ANS+=A[x];
   		x-=lowbit(x);
   	}
    return ANS;
}


五:lowbit函数

 

首先是lowbit的定义:

 

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

技术分享

 

图中~i是对i按位取反,由于机器中数字是用补码存的,所以- i =(~i)+1

x&(-x) 自然就可以求出lowbit(x) 了。

六、时间复杂度与空间复杂度

令 k1=x+lowbit(x) . k2=x-lowbit(x). 易得lowbit(k1) > x   且  lowbit(k2) > x.

即k1和k2都至少比 x 高一层,x的层数越来越高,而总层数为  floor( log2(n) )+1 于是Add和Sum操作都是O(log2(n))的。

跟线段树比较的话,线段树的修改操作几乎要更改  floor( log2(n) )+1 个节点,

而树状数组最多才改  floor( log2(n) )+1 节点,最少只用改1个节点(比如n=11的时候更改A[8])

树状数组的优点第一是快,第二是它真真正正只用了1倍空间,都不需要向上扩充到2的某个次方。

线段树是要先把n向上扩充到最小的某个2的次方,然后再乘个2(一般为了省事就直接开4*n的数组了)。

《统计的力量》里面提到的用非递归线段树省掉一半空间(省掉所有右子树)也可以用一倍空间的说法其实不是很对,

就算非递归也是需要向上扩充到最小的某个2的次方的。

省掉一半空间的非递归线段树需要的数组元素个数为:技术分享

而它的范围见右边公式:技术分享

所以是需要向上扩充一定的空间的,n个数组元素是不够用的。

为了方便,直接扩充到大于n的最小2的次方就行了(省了一半空间,但也只能求前缀和,不能直接求区间和了)。

附上省掉一半空间的非递归线段树代码:

 

 
int N;
int A[maxn],sum[maxn];
int Sum(int x){//前x项的和 
    int ANS=0;
    for(int t=x+N;t^1;t>>=1){
        if(t&1) ANS+=sum[t/2];//在省掉空间之前是要访问节点t^1的,由于t^1一定为偶数,所以用(t^1)/2 = t/2来储存原来t^1的节点内容</span>
    }//由于这样做,所有奇数节点都不会被访问,所以奇数节点全部不存了(省掉一半空间)。</span>
    return ANS;
}
void Add(int x,int c){
	for(int t=x+N-1;t^1;t>>=1){
		if(~t&1) sum[t/2]+=c;
	}
}
void Build(int n){//n为数组元素个数,将数组存入A中,然后调用Build然后就可以正常的使用Add和Sum函数了 
	N=1;while(N <= n) N <<= 1;
	memset(sum,0,sizeof(sum));
    for(int i=1;i<=n;++i) Add(i,A[i]);
}

 

 

再来说说树状数组的建树效率:

按之前的图片那样每次筛选出偶数来建树的的操作次数为技术分享

而直接使用n次Add函数的操作次数为:技术分享,这个式子好像没办法再化简了。

但它每一项约等于n/2,共技术分享个非零项,所以总复杂度是技术分享

 

七、树状数组其它用法
第一种:区间修改,点查询

思想其实是化区间修改为点修改,不使用C[i]数组了。

增加B数组。假设B[i]=t , 则表示对区间A[i],A[i+1],...,A[n]都增加了t.

那么,把区间[L,R]的数都加上X的操作就变成了两个点修改:B[L]+=X  和  B[R+1]-=X;//这里是简写,还需要更新它们的父节点

然后查询点X的时候,要求B[1]+B[2]+...+B[X]的和再加上A[X],因为这些点上的修改都会影响到X.

第二种:区间修改,区间查询

这里就用到一点数学了,用a[i]表示A[i]-A[i-1](假设A[0]=0)

 

用S[i]表示a[1]+a[2]+...+a[i] ,用 P[i] 表示 a[1]+2*a[2]+3*a[3]+...+i*a[i]

则前缀和的前缀和 SS[x]=S[1]+S[2]+S[3]+...+S[x]=n*a[1]+(n-1)*a[2] +...+2*a[x-1] + a[x] = (n+1)S[x]- P[x]

于是只要对差分数列维护S[i]和P[i]两个性质就好了。

S[i]维护原数组为a[i]的前缀和(=A[i]),P[i]维护原数组为 i*a[i]的前缀和

区间修改的话,A[L]-A[L-1]多了X。A[R+1]-A[R] 少了X。即 a[L]+=X.  a[R+1]-=X; 两个点修改

每个点都要修改S和P两个数组。

代码如下:

 

 
#define LL long long
#define maxn 100001
int N,Q;
LL A[maxn];
LL P[maxn],S[maxn];
void AddP(LL x,LL v){
	if(!x) return;
	while(x <=N){
		P[x]+=v;
		x+=x&-x;
	}
} 
void AddS(LL x,LL v){
	if(!x) return;
	while(x <=N){
		S[x]+=v;
		x+=x&-x;
	}
}
LL SumP(LL x){
	LL ans=0;
	while(x>0){
		ans+=P[x];
		x-=x&-x;
	}
	return ans;
} 
LL SumS(LL x){
	LL ans=0;
	while(x>0){
		ans+=S[x];
		x-=x&-x;
	}
	return ans;
} 
void INC(LL L,LL R,LL C){//区间[L,R]都加C 
	AddS(L,C);AddS(R+1,-C);
	AddP(L,L*C);AddP(R+1,-(R+1)*C);
}
LL QUE(LL a){//询问前缀和 
	return (a+1)*SumS(a)-SumP(a);
}



 

第三种:二维树状数组

二维树状数组就是可以在技术分享的时间内求一个矩阵的一个矩形区域的和,和改变矩阵中一点的值。

理解了一维的树状数组的话,仔细想想就知道二维的情况了。

 

 
int n;
int A[1002][1002];
void Add(int x,int y,int K){//A[x][y]增加K 
	while(x <= n){
		int yy=y; 
		while(yy <= n){
			A[x][yy]+=K;
			yy+=yy&-yy; 
		}
		x+=x&-x;
	}
}
int Sum(int x,int y){//求矩阵i<=x,j<=y的所有元素和
	int ANS=0;
	while(x > 0){
		int yy=y;
		while(yy > 0){
			ANS+=A[x][yy];
			yy-=yy&-yy;
		}
		x-=x&-x;
	}
	return ANS;
}

 

 

八、结语

以前学了线段树,听说树状数组功能比线段树少,就一直没有学,直到去查 动态区间第k大的题解的时候,满眼的 树状数组套主席树

于是决定学一下树状数组,然后发现树状数组还是很好用的,代码简单而且效率高,只用开n的空间不需要向上扩充。

效率上非递归线段树其实也差不多,但是非递归线段树的要进行复杂的下标变换,每次写都要想半天,

还是树状数组写起来简洁一些,所以能写树状数组的就可以避免写复杂的线段树了。

树状数组

标签:快速   alt   防止   i+1   replace   题解   下标   fine   概述   

原文地址:http://www.cnblogs.com/mhpp/p/6771422.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!