参考文章: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;
}
事实证明,区间修改(加上一个值)+区间查询(询问和),树状数组在代码长度和运行时间上都完全吊打线段树!
当然,不能因为这一道题就完全否定线段树。线段树的适用范围更广,而且模板易于更改,也不需要像这题这样推导数学公式,是一种“傻瓜式”的强大数据结构(比较适合我这种只会做模板题的小蒟蒻)。
总结:
当题目出现特定的要求,例如 单点修改+区间查询 或 区间修改+单点查询,推荐树状数组! 类似本题的区间修改+区间查询,如果能推导出数学公式也推荐用树状数组!
树状数组不好解决的题,就用线段树吧,稍微改改线段树模板就行了!