【模板】一维树状数组

本文详细介绍了树状数组这一数据结构,用于实现单点更新和区间查询操作,时间复杂度为O(1)。通过讲解前缀和、树状数组的基本操作和局限性,引入了差分的概念,进一步展示了如何利用差分在树状数组中进行区间更新和区间查询。最后给出了实例代码,解释了如何应用树状数组解决区间更新和查询的问题。

ACM模板


聊聊前缀和

比如数组

int a[7]={1,2,3,4,5,6,7}

如果需询问数组从第l个数到第r个数的和暴力做法时间复杂度为 O ( n ) O(n) O(n)

不过我们可以预处理一个前缀和数组

int b[7]={1,3,6,10,15,21,28}

比如要询问[l,r]区间的和我们可以这样做b[r]-b[l-1]这也时间复杂度为 O ( 1 ) O(1) O(1)

但是问题来了,如果我们要既要修改数组中元素的值,有要进行上述区间查询操作呢?

我们发现每次我们修改原数组中元素的值时间复杂度为 O ( 1 ) O(1) O(1)但是如果修改前缀和数组中元素的值时间复杂度将会退化到 O ( n ) O(n) O(n)

总结一下:

数组修改元素的值时间复杂度区间求和时间复杂度
原数组O(1)O(n)
前缀和数组O(n)O(1)

我们可以发现如果需要单点更新区间查询两种操作时间复杂度都是 O ( n ) O(n) O(n)

什么是树状数组?

树状数组是一种便于进行单点更新区间查询的数据结构

树状数组相关操作

  1. 二进制中最后一个1—— l o w b i t lowbit lowbit
   int lowbit(int x)
   {
   	return x&-x;
   }
  1. 单点更新

    我们对数组位置为x的元素加上c

   //树状数组为tree,数组元素个数为n,数组下标从0开始
   void add(int x,int c)
   {
   	for(;x<=n;x+=lowbit(x)) tree[x]+=c;
   }
  1. 区间求和
  //求出[1,x]数组中的总和即前缀和
  int sum(int x)
  {
  	int res=0;
  	for(;x;x-=lowbit(x)) res+=tree[x];
  	return res;
  }

局限性

我们很容易发现上述树状数组只适用于单点更新区间查询,但是如果是区间修改单点查询好像力不从心

差分在树状数组中的应用

告诉你个好消息如果有差分的介入,那么树状数组可以进行区间更新单点查询当然也可以进行更厉害的区间更新区间查询

区间更新、单点查询

我们把 t r e e [ ] tree[] tree[]数组构造成一个差分数组还是看题吧

题目

给定长度为N的数列A,然后输入M行操作指令。

第一类指令形如“C l r d”,表示把数列中第l~r个数都加d。

第二类指令形如“Q X”,表示询问数列中第x个数的值。

对于每个询问,输出一个整数表示答案。

输入格式

第一行包含两个整数N和M。

第二行包含N个整数A[i]。

接下来M行表示M条指令,每条指令的格式如题目描述所示。

输出格式

对于每个询问,输出一个整数表示答案。

每个答案占一行。

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int N=100010;
ll tree[N];
int n,m;
int lowbit(int x) 
{
    return x&-x;
}
void add(int x,int c)
{
    for(;x<=n;x+=lowbit(x)) tree[x]+=c;
}
ll sum(int x)
{
    int res=0;
    for(;x;x-=lowbit(x)) res+=tree[x];
    return res;
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        int a;
        cin>>a;
        add(i,a);
        add(i+1,-a);
    }
    while(m--)
    {
        char t;
        cin>>t;
        if(t=='Q')
        {
            int x;
            cin>>x;
            cout<<sum(x)<<endl;
        }
        else
        {
            int a,b,c;
            cin>>a>>b>>c;
            add(a,c);
            add(b+1,-c);
        }
    }
    return 0;
}

我们可以发现对于上述代码即在建树的过程中建成差分树的形式即可

区间更新、区间查询

原数组a[],对于区间更新我们可以维护一个差分数组b[]

如果我们维护数组a的前缀和我们可以发现有下面等式:
∑ i = 1 x a i = ∑ i = 1 x ∑ j = 1 i b i = ∑ i = 1 x ( x − i + 1 ) b i \sum_{i=1}^x a_i=\sum_{i=1}^x\sum_{j=1}^i b_i=\sum_{i=1}^x(x-i+1)b_i i=1xai=i=1xj=1ibi=i=1x(xi+1)bi
变换一下:
∑ i = 1 x a i = ( x + 1 ) ∑ i = 1 x b i − ∑ i = 1 x b i × i \sum_{i=1}^x a_i=(x+1)\sum_{i=1}^x b_i-\sum_{i=1}^x b_i×i i=1xai=(x+1)i=1xbii=1xbi×i

于是我们可以维护两个差分树状数组 t r e e 1 [ ] tree1[] tree1[]维护$ bi 、 、 tree2[] 维 护 维护 i*bi$

给定一个长度为N的数列A,以及M条指令,每条指令可能是以下两种之一:

1、“C l r d”,表示把 A[l],A[l+1],…,A[r] 都加上 d。

2、“Q l r”,表示询问 数列中第 l~r 个数的和。

对于每个询问,输出一个整数表示答案。

输入格式

第一行两个整数N,M。

第二行N个整数A[i]。

接下来M行表示M条指令,每条指令的格式如题目描述所示。

输出格式

对于每个询问,输出一个整数表示答案。

每个答案占一行。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>

using namespace std;

typedef long long ll;

const int N=100010;

int n,m;
ll tree1[N],tree2[N];  //维护b[i]  维护i*b[i]
int lowbit(int x)
{
    return x&-x;
}

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

ll sum(ll tree[],int x)
{
    ll res=0;
    for(;x;x-=lowbit(x)) res+=tree[x];
    return res;
}
ll prefix_sum(int x)
{
    return (x+1)*sum(tree1,x)-sum(tree2,x);
}

int main()
{
    scanf("%d%d",&n,&m);
    
    for(int i=1;i<=n;i++)
    {
        int a;
        scanf("%d",&a);
        
        add(tree1,i,a);
        add(tree1,i+1,-a);
        add(tree2,i,1ll*i*a);
        add(tree2,i+1,-1ll*(i+1)*a);
    }
    while(m--)
    {
        char t;
        int l,r;
        cin>>t>>l>>r;
        if(t=='Q')
        {
            scanf("%d%d",&l,&r);
            cout<<prefix_sum(r)-prefix_sum(l-1)<<endl;
        }
        else
        {
            int d;
            scanf("%d",&d);
            add(tree1,l,d),add(tree1,r+1,-d);
            add(tree2,l,l*d),add(tree2, r+1,-1ll*(r+1)*d);
            
        }
    }
    return 0;
}

树状数组应用

  1. 区间更新区间查询

  2. 逆序对:首先有一种求逆序对的方法:开一个数组cnt[n]​然后遍历数组中的每一个数,在cnt[]数组中留下标记:比如数组中第i个元素为x,那就cnt[x]=1,所谓逆序即:我比你先出现而且比你大,对于上述例子所谓比你大即在[x+1,n]这个区间中的值,所谓比你先出现即在第i-1次遍历时被标记过。转化一下相当于求 c n t cnt cnt数组中[x+1,n]区间内的和

  3. 第k小的数

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值