前言
仍然是一种数据结构,实用性在线段树之下,难度在线段树以下。
什么是树状数组?
全称为Binary Indexed Tree (BIT),是一种能以O(
l
o
g
n
log_n
logn)的时间复杂度解决区间修改或查询问题的数据结构
一个树状数组如下图所示
如图,我们可以看出每一个BIT数组的元素的叶节点个数就等于此数二进制下
的最低位的1的位置,设这个位置为lowbit(x),则可以用x&-x来得到这个数
例如:lowbit(22)=2
22的二进制原码011010,正数的补码等于它的原码011010
-22的二进制原码111010,负数的补码等于它的原码取反加1,为100110
011010 & 100110 = 000010 正数转换成原码后依然是000010
所以lowbit(22)=2
单点修改方法
再次观察图片,我们发现每个叶结点要到达它的上一层结点,等于它的二进制加上它的lowbit
如,2->4 即0010+10=0100(4)
4->8即0100+100=1000(8)
同时每个BIT里的元素至少包含原数组上相同位置的元素
从左端点开始,一直枚举到n,每次i的值+=lowbit(i)来查找上一层节点
void Update(int k,int y)
{
for(int i=k;i<=n;i+=Lowbit(i))
{
c[i]+=y;
}
}
区间查询方法
从当前的k开始,每次加上它的下一个叶节点的值,每次i的值-=lowbit(i),相当于把前缀和的计算反过来求
long long Sum(int k)
{
long long ans = 0;
for(int i=k;i>0;i-=Lowbit(i))
{
ans+=c[i];
}
return ans;
}
一、单点修改,区间查询
做法:
1.前缀和
在输入时计算前缀和,能够快速得到第二题的答案,但在解决第一题时改变一个元素的值,后面的元素都要改变,时间复杂度为O(n)!显然当数据范围很大时此方法不宜使用
2.树状数组
#include<cstdio>
#include<iostream>
using namespace std;
long long n,q,a[1000005],b[1000005],c[1000005],p1,x,y;
int Lowbit(int x)//返回最低位1的位置
{
return x&-x;
}
void Update(int k,int y)//修改
{
for(int i=k;i<=n;i+=Lowbit(i))
{
c[i]+=y;
}
}
long long Sum(int k)//计算区间和
{
long long ans = 0;
for(int i=k;i>0;i-=Lowbit(i))
{
ans+=c[i];
}
return ans;
}
int main()
{
scanf("%lld%lld",&n,&q);
for(int i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
Update(i,a[i]);
}
for(int i=1;i<=q;i++)
{
scanf("%lld%lld%lld",&p1,&x,&y);
if(p1==1)//进行单点增加
{
Update(x,y);
}
else{//输出区间和
printf("%lld\n",Sum(y)-Sum(x-1));
}
}
return 0;
}
2.区间修改,单点查询
传送门
题目反过来了,其他不变,修改和查询的东西要变化
证明:
设原数组为a。差分数组为p,前缀和数组为sum
则p[1]=a[1]-a[0],p[2]=a[2]-a[1],p[3]=a[3]-a[2]…p[i]=a[i]-a[i-1]
因为a[0]=0,则此时
sum[1]=p[1]
sum[2]=p[2]+sum[1]=a[2]
sum[3]=p[3]+sum[2]=a[3]
…
sum[i]=p[i]+sum[i-1]=a[i]
所以可以证明一个数组的差分数组的前缀和等于原数组
也可以证明一个数组的前缀和数组的差分数组等于原数组
#include<cstdio>
#include<iostream>
using namespace std;
long long a[1000005],BIT[1000006],sum[1000006];
int n,q,p1,l,r,x;
int lowbit(int x)
{
return x&-x;
}
void update(int k,int x)
{
for(int i=k;i<=n;i+=lowbit(i))
{
BIT[i]+=x;
}
}
long long Sum(int k)
{
long long ans=0;
for(int i=k;i>0;i-=lowbit(i))
{
ans+=BIT[i];
}
return ans;
}
int main()
{
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
update(i,a[i]-a[i-1]);
}
for(int i=1;i<=q;i++)
{
scanf("%d",&p1);
if(p1==1)
{
scanf("%d%d%d",&l,&r,&x);
update(l,x);//增加l~n的值
update(r+1,-x);//增加完l~n的值后要从r+1开始减去加上的数,否则后面的数会被影响
}
else{
scanf("%d",&x);
printf("%lld\n",Sum(x));
}
}
return 0;
}
3.区间修改,区间查询
传送门
由于修改和查询都是在区间内进行,所以我们可以把上面两种例题的思路结合起来
大体思路是用两个树状数组来维护左端点和右端点,则Sum( r)-Sum(l)就是我们要求的值,证明的话因为我没有最开先的式子,所以没办法推,望谅解QAQ
感觉记一下模板代码也比较轻松吧
#include<cstdio>
#include<iostream>
using namespace std;
long long BIT1[1000005],BIT2[1000005];
int n,q,p1,l,r,f,a[1000005];
int lowbit(int x)
{
return x&-x;
}
void update(int k,int x)
{
for(int i=k;i<=n;i+=lowbit(i))
{
BIT1[i]+=x;
BIT2[i]+=(long long)(k-1)*x;
}
}
long long Sum(int x)
{
long long ans=0;
for(int i=x;i>0;i-=lowbit(i))
{
ans+=BIT1[i]*x-BIT2[i];
}
return ans;
}
int main()
{
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
update(i,a[i]-a[i-1]);//区间修改
}
for(int i=1;i<=q;i++)
{
scanf("%d%d%d",&p1,&l,&r);
if(p1==1)
{
scanf("%d",&f);
update(l,f);//增加l~n区间内的值
update(r+1,-f);//加回r~n区间内的值
}
else{
printf("%lld\n",Sum(r)-Sum(l-1));
}
}
}
4.树状数组求逆序对
逆序对,其实就相当于这个数后面有多少个小于它的数
因为这道题数字可能会很大,所以采用离散化来降低时间复杂度
思路是把离散化后的数组元素放进树状数组中,比较前面有多少个数大于等于它
这样的话求的是此元素的非逆序对,用i减去它非逆序对的个数就是逆序对的个数了
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
int n,BIT[1005];//树状数组
int b[1005];//离散化后数组
int a[1005];//原数组
int cnt=0;
int ans=0;
int lowbit(int x)
{
return x&-x;
}
void update(int k,int x)
{
for(int i=k;i<=n;i+=lowbit(i))
{
BIT[i]+=x;
}
}
int Sum(int k)
{
int ans=0;
for(int i=k;i>0;i-=lowbit(i))
{
ans+=BIT[i];
}
return ans;
}
int lsh(int x)
{
return lower_bound(a+1,a+1+cnt,x)-a;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
b[i]=a[i];
}
sort(a+1,a+1+n);
cnt=unique(a+1,a+1+n)-a-1;
for(int i=1;i<=n;i++)
{
b[i]=lsh(b[i]);//离散化
}
for(int i=1;i<=n;i++)
{
update(b[i],1);//b[i]+1即把b放回离散化后的位置
ans+=i-Sum(b[i]);//统计第i个元素的逆序对个数
}
printf("%d\n",ans);
return 0;
}
树状数组还有二维的版本,其原理和一维基本一致,所以不再展开
总结
在做题时最重要的是能否看出题目和树状数组的关联以及如何根据题意构建框架,并要注意题目的数据范围,不要因为数组开小了或未使用long long而丢分!
要继续加油啊!