题意:就是一个数列,支持 查询区间和 以及 区间内的数都加上 C 。
递归线段树很好写,就不讲了。
递归版本 : 内存:6500K 时间:2.6 秒
非递归版本一: 内存:4272K 时间:1.1秒
非递归版本二: 内存:4272K 时间:1.3秒
------------------------------------------------------------------------------------------------------------------------------------------
----------------------- 非递归思路都来自张昆玮的PPT《统计的力量》 ----------------------------
------------------------------------------------------------------------------------------------------------------------------------------
看了神一样的PPT《统计的力量》之后,想试试非递归线段树的区间修改和求和,于是就找了这题来测试。
方法一(差分再求和):
大意就是先将数列差分,每个数减去前一个数。然后,原本的数就变成了新数列的前缀和。
原本的前缀和就变成了新数列的前缀和的前缀和。
用S[i]表示a[1]+a[2]+...+a[i] ,用 P[i] 表示 a[1]+2*a[2]+3*a[3]+...+i*a[i]
则前缀和的前缀和 SS[x]=S[1]+S[2]+S[3]+...+S[x]=n*a[1]+(n-1)*a[2] +...+2*a[x-1] + a[x] = (n+1)S[x]- P[x]
于是只要对差分数列维护S[i]和P[i]两个性质就好了。
由于数组变成了相对的值,区间[L,R]加上C,只是把L的值加上C,把R+1的值减去C。也就是把区间修改简化到了点修改。
于是代码就很好写了。
代码中S[i]只代表 a[i] 这一项,要对S[i]求前缀和才得到上面公式中的S[i].
代码中P[i]只代表i*a[i]这一项,要对P[i]求前缀和才得到上面公式中的P[i].
最后求区间和[L,R]就是求两个SS再相减(SS[R]-SS[L-1])。
方法二(标记永久化):
《统计的力量》中只对这个方法作了简短的说明,想了好久才想出怎么实现。
大致思想就是,由于非递归的查询是自下而上的,不可能下传标记,那么就干脆不下传标记(也就是标记永久化)。
而是改成往上查询区间的过程中遇到标记就更新答案,以前一直不知道怎么做到这一点,最近重新看的时候才想到。
这题需要add标记和sum标记(节点的sum标记并没有考虑本节点的add)。
区间查询:
s和t 的区间查询过程本来就是在它们变成同一颗树的左右子树之前,若s是左节点,就将s^1节点的值加上,若t是右节点,则将t^1的节点的值加上。
现在有了标记,注意到,在每次for循环中 树s上的标记是对s的叶节点有效的,而目前s这边已经计算的所有节点都是s的子树,
所以只需要记录s这边已经被计算的节点数量Ln就可以做到按标记更新左边的答案。t 的那边是一样的。
for循环结束后并没有到此为止,还需要处理此时s和t 的标记,之后还要处理s和t的所有公共祖先上的标记。
一个小问题:这里解决了所求区间段以上的add标记,那么这些区间以下的标记怎么办?
比如只对某元素做了add标记(非递归的区间加标记也是自下而上的,所以顶层并不知道下面有标记),
但是区间查询的时候是对整体查询的话,非递归的查询会直接查询上面的区间,而忽略下面的标记。
答案是以下的标记信息存于sum中,于是区间修改也需要修改 被修改的段 所影响的所有祖先的sum(其实要修改的并不多),
通过sum来知道该节点以下有多少被add了。
也就是说,add是直接加到需要加的区间上,然后向上处理所有被影响的sum.
区间修改:自下而上地更新所有改变的add和sum
修改的整体框架跟查询一样。
核心思想:每次for循环中,s的标记代表了所有s这边已经处理过的数,s^1的标记是需要被修改(区间修改中)或加上(区间求和中)的数据。
修改或计算完s^1之后不要忘了更新已经被计算的节点数量Ln的值。
for循环结束后要分别处理s,t节点,并且再处理s和t的所有公共祖先。
小小总结:
第一种方法比第二种稍微快一点,写起来也简单一点,但是局限性更大一些,没发现如何修改成求区间最大最小值,也好像不能处理把一个区间的数都修改为C这种操作。
第二种方法更加常规一些,同样的思路可以支持更多标记的维护,可以处理把一个区间的数都修改为C这种操作,而且数组的定义上也跟递归线段树一样(sum和add)。
感觉我写的不够简洁,第二种方法的写法上应该还可以优化。
代码:
下面是第一种方法的核心代码(先差分再求前缀和的前缀和):
#define LL long long #define maxn 100001 LL S[maxn<<2]; LL SS[maxn<<2]; int N,Q,X; void PushUp(int x){//更新 S[x]=S[x<<1]+S[x<<1|1]; P[x]=P[x<<1]+P[x<<1|1]; } void init(){//init之前给 N 赋值 X=1;while(X <N+2) X <<=1;//计算偏移量 for(int i=1;i<=N;++i) scanf("%lld",&S[X+i]);//读取N个数 S[X]=P[X]=0;for(int i=N+1;i<X;++i) S[X+i]=0; for(int i=X-1;i>0;--i) S[X+i]-=S[X+i-1];//差分 for(int i=1;i<X;++i) P[X+i]=S[X+i]*i; //计算P for(int i=X-1;i>0;--i) PushUp(i);//建树 } void INC(LL L,LL R,LL C){//区间修改简化成点修改 int s=X+L,t=X+R+1; S[s]+=C;S[t]-=C; P[s]+=C*L;P[t]-=C*(R+1); while(s^1) s>>=1,PushUp(s); while(t^1) t>>=1,PushUp(t); } LL QUE(LL R){//前缀和 LL SumP=0,SumS=0; for(int t=X+R+1;t^1;t>>=1){ if(t&1) SumP+=P[t^1],SumS+=S[t^1]; } return (R+1)*SumS-SumP; }
第二种方法(标记永久化):
#define LL long long #define maxn 100001 LL sum[maxn<<2]; LL add[maxn<<2]; int N,Q,X; void init(){//init之前给 N 赋值 X=1;while(X <N+2) X <<=1; memset(add,0,sizeof(add)); for(int i=1;i<=N;++i) scanf("%lld",&sum[X+i]); sum[X]=0;for(int i=N+1;i<X;++i) sum[X+i]=0; for(int i=X-1;i>0;--i) sum[x]=sum[x << 1] + sum[x << 1 | 1]; } LL QUE(int L,int R){//区间求和 int s=X+L-1,t=X+R+1;//叶节点 int Ln=0,Rn=0,x=1;//左右支的已加点数,以及每树元素个数 LL Ans=0;//如果是set标记的话,可以加左右Ans分开求和 for(;s^t^1;s >>= 1,t >>= 1,x <<= 1){ //先读取标记更新 if(add[s]) Ans+=add[s]*Ln; if(add[t]) Ans+=add[t]*Rn; //再常规求和 if(~s&1) Ans+=sum[s^1]+x*add[s^1],Ln+=x; if(t&1) Ans+=sum[t^1]+x*add[t^1],Rn+=x; } //处理同层的情况 if(add[s]) Ans+=add[s]*Ln; if(add[t]) Ans+=add[t]*Rn; s>>=1;Ln+=Rn; //处理上层的情况 for(;s^1;s>>=1) if(add[s]) Ans+=add[s]*Ln; return Ans; } void INC(int L,int R,int C){//区间+C int s=X+L-1,t=X+R+1;//叶节点 int Ln=0,Rn=0,x=1;//左右支的已加点数,以及每树元素个数 for(;s^t^1;s >>= 1,t >>= 1,x <<= 1){ //先处理sum sum[s]+=(LL) C*Ln; sum[t]+=(LL) C*Rn; //再处理add if(~s&1) add[s^1]+=C,Ln+=x; if(t&1) add[t^1]+=C,Rn+=x; } //处理同层 sum[s]+=(LL) C*Ln; sum[t]+=(LL) C*Rn; s>>=1;Ln+=Rn; //处理上层 for(;s^1;s>>=1) sum[s]+=(LL)C*Ln; }