您的位置:首页 > 其它

POJ 3468 线段树成段更新区间求和 + 延迟标记详解 A Simple Problem with Integers

2017-11-04 14:54 288 查看
接触线段树有很长时间了,感觉最近才慢慢的弄懂线段树的思想,它的精华所在。所以想跟刚开始学习线段树的朋友分享一下,希望对你们有所帮助。

下面我来基于 POJ 3468 题来讲解一下线段树成段更新区间求和问题的解法。POJ 3468 的题目大意是给出 n 个数,可以进行两种操作,第一种是求某个区间内数的和,第二种是将某个区间内的数都加上一个数。



线段树:
struct node
{ //线段树节点
LL val,mark; //区间和,延迟标记
}T[MAXN*4];
线段树是一种基于完全二叉树的数据结构,它的每一个点都携带有基本的几种信息:节点的编号、节点表示的区间范围 [ L , R ],节点代表的含义(如区间和、区间最大值等),还有就是延迟标记和其他信息。由于是二叉树的结构,操作时候的时间复杂度就是 O(nlogn)。

如上图所示就是一棵线段树,它对应的原数组有 8 个元素,如图的右上方所示,每个节点(矩形框)的左、右边表示该节点可以表示区间的左右端点,而中间绿色的数字表示节点的编号,由于是递归生成的线段树,所以编号的顺序是先根遍历的顺序。图中并没有给出节点所代表的含义和延迟标记的值。线段树的第 i 个叶子节点就表示第 i 个数为多少。

一般的有关区间的问题都可以考虑用线段树来解决,不过线段树能够解决的问题是可以求和的,或者说是可以将大问题划分为小问题的。如上面提到的区间最大值问题和区间和问题。

对于本题来说,由于求解的是区间和,所以每个节点代表的意思是区间 [ L , R ] 的和,其中 L 、R 是该节点表示的范围,延迟标记 mark的值 x 表示当前节点所表示的区间 [ L , R ] 内的所有数都加了数字 x。



延迟标记:
延迟标记是线段树的精华,如果没有延迟标记,我觉得线段树就没有存在的必要了。那么什么是延迟标记呢?简单的说就是每次进行区间更新时不把更新的值更新到每个节点,而是更新可以表示这个区间的最少节点的mark值。将一个节点标记和将其所有子孙节点全部标记的效果是一样的。只有再次处理到该点的时候才将延迟标记往下传。

什么意思?如上图所示,例如将区间 [ 2 , 8 ] 内的数都加上一个数 x 的问题,一般的思路是遍历把每个数都处理一遍。如果用了延迟标记,那么我们就可以只把第 5、6、9 个节点(上图中红框的节点)的延迟标记加上 x 。这样就表示区间 [ 2 , 8 ] 内的数都加了 x ,而不用继续加到它们的子节点。而下次要更新或查询区间 [ 7 , 8 ] 的和话,就需要把第 9个节点的延迟标记往下传递,不然的话可能会更改区间 [ 7 , 8 ] 而导致第 9 个节点的延迟标记的值不正确。

需要一提的是,某个点的延迟标记只会影响到它的左右子树,而对该点本身的值无影响,所以在更新某个点时,只需要更改该点的值并把延迟标记往下传。

void pushdown(int rt,int len)
{ //参数:根节点,区间长度
if(T[rt].mark!=0)
{ //如果延迟标记不为 0
T[rt<<1].mark+=T[rt].mark; //左子树的延迟标记增加
T[rt<<1|1].mark+=T[rt].mark; //右子树的延迟标记增加
//左子树的值为区间长度减去区间长度的一半 * 延迟标记的值
T[rt<<1].val+=T[rt].mark*(len-(len>>1));
//右子树的值为区间长度的一半 * 延迟标记的值
T[rt<<1|1].val+=T[rt].mark*(len>>1);
T[rt].mark=0; //根节点的延迟标记归零
}
}mark是延迟标记,pushdown()函数是将当前节点的延迟标记网左右子树传递。为 0 时没有处理的必要,非 0 时,先将当前节点的延迟标记分给其左右子树,这样延迟标记和之前的效果是一样的。然后将左右子树根节点的值更新,并把当前节点的延迟标记值清零。

什么?将左右子树根节点的值更新不就和update()函数更新时的操作重复了吗?update()里面更新的是在所要更新的区间内的,而延迟标记下放这更新的是当前区间不在所要更新区间内的。

建树:
void build(int rt,int L,int R)
{ //参数:当前树的根节点,区间的左右端点
T[rt].mark=0; //延迟标记初始化
if(L==R)
{ //如果当前区间只有一个元素
T[rt].val=a[L];
return;
}
int m=(L+R)>>1; //区间的中点
build(rt<<1,L,m); //建立左子树
build(rt<<1|1,m+1,R); //建立右子树
//回溯时更新当前节点的值
T[rt].val=T[rt<<1].val+T[rt<<1|1].val;
}建树是一个递归的过程,线段树的根节点编号为 1。对于任意节点 rt,其左子树的根节点为 rt * 2 即代码中的 rt<<1.
其右子树的根节点为 rt *2 + 1 即代码中的 rt<<1|1 。 当 L==R 即区间长度为 1 时,就更新叶子节点为第 i 个数。当当前节点的左右子树都建立完后,则更新当前节点的值。因为前面只更新了叶子节点,我们需要在回溯时更新所有的节点。

更新:
void update(int rt,int L,int R,int l,int r,int x)
{ //参数:根节点,当前区间的左右端点,要更新区间的左右端点,要更新的值
if(l<=L&&R<=r)
{ //如果当前区间在更新区间内
T[rt].val+=(R-L+1)*x; //当前节点的值增加了区间长度 * x
T[rt].mark+=x; //延迟标记增加了 x
return;
}
pushdown(rt,R-L+1); //将延迟标记下传
int m=(L+R)>>1; //当前区间的中点
//如果中点在更新区间左端点的右边,则更新左子树
if(l<=m) update(rt<<1,L,m,l,r,x);
//如果中点在更新区间右端点的左边,则更新右子树
if(r>m) update(rt<<1|1,m+1,R,l,r,x);
T[rt].val=T[rt<<1].val+T[rt<<1|1].val; //回溯时更新当前节点
}


如果当前区间是要更新区间的子区间的话则更新,否则不必更新,而是将延迟标记下放,因为之前的更新可能没到叶子节点,所以叶子节点的值还没更新。取当前区间的中点,如果更新区间的左端点在中点的左边则更新左子树,否则没必要更改,右子树也类似。在回溯时也要更新当前节点,为什么?之前不是更新过了吗?之前更新的只是在所要更新区间内的,而其他的并没有更新。

查询:
LL query(int rt,int L,int R,int l,int r)
{ //参数:根节点,当前区间的左右端点,要查询区间的左右端点
if(l<=L&&R<=r) return T[rt].val; //如果是子区间
pushdown(rt,R-L+1); //延迟标记下放
LL ans=0;
int m=(L+R)>>1; //中点
if(l<=m) ans+=query(rt<<1,L,m,l,r); //左子树满足条件区间的和
if(r>m) ans+=query(rt<<1|1,m+1,R,l,r);//右子树满足条件区间的和
return ans;
}查询和更新类似,只是由于对点的值没有改动就不必在回溯的时候更新当前节点的值了。由于之前更新可能没更新到叶子节点,所以仍然要做延迟标记的下放。

关于线段树数组开多大的问题:

假设有 n 个不同的数,线段树的根节点要表示区间 [1, n ],也就是叶子节点有 n 个,所以线段树节点应该有 2*n-1个(不明白的自行百度),也就是开原数组的两倍大小喽?也对也不对,因为理论值是正确的,但是,当递归进行到线段树的最后一层时,当前节点的左、右孩子是 2*rt 和 2*rt+1,程序是不知道已经结束了的,还会再一次递归,所以要开原数组的4倍大小。

总代码:
#include<stdio.h>
#define MAXN 100010
typedef long long LL;

int a[MAXN];

struct node
{
LL val,mark;
}T[MAXN*4];

void pushdown(int rt,int len)
{ //参数:根节点,区间长度
if(T[rt].mark!=0)
{ //如果延迟标记不为 0
T[rt<<1].mark+=T[rt].mark; //左子树的延迟标记增加
T[rt<<1|1].mark+=T[rt].mark; //右子树的延迟标记增加
//左子树的值为区间长度减去区间长度的一半 * 延迟标记的值
T[rt<<1].val+=T[rt].mark*(len-(len>>1));
//右子树的值为区间长度的一半 * 延迟标记的值
T[rt<<1|1].val+=T[rt].mark*(len>>1);
T[rt].mark=0; //根节点的延迟标记归零
}
}

void build(int rt,int L,int R)
{ //参数:当前树的根节点,区间的左右端点
T[rt].mark=0; //延迟标记初始化
if(L==R)
{ //如果当前区间只有一个元素
T[rt].val=a[L];
return;
}
int m=(L+R)>>1; //区间的中点
build(rt<<1,L,m); //建立左子树
build(rt<<1|1,m+1,R); //建立右子树
//回溯时更新当前节点的值
T[rt].val=T[rt<<1].val+T[rt<<1|1].val;
}

void update(int rt,int L,int R,int l,int r,int x)
{ //参数:根节点,当前区间的左右端点,要更新区间的左右端点,要更新的值
if(l<=L&&R<=r)
{ //如果当前区间在更新区间内
T[rt].val+=(R-L+1)*x; //当前节点的值增加了区间长度 * x
T[rt].mark+=x; //延迟标记增加了 x
return;
}
pushdown(rt,R-L+1); //将延迟标记下传
int m=(L+R)>>1; //当前区间的中点
//如果中点在更新区间左端点的右边,则更新左子树
if(l<=m) update(rt<<1,L,m,l,r,x);
//如果中点在更新区间右端点的左边,则更新右子树
if(r>m) update(rt<<1|1,m+1,R,l,r,x);
T[rt].val=T[rt<<1].val+T[rt<<1|1].val; //回溯时更新当前节点
}

LL query(int rt,int L,int R,int l,int r)
{ //参数:根节点,当前区间的左右端点,要查询区间的左右端点
if(l<=L&&R<=r) return T[rt].val; //如果是子区间
pushdown(rt,R-L+1); //延迟标记下放
LL ans=0;
int m=(L+R)>>1; //中点
if(l<=m) ans+=query(rt<<1,L,m,l,r); //左子树满足条件区间的和
if(r>m) ans+=query(rt<<1|1,m+1,R,l,r);//右子树满足条件区间的和
return ans;
}

int main()
{
int i,n,q,x,y,z;
LL ans;
char c;
while(~scanf("%d%d",&n,&q))
{
for(i=1;i<=n;i++) scanf("%d",&a[i]);
build(1,1,n);
while(q--)
{
scanf("%*c%c%d%d",&c,&x,&y);
if(c=='Q')
{
ans=query(1,1,n,x,y);
printf("%lld\n",ans);
}
else
{
scanf("%d",&z);
update(1,1,n,x,y,z);
}
}
}
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐