树状数组是一种支持动态修改、区间查询的数据结构,常用于优化查询会变化的序列的前缀和。下面是一些例题:
例题1:AcWing 242.一个简单的整数问题
这个问题确实很简单,主要是用来介绍树状数组的基本用途和功能及其实现方式的。这题只需要支持单点修改和求前缀和,非常容易实现,代码如下:
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e5+10;
typedef long long ll;
int n,m;
int a[N],c[N];
int lowbit(int x){
return x&-x;
}
void add(int l,int d){//单点修改
for(int i=l;i<=n;i+=lowbit(i)) c[i]+=d;
}
int ask(int x){//求1-x的前缀和
int sum=0;
for(int i=x;i;i-=lowbit(i)){
sum+=c[i];
}
return sum;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
add(i,a[i]-a[i-1]);
}
while(m--){
char c;
cin>>c;
if(c=='C'){
int l,r,d;
cin>>l>>r>>d;
add(l,d);
add(r+1,-d);
}
else{
int x;
cin>>x;
cout<<ask(x)<<endl;
}
}
return 0;
}
例题2:AcWing 241.楼兰图腾
这题其实可以等效为:准确地求出每一个位置分别与其前面、后面构成的“逆序对”个数。那么思路是什么呢?以求位置i以前的逆序对个数为例:我们只要从1-n循环遍历y数组,每次将位置y[i](树状数组的下标)修改为1,然后每次求出y[i]+1~n的前缀和即可。其它情况同理,代码如下:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=2e5+5;
int n;
int a[N],tr[N];
int sma[N],big[N];
typedef long long ll;
inline int lowbit(int x){
return x & -x;
}
void add(int x,int d){
for(int i=x;i<=n;i+=lowbit(i)){
tr[i]+=d;
}
}
int ask(int x){
int res=0;
for(int i=x;i;i-=lowbit(i)){
res+=tr[i];
}
return res;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++){//处理前段
add(a[i],1);
sma[i]=ask(a[i]-1);
big[i]=ask(n)-ask(a[i]);
}
memset(tr,0,sizeof tr);
ll resV=0,resA=0;
for(int i=n;i>=1;i--){//处理后段
add(a[i],1);
resV+=(ll)big[i]*(ask(n)-ask(a[i]));
resA+=(ll)sma[i]*ask(a[i]-1);
}
cout<<resV<<' '<<resA<<endl;
return 0;
}
例题3:一个简单的整数问题2
这题可不简单,首先要想到参考例题1的解法,用b数组来表示偏移量的差分数组,由此来推导公式。具体可见蓝书207。但是我要说的是我自己之气那没有想明白的问题:为什么对于i*b[i]数组的修改可以变成单点修改。其实是走进了这样一个误区:b[i]表示的不是单点的偏移量本身,而是偏移量的差分数组,也就是说:b[1]-b[i]之和才是a[i]的偏移量。那么i * b[i]表示的意义也就不存在了,只是用来维护公式的一部分的工具罢了。那么我们就明确了:b数组只需要单点修改就可以影响偏移量了,那么既然b[i]只有单点变化了,那么i *b[i]当然也只有单点变化了,也就是变化了i *
d。蓝书上总结的好啊:对于这种多变量的式子,我们要通过变形转化,尽量将相关的变量分在一组中,减少变量个数。代码如下:
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e5+5;
typedef long long ll;
int n,m;
ll sum[N];
ll c[2][N];//c是偏移量数组,c[0][]表示求b[i]的和,c[1][]表示求i*b[i]的和
inline int lowbit(int x){
return x&-x;
}
void add(int k,int x,int d){
for(int i=x;i<=n;i+=lowbit(i)){
c[k][i]+=d;
}
}
ll ask(int k,int x){
ll res=0;
for(int i=x;i;i-=lowbit(i)){
res+=c[k][i];
}
return res;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%lld",&sum[i]);
sum[i]+=sum[i-1];
}
while(m--){
char op;
int l,r;
cin>>op>>l>>r;
if(op=='Q'){
ll dlt=(r+1)*ask(0,r)-ask(1,r);
dlt-=l*ask(0,l-1)-ask(1,l-1);
printf("%lld\n",sum[r]-sum[l-1]+dlt);
}
else{
int d;
scanf("%d",&d);
add(0,l,d);
add(0,r+1,-d);
add(1,l,l*d);
add(1,r+1,-(r+1)*d);
}
}
return 0;
}
例题4:AcWing 244.谜一样的牛
这题的关键在于:寻找突破口。不妨这样思考:假如考初赛时碰到了这个题目,但是只让你想办法求样例的答案,你应该怎么做?显然应当要寻找突破口,找到最容易填出最唯一确定的位置来填。显然对于最后一个数,到它这里数据已经输完了。那么假设它前面有x个数比它小,那么它自身的身高显然为x+1。那么同理,对于倒数第二个数,我们应当在[1,n]中继续寻找第a[n-1]+1大的数(x+1这个点此时已经被删除,不在考虑范围之内)。那么这样这个问题就变得确定可解了。所以,如何高效的执行删除和查询排名这两项操作呢?我们想到利用前缀和,原始数组全部初始化为1,一个点被删除后就将其置为0。那么只需要二分一个位置l,使得sum[l]==a[i]+1,那么l就是第i头牛的身高。代码如下:
#include<iostream>
using namespace std;
const int N=1e5+5;
int c[N],a[N],ans[N];
int n;
int lowbit(int x){
return x&-x;
}
void add(int x,int y){
while(x<=n){
c[x]+=y;
x+=lowbit(x);
}
}
int query(int x){
int res=0;
while(x){
res+=c[x];
x-=lowbit(x);
}
return res;
}
int main(){
cin>>n;
add(1,1);
for(int i=2;i<=n;i++){
cin>>a[i];
add(i,1);
}
for(int i=n;i>=1;i--){
int l=1,r=n;
while(l<r){
int mid=(l+r)>>1;
if(a[i]+1>query(mid)) l=mid+1;
else r=mid;
}
ans[i]=l;
add(l,-1);
}
for(int i=1;i<=n;i++) cout<<ans[i]<<endl;
}
Extend exercise:AcWing 260.买票
这题是借鉴了上一题的思想,几乎可以说是一模一样的代码。考虑这题的性质:显然最后一个人占据的位置就是pn+1,而前面的人如果有人也需要占掉这个位置,那就只能到后面去找到第一个空余位置,而且后面的人的优先级更大。所以这题只要对于每次输入的pi,找到空余的第pi个数,然后将该位置标记为不空即可。代码如下:
#include<iostream>
using namespace std;
const int N=2e5+5;
int p[N],v[N];
int tr[N],ans[N];
int n;
inline int lowbit(int x){
return x & -x;
}
void add(int x,int d){
for(int i=x;i<=n;i+=lowbit(i)){
tr[i]+=d;
}
}
int query(int x){
int res=0;
for(int i=x;i;i-=lowbit(i)){
res+=tr[i];
}
return res;
}
int main(){
while(cin>>n){
for(int i=1;i<=n;i++){
cin>>p[i]>>v[i];
add(i,1);
}
for(int i=n;i>=1;i--){
int l=1,r=n;
while(l<r){
int mid=(l+r)>>1;
if(p[i]+1<=query(mid)) r=mid;
else l=mid+1;
}
ans[l]=v[i];
add(l,-1);
}
for(int i=1;i<=n;i++) cout<<ans[i]<<' ';
cout<<endl;
}
return 0;
}