目录
什么是树状数组?
对于树状数组 tr[u] 区间,将u进行二进制拆分,可以得到的最低位的1的位数,即为tr[u] 的“管辖范围”。一个 tr[u] 存储了一定区间内所有的元素的和,这个区间的长度由该区间末位的二进制最低位的1的位数(即 lowbit(x)= x&-x)决定。
如 ,我们可以将三个小区间[1,4] ,[5,6] ,[7,7]作为以7结尾的区间划分方式(区间始终从1开始)。tr[u] 则存储 区间 [ x-lowbit(x)+1 , x]中所有数的和。对应上式中的实例,我们有图可知,tr[4] 可管辖区间 [1,4] 的和,tr[6] 管辖区间 [1,6] 的和,tr[7] 管辖区间 [7,7] 的和。
注意,树状数组存储的是“和”,因此我们很容易把它与前缀和相联系做到对区间的动态维护和查询。通过活用树状数组,我们可以做到以下几点:
基本操作:
(1)将一个数加上k:
inline void add(int x,int k){
while(x<=n){
tree[x]+=k;
x+=lowbit(x);
}
}
(2)将区间内每一个数加上k:(差分思想,tree数组存储差分前缀和)
add(x,k),add(y+1,-k);
(3)求一段区间内的和:
inline int sum(int x){
int res=0;
while(x){
res+=tree[x];
x-=lowbit(x);
}
return res;
}
printf("%d\n",sum(y)-sum(x-1));
(4)查询任意一个数的值:(差分思想,tree数组存储差分前缀和)
拓展操作:
(1)查询 序列中 某一个数 前/后 比它 大/小 的数有多少(ACWing241.楼兰图腾 & 洛谷P1908逆序对)
我们可以通过for循环遍历顺序决定序列中各个数的出现先后。在每一次查询后更新,查询,表示我先“查询这个数之前/后,比它大/小的数出现的次数”;更新数组,表示查询这个数后,这个数出现了,我要让它,被后续出现的数查询时统计上,因此更新数组。具体操作如下,以逆序对这题为例:
for(int i=1;i<=n;i++)
{
add(b[i],1);
ans+=query(b[i]-1);
}
通过离散化,此时 b[i] 的值表示了原数组 a[i]在 序列中是排第几大的。当我们离散化后从1~n遍历序列,每次将一个元素加入后,查询(query(b[i]-1))的返回值 x 代表在前i位中已出现了x个比 b[i] 值更大的数。
(2)配合二分查找来查询剩余的数中第k小的数(AcWing244.谜一样的牛)
for(int i=1;i<=n;i++)//赋初值:原数列中每个数都未被使用
tr[i]=lowbit(i);
for(int i=n;i;i--){
int l=1,r=n;
while(l<r){ //二分查找第一个比a[i]+1大的数
int mid=l+r>>1; // a[i]:表示在队列中排 i 前面的元素,有a[i]个比他小
if(sum(mid)>=a[i]+1) r=mid; // 因此 i 是剩余数中第 a[i]+1 小的数
else l=mid+1;
}
pos[i]=r;
add(r,-1);
}
树状数组的相关题目
(1) AcWing楼兰图腾241:
记录两遍树状数组,存储位置i之前和之后分别有多少比它高,利用乘法原理得答案。
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
int const N=200005;
int n,a[N];
long long tree[N],great[N],low[N],ans1,ans2;
inline int lowbit(int x){
return x&-x;
}
inline long long sum(int x){
long long ans=0;
while(x){
ans+=tree[x];
x-=lowbit(x);
}
return ans;
}
inline void add(int x,int k){
while(x<=n){
tree[x]+=k;
x+=lowbit(x);
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
int y=a[i];
great[y]=sum(n)-sum(y);
low[y]=sum(y-1);
add(y,1);
}
memset(tree,0,sizeof(tree));
for(int i=n;i;i--){
int y=a[i];
ans1+=great[y]*(sum(n)-sum(y));
ans2+=low[y]*sum(y-1);
add(y,1);
}
printf("%lld %lld",ans1,ans2);
return 0;
}
(2) AcWing 一个简单的整数问题245、264
板子,方便熟悉和理解树状数组。
#include<cstdio>
#include<algorithm>
#include<iostream>
#define ll long long
using namespace std;
int const N=100005;
int n,m,d,a[N];
ll tree[N];
inline int lowbit(int x){
return x&-x;
}
inline void add(int x,int k){
while(x<=n){
tree[x]+=k;
x+=lowbit(x);
}
}
inline ll sum(int x){
ll ans=0;
while(x){
ans+=tree[x];
x-=lowbit(x);
}
return ans;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
add(i,a[i]-a[i-1]);
char opt;
for(int i=1,l,r,x;i<=m;i++){
cin>>opt;
if(opt=='Q'){
scanf("%d",&x);
printf("%lld\n",sum(x));
}
else{
scanf("%d%d%d",&l,&r,&d);
add(l,d),add(r+1,-d);
}
}
return 0;
}
关于二分
二分查找针对于一段有明确分界线的序列。有明确分界线是指在该分界线以左和以右对于一个条件不能同时满足。最常见的情况是该序列单增或单减。二分查找有以下两个模板:
x在递增的序列a中:
(1)查找序列中x或x的前驱( 的最后一个数)
while(l<r){
int mid=(l+r+1)>>1; //注意这里:若mid=l+r>>1,当r-l==1,mid=l,
if(a[mid]<=x) l=mid;//若此时再进入l=mid分支,则陷入死循环
else r=mid-1;
}
return a[l];
(2)查找序列中x或x的后继(的第一个数)
while(l<r){
int mid=(l+r)>>1;
if(a[mid]>=x) r=mid;
else l=mid+1;
}
return a[l];
实数域上的二分:
注意此时的 r-l 永不会精确为0,一般的限制条件变为了 ,k是答案保留k位小数的格式要求。