[AcWing算法提高课]之 高阶数据结构 树状数组(C++题解)

本文详细介绍了树状数组的基本概念和作用,包括单点增加和区间查询前缀和的操作,以及在求逆序对、差分等问题中的应用。通过经典模板展示了树状数组的实现,并给出了相关题目实例,如楼兰图腾、整数问题等,解释了如何利用树状数组解决区间修改和单点查询问题。此外,还分析了在特定问题中如何优化树状数组的使用,如谜一样的牛问题。
摘要由CSDN通过智能技术生成

目录

树状数组的作用

(1)树状数组的经典模板

(2)关于记忆模板

楼兰图腾

一个简单的整数问题

一个简单的整数问题2(困难!)

谜一样的牛


我不会数学证明,但我可以学,会用就行,你知道我听了y总讲了一个小时证明的痛楚吗

树状数组的作用

  1. 单点增加(时间复杂度为O (logN) )
  2. 区间查询前缀和(时间复杂度为O (logN) )
  3. 求逆序对(但是不如归并排序)
  4. 扩展:差分+公式

相较于原数组a[N],单点增加的时间复杂度为O(1),但区间查询和的时间复杂度为O(N)

相较于前缀和数组pre[N],区间查询和的时间复杂度为O(1),但单点增加的复杂度为O(N)

因此我们选择了折中的办法就是 让这两个操作都为logN的复杂度 

(1)树状数组的经典模板

#include<iostream>
#include<algorithm>
using namespace std;

const int N=1e5+10;
int a[N];
int tr[N];
int n;//点数

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

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

int sum(int x)//求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];
    while(1)
    {
        cout<<"第一次查询前缀和(1)"<<endl;
        for(int i=1;i<=n;i++) cout<<sum(i)<<" ";
        cout<<endl;
        for(int i=1;i<=n;i++) add(i,1);//在每个位置上都加上1

        cout<<"每个位置都加上1后第二次查询前缀和(2)"<<endl;
        for(int i=1;i<=n;i++) cout<<sum(i)<<" ";//前缀和
        cout<<endl;
        cout<<"查询每个点的数值大小(3)"<<endl;
        for(int i=1;i<=n;i++) cout<<sum(i)-sum(i-1)<<" ";//查询单点的数值大小
        cout<<endl;
        break;
    }
    system("pause");

    return 0;
}

(2)关于记忆模板

  1.  首先先把这张图记住
  2. 对于add函数,因为树状数组维护的是一部分树的前缀和,那么因此,它只能影响到这个位置及其后面的树状数组,因为需要将后面的树状数组更新,那么以lowbit(i)为单位向后更新
  3. 对于sum函数,因为树状数组维护的是一部分树的前缀和,因此它向前查询,其中每一段的大小是lowbit(i),用res变量记录每一段树状数组的累计和,返回即可
  4. 如果还不理解的话,多写几遍,或者回去看证明= - =


楼兰图腾

241. 楼兰图腾 - AcWing题库

核心思路:

  1. (乘法原理)我们可以知道,枚举中间的一个点,统计其左右两边满足条件的数量,那么左右两边的乘积结果就是 一个结果;举个例子:比如我们要求’V‘型的 点的个数,那么可以先预处理出 在第i个位置 ,其左边的点高于中间点的高度的个数,和右边的点高于中间点的高度的个数,然后相乘就是 枚举到的该点作为中间点的个数
  2.  那么现在的问题就是<在一个区间中 快速地求出 小于或大于这个点的高度的 个数>
  3. 注意此时因为目标是 数字的个数,而非数字的大小,这启发了我们对于一个位置上的值,它是 数字的个数更为合理;那么对于这个位置而言呢,注意 条件是 <小于或大于这个数>,也就是说数的大小其实是作为 键的,也就是索引
  4. 问题分析到这里已经有了些许眉目,接下来的细节请看代码+详细注释
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

const int N=2000010;
typedef long long ll;
int n;
int a[N],t[N];//t[i]表示树状数组i结点覆盖的范围和
//Lower[i]表示左边比第i个位置小的数的个数
//Greater[i]表示左边比第i个位置大的数的个数
int Lower[N],Greater[N];

//返回非负整数x在二进制表示下最低位1及其后面的0构成的数值
int lowbit(int x)
{
    return x&-x;
}

//将序列中第x个数加上k
void add(int x,int k)//向右上方以lowbit(i) 为单位 在t数组中 区间加上k 维护
{
    for(int i=x;i<=n;i+=lowbit(i)) t[i]+=k;
}

//查询序列前x个数的和
int ask(int x)//向左上方以lowbit(i) 为单位 查询
{
    int sum=0;
    for(int i=x;i;i-=lowbit(i)) sum+=t[i];
    return sum;
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);

    从左向右,依次统计每个位置左边比第i个数y小的数的个数、以及大的数的个数
    for(int i=1;i<=n;i++)
    {
        int y=a[i];//第i个数

        //在前面已加入树状数组的所有数种统计在区间[1,y-1]的数字 的出现次数
        Lower[i]=ask(y-1);
        
        //在前面已加入树状数组的所有数中统计在区间[y+1,n]的数字 的出现次数
        Greater[i]=ask(n)-ask(y);
        
        将y加入树状数组,即数字y出现1次
        add(y,1);//注意这里的y是a[i],也就是数字的大小,因为是已经按位置进行枚举的了
        //因此,不需要考虑 位置的先后顺序,而只用考虑数字的大小关系
    }

    //清空树状数组,从右往左统计每个位置右边比第i个数y小的数的个数、以及大的数的个数
    memset(t,0,sizeof t);

    ll resA=0,resV=0;
    //从右往左统计
    for(int i=n;i>=1;i--)
    {
        int y=a[i];

        resA+=(ll)Lower[i]*ask(y-1);//向右查询 [1,y-1]的数字的 总个数
        resV+=(ll)Greater[i]*(ask(n)-ask(y));//向右查询 [y+1,n]的数字的 总个数

        add(y,1);//同理
    }

    printf("%lld %lld\n",resV,resA);

    return 0;
}

一个简单的整数问题

242. 一个简单的整数问题 - AcWing题库

  1. 与普通的 树状数组模板题不同的是:这题的操作是:[区间修改]+[单点查询]
  2. 而普通的 树状数组模板提是:[单点修改]+[区间查询]
  3. 单点查询其实并不困难:比如在普通的维护前缀和的树状数组中想要知道i点位置对应的大小,那么只需要sum(i)-sum(i-1)即可
  4. 难点是:区间修改;不管是 原数组的[区间修改],还是前缀和数组的[区间修改],它们所花费的时间复杂度都为O(N)
  5. 对于区间修改,不难想到 差分数组,差分是前缀和数组的逆运算;                                            对于[区间修改]:比如想要在[l,r]的范围内的所有数字都加上 常数C                                            那么就有 tr[l]+=c  tr[r+1]-=c    <至于为什么这样写,我只能说回去看基础课>
  6. 对于[单点查询]:想要求<i点位置对应的大小>,就是将i之前(包括i)的所有数组和求和,得到的结果就是就是 <i点位置对应的大小> ,<因此可以维护一个可以计算差分数组前缀和的树状数组> ,来使得O(N)的复杂度 降低到 O(logN)

 Code:预处理(包括其本身的差分)

#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;

typedef long long ll;
const int N=100010;
int n,m;
int a[N];
ll tr[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;
}

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

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,r,d;
        scanf("%s%d",op,&l);
        if(*op=='C')//区间修改
        {
            scanf("%d%d",&r,&d);
            add(l,d);
            add(r+1,-d);
        }
        else printf("%lld\n",sum(l));
        //树状数组中的前缀和维护使得差分数组能更快地求出 1~i的差分数组的和
    }

    return 0;
}

 Code:预处理:只记录变化区间的差分《算法竞赛进阶指南》

#include<iostream>
#include<algorithm>
using namespace std;

const int N=1e5+10;
int a[N];
int tr[N];//树上差分
int n,m;

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%d",&n,&m);
    
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    
    while(m--)
    {
        char op[2];
        int l;
        scanf("%s%d",op,&l);
        
        if(*op=='C')//区间添加操作
        {
            int r,d;
            scanf("%d%d",&r,&d);
            add(l,d),add(r+1,-d);
        }
        else
        {
            printf("%d\n",sum(l)+a[l]);
        }
    }
    
    return 0;
}

一个简单的整数问题2(困难!)

243. 一个简单的整数问题2 - AcWing题库

  1. 问题的核心变为了:[区间修改]+[区间求和]
  2. [区间修改]还是一样的思路,树上差分即可
  3. [区间求和]:注意是原数组的区间求和,那么就是在[l,r]范围内对所有的 a[l]~a[r]求和,在上一题中我们已知道了a[i]的求法是 sum(i),因此可以有下图的写法

 

 y总的字有待提高....,我简单模拟一下吧,方便看懂

例子:比如从在区间[1,x]中 求a[]的和
规定:b[]为a[]的差分数组

解法:
朴素解法:a[1]+a[2]+a[3]+...+a[x]  ----(1)
那么根据差分数组的特性则有:
a[1]=b[1]
a[2]=b[1]+b[2]
a[3]=b[1]+b[2]+b[3]
a[x]=b[1]+b[2]+b[3]+...+b[x]      ----(2)

那么根据(2)式的特性,可以将(1)式改写为:
(b[1])+(b[1]+b[2])+(b[1]+b[2]+b[3])+...+(b[1]+b[2]+b[3]+...+b[x])  ----(3)

不难得出规律,在[1,x]区间中:
有 x-0个     b[1]
有 x-1个     b[2]
有 x-2个     b[3]
.....
有 x-(x-1)个 b[x]

那么y总的思路是:构造下面图片的这个  矩阵
目的是:对于(3)式 而言:就算用前缀和求出了 每一段()的和,但是仍然要用一层循环将其相加
所以实际上的时间复杂度仍然为O(N),那么我们就要考虑是不是可以构造出 *两个前缀和数组来优化*

实际上:图片中蓝色区域中的 数字才是 我们要求的数值
因此根据很简单的填充法:我们可以先求出整个 矩阵的数值,然后再减去红色区域的数值

那么根据图形不难得出如下公式:
矩阵的数值:(b[1]+b[2]+b[3]+...+b[x])*(x+1)           ————(4)
红色区域的数值:(1* b[1]+2* b[2]+3* b[3]+...+x*b[x])   ————(5)

观察(4) 和 (5) 式子
不难得出 已经构造出了 两个前缀和数组
(4) 中的前缀和数组是 sum(x) 用tr1维护
(5) 中的前缀和数组是 i*sum(x) 用tr2维护

~~剩下的就与一个简单整数问题一样了~~

 看不清的同学:我用截图吧QAQ md格式放到这里就好丑.....

 

 Code:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;

typedef long long ll;
const int N=100010;

int n,m;
int a[N];
ll tr1[N];//维护b[i]的前缀和
ll tr2[N];//维护b[i]*i的前缀和

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

void add(ll tr[],int x,ll c)
{
    for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}

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

ll prefix_sum(int x)//数学构造:详细见图片
{ 
    return sum(tr1,x)*(x+1)-sum(tr2,x);
}

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++)//构造树上差分
    {
        int b=a[i]-a[i-1];
        add(tr1,i,b);
        add(tr2,i,(ll)b*i);
    }

    while(m--)
    {
        char op[2];
        int l,r,d;
        scanf("%s%d%d",op,&l,&r);
        if(*op=='Q')
        {
            printf("%lld\n",prefix_sum(r)-prefix_sum(l-1));
        }
        else
        {
            scanf("%d",&d);
            //a[l]+=d
            add(tr1,l,d),add(tr2,l,l*d);
            //a[r+1]-=d
            add(tr1,r+1,-d),add(tr2,r+1,(r+1)*(-d));
        }
    }
    return 0;
}

谜一样的牛

244. 谜一样的牛 - AcWing题库

样例解释:

样例解释

  1.  第一行是索引
  2. 第二行表示的意思是: 第i个位置前有 第A[i]头牛 的身高比它低
  3. 特殊解释:因为 在第一个位置的牛,它前面没有牛,因此A[i] 没有给出(本质上是一个不存在的数)
  4. 从后往前推是更容易知道结果的,这个至于为什么是,,我只能说 显然
  5. 对于5位置上的值:0,前面没有比它身高低的,所以它就是 身高最低的:1
  6. 对于4位置上的值:1,也就是说前面 有一个比它身高低的,那么 对于 答案序列 1 2 3 4 5,我们已经用掉了1;因此此时的 答案序列为: 2  3  4  5,在这个序列中 寻找 <最小的第二个数(也就是 (1+1)=2)> :(原谅我语言表达不行....)理解这个意思就好:所以此时的答案是:3
  7. 对于3位置上的值:2,同理了,此时的答案序列为 13  4  5,那么就要寻找  (2+1)=3,最小的第三个数,也就是 5
  8. 此后以此类推

核心思路:

这道题其实和 楼兰图腾 很像

  1. 在本题中 数字的大小实际上是作为 比较时的索引,而非真正的值,与楼兰图腾同理,值是这个数的个数;因此 数值的大小作为索引,1则作为val,当这个值被用过了,那么就需要让这个位置的值-1,表示为不可重复使用
  2. 其实相对于树状数组,本题更容易想到的是:二分,注意上面 中我们推导出 它是第几小的数的规律,是这个 数值的大小+1,那么也就是说,要寻找 第一个满足 “第几小这个性质的”,由左右区间可知,答案必然在 右区间;因此左半边是 不满足的,右半边是满足的,具有二段性,因此可以二分
  3. 那么 check函数的作用就是 在 序列 1 2 3 ..h[i]+1 ....N,判断从1~x(某个数)中,是否其有效数字的个数,已经恰好大于等于 h[i]+1,如果是则true,right=mid;反之false,left=mid+1
  4. 对于3中的查询操作,可以使用 树状数组中的 维护前缀和完成,使得效率更高
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

const int N=100010;
int n;
int h[N];
int ans[N];
int tr[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=2;i<=n;i++) scanf("%d",&h[i]);

    for(int i=1;i<=n;i++) add(i,1);

    for(int i=n;i;i--)
    {
        int k=h[i]+1;
        int l=1,r=n;
        while(l<r)
        {
            int mid=l+r>>1;
            if(sum(mid)>=k) r=mid;
            else l=mid+1;
        }
        ans[i]=l;
        add(l,-1);
    }

    for(int i=1;i<=n;i++) printf("%d\n",ans[i]);

    return 0;
}

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值