树状数组经典用法:
对普通数组进行M次修改或求和,时间复杂度为O(M*N),N为修改或求和需要扫描的区间大小。而对于树状数组,时间复杂度则为O(M*lgN)。加了一个lg,学过数学的我们应该都知道差距有多大。
#include<stdio.h>
/*函数lowbit(x)作用就是返回x的二进制表示中最后一位1的权值 */
int lowbit(int x)//位运算,利用计算机补码特性
{
return x&-x;
}
int main()
{
int n=10;//00000011
printf("%d\n",lowbit(10));
return 0;
}
/*
假设x=10
10的二进制:1010,我们知道,一个数前加负号,就是用这个数的二进制取反加一。
-10的二进制:0101+1=0110
然后位运算符&(按位与),1010&0110=10,十进制表示就是2 ,同1为1,其余为零。
所以lowbit(10)=2
*/
图中有两个数组,数据都接收到底层数组a中,而数组c则是树状数组(看形状是不是很像个树)。 从图中可以直观的看到 。
1的低位1
2的低位2
3的低位1
4的低位4
5的低位1
6的低位2
7的低位1
8的低位8
--------------------------------
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];
假设结点为n,那么结点n所管辖的区间为2的lowbit(n)次方
即:c[n]=a[n-(2^lowbit(n))+1]+…..+a[n];
这样通过lowbit函数,把底层数组a和树状数组c联系了起来。
假设n=8,lowbit(8)=8,while循环只持续了一次,得到的结果c[8]也与实际吻合
(发现没有,求前8位和,实际上只循环运算了1次!)
假设n=6,lowbit(6)=2,获取c[6]=a[5]+a[6]后,
n=4;lowbit(4)=4;获取c[4]=a[1]+a[2]+a[3]+a[4];
最终获取的就是a[1]+..+a[6];
(求前6位和,实际只循环运算了2次!)
还理解不了?那简单,把代码背下来,记住这个函数的返回值就是前n位和。
下面是修改,将数组a的第k位增加(或减少)num
代码
void add(int k,int num)
{
while(k<=n)//n是树状数组的大小
{
c[k]+=num;
k=k+lowbit(k);//由于每次跳lowbit(k)位,所以时间复杂度大大降低,为什么跳lowbit(k)位操作,因为你求和的时候也是跳 //lowbit(k)位求和啊。
}
}
是不是很像一颗树?对,这就是为什么叫树状数组了~先看A图,a数组就是我们要维护和查询的数组,但是其实我们整个过程中根本用不到a数组,你可以把它当作一个摆设!c数组才是我们全程关心和操纵的重心。
先由图来看看c数组的规则,其中c8 = c4+c6+c7+a8,c6 = c5+a6……先不必纠结怎么做到的,我们只要知道c数组的大致规则即可,很容易知道c8表示a1~a8的和,但是c6却是表示a5~a6的和,为什么会产生这样的区别的呢?或者说发明她的人为什么这样区别对待呢?答案是,这样会使操作更简单!看到这相信有些人就有些感觉了,为什么复杂度被lg了呢?可以看到,
c8可以看作a1~a8的左半边和+右半边和,
而其中左半边和是确定的c4,右半边其实也是同样的规则把a5~a8一分为二……继续下去都是一分为二直到不能分,可以看看B图。怎么样?是不是有点二分的味道了?对,说白了树状数组就是巧妙的利用了二分,她并不神秘,关键是她的巧妙!
她又是怎样做到不断的一分为二呢?说这个之前我先说个叫lowbit的东西,lowbit(k)就是把k的二进制的高位1全部清空,只留下最低位的1,比如10的二进制是1010,则lowbit(k)=lowbit(1010)=0010(2进制)。
介于这个lowbit在下面会经常用到,这里给一个非常方便的实现方式,
比较普遍的方法lowbit(k)=k&-k,这是位运算,我们知道一个数加一个负号是把这个数的二进制取反+1,如-10的二进制就是-1010=0101+1=0110,然后用1010&0110,答案就是0010了!明白了求解lowbit的方法就可以了,继续下面。介于下面讨论十进制已经没有意义(这个世界本来就是二进制的,人非要主观的构建一个十进制),下面所有的数没有特别说明都当作二进制。
上面那么多文字说lowbit,还没说它的用处呢,
它就是为了联系a数组和c数组的!ck表示从ak开始往左连续求lowbit(k)个数的和,
比如c[0110](6)=a[0110](6)+a[0101](5),就是从110(6)开始计算了0010(2)个数的和,因为lowbit(0110)(6)=0010(2),可以看到其实只有低位的1起作用,因为很显然可以写出c[0010]=a[0010]+a[0001],这就为什么我们任何数都只关心它的lowbit,因为高位不起作用(基于我们的二分规则它必须如此!),
除非除了高位其余位都是0,这时本身就是lowbit。
既然关系建立好了,看看如何实现a某一个位置数据跟改的,她不会直接改的(开始就说了,a根本不存在),她每次改其实都要维护c数组应有的性质,因为后面求和要用到。而维护也很简单,
比如更改了a[0011](3),我们接着要修改c[0011](3),c[0100](4),c[1000](8),这是很容易从图上看出来的,但是你可能会问,他们之间有申明必然联系吗?每次求解总不能总要拿图来看吧?
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];
其实从0011(3)——>0100(4)——>1000(8)的变化都是进行“去尾”操作,又是自己造的词--'',我来解释下,就是把尾部应该去掉的1都去掉转而换到更高位的1,记住每次变换都要有一个高位的1产生,所以0100是不能变换到0101的,因为没有新的高位1产生,这个变换过程恰好是可以借助我们的lowbit进行的,k +=lowbit(k)。
好吧,现在更新的次序都有了,可能又会产生新的疑问了:为什么它非要是这种关系啊?
这就要追究到之前我们说c8可以看作a1~a8的左半边和+右半边和……的内容了,为什么c[0011]会影响到c[0100]而不会影响到c[0101],这就是之前说的c[0100]的求解实际上是这样分段的区间 c[0001]~c[0001] 和区间c[0011]~c[0011]的和,数字太小,可能这样不太理解,在比如c[0100]会影响c[1000],为什么呢?因为c[1000]可以看作0001~0100的和加上0101~1000的和,但是0101位置的数变化并会直接作用于c[1000],因为它的尾部1不能一下在跳两级在产生两次高位1,是通过c[0110]间接影响的,但是,c[0100]却可以跳一级产生一次高位1。
可能上面说的你比较绕了,那么此时你只需注意:c的构成性质(其实是分组性质)决定了c[0011]只会直接影响c[0100],而c[0100]只会直接影响[1000],而下表之间的关系恰好是也必须是k +=lowbit(k)。
此时我们就是写出跟新维护树的代码:
void add(int k,int num)
{
while(k<=n)
{
tree[k]+=num;
k+=k&-k;
}
}
有了上面的基础,说求和就比较简单了。比如求0001(1)~0110(6)的和就直接c[0100](4)+c[0110](6),分析方法与上面的恰好逆过来,而且写法也是逆过来的,具体就不累述了:
int read(int k)//1~k的区间和
{
int sum=0;
while(k)
{
sum+=tree[k];
k-=k&-k;
}
return sum;
}
三、总结一下吧
首先,明白树状数组所白了是按照二分对数组进行分组;维护和查询都是O(lgn)的复杂度,复杂度取决于最坏的情况,也是O(lgn);lowbit这里只是一个技巧,关键在于明白c数组的构成规律;分析的过程二进制一定要深入人心,当作心目中的十进制。
应用:南阳理工学院oj,士兵杀敌1
#include<iostream>
#include<stdio.h>
using namespace std;
int a[1000005]={0},c[1000005]={0},N;
int boobit(int x)
{
return x&(-x);
}
inline int sum(int i)
{
int s=0;
while(i>0)
{
s+=c[i];
i-=i&(-i);
}
return s;
}
int main()
{
int m,n,M,i=0,j=0,k=0;
scanf("%d%d",&N,&M);
for(i=1;i<=N;i++)
{
scanf("%d",&a[i]);
for(k=i-boobit(i)+1;k<=i;k++)
c[i]=c[i]+a[k];
}
while(M--)
{
scanf("%d%d",&m,&n);
printf("%d\n",sum(n)-sum(m-1) );
}
return 0;
}
【引言】
在解题过程中,我们有时需要维护一个数组的前缀和S[i]=A[1]+A[2]+...+A[i]。
但是不难发现,如果我们修改了任意一个A[i],
S[i]、S[i+1]...S[n]都会发生变化。
可以说,每次修改A[i]后,调整前缀和S[]在最坏情况下会需要O(n)的时间。当n非常大时,程序会运行得非常缓慢。因此,这里我们引入“树状数组”,它的修改与求和都是O(logn)的,效率非常高。
【理论】
为了对树状数组有个形 象的认识,我们先看下面这张图。
如图所示,红色矩形表示的数组C[]就是树状数组。
这里,C[i]表示A[i-2^k+1]到A[i]的和,而k则是i在二进制时末尾0的个数,或者说是i用2的幂方和表示时的最小指数。( 当然,利用位运算,我们可以直接计算出2^k=i&(i^(i-1)) )同时,我们也不难发现,这个k就是该节点在树中的高度,因而这个树的高度不会超过logn。
所以,当我们修改A[i]的值时,可以从C[i]往根节点一路上溯,调整这条路上的所有C[]即可,这个操作的复杂度在最坏情况下就是树的高度即O(logn)。
另外,对于求数列的前n项和,只需找到n以前的所有最大子树,把其根节点的C加起来即可。不难发现,这些子树数目是n在二进制时1的个数,或者说是把n展开成2的幂方和时的项数,因此,求和操作的复杂度也是O(logn)。
接着,我们考察这两种操作下标变化的规律:
首先看修改操作:
已知下标i,求其父节点的下标。我们可以考虑对树从逻辑上转化:
//求前n项和 :树状数组
# include <stdio.h>
# define lowbit(x) x&(-x)
int a[1000001],b;
void add(int i, int x)
{
while (i<b)
{
a[i]+=x;
i+=lowbit(i);
}
}
int sum(int m)
{
int sum=0;
while(m>0)
{
sum+=a[m];
m-=lowbit(m);
}
return sum;
}
int main()
{
int i, c, m, x, y;
//scanf("%d%d", &b, &m);
b=5;
m=2;
int a[10]={0,1,2,3,4,5};
for (i = 1; i<=b; i++)
{
//scanf("%d", &c);
add(i, a[i]);
}
printf("%d\n", sum(4));
return 0;
}