一:树状数组定义
望文生义,树状数组就是用树形结构来模拟数组的一种数据结构。
二:图解(纯手绘,难看勿喷)
C表示从1-k的和,
C[1]=a[1]
C[2]=C[1]+a[2]
C[3]=a[3]
C[4]=C[2]+C[3]+a[4]
C[5]=a[5]
C[6]=C[5]+a[6]
C[7]=a[7]
C[8]=C[4]+C[6]+C[7]+a[8]
C[9]=a[9]
C[10]=C[9]+a[10]
C[11]=a[11]
C[12]=C[10]+C[11]+a[12]
C[13]=a[13]
C[14]=C[13]+a[12]
C[15]=a[15]
C[16]=C[8]+C[12]+C[14]+C[15]+a[16]
三:可解决问题
单点修改,区间求和。
四:为何建立树状数组
我们可以发现树状数组中,一个数直接对应的数最多有logk+1个数,因此我们在求解一个区间和时,可以在O(log n) 内求解出。
五:与线段树的区别
树状数组能解决的问题线段树都能解决,线段树能解决的问题树状数组不一定能解决,但是!!!树状数组更快!!!树状数组更好调试!!!毕竟,能简单点谁不想简单呢。
六:如何查询树状数组子节点
根据上述建树过程我们可以发现,当前点所存的范围是(x-lowbit(x)+1,x)。
那么怎么实现lowbit()呢?
这需要用到二进制了。
计算机中有源码,反码和补码。
源码就是数据本来对应的二进制数;
正数的反码等于它本身。
正数的补码等于它本身。
例如14=1110
反码:1110
-14的补码:1111
那么他的最低一位1可以油 14&-14。
我们也可以这样想,只要x不为0,那么x必有一位是1,取得他的负数后,最后那些是0的位数都会变为1,再加上1,则会将1都变为0,现在出现的最低一位1就是所求的最低的1,即lowbit()。
由于我们在树状数组中需要经常用到lowbit,因此我们可以将其写为一个函数,以便使用。
int lowbit(x)
{
return x & -x
}
七:如何修改树状数组的值
对于上面的建树过程分析,我们可以发现当前这个数x会涵盖(x-lowbit(x)+1,x)内的元素,因此不难知道,对于每一个x的修改,都会影响到x+lowbit(x)这个数,直到影响到自己设定的最大值。
可以写出add函数
void add(int x,int k)
{
for(int i=x;i<=maxx;i+=lowbit(x)) tr[i]+=k;
}
八:如何求和
继续分析建树过程,我们会发现x不会涵盖x-lowbit(x)的内容,所以我们求和时只需要加上
x-lowbit(x)所涵盖的元素即可。
void sum(int x)
{
int sum=0;
for(int i=x;i;i-=lowbit(i)) sum+=tr[i];
return sum;
}
九:模板题目:洛谷:P3374 【模板】树状数组 1
#include<iostream>
using namespace std;
const int N=5e5+10;
int tr[N],n,m;
int lowbit(int x)
{
return x & -x;
}
void add(int x,int k)
{
for(;x<=n;x+=lowbit(x)) tr[x]+=k;
}
int sum(int x)
{
int sum=0;
for(;x;x-=lowbit(x)) sum+=tr[x];
return sum;
}
int main()
{
ios::sync_with_stdio(false);//树状数组题目中数据范围一般都比较大,最好用scanf或者cin加速读入
cin.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int x;
cin>>x;
add(i,x);//在第i个位置加上x
}
while(m--)
{
int op;
cin>>op;
int x,y;
cin>>x>>y;
if(op==1) add(x,y);
else cout<<sum(y)-sum(x-1)<<endl;//求出1到y和1到x-1的和,相减即为x-y的和
}
}
模板题,就不需要再做过多解释了吧。
模板题二:洛谷:【模板】树状数组 2
有些小伙伴可能看见这题,就会问了,树状数组不是只能做单点修改吗?
是的,但是我们可以用差分做这题啊!
想一想,1-n的差分加起来不就是n这个数么?有没有恍然大悟的感觉?
我们只需要在第x个位置上加上数据k,再在第y+1个位置上减去k不就能实现求任何一个位置的数了么?
#include<iostream>
using namespace std;
const int N=5e5+10;
int tr[N],n,m;
int lowbit(int x)
{
return x & -x;
}
void add(int x,int k)
{
for(;x<=n;x+=lowbit(x)) tr[x]+=k;
}
int sum(int x)
{
int sum=0;
for(;x;x-=lowbit(x)) sum+=tr[x];
return sum;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>m;
int last=0;
for(int i=1;i<=n;i++)
{
int x;
cin>>x;
add(i,x-last);
last=x;
}
while(m--)
{
int op;
cin>>op;
if(op==1)
{
int x,y,k;
cin>>x>>y>>k;
add(x,k),add(y+1,-k);
}
else
{
int x;
cin>>x;
cout<<sum(x)<<endl;
}
}
}
十:拓展内容:
1:求逆序对:洛谷:P1908 逆序对
此题可以用树状数组边插入边求解。
何谓逆序对?就是i<j时,第i个数大于第j个数此类的。
我们可以用树状数组,在这个数的数值的位置加上一代表这个数被加入到了树状数组。(是数值,不是序号!!!)
那么怎么求有多少个逆序对呢?
我们可以用sum(x)求出在x之前有多少个数,可以边add()边求逆序对,因此第i个数逆序对的数量就等于i-sum(a[i]).
总体逆序对的数量只需要对其求和即可。
但是有一个问题,如果数据过大咋办,我们是不能开这么大的数组的。
这时候就需要用到离散化了。(可以选择的离散方式有很多,这里选择哈希离散)
#include<iostream>
#include<unordered_map>
#include<algorithm>
using namespace std;
const int N=5e5+10;
int tr[N],n,a[N];
inline int lowbit(int x)
{
return x& -x;
}
inline void add(int x,int k)
{
for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=k;
}
inline int sum(int x)
{
int sum=0;
for(int i=x;i;i-=lowbit(i)) sum+=tr[i];
return sum;
}
unordered_map<int,int> s;
int l=0;
inline int get(int x)
{
if(!s.count(x)) s[x]=++l;
return s[x];
}
int b[N];
inline void pai()
{
copy(begin(a),end(a),begin(b));
sort(b+1,b+1+n);
for(int i=1;i<=n;i++) get(b[i]);
}
int main()
{
scanf("%d",&n);
long long ans=0;
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
pai();
for(int i=1;i<=n;i++)
{
int id=get(a[i]);
ans=ans+i-1-sum(id);
add(id,1);
}
cout<<ans<<endl;
return 0;
}
注:由于笔者比较笨,又使用了一次copy函数,导致此题慢了很多。
2 .求树状数组中第k大数
1.二分法:
int find(int x)
{
int l=0,r=maxx+1;
while(l<r)
{
int mid=l+r>>1;
if(sum(mid)<x) l=mid-1;
else r=mid+1;
}
return r;
}
2.二进制法:
int find(int x)
{
int ans=0,cnt=0;
for(int i=20 ;i>=0;i--)//可以随意设置需要的大小,表示从二进制第i为开始
{
ans+=(1<<i);
if(ans>=maxx||cnt+tr[ans]>=x) ans-=(1<<i);//目前的数大于最大值或者已经求出来的个数+当前值对应的数的个数>=自己要求的数则返回
else cnt+=tr[ans];
}
return ans+1;//求出的是第x-1大的数,则ans+1为第x大的数。
}
高中蒟蒻的学习笔记。
(学习自acwing和网上大佬)