题目大意
有一个数组长度为N的数组,总共输入T组数据指令,当输入 Q 指令时,代表要将 下标在 a,b之间的元素求和并将其输出,而当输入C指令时,要求要将下标在a,b之间的所有数组元素都加上c.
题目思路
由于N的长度较大,对于求和我们往往会采用前缀和的思想来降低时间复杂度。但是本题还要求我们改变数组的元素值,那我们要采用前缀和的解题模板会导致我们修改数组元素时的效率很低。所以我们就要用到线段树的思想来降低时间复杂度.所谓线段树,就是将一个连续的区间,用线段树这种数据结构分割开来。线段树的节点储存的一个区间。比如,在本题中,我将实现区间求和,和区间的修改。我们将这个区间分割为一个个的小区间。这样的话,我们需要修改的区间 就极大的减少了。
我们用二分了的思想,来将区间分解。比如将【1,13】。拆分为【1,7】。【8,13】;再分别将【1,7】。【8,13】进行拆分建立了一颗像二叉树的结构。我们再利用一点前缀和的思想,计算出每一个区间代表着这个区间的和。(因为前缀和计算和的差值非常迅速)
在本题中,由于我们要实现区间修改,为了提高效率,我们往往会需要一个懒惰标记,来防止递归到叶节点,增加整个程序的运行速率。标记的含义就是,本节点的信息已经根据标记更新过了,但本节点的子节点仍然需要更新。举个栗子,我们要将一个区间的值全部加上1,实际上,我们并不给加+1.而是打个标记,记录下,这个区间的每个值需要加1.打上标记后,我们要根据标记来更新这个节点的统计信息。比如,如果本节点维护的是区间和,而本节点包含5个数,那么,打上+1的标记之后,要给本节点维护的和+5。这是向下延迟修改,但是向上显示的信息是修改以后的信息,所以查询的时候可以得到正确的结果。有的标记之间会相互影响,所以比较简单的做法是,每递归到一个区间,首先下推标记(若本节点有标记,就下推标记),然后再打上新的标记,这样仍然每个区间操作的复杂度是O(log2(n))。借鉴了这个大佬的思路。
注意事项
在线段树中,我们有不少需要注意的点。
- 建立线段树,数组大小一般不超过最大值 maxn的4倍。(基于定理:n>=3时,一个[1,n]的线段树可以将[1,n]的任意子区间[L,R]分解为不超过个子区间。)所以一般开4倍大小的数组足矣保证使用。
- 向上更新,由于子区间的更新,我们需要保证父节点的正确性就必须向上更新。由于线段树的建立都是从根节点出发,比如,我们想要保证一个父节点的正确性,那么只需要保证他的左右儿子的正确性即可。所以在建树或者修改的区间的时候,我们
由于是前缀和的思想,比如【1,1】的值改变了,那包含【1,1】所有子区间的区间的值都要改变 - 向下更新,我们每次在进行区间修改的时候都要进行一次向下更新,因为我们之前标记了区间,而我们没有继续的向下修改子区间的值,所有我们在进行新数据更新时,要先把前面的值处理完毕,之所以这样处理是之前为了提高代码的效率,选择了只标记不更新。故,新数据更新前要先根据标记,向下更新之前没有更新的值,保证子节点的正确性,从而保证父节点的正确性。
题目代码
#include <cstring>
#include <string>
#include <stdio.h>
#include<algorithm>
#define ll long long
using namespace std;
const int maxn = 1e5+10 ;
int n,m;
ll sum[maxn<<2],lazy[maxn<<2];
void pushup(ll k) //向上更新函数
{
sum[k]= sum[k<<1]+sum[k<<1 | 1];
}
void pushdown (ll k,ll l,ll r)
{
if(lazy[k])
{
int mid = (r + l )>>1;
lazy[k<<1] +=lazy[k];
lazy[k<<1 | 1 ] +=lazy[k];
sum[k<<1] += (mid-l+1 ) * lazy[k];
sum[k<<1 | 1] += (r-mid) *lazy[k];
lazy[k]=0; //更新好区级后,标记要归零。
}
}
void build (ll k,ll l,ll r)
{
lazy[k]= 0;
if(l==r) //递归到了叶节点就输入数据填充最底层的叶节点
{
scanf("%lld",&sum[k]);
return ;
}
ll mid = ( l+r)>>1 ;
build(k<<1,l,mid);//向左儿子方向建树
build(k<<1 | 1,mid+1,r); //向右儿子方向建树
pushup(k); //建好了左右的两个儿子,为了一直保证当前节点的重要性,都必须保证左右儿子的正确性。
void update(ll k,ll L ,ll R,ll l,ll r, ll c)
{
if(l>=L && r <<R) //如果当前区间是大区间的子区间,那么就要打好标记。处理好区间
{
lazy[k] += c; //打好标记
sum[k]+= (r-l+1)*c; //修改好区间的值
return ;
}
pushdown(k,l,r); //每次更新前先向下更新,处理好之前未完成的步骤。
ll mid = (r+l) >>1 ;
if(mid >=L) //如果现在的区间的中点大于 大区间的左边界,证明小区间左侧与大区间重合
{
update(k<<1,L,R,l,mid,c);
}
if (mid <R) //如果现在的区间的中点小于 大区间的右边界,证明小区间右侧与大区间重合
{
update(k<<1|1,L,R,mid+1,r,c);
}
pushup(k); //每次更新完后,都要向上更新,保证当前节点的正确性
}
ll getsum (ll k,ll L,ll R,ll l,ll r) //求和函数
{
if( l>= L && r <=R) //如果当前区间 完全在 大区间
{
return sum[k];
}
ll rsum = 0;
ll mid = ( r+ l)>>1;
pushdown(k,l,r);
if(mid>=L)
{
rsum+=getsum(k<<1,L,R,l,mid);
}
if(mid<R)
{
rsum+=getsum(k<<1 | 1,L,R,mid+1,r);
}
return rsum;
}
int main()
{
char s [5];
ll n ,t,x ,y ,c;
scanf("%lld%lld",&n,&t);
build(1,1,n);
while(t--)
{
scanf("%s%lld%lld",s,&x,&y);
if(s[0]=='C')
{
scanf("%lld",&c);
update(1,x,y,1,n,c);
}
else printf("%lld\n",getsum(1,x,y,1,n));
}
return 0;
}