码迷,mamicode.com
首页 > 其他好文 > 详细

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

时间:2015-04-07 12:13:14      阅读:131      评论:0      收藏:0      [点我收藏+]

标签:线段树   poj   

技术分享

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



  

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

标签:线段树   poj   

原文地址:http://blog.csdn.net/zearot/article/details/44915853

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!