区间修改+区间查询【树状数组实现,超越线段树】

参考文章:https://blog.csdn.net/MM__1997/article/details/76691744

以之前做过的一个线段树模板题为例:洛谷 P3372 线段树1

如何用树状数组实现区间修改+区间查询呢?推导数学公式:

sum[n]
= a[1] + a[2] +…+ a[n]
= (d[1]) + (d[1]+d[2]) +…+ (d[1]+d[2]+…+d[n])
= n*d[1] + (n-1)*d[2] +…+ d[n]
= n*(d[1]+d[2]+…+d[n]) - (0*d[1]+1*d[2]+…+(n-1)*d[n])
(a[ ]为原数组,d[ ]为a[ ]的差分数组)

所以可以开两个树状数组维护,一个维护d[i],一个维护(i-1)*d[i]。

如果我们要实现对原数组区间[l,r]内的元素+k,借助差分数组,只需使 tr[l]+=k,tr[r+1] -=k 即可。

洛谷 P3372 线段树1 树状数组 AC代码:

#include 
using namespace std;
typedef long long ll;
const int N=1e5+10;
int n,m,l,r,k,d,opt,a[N];
ll s1,s2,t1[N],t2[N];//t1维护d[i],t2维护(i-1)*d[i]
void add(ll tr[],int i,int k)
{
    while(i<=n)
    {
        tr[i]+=k;
        i+=(i&-i);
    }
}
ll sum(ll tr[],int i)
{
    ll s=0;
    while(i)
    {
        s+=tr[i];
        i-=(i&-i);
    }
    return s;
}
int main()
{
    ios::sync_with_stdio(false);
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        d=a[i]-a[i-1];//差分
        add(t1,i,d);//维护d[i]
        add(t2,i,(i-1)*d);//维护(i-1)*d[i]
    }
    while(m--)
    {
        cin>>opt;
        if(opt==1)
        {
            cin>>l>>r>>k;
            add(t1,l,k);add(t1,r+1,-k);//维护d[i]
            add(t2,l,k*(l-1));add(t2,r+1,-k*r);//维护(i-1)*d[i]
        }
        else
        {
            cin>>l>>r;
            s1=(l-1)*sum(t1,l-1)-sum(t2,l-1);//求sum[l-1]
            s2=r*sum(t1,r)-sum(t2,r);//求sum[r]
            printf("%lld\n",s2-s1);//[l,r]区间和=sum[r]-sum[l-1]
        }
    }
    return 0;
}

对比一下线段树代码:

#include 
using namespace std;
typedef long long ll;
const int N=1e5+10;
ll n,m,x,y,k,opt,a[N],add[4*N],sum[4*N];//add[i]表示第i个区间内每个数要加的值,也就是懒标记(延迟标记)
void build(ll i,ll l,ll r)//建树
{
    if(l==r)
    {
        sum[i]=a[l];
        return;
    }
    ll mid=l+r>>1;
    build(2*i,l,mid);
    build(2*i+1,mid+1,r);
    sum[i]=sum[2*i]+sum[2*i+1];
}
void Add(ll i,ll l,ll r,ll k)//[l,r]区间内每个数加k
{
    add[i]=add[i]+k;//add[i]表示第i个区间内每个数要加的值
    sum[i]=sum[i]+(r-l+1)*k;
}
void pushdown(ll i,ll l,ll r,ll mid)//下传标记到左右子区间
{
    if(add[i]==0)return;//没有标记,直接返回
    Add(2*i,l,mid,add[i]);
    Add(2*i+1,mid+1,r,add[i]);
    add[i]=0;//清除标记
}
void update(ll i,ll l,ll r,ll x,ll y,ll k)//更新[x,y]区间,将[x,y]区间内每个数加k
{
    if(l>y||r<x)return;
    if(l>=x&&r<=y)return Add(i,l,r,k);
    ll mid=l+r>>1;
    pushdown(i,l,r,mid);//下传标记后更新左右子区间
    update(2*i,l,mid,x,y,k);//更新左子区间
    update(2*i+1,mid+1,r,x,y,k);//更新右子区间
    sum[i]=sum[2*i]+sum[2*i+1];
}
ll query(ll i,ll l,ll r,ll x,ll y)//查询[x,y]区间和
{
    if(l>y||r<x)return 0;
    if(l>=x&&r<=y)return sum[i];
    ll mid=l+r>>1;
    pushdown(i,l,r,mid);//下传标记后查询左右子区间
    return query(2*i,l,mid,x,y)+query(2*i+1,mid+1,r,x,y);//查询左右子区间之和
}
int main()
{
    ios::sync_with_stdio(false);
    cin>>n>>m;
    for(ll i=1;i<=n;i++)
        cin>>a[i];
    build(1,1,n);
    while(m--)
    {
        cin>>opt;
        if(opt==1)
        {
            cin>>x>>y>>k;
            update(1,1,n,x,y,k);
        }
        else
        {
            cin>>x>>y;
            printf("%lld\n",query(1,1,n,x,y));
        }
    }
    return 0;
}

树状数组 运行结果:
在这里插入图片描述
线段树 运行结果:
在这里插入图片描述

事实证明,区间修改(加上一个值)+区间查询(询问和),树状数组在代码长度和运行时间上都完全吊打线段树!

当然,不能因为这一道题就完全否定线段树。线段树的适用范围更广,而且模板易于更改,也不需要像这题这样推导数学公式,是一种“傻瓜式”的强大数据结构(比较适合我这种只会做模板题的小蒟蒻)。

总结:
当题目出现特定的要求,例如 单点修改+区间查询 或 区间修改+单点查询,推荐树状数组! 类似本题的区间修改+区间查询,如果能推导出数学公式也推荐用树状数组!
树状数组不好解决的题,就用线段树吧,稍微改改线段树模板就行了!

你可能感兴趣的:(ACM-数据结构)