树状数组是一种常见的计算优化方法,复杂度一般为nlog(n),有着非常强大的功能,如统计前缀和,部分和,逆序对,dp优化等都可以解决,有人常说它是线段树的简化版,但是它也有一定的局限性,对于很多区间更新统计的问题,树状数组往往有心无力,鉴于大部分资料都以树的形式来讲解,刚开始我也是这样入门的,现在重新看一遍,发现有了更加深刻的理解,同时也感慨位运算的神奇以及对发明树状数组那个人的无比敬仰。
树状数组,顾名思义它是一个数组,保存形式是树状的,很容易想到《数据结构》里的二叉树,但是这里并不是二叉树,确切的说,应该是多叉树,想看的可以去百度百科看看,那里有配图。
树状数组的巧妙所在,它利用了数位统计的分段统计的方法,结合二进制的表示方法,构造出了一颗树,一颗没有边的树。理解数状数组我觉得要从它的分段方法,和二进制表示法出发讲解。
先看个例子:
数组 a1=2,a2=1,a3=4,a4=2,a5=5,a6=3,a7=6,a8=4,a9=1,a10=2,a11=4,a12=7,a13=9,a14=8,a15=5,a16=6
1111(b)这是15的二进制表示,假设我们要统计元素1~15的总和,那么我们可以分为4段统计,0001~1000(1~8),1001~1100(9~12),1101~1110(13~14),1111~1111(15~15),可以看出分段的方法,以2^k这样长度为一段,4段分别对应2^3,2^2,2^1,2^0.
讲完分段后,我们来看看树状数组的表示方法,树状数组里面放的究竟是什么呢?我们设树状数组为c[size],那么
c[i]表示什么呢,再举个例子,我们要统计前10个元素的总和,我们令c[10]=c[1010(b)]=a[1001(b)]+a[1010(b)];也就是表示以最后一位1的位置划分出来的段长,这里最后一位1在第2位,那么就是2^1长度,表示从第i个数到前2^k(即i的二进制数最后一位1表示的位权)个数的总和,这和我们上面例子讲的分段方法完全相同,c[1010]可以这样理解,元素号前两位为10且<=1010的元素的总和,也就是2^1长度元素的总和,是不是有种数位统计的感觉,其实差不多,只是,树状数组可以修改调整,简单的来说,树状数组是动态的,数位统计是静态的,树状数组巧妙的设计,使得二进制可以表示一颗多叉树上所有的联系。
那么我们统计c[10]=c[1010]的时候,我们就可以先统计c[1010]的值,即a[1010]+a[1001];然后我们就可以减掉这两个数,即变成统计前8个元素的总和,c[8]=c[1000]=a[0001]+a[0010]+.....+a[1000]=sum[8],显然可以发现c[2^k]表示的就是1~2^k总和。那么前10个元素的总和就是sum = c[10] + c[8];
以2^k为分段长度,以及树状数组c的含义知道后,可以知道,要统计前n项和,我们必须把n分成多个2^k段,长度不可能相同,统计的时候只要统计它包含的所有长度段就可以知道要加上哪些树状数组元素,这里的段长表示就是n二进制时最后一位1的位权,所以只要从最后找到1的位置就ok了。
那么我们怎么样找到最后一位呢?前人已经为我们开路了,找x的最后一位1的位权lowbit=x&(-x)。
这里我们只讲了怎么样统计,却没有讲怎么样插入,有了统计的铺垫,查找就变的简单了,显然只要包含插入元素的树状数组才需要改变,假设我们要插入a[i],那么哪些数组包含a[i]呢?a[i]至少包含在一个段长中,只要找到这个最短段长即可,显然包含在最小那个段长中,那么我们也只需要找到最后一个1的位权即可,那么下一个要更新的就是x=i+lowbit(i)元素了,x更新了,那么包含x的也应该要更新,y=x+lowbit(x);这样一直下去直到超过最大的元素标号为止。
具体实现代码如下
int lowbit(int n)
{
return n&(-n);
}
int sum(int n)
{
int ans=0;
while(n)
{
ans+=c[n];
n-=lowbit(n);
}
return ans;
}
void up(int n,int k)
{
int i;
for(i=n;i<=tem;i+=lowbit(i))
{
c[i]+=k;
}
}