树状数组模型及例题整理

树状数组能解决的根本问题就是前缀和类型的问题。

对于一个数组,我们如果想进行修改和查询前缀和两种操作的话,之前的方法分为预处理和不预处理两种,如果预处理,那么查询的时间复杂度是O(1),修改的时间复杂度是O(1),因为修改之后还要维护一个前缀和。如果不预处理,那么修改的时间复杂度是O(1),查询的时间复杂度是O(n),要遍历计算结果。

这里我们引入树状数组来平衡时间复杂度,树状数组可以保证修改和查询的时间复杂度都是O(logn)

 树状数组的原理:

定义tr[i]表示以i为结尾的,长度为lowbit(i)的区间的和。 

那么很显然,我们如果想知道sum[i],那么就是将i二进制下的每一位拆出来,找到对应的tr[i]数组即可,那么时间复杂度就是O(logn),实现代码如下:

int sum(int x)
{
    int res=0;
    for(int i=x;i;i-=lowbit(i)) res += tr[i];
    return res;
} 

如果想要修改某个值,然后同时实现sum的更新,那么显然我们更改单点后只用更新它对应的父节点,递归往前,是一条单链,所以时间复杂度也是O(logn)(或者换个角度,只有顺着上去的这条链是与修改位置的值有关的)

void add(int x,int c)//在位置x加上c
{
    for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}

至于lowbit的实现就更简单了:

int lowbit(int x)
{
    return x&-x;
}

树状数组主要有三种类型的应用:

修改单点求区间和

这个就正常的用上面三个函数就能解决

修改区间求单点值

这里需要用到差分,修改的时候只修改l和r+1这两个点,查询的时候,查询区间和。

修改区间求区间和

这里需要结合差分和区间和推导一下:

我们假设b[]为差分数组,a[]为数组

sum[i]=a[1]+a[2]+...+a[i]

sum[i]=(b[1])+(b[1]+b[2])+...+(b[1]+b[2]+...+b[i])

这样看不太直观,我们把式子写出来:

黑色的为实际的a[i],红色的为补齐后的结果,如果要求和的话,我们显然可以用补齐后的全部加起来再减去补上的部分,补上的部分我们竖着看,那么就是1个b1,2个b2,...,那么就是(b1+b2+b3+...+bi)*(i+1)-(b1+2*b2+3*b3+...i*bi)

所以我们可以维护两个树状数组,一个维护bi,一个维护i*bi,剩下的部分同上,计算结果的时候稍微处理一下即可。

所以说白了,树状数组实际上就相当于预处理了一部分区间和,然后通过二进制下数的关系来进行优化。

241. 楼兰图腾(活动 - AcWing)

 

思路:我们先来分析题目,相当于就是要从数组中找出任意三个数,使得按照它们的顺序来看,它们的值满足一些关系,情况太多了,所以我们可以考虑通过分类来实现,按照中间那个数是哪个数来分类,很显然对于"V"我们要求的就是每个数的左边有多少个大于它的数,右边有多少大于它的数,两者一相乘就是以当前位置的数为最小的数的话,有多少个符合条件的。

那么我们该如何统计呢,如果一个位置上的数出现,那么我们可以对它进行标记,即让a[]数组的这一位为1,进而去更新tr[]数组,那么查询的时候查询sum(y),则查询到在1-y中有多少个标记,也就是小于等于y的数有多少个,我们再查询一下sum(n),n是最大的数,这里查询出来的结果就是在1-n之间有多少个标记,那么就是小于等于n的数有多少个,也即当前已经插入多少个数了,两者相减就可以得到当前有多少个大于y的数,因为我们是按顺序插入,所以i左边大于y的数的个数我们就统计出来了。统计右边有两种做法,一种是将所有的都算出来后再计算sum(n)-sum(y)-tj[y](即总的大于y的数减去我们在加数过程中统计到的,也即该数左边的符合要求的数的个数,那么剩下的就是右边符合要求的数的个数。),另一种做法是,将tr[]数组清空,然后从后往前插入再统计。这里就不用真的记录了,直接将查询结果与前面统计的值相乘就可以求了。

至于统计"/\"则是同理的。

求两遍树状数组的解法:

#include<bits/stdc++.h>
using namespace std;
const int N=200010;
int n,a[N],tr[N],g[N],l[N];
int lowbit(int x)
{
    return x&-x;
}
void add(int x,int c)
{
    for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
int sum(int x)
{
    int res=0;
    for(int i=x;i;i-=lowbit(i)) res += tr[i];
    return res;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        int x;
        scanf("%d",&x);
        g[i]=sum(n)-sum(x);
        l[i]=sum(x-1);
        add(x,1);//标记x已经出现过
        a[i]=x;
    }
    memset(tr,0,sizeof tr);
    long long res1=0,res2=0;
    for(int i=n;i>=1;i--)
    {
        int x=a[i];
        res1 += g[i]*(long long)(sum(n)-sum(x));
        res2 += l[i]*(long long)(sum(x-1));
        add(x,1);
    }
    cout<<res1<<" "<<res2;
}

求一遍树状数组的解法: 

#include<bits/stdc++.h>
using namespace std;
const int N=200010;
int n,a[N],tr[N],g[N],l[N];
int lowbit(int x)
{
    return x&-x;
}
void add(int x,int c)
{
    for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
int sum(int x)
{
    int res=0;
    for(int i=x;i;i-=lowbit(i)) res += tr[i];
    return res;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        int x;
        scanf("%d",&x);
        g[i]=sum(n)-sum(x);
        l[i]=sum(x-1);
        add(x,1);//标记x已经出现过
        a[i]=x;
    }
    long long res1=0,res2=0;
    for(int i=1;i<=n;i++)
    {
        int x=a[i];
        res1 += g[i]*(long long)(sum(n)-sum(x)-g[i]);
        res2 += l[i]*(long long)(sum(x-1)-l[i]);
    }
    cout<<res1<<" "<<res2;
}

242. 一个简单的整数问题(活动 - AcWing

很裸的该区间求单点的题,这里我们维护差分数组即可实现。

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int tr[N],a[N];
int n,m;
int lowbit(int x)
{
    return x&-x;
}
int sum(int x)
{
    int res=0;
    for(int i=x;i;i-=lowbit(i)) res += tr[i];
    return res;
}
void add(int x,int c)
{
    for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
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]);
    while(m--)
    {
        char op[2];
        int l;
        cin>>op>>l;
        if(op[0]=='Q')
        {
            cout<<sum(l)<<endl;
        }
        else
        {
            int r,c;
            cin>>r>>c;
            add(l,c),add(r+1,-c);
        }
    }
}

 243. 一个简单的整数问题2(活动 - AcWing)

这题是很裸的修改区间求区间的题目,思路分析如上。

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=100010;
int n,m,tr1[N],tr2[N],a[N];
int lowbit(int x)
{
    return x&-x;
}
void add(int tr[],int x,int c)
{
    for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
int sum(int tr[],int x)
{
    int res=0;
    for(int i=x;i;i-=lowbit(i)) res += tr[i];
    return res;
}
int get(int x)
{
    return sum(tr1,x)*(x+1)-sum(tr2,x);
}
signed main()
{
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
    for(int i=1;i<=n;i++) add(tr1,i,a[i]-a[i-1]),add(tr2,i,i*(a[i]-a[i-1]));
    while(m--)
    {
        char op[2];
        int l,r;
        cin>>op>>l>>r;
        if(op[0]=='Q')
        {
            cout<<get(r)-get(l-1)<<endl;
        }
        else
        {
            int c;
            cin>>c;
            add(tr1,l,c),add(tr2,l,l*c);
            add(tr1,r+1,-c),add(tr2,r+1,-(r+1)*c);
        }
    }
}

 244. 谜一样的牛(244. 谜一样的牛 - AcWing题库)

我们从前往后看的话,即使已知当前牛的前面有多少比它高的牛也没办法确定当前牛的高度,但是如果我们从最后一个开始看,如果最后一个牛前面有a头牛比它高,那么显然它就是第a+1高的牛,然后再看倒数第二头牛,我们确定了倒数第一头牛的高度,同时知道倒数第二头牛前面有多少头牛比它高,那么很显然倒数第二头牛的高度也可以确定了,如果前面有b头牛比它高,那么它的高度就是出去最后一头牛剩下的高度中,第b+1高的,依次类推。

这道题的核心在于要想确定一头牛的高度,必须要是唯一确定的才行。和这题Imbalanced Arrays(Problem - D - Codeforces)有点像,我们要想确定某个位置上的值,必须是唯一确定的才可以。

那么现在问题就转化成如何求第k大的数,以及如何修改。

我们可以假定a[i]的每一位都是1,然后求前缀和,当前缀和恰好是目标的时候,那么对应的下标就是高度,修改的时候,我们直接将a[i]减1即可。那么这里要求使得前缀和最小等于k的下标,我们可以用二分来实现。

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n;
int a[N],tr[N],ans[N];
int lowbit(int x)
{
    return x&-x;
}
void add(int x,int c)
{
    for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
int sum(int x)
{
    int res=0;
    for(int i=x;i;i-=lowbit(i)) res += tr[i];
    return res;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) add(i,1);
    //for(int i=1;i<=n;i++) tr[i]=lownbit(i);//因为都是1,所以也可以这么来写
    for(int i=2;i<=n;i++) scanf("%d",&a[i]);
    for(int i=n;i;i--)
    {
        int x=a[i];
        x++;
        int l=1,r=n;
        while(l<r)
        {
            int mid=l+r>>1;
            if(sum(mid)>=x) r=mid;
            else l=mid+1;
        }
        ans[i]=l;
        add(l,-1);
    }
    for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值