树状数组的作用是求区间和和做点更新。
和线段树相比,线段树和树状数组时间复杂度都是nlogn,但是在空间利用上,我们知道,线段树接近满二叉,需要的空间是4倍N,但是我们树状数组的空间和N相同,而且在代码上有明显的优势。
这里我们先看两幅图:
树状数组就是上面的C[],表示的是下面这些数的和。
这里我们可以看到,所有的奇数位置都会表示它本身,如1,3,5,7……它们只包含自己
所有的2的倍数的位置都会除了本身都会包含它前面一个数,如2,4,6,8……除了他们本身,还包含了前一个数的和。
所有4的倍数都会再包含再之前的两个数,如4,8……不仅包含了本身和它前面一个数,还包含了再前面两个数的和。
所有8的倍数都会再包含再之前的4个数如8……
……
所有能被2^n整除的数都表示从该点前2^n个数的和。
通过上面这规律,如果我们要求1~n的和,只需要把它拆成二进制数的组合就行了。
比如求1~45的和,就等于c32+c40+c44+c45,这里因为c32表示1~32的和(能被32整除),c40表示33到40的和(能被8整除),c44表示41到44的和(能被4整除),c45表示45到45的和。
所以我们用树状数组的做法可以达到时间上是nlogn,空间上是n的效率。
那么我们如何实现呢?假如我们要寻找1~n的和。
首先我们得构建这个树状数组。
我们还是从左到右一步一步来。
假如n=8,我们的8个数分别是1 8 7 6 5 2 4 3
我们加第一个点,c1=1,那么反向推,c2会包含c1值,所以c2=c2+c1,c4会包含c2的值,所以c4=c4+c2,同理c8=c8+c4。所以我们第一次建完树是这样的。
第二次我们加点2,那么显然2,4,8处的点会被更新。
第三次我们加入点3,只有3,4,8会被更新。
第四次我们加点4,只有4和8要更新。
同理,加点5更新5,6,8;加点6更新6,8;加点7更新7,8;加点8更新8。
手动模拟建树我们会了,如何用代码实现呢,或许你有一大堆想法,但可能都不及下面这么简单:
int lowbit(int x)
{
return x&(-x);
}
void modify(int x,int n,int val)
{
for(int i=x;i<=n;i=i+lowbit(i))
{
c[i]=c[i]+val;
}
}
第二个函数大家应该都看得懂,相隔相应位置都加上该点的值嘛,但是这相隔位置是怎么个算的,逻辑运算符大家可能会很陌生,这里举个例子:
大家都知道5的二进制是0000 0101 ,那么-5的二进制是1111 1011,(二进制数取负数是先按位取反,再在最后一位+1),现在5&(-5)的二进制就是0000 0001,因为&操作符只有1&1=1,其他情况都是0。
这里大家可以发现,所有奇数的二进制末尾都是1,我们不管前面,按位取反后最后是0,再加1就末尾是1,其他位和原来相反,只有末尾和原来相同是1,所以得到的结果就是1。也就是我们更新了奇数点后接着要更新该点+1的位置。
假如我们的值是2的倍数(不包含2^2,2^3等),那么末尾是10,同理不管前面,取反变01,加1变10,前面和原来的数相反,末尾两位相同,所以得到的结果是2,也就是我们对2的倍数要继续更新它后面两个位置的点。比如2跳到4,6跳到8。
同理,对于4的倍数,末尾是100,取反+1后就还是100,结果是4,所以4的位置要更新完要跳到8。12的位置要跳到16。
同理,对于2^n的倍数,末尾是1000……(n个0),取反+1是本身,结果就是2^n,所以该点要跳到该点+2^n的点。
直到下一个要更新的点不在区间。
这样我们就完成了树状数组的建立。
对于查询,我们可以按建树的逆向思维来做,大家可以把上面45那个例子带入下面的代码手动模拟,求得的是1到x的和。
int getsum(int x)
{
int sum=0;
for(int i=x;i>=1;i=i-lowbit(i))
{
sum=sum+c[i];
}
return sum;
}
最后我们求区间x-y和的时候,只需要用1~y的和减去1~(x-1)的和就行了。
下面给个测试代码供大家细读。
#include <iostream>
#include"stdio.h"
const int N = 105;
int c[N];
using namespace std;
int lowbit(int x)
{
return x&(-x);
}
void modify(int x,int n,int val)
{
for(int i=x;i<=n;i=i+lowbit(i))
{
c[i]=c[i]+val;
}
}
int getsum(int x)
{
int sum=0;
for(int i=x;i>=1;i=i-lowbit(i))
{
sum=sum+c[i];
}
return sum;
}
int main()
{
printf("请输入10个数\n");
for(int i=1;i<=10;i++)
{
int t;
scanf("%d",&t);
modify(i,10,t);
}
printf("树状数组的值分别为:\n");
for(int i=1;i<=10;i++)
{
printf("%d ",c[i]);
}
printf("\n从1到i的和分别是\n");
for(int i=1;i<=10;i++)
{
printf("%d ",getsum(i));
}
}
模拟运行结果如下: