数据结构进阶之树状数组

树状数组是一种高效的数据结构,常用于动态修改和区间查询,例如求解前缀和问题。本文通过四个例题详细介绍了树状数组的使用,包括单点修改、区间查询、逆序对计算和寻找唯一确定的位置。同时,还讨论了如何利用树状数组解决实际问题,如牛的身高排序和买票问题。
摘要由CSDN通过智能技术生成

树状数组是一种支持动态修改、区间查询的数据结构,常用于优化查询会变化的序列的前缀和。下面是一些例题:
例题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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值