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

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

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

非递归线段树专题

学习了自底向上的非递归线段树,深感精妙!! 大牛的博客:http://blog.csdn.net/zearot/article/details/48299459 张坤玮---统计的力量 The Union of k-Segments CodeForces - 612D 题意:求被覆盖k次及以上的点或者线段. 看到别人直接用扫描线写的,更方便一些. 不过拿来练习线段树也不错. 1 #include <bits/stdc++.h> 2 using namespace std; 3 const in

树链剖分+线段树 单点修改 区间求和 模板

马上要去西安打邀请赛了,存下板子 首先是vector存图的: #include<bits/stdc++.h> using namespace std; #define ll long long #define lson l,m,rt<<1 #define rson m+1,r,rt<<1|1 #define mid int m = (l + r) >> 1 const int M = 2e5+10; int fa[M],dep[M],siz[M],son[M

Lightoj 1348 Aladdin and the Return Journey (树链剖分)(线段树单点修改区间求和)

Finally the Great Magical Lamp was in Aladdin's hand. Now he wanted to return home. But he didn't want to take any help from the Genie because he thought that it might be another adventure for him. All he remembered was the paths he had taken to reac

Spoj 1716 Can you answer these queries III 线段树 单点修改 区间求最大子段和

题目链接:点击打开链接 == 原来写1的时候已经把更新函数写好了.. #include <cstdio> #include <iostream> #include <algorithm> #include <string.h> #include <math.h> #include <vector> #include <map> using namespace std; #define N 50050 #define Lso

非递归线段树

支持区间加,区间查询最大值 模板: 1 struct Segment_tree 2 { 3 int size; 4 int *node; 5 void build(int n) 6 { 7 n+=4; 8 size=1; 9 while(size<n) 10 size<<=1; 11 node=new int[size+size+4]; 12 } 13 void updata(int pos) 14 { 15 int A=max(node[pos<<1],node[pos&l

POJ 3468 A Simple Problem with Integers(线段树模板之区间增减更新 区间求和查询)

A Simple Problem with Integers Time Limit: 5000MS   Memory Limit: 131072K Total Submissions: 140120   Accepted: 43425 Case Time Limit: 2000MS Description You have N integers, A1, A2, ... , AN. You need to deal with two kinds of operations. One type o

线段树2 求区间最小值

线段树2 求区间最小值 从数组arr[0...n-1]中查找某个数组某个区间内的最小值,其中数组大小固定,但是数组中的元素的值可以随时更新. 数组[2, 5, 1, 4, 9, 3]可以构造如下的二叉树(背景为白色表示叶子节点,非叶子节点的值是其对应数组区间内的最小值,例如根节点表示数组区间arr[0...5]内的最小值是1): 线段树的四种操作: 1.线段树的创建 2.线段树的查询 3.线段树的更新单节点 4.线段树的更新区间 直接上完整代码吧 1 #include <bits/stdc++.

HDOJ--4893--Wow! Such Sequence!【线段树+单点、区间更新】

链接:http://acm.hdu.edu.cn/showproblem.php?pid=4893 题意:给你一个长度n的数列,初始都为0,有三种操作,第一种给第k个位置的数加d,第二种是查询区间 [l , r] 的总和,第三种是使区间 [l , r] 的值改为离它最近的那个斐波那契数的值. 我刚开始用sum数组存储节点的值,第三种操作是个区间更新,但是区间更新的值不一样,我就想当然的搜到最底部的节点来处理更新,果断T了.后来想了想,其实可以在节点上再加一个信息,就是这个值下次进行第三种操作要变

UPC 2224 Boring Counting (离线线段树,统计区间[l,r]之间大小在[A,B]中的数的个数)

题目链接:http://acm.upc.edu.cn/problem.php?id=2224 题意:给出n个数pi,和m个查询,每个查询给出l,r,a,b,让你求在区间l~r之间的pi的个数(A<=pi<=B,l<=i<=r). 参考链接:http://www.cnblogs.com/zj62/p/3558967.html #include <iostream> #include <cstdio> #include <cstring> #incl