浅谈树状数组
AC_borten_qwq · · 算法·理论
下面我将讲解一种非常好用的数据结构,树状数组!
前置知识
在讲解之前,我们首先来点前置知识,
什么是
那么如何求这个
切入正题
刚刚讲了好长的前置知识,总算切入正题了。
我们首先有一个例题,就是说给定一个长度为
这个数据范围,暴力求和肯定是不行的。如果只有求和,那么完全可以前缀和,如果只有修改完全可以差分。但两个都有就必须要用高级一点的数据结构,例如树状数组。
树状数组就是又有一个数组
可以看出很像一棵树,不然为什么叫树状数组吗。
其实这是个线段树的板子题,如果你希望学习请看 这里,但这篇文章讲的是树状数组,所以我们用树状数组来做。
同样
展开后得
那么就维护两个树状数组,第一个存
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,a[500005],b[500005],c[500005],o,x,y,z;
int lo(int x){return x&(-x);}
void cr(int oo,int sz){
while(oo<=n)b[oo]+=sz,oo+=lo(oo);
}
int js(int oo){
int ans=0;
while(oo)ans+=b[oo],oo-=lo(oo);
return ans;
}
void cr2(int oo,int sz){
while(oo<=n)c[oo]+=sz,oo+=lo(oo);
}
int js2(int oo){
int ans=0;
while(oo)ans+=c[oo],oo-=lo(oo);
return ans;
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i],cr(i,a[i]-a[i-1]),cr2(i,(a[i]-a[i-1])*i);
for(int i=1;i<=m;i++){
cin>>o;
if(o==1)cin>>x>>y>>z,cr(x,z),cr(y+1,-z),cr2(x,z*x),cr2(y+1,-z*(y+1));
else cin>>x>>y,cout<<(js(y)*y+js(y)-js2(y))-(js(x-1)*(x-1)+js(x-1)-js2(x-1))<<endl;
}
}
所以树状数组常数小,耗时短,耗空间小,代码短,能有树状数组的题为何偏要写线段树呢?
上述所有问题都是树状数组里存的是数值,但通过下面问题你可以发现,树状数组里可以存的不是数值。
给定一个数
大家在学归并排序思想时肯定做过,但是这题也可以用树状数组做,而且思维难度更低。我们不能盲目枚举所有的逆序对,但是我们可以用树状数组存储每一个数出现的次数。什么意思,比如说,这里出现了一个
什么时候才会出现逆序对?显然是前面的数比后面的数大。那么我们就看前面有几个数比这个数大不就可以了吗。虽然树状数组板子是绿但是明明这么做更简单。然后放一下我的代码。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,o,x,y,ans;
map<int,int>a,b;
int lo(int x){return x&(-x);}
void cr(int oo,int sz){
while(oo<=1e9+1)b[oo]+=sz,oo+=lo(oo);
}
int js(int oo){
int ans=0;
while(oo)ans+=b[oo],oo-=lo(oo);
return ans;
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i],cr(a[i],1),ans+=i-js(a[i]);
}
cout<<ans;
}
顺便警示后人一下,修改函数的上限千万不要写成
例题选讲
刚刚讲的东西都是模版,从这里进入例题选讲阶段。
AT_abc441_e
题目传送门
这题同样有别的解法,但是我们这里讲树状数组。
如果直接算,感觉没啥头绪。我们可以考虑转化成前缀 A 比 B 多的数量。我们可以发现,如果 A 比 B 多,那么 A 比 B 多的数量一定比
那么就变成了一个正序对的板子。
由于代码和刚刚的逆序对实在没啥大区别所以不放了。唯一需要注意的就是树状数组里不能存负数,为了防止 A 比 B 少,我们将树状数组里所有存的数都加上一个固定的数,这样不影响大小。
AT_abc436_f
题目传送门
我们看这题,发现直接枚举位置不太好做。那么考虑枚举别的东西,比如说枚举最暗的星星,然后任意区间里比这颗星星暗的一定不能出现,比这颗星星亮的必须出现。
那就好说了,对于每一颗星星,它左边比它亮的星星数量加一再乘上它右边它亮的星星数量加一就是答案。
对于为什么要加一,因为区间可以正好卡到它那里,左边或右边就一颗星星也拍不到了。
然后把此题我的代码也放上来。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,o,x,y,ans,a[500005],b[500005],c[500005];
int lo(int x){return x&(-x);}
void cr(int oo,int sz){while(oo<=n)b[oo]+=sz,oo+=lo(oo);}
int js(int oo){
int ans=0;
while(oo)ans+=b[oo],oo-=lo(oo);
return ans;
}
void cr2(int oo,int sz){while(oo<=n)c[oo]+=sz,oo+=lo(oo);}
int js2(int oo){
int ans=0;
while(oo)ans+=c[oo],oo-=lo(oo);
return ans;
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i],cr(a[i],1);
for(int i=1;i<=n;i++){
ans+=(js2(a[i]-1)+1)*(js(a[i]-1)-js2(a[i]-1)+1),cr2(a[i],1);
}
cout<<ans;
}
P10814
这是一个二维数点题,一看题目就很像树状数组,但你可能会被一个问题卡住。就是说树状数组只能说从左往右边存储边计算的,这里的
但是,没关系,因为这题没有强制要求在线。这题的做法就是先按
顺便讲一下什么是离线和在线。在线就是说每输入一组数据就能立即输出。离线就是说把所有数据输出完之后一起统计。有的时候题目会让你用这组数据输入的数异或上上一组数据的答案再进行计算,这样就强制在线了。
同样放一下我的代码呀。
#include<bits/stdc++.h>
using namespace std;
int n,m,a[2000005],b[2000005],c[2000005],ans[2000005],cnt;
struct node{
int x,y,z,id,x2,y2,fl;
}d[4000005];
bool cmp(node xx,node yy){return xx.x<yy.x;}
bool cnp(node xx,node yy){return xx.y<yy.y;}
bool czp(node xx,node yy){return xx.id<yy.id;}
int lo(int x){return x&(-x);}
void cr(int oo,int sz){while(oo<=2e6)b[oo]+=sz,oo+=lo(oo);}
void cr2(int oo,int sz){while(oo<=2e6)c[oo]+=sz,oo+=lo(oo);}
int js(int oo){
int ans=0;
while(oo)ans+=b[oo],oo-=lo(oo);
return ans;
}
int js2(int oo){
int ans=0;
while(oo)ans+=c[oo],oo-=lo(oo);
return ans;
}
int main(){
int x,y,z;
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=m;i++){
cin>>x>>y>>z;
d[++cnt].x=x-1;
d[cnt].z=z;
d[cnt].id=i;
d[cnt].fl=0;
d[++cnt].x=y;
d[cnt].z=z;
d[cnt].id=i;
d[cnt].fl=1;
}
sort(d+1,d+cnt+1,cmp);
for(int i=0,jl=1;i<=n;i++){
if(i!=0)cr(a[i],1);
while(d[jl].x==i){
ans[d[jl].id]+=(d[jl].fl?1:-1)*js(d[jl].z),jl++;
}
}
for(int i=1;i<=m;i++)
cout<<ans[i]<<'\n';
}
总结
树状数组是一种非常好用的数据结构,代码段,常数低。无论是做单点修改区间查询,区间修改单点查询,还是区间修改区间查询,都很实用。在做逆序对,二维数点等题时同样适用。非常建议好好学学。本文中也提到了几个写树状数组代码时需注意的细节,同样非常重要的。
希望你们看完之后都能收获满满!别忘了点个赞!