树状数组学习笔记
题面传送门
算法简介:树状数组是一颗类似于树的数组,他虽然画出来是一棵树,但在实现中还是一个数组,它可以维护具有传递性质的信息,比如区间和,区间乘积等。他可以查询\(1-i\)的信息。他支持单点修改,单点查询与区间查询,区间修改较为繁琐,需要用到差分数组,他的复杂度是查询\(o(log^2n)\),修改\(o(log^2n)\)。
算法实现:这是一棵不平衡的多叉树,把它画出来一定是左边多右边少。所以在查询完全均匀的区间时不如线段树,但大部分区间都是不完全均匀的。所以常数比线段树更优。
首先我们要定义一个数组f,其中\(f_i\)表示\(i\)这个点管辖下的所有点的值的和。
修改:它支持单点修改。对于一个点\(x=5\)加\(y=1\)。我们肯定要把\(f_x\)加上\(y\),那剩下怎么办呢?我们发现\(f_6\)管辖\(f_5\),而\(5\)的二进制是\(101\),6的二进制是\(110\),所以我们只要把\(x\)最后一位\(1\)加上去就好了。
但接下来又有一个问题,那就是怎么找最后一个\(1\).如果用二进制拆分一个一个找,那复杂度就变成了\(o(log^2n^2)\)了,那肯定不行。得用常数级别的方法找到他。
然后就有了一个公式:\(x\&-x\);初学者肯定一脸懵逼,这是啥?
我们以5为例。五的二进制是\(101\).那么\(-5\)就是\(5\)的补码也就是\(011\).然后再按位与一下
\(101\& 011=001\)
然后\(5\)再加上\(1(001)\)就是\(6\)了。
神不神奇?我觉得这个公式太奇妙了,就是不知道是怎么推出来的。
于是我们只要一个\(while\)循环就可以解决问题了。
查询:对于一个区间\(x\),\(y\)(或:为什么没有单点查询?我:滚一边用数组去),因为树状数组维护的是一个前缀和。所以我们可以仿照前缀和的公式\(sum_y-sum_{x-1}\)。其中\(sum_z\)表示从\(1-z\)的和。但\(sum_z\)的求法。。。。。。
我们要查找\(1-x=7\)的区间时,自然要加上\(f_x\),那么接下来。。。。。。
我们又可以运用那个公式。\(7\)的二进制是\(111\),\(-7\)为\(001\)。然后按位与一下
\(111\&001=001\)
\(7-1(0001)=6\).而\(f_6\)正是我们要加的。对\(6\)进行下一步。\(6\)的二进制是\(110\),\(-6\)为\(010\),按位与一下
\(110\&010=010\)
\(6-2(010)=4\),\(f_4\)也是我们要找的区间。所以区间查询也是一个\(while\)。
个人理解:树状数组与st表恰恰相反,一个只能查询有传递性的,一个只能查询没有传递性的。如果把两个结合在一起,做到有一种数据结构即可修改,可查询所有信息的数据结构(好像是线段树)就更好。
代码实现:
#include<cstdio>
using namespace std;
int n,m,f[500039],a[500039],x,y,z,ans,tot;
inline void read(int &x) {
x=0;char s=getchar();int fs=1;
while(s<'0'||s>'9') {if(s=='-')fs=-1;s=getchar();}
while(s>='0'&&s<='9') x=(x<<3)+(x<<1)+(s^48),s=getchar();
x*=fs;
}
inline void print(int x) {
if(x<0) {putchar('-');print(-x);return;}
if(x>9)print(x/10);
putchar(x%10+'0');
}
inline void get(int x,int z){while(x<=n)f[x]+=z,x+=x&-x;}
inline int find(int x){
ans=0;
while(x)ans+=f[x],x-=x&-x;
return ans;
}
int main(){
register int i;
read(n);read(m);
for(i=1;i<=n;i++){read(a[i]);get(i,a[i]);}
for(i=1;i<=m;i++){
read(x);
if(x==1){
read(x);read(z);
get(x,z);
}
else{
read(x);read(y);
print(find(y)-find(x-1));
putchar('\n');
}
}
return 0;
}
这只是一个模板,我们来看看一个简单应用。
题面传送门
逆序对,虽然用归并也可以做,而且还比树状数组快,但这作为树状数组应用的裸题,是十分经典的一道题。
这道题因数据过大须离散化,而离散化这里先不提,我们直接看离散化后的树状数组。
从左到右遍历,碰到一个数,先统计他右边的数,再把它加入其中。
具体地说,我们开一个f数组,以\(f_i\)表示值为\(i\)的有几个,再在\(f\)上用树状数组维护区间。
当来了一个数时,我们先查询i右边的一段区间有几个,把它加入答案中,然后再把\(f_i+1\).
代码实现:
#include<cstdio>
#include<algorithm>
using namespace std;
int n,m,a[500039],x,y,z,s[500039],fs[500039];
long long ans,f[500039],tot;
inline void read(int &x) {
x=0;char s=getchar();int fs=1;
while(s<'0'||s>'9') {if(s=='-')fs=-1;s=getchar();}
while(s>='0'&&s<='9') x=(x<<3)+(x<<1)+(s^48),s=getchar();
x*=fs;
}
inline void get(int x,int z){while(x<=n)f[x]+=z,x+=x&-x;}
inline long long find(int x){
tot=0;
while(x)tot+=f[x],x-=x&-x;
return tot;
}
inline bool cmp(int x,int y){return a[x]<a[y];}
int main(){
register int i;
read(n);
for(i=1;i<=n;i++)read(a[i]),s[i]=i;
sort(s+1,s+n+1,cmp);
fs[s[1]]=1;
for(i=2;i<=n;i++){
if(a[s[i]]==a[s[i-1]]) fs[s[i]]=fs[s[i-1]];
else fs[s[i]]=i;
}
for(i=1;i<=n;i++){
ans+=find(n)-find(fs[i]);
get(fs[i],1);
}
printf("%lld",ans);
return 0;
}