非递归线段树区间修改区间求和的两种实现(以POJ 3468为例)

来源:互联网 发布:国足球员数据 编辑:程序博客网 时间:2024/06/10 08:24

题意:就是一个数列,支持  查询区间和  以及  区间内的数都加上 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的所有公共祖先。


小小总结:

第一种方法比第二种稍微快一点,写起来也简单一点,但是局限性更大一些,没发现如何修改成求区间最大最小值。

第二种方法更加常规一些,同样的思路可以支持更多标记的维护,而且数组的定义上也跟递归线段树一样(sum和add)。

感觉我写的不够简洁,第二种方法的写法上应该还可以优化。

代码:

下面是第一种方法的核心代码(先差分再求前缀和的前缀和):

#define LL long long#define maxn 100001LL 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; //计算Pfor(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 100001LL 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){//区间+Cint 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;//再处理addif(~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;}



  

0 0
原创粉丝点击