标签:快速 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