题目描述
给定一个长度为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条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
数据范围
1≤N,M≤105,
|d|≤10000,
|A[i]|≤1000000000
输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4
输出样例:
4
55
9
15
题目分析
树状数组解
这个题有两个操作:将[l,r]区间的数都加d 和 求出[l,r]区间的和。
我们先看第一个操作:将[l,r]区间的数都加d。要快速的完成这个操作,还是得用差分数组来完成。(即用线段树维护一个差分数组)
但对于第二个操作来说,一个差分数组并不能快速的求出a[l,r]的和。
设b[]为a[]的差分数组。求出1-x的a[i]的和:
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[4]……b[x]
上面所有的b[]全部加起来才能得到a[1-x]的和。由上面的方阵我们可以得到:
b[1] b[2] b[3] b[4]……b[x]
a[1]:b[1] b[2] b[3] b[4]……b[x]
a[2]:b[1] b[2] b[3] b[4]……b[x]
a[3]:b[1] b[2] b[3] b[4]……b[x]
……
a[x-1]:b[1] b[2] b[3]…b[x-1] b[x]
a[x]:b[1] b[2] b[3] b[4]……b[x]
这个方阵的和为(b[1]+b[2]+...+b[x])*(x+1),而补上的蓝色部分的和为(b[1]*1+b[2]*2+...+xb[x])。
因此a[1-x]的前缀和为:(b[1]+b[2]+...+b[x])*(x+1)-(b[1]*1+b[2]*2+...+xb[x])==sum(b[i])*(x+1)-sum(b[i]*i)[1<=i<=x]
。
所以,我们只需要维护b[i]和i*b[i]两个树状数组即可快速的完成这两个操作。
代码如下
#include <iostream>
#include <cstdio>
#include <cmath>
#include <string>
#include <cstring>
#include <map>
#include <queue>
#include <vector>
#include <set>
#include <algorithm>
#define LL long long
#define PII pair<int,int>
#define x first
#define y second
using namespace std;
const int N=1e5+5,M=350,INF=0x3f3f3f3f;
int n,m,a[N];
LL tr1[N],tr2[N];
int lowbit(int x)
{
return x&-x;
}
void add(int x,int c)
{
for(int i=x;i<=n;i+=lowbit(i))
tr1[i]+=c,tr2[i]+=(LL)c*x;
}
LL sum(int x)
{
LL res=0;
for(int i=x;i;i-=lowbit(i))
res+=tr1[i]*(x+1)-tr2[i];
return res;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
add(i,a[i]-a[i-1]);
}
while(m--)
{
char op[2];
int l,r,d;
scanf("%s",op);
if(op[0]=='C')
{
scanf("%d%d%d",&l,&r,&d);
add(l,d),add(r+1,-d);
}
else
{
scanf("%d%d",&l,&r);
printf("%lld\n",sum(r)-sum(l-1));
}
}
return 0;
}
线段树解
除了树状数组的解法,我们也可以用线段树来解这道题。
因为我们要求的是区间和,因此辅助信息即为区间和sum。 这个辅助信息比较好求:u段的sum即为左子段的sum+右子段的sum。
但是我们还需要进行区间修改,这时加一个懒标记add来记录某一段区间要加多少数就行了。
一道线段树+懒标记的裸题。
代码如下
#include <iostream>
#include <cstring>
#define LL long long
using namespace std;
const int N=1e5+5;
struct Node{
int l,r;
LL sum,add; //sum记录区间,add(懒标记)记录子段的sum要加的值
}tr[N*4];
int n,m;
int w[N]; //记录原序列
void pushup(int u) //u段的sum即为左子段的sum+右子段的sum
{
tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
void pushdown(int u) //将懒标记向下传递
{
auto &root=tr[u],&left=tr[u<<1],&right=tr[u<<1|1];
if(root.add)
{
left.add+=root.add; //更新子段的懒标记
left.sum+=(LL)(left.r-left.l+1)*root.add; //更新子段的sum
right.add+=root.add;
right.sum+=(LL)(right.r-right.l+1)*root.add;
root.add=0; //将u段的懒标记清0
}
}
void build(int u,int l,int r) //建树
{
if(l==r) tr[u]={l,r,w[l],0};
else
{
tr[u]={l,r};
int mid=l+r>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
pushup(u);
}
}
void modify(int u,int l,int r,int v) //区间修改
{
if(tr[u].l>=l&&tr[u].r<=r)
{
tr[u].sum+=(LL)(tr[u].r-tr[u].l+1)*v; //u段的sum加的值即为:u段的长度*v
tr[u].add+=v; //更新懒标记
}
else
{
pushdown(u);
int mid=tr[u].l+tr[u].r>>1;
if(l<=mid) modify(u<<1,l,r,v); //如果l<=mid,说明[l,r]涉及左半段,因此要修改左半边
if(r>mid) modify(u<<1|1,l,r,v); //如果r>mid,说明[l,r]涉及右半段,因此要修改右半边
pushup(u);
}
}
LL query(int u,int l,int r)
{
if(tr[u].l>=l&&tr[u].r<=r) return tr[u].sum; //如果u段在[l,r]内,那么和即为tr[u].sum
pushdown(u);
int mid=tr[u].l+tr[u].r>>1;
LL sum=0;
if(l<=mid) sum+=query(u<<1,l,r); //如果l<=mid,说明[l,r]涉及左半段,因此要求左半段的和
if(r>mid) sum+=query(u<<1|1,l,r); //如果r>mid,说明[l,r]涉及右半段,因此要求右半段的和
return sum;
}
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&w[i]);
build(1,1,n); //建树
while(m--)
{
int l,r;
char op[2];
scanf("%s %d %d",op,&l,&r);
if(op[0]=='C')
{
int d;
scanf("%d",&d);
modify(1,l,r,d);
}
else printf("%lld\n",query(1,l,r));
}
return 0;
}