树状数组是对一个数组改变某个元素和求和比较实用的数据结构。两中操作都是O(logn)。
传统数组(共n个元素)的元素修改和连续元素求和的复杂度分别为O(1)和O(n)。树状数组通过将线性结构转换成伪树状结构(线性结构只能逐个扫描元素,而树状结构可以实现跳跃式扫描),使得修改和求和复杂度均为O(lgn),大大提高了整体效率。
给定序列(数列)A,我们设一个数组C满足
C[i] = A[i–2^k+ 1] + … + A[i]
其中,k为i在二进制下末尾0的个数,i从1开始算!
则我们称C为树状数组。
下面的问题是,给定i,如何求2^k?
答案很简单:2k=i&(i(i-1)) ,也就是i&(-i) 为什么呢?? 请看下面:
整数运算 x&(-x),当x为0时结果为0;x为奇数时,结果为1;x为偶数时,结果为x中2的最大次方的因子。
因为:x &(-x) 就是整数x与其相反数(负号取反)的按位与:1&1=1,0&1 =0, 0&0 =1。具体分析如下:
□ 当x为0时,x&(-x) 即 0 & 0,结果为0;
□ 当x不为0时,x和-x必有一个为正。不失一般性,设x为正。
●当x为奇数时,最后一个比特为1,取反加1没有进位,故x和-x除最后一位外前面的位正好相反,按位与结果为0。最后一位都为1,故结果为 1。
●当x为偶数,且为2的m次方(m>0)时,x的二进制表示中只有一位是1(从右往左的第m+1位),其右边有m位0,左边也都是0(个数由表示 x的字 节数决定),故x取反加1后,从右到左第有m个0,第m+1位及其左边全是1。这样,x& (-x) 得到的就是x。
●当x为偶数,却不为2的m次方的形式时,可以写作x= y * (2^k)。其中,y的最低位为1。实际上就是把x用一个奇数左移k位来表示。这时,x的 二进制 表示最右边有k个0,从右往左第k+1位为1。当对x取反时,最右边的k位0变成1,第k+1位变为0;再加1,最右边的k位就又变成了0,第 k+1位因为进 位的关系变成了1。左边的位因为没有进位,正好和x原来对应的位上的值相反。二者按位与,得到:第k+1位上为1,左边右边都为 0。结果为2^k,即 x中包含的2的最大次方的因子。
总结一下:x&(-x),当x为0时结果为0;x为奇数时,结果为1;x为偶数时,结果为x中2的最大次方的因子。 比如x=32,其中2的最大次方因子 为 2^5,故x&(-x)结果为32;当x=28,其中2的最大次方因子为4,故x & (-x)结果为4。当x=24,其中2的最大次方因子为8,故 x&(-x)结果为 8。
下面进行解释:
以i=6为例(注意:a_x表示数字a是x进制表示形式):
(i)_10 = (0110)_2
(i-1)_10=(0101)_2
i xor (i-1) =(0011)_2
i and (i xor (i-1)) =(0010)_2
2^k = 2
C[6] = C[6-2+1]+…+A[6]=A[5]+A[6]
数组C的具体含义如下图所示:
当我们修改A[i]的值时,可以从C[i]往根节点一路上溯,调整这条路上的所有C[]即可,这个操作的复杂度在最坏情况下就是树的高度即O(logn)。另外,对于求数列的前n项和,只需找到n以前的所有最大子树,把其根节点的C加起来即可。不难发现,这些子树的数目是n在二进制时1的个数,或者说是把n展开成2的幂方和时的项数,因此,求和操作的复杂度也是O(logn)。
树状数组能快速求任意区间的和:A[i] + A[i+1] + … + A[j],设sum(k) = A[1]+A[2]+…+A[k],则A[i] + A[i+1] + … + A[j] = sum(j)-sum(i-1)。
#include<iostream>
#include <bits/stdc++.h>
using namespace std;
int a[51000];
int b[51000];
int lowbit(int n)
{
return n & (-n);
}
void Add(int x,int y,int n)
{
//cout<<"jajfsj"<<endl;
for(int i=x; i<=n; i+=lowbit(i))
b[i]+=y;
}
void Sub(int x,int y,int n)
{
for(int i=x; i<=n ; i+=lowbit(i))
b[i]-=y;
}
int Query(int x,int y,int n)
{
long long ans1=0;
long long ans2=0;
for(int i=y; i>=1; i-=lowbit(i))
ans1+=b[i];
for(int i=x-1;i>=1;i-=lowbit(i))
ans2+=b[i];
return ans1-ans2;
}
int main()
{
int t;
cin>>t;
for(int i=1; i<=t; i++)
{
int n;
printf("Case %d:\n",i);
cin>>n;
memset(b,0,sizeof b);
for(int k=1; k<=n; k++)
{
cin>>a[k];
Add(k,a[k],n);
}
//solve(n);
string s;
for(cin>>s ; s!="End"; cin>>s)
{
if(s=="End")
continue;
if(s=="Add")
{
int x,y;
cin>>x>>y;
Add(x,y,n);
}
else if(s=="Sub")
{
int x,y;
cin>>x>>y;
Sub(x,y,n);
}
else
{
int x,y;
cin>>x>>y;
int ans=Query(x,y,n);
cout<<ans<<endl;
}
}
}
return 0;
}