树状数组
树状数组是相比于数组计算前缀和更优的算法
原理
如果一个数组a有八个数,它对应的树状数组c可以表示成这样:
c1=a1;
c2=a1+a2;
c3=a3;
c4=a1+a2+a3+a4;
以此类推。。。。。。 很难说出他们的关系,但是如果把它们变为二进制
c0001=a0001
c0010=a0001+a0010
c0011=a0011
c0100=a0001+a0010+a0011+a0100
你会发现,将每一个二进制,去掉所有高位1,只留下最低位的1,然后从那个数一直加到1,看一看是不是这样。
这个操作可以写一个lowerbits函数来完成
这里用了按位与的位运算和计算机编码中的补码(看注释吧)
int lowbits(int x)
{
return x&(-x);//so,这里,我们用到的是补码
//所求数的补码与它复数的补码按位与,得到的是补码最右边的1所代表的数
}
//原码 :符号位加对应二进制数
// 正整数的原码,反码,补码都一样
//负正数的原码是,反码是原码除符号位外按位取反,补码是反码加1;
//但实际上是这样的
// 负数的补码等于他的原码自低位向高位,尾数的第一个‘1’及其右边的‘0’保持不变,左边的各位按位取反,符号位不变。
对于某一数的更新,由树状数组的结构决定了我们只要更新它的父段直到顶端即可。
void update(int p,int k)
{
while(p<=n)
{
c[p]+=k;
p+=lowbits(p);
}
}
相关操作
求和
int getsum(int p)
{
int res=0;
while(p>=1)
{
res+=c[p];
p-=lowbits(p);
}
return res;
}//这里求的是第一个数a[1]到该数a[p]的区间和
附模板题的ac代码
https://www.luogu.org/problem/P3374
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <string>
#include <cstring>
#include <queue>
using namespace std;
#define ll long long
#define maxn 2000005
ll a[maxn],c[maxn];
int m,n;
int lowbits(int x)
{
return x&(-x);
}
void update1(int p,ll k)
{
while(p<=n)
{
c[p]+=k;
p+=lowbits(p);
}
}
ll getsum(ll p)
{
int res=0;
while(p>=1)
{
res+=c[p];
p-=lowbits(p);
}
return res;
}
int main()
{
while(~scanf("%d %d",&n,&m))
{
memset(c,0,sizeof(c));
for(int i=1;i<=n;++i)
{
scanf("%d",&a[i]);
update(i,a[i]);
}
ll flag,x,y;
for(int i=0;i<m;++i)
{
scanf("%lld %lld %lld",&flag,&x,&y);
if(flag==1)
{
update(x,y);
}
if(flag==2)
{
printf("%lld\n",getsum(y)-getsum(x-1));
}
}
}
return 0;
}
树状数组的第二个操作:差分
所以,差分是啥?
设数组a[]={1,6,8,5,10},那么差分数组b[]={1,5,2,-3,5}
也就是说b[i]=a[i]-a[i-1];(a[0]=0;),那么a[i]=b[1]+…+b[i];(这个很好证的)。
假如区间[2,4]都加上2的话,
a数组变为a[]={1,8,10,7,10},b数组变为b={1,7,2,-3,3}。
发现了没有,b数组只有b[2]和b[5]变了,因为区间[2,4]是同时加上2的,所以在区间内b[i]-b[i-1]是不变的。
所以对区间[x,y]进行修改,只用修改b[x]与b[y+1]。
即:b[x]=b[x]+k,b[y+1]=b[y+1]-k
那么实现起来就非常简单了。。。
// https://www.luogu.com.cn/problem/P3368 模板题ac代码:
//ex树状数组,差分思想
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <string>
#include <cstring>
#include <queue>
using namespace std;
#define ll long long
#define maxn 530000
ll a[maxn],b[maxn],c[maxn];
int n,m;
ll lowbit(ll x)
{
return x&(-x);
}
void update(ll x,ll k)
{
while(x<=n)
{
c[x]+=k;
x+=lowbit(x);
}
}
ll getsum(int x)
{
ll res=0;
while(x>=1)
{
res+=c[x];
x-=lowbit(x);
}
return res;
}
int main()
{
cin>>n>>m;
a[0]=0;
for(int i=1;i<=n;++i)
{
scanf("%d",&a[i]);
update(i,a[i]-a[i-1]);
}
for(int i=1;i<=m;++i)
{
int flag,x,y,k;
scanf("%d",&flag);
if(flag==2)
{
scanf("%d",&x);
printf("%lld\n",getsum(x));
}
if(flag==1)
{
scanf("%d %d %d\n",&x,&y,&k);
update(y+1,-k);
update(x,k);
}
}
return 0;
}
总的来说,在只有求区间和以及单点修改操作的时候,树状数组还是非常好用的,就那么几行,也好记。但对于复杂一点的问题,比如区间修改,区间覆盖,区间加法、乘法,区间最大最小值等等问题的时候还是需要使用线段树。所以不要过度依赖树状数组。。。这玩意其实用法很单一。