您的位置:首页 > 其它

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

2015-04-07 08:37 295 查看


题意:就是一个数列,支持  查询区间和  以及  区间内的数都加上 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 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;
}


  
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  线段树 poj