线段树有的时候是比较复杂且没有必要的,于是就会造成很大的时间复杂度和空间复杂度。于是,树状数组是线段树在某些情况下很大的优化。
首先先代入一张数状数组的图。
(注:此图乃是转载之)
图中将c数组标记出来。
C1=a1
C2=a1+a2
C3=a3
C4=a1+a2+a3+a4
C5=a5
C6=a5+a6
C7=a7
C8=a1+a2+a3+a4+a5+a6+a7+a8
到了c8停止我们可以看到8分为可以分为1-4,5-8两个部分,而这两个部分又可以分为1-2与3-4和5-6和7-8,在其他情况下也亦是这样。
那么很容易可以看出,树状数组是利用二分的思想来的。
那么接下来就是很巧妙的地方。
有一个东西two,那么two(k)表示的意思是将k的后缀到第一个1位置的数(二进制情况下),也就是说在k的二进制情况下,保留后面的0和最后一个1.
具体怎么做呢?
Two(k)=k&(-k)
举一个例子:
E.g:
当k为10是
(10)10=(1010)2
(-10)10=(0110)2 //显而易见,一个正数的二进制情况下的负数形式是正数的二进制取反加一。
那么
1010
&0110
----------
0010
也就是(10)2
对于一个树状数组c,c[k]表示从a[k]开始往左two(k)个数的和,这个在图中很容易可以看出来。
那么怎么对于一个树状数组进行更改呢?
这也用到了two这个东西,是的,特别巧妙。
再在图中找一个样例吧。
就比如我们当前要把a3更改为5。
那么我们就必须更改c3,c4和c8,这一点在图中也可以看出来。
那么(3)10=(0011)2
(4)10=(0100)2
(8)10=(1000)2
可以在二进制中发现,在二进制中,每一次都往高位改1
那么就可以借助two继续修改,也就是设当前这个位置在k,那么下一个位置就是在k+two(k)。将例子带进来看一下。
(0011)2+(0011)2&(-0011)2
=(0011)2+(0011)2&(1101)2
=(0011)2+(0001)2
=(0100)2
=(4)10
下一个就不举例了,是不是很神奇?
那为什么会这么神奇呢?主要是因为树状数组的二分的分组性质吧,这么奇妙的东西我只能抽象化的理解,就比如一个c,他的父亲肯定包含他,他的爷爷肯定也包含他,以此类推。
那么再说说求和的问题。
前面说过,c[k]表示从a[k]开始往左two(k)个数的和。
比如求a2到a4的和
那么就可以将将1到4的和找出来,再将1到2-1的和找出来,相减,很显然是一个前缀和的做法,就不必多说了。
那么接下来就看一下树状数组是怎么实现的。
修改:
void xiugai(int v,int p)
{
while (p<=n)
{
c[p]=c[p]+v;
p=p+two(p);
}
}
求和
int qiuhe(int z)
{
int sum1=0;
while (z!=0)
{
sum1=sum1+c[z];
z=z-two(z);
}
return sum1;
}
关于two很简单
int two(int x)
{
return (x&(-x));
}
这就是最基本的树状数组,应根据不同的题中不同情况来决定如何使用。
难点:
1,如何理解树状数组的思想(√)
2,two的具体作用(×)
最后,将树状数组的思想稍微抽象化一点,从二进制来考虑树状数组的奇妙应该不难了(即使我现在还是没有完全弄懂)
题目:
1,求和(√)
2,星星点灯(√)
3,指纹(×)