线段树应用

大家都知道贝贝侠喜欢小龙女MM,但是不幸的是小龙女MM现在被可恶的Segmen大魔头给抓走了,小龙女的爸爸海龙王自是伤心不已,但是最着急的还是贝贝侠,但是贝贝侠跑的没有Segmen大魔头快呀,这可怎木办?也许最好的办法就是学会线段树,这样就能增长指数的速度,救会小龙女MM了。
线段树是一种类型的二叉搜索树,不同的是他的每一个节点保留的是一段的信息,就是一个线段的左右端点,这样,通过反馈,更新,就能把某个端点的左右儿子的信息和并到其自身上面去,从而将其左右儿子包含的两个连续的线段和并成其自身的一个线段,从而实现其自身信息的更新。
首先是建树build(int l,int r,int rt)的过程:如果要建一棵包含n个数的信息的线段树,那么必须要求這棵线段树的最底层有n个结点,那么根结点所包含的信息就是一个长度为n的线段,其[1...n]的若干端点终会延展成n个独立的包含端点信息的点。
令PushUP(int rt)表示把当前结点的信息更新到父结点,PushDown(int rt)表示把当前结点的信息更新给儿子结点。
单点更新:最最基础的线段树,只更新叶子节点,然后把信息用PushUP(int rt)这个函数更新上来。
我们可以用线段树对任意区间[L...R]进行去间的添加,删除,求和操作,但是对于每一棵线段树,在建树的时候每个点所包含的线段的信息都已经固定了下来,对于区间[L...R]和结点包含的信息[l...r]之间可能出现交叉,包含的关系;我们关心的更多的出了怎么救回小龙女MM就是交叉的信息了,当[L...R]属于[l...r]时,进行对儿子节点的操作只能得到正好重合或者交叉的情况;那么只须处理交叉的情况就可以了。如果[L...R]属于两个连续的区间[l1...r1]+[l2...r2](满足r1+1==l2),那么添加和删除的操作就可以直接把[L...R]拆分成[L...r1]+[l2...R]就可以了,然后继续向下直到更新到端点。
对于每一次询问query( L, R, l, r, rt)或更新update,首先判断是否已到端点,是则更新端点信息并返回,否则直接返回;把结点的信息分成两部分,也就是他的两个子结点所包含的线段信息,就将结点rt分成了两部分(左子树)[l...m]+(右子树)[m+1...r],其中m=(l+r)/2;如果L<=m,说明区间[L...R]有一部分信息包含在左子树中,继续对左子树进行query操作;如果R>m,说明区间[L...R]有一部分信息包含在右子树中,继续对右子树进行query操作。
关于成段更新的操作:需要用到延迟标记(或者说懒惰标记),简单来说就是每次更新的时候不要更新到底,用延迟标记使得更新延迟到下次需要更新or询问到的时候,比如说区间染色问题,一旦能够确定某点包含的区间信息,暂时不去更新它的子结点的信息,因为子结点的信息,只须最后一次大规模的全体更新就能得到,而我们就不需要每一次都去更新它们的信息,因为这种情况下子结点的信息完全是跟它们的父亲结点的信息联系到一起的。
在PushDown操作中进行的是:如果有要更新的信息,那么将其需要更新的信息更新到他的两个子结点。
而在update操作中:如果是符合要求的区间(即L<=l&&r<=R),那么update操作就进行到这里,并且在终止update操作之前就在该处进行懒惰标记。
对于有些数据量比较大的,点比较分散的情况,比如说一维二维三维空间上分散的的点,如果我们采用传统的方法进行,势必会造成大量空间的浪费,这个时候就需要引进离散化的概念。假设现在有一个线段覆盖的问题,给你一些线段,后给的线段可以覆盖先给的线段,问最后可以看见几根原来的线段。离散化简单的来说就是只取我们需要的值来用,比如说给定两条线段[100,205],[1993,2023],我们用不到[-oo,99],[101,204],[206,1992],[1994,2022],[2024,+oo]这些值,所以我们只需要100,105,1993,2023就可以了,将这几个数hash直接映射到0,1,2,3,这样就省去了大量的时间空间。所以离散化要保存所有需要用到的值,排序后,分别映射到1~n,这样复杂度就会小很多很多。
线段树+扫描法解决矩形的面积并、周长并问题。
面积并的问题:平面上有n个矩形,各边均平行于坐标轴,求它们覆盖的总面积(重复覆盖的只计一次)。算法是先将所有矩形的上边界和下边界作为水平线段记录下来,并对所有矩形的左右边界对应的横坐标离散化,设离散化后有n个横坐标,则中间有n-1段。对这n-1段建立线段树(仍然和普通线段树一样,双闭区间),然后,按照纵坐标递增顺序扫描前面记录的水平线段(设有m段),对每一段,如果是上边界,找到其离散化后的范围(只需找到其左右端点离散化后的值l、r,则对应范围为[l, r-1]),并插入线段[l, r-1],否则(下边界),删除线段[l, r-1]。再然后,线段树中的每个结点需要记录该区间内的线段覆盖的总长度len(若该区间被某条尚未删除的线段整体覆盖,则len=总长,否则len=左右子结点len之和),每次操作后,累加面积:T[root].len*该水平线段与下一条水平线段的纵坐标之差。*(引自Mato)
首先定义一个结构体Seg(.(double)h(高度),l(左边界),r(右边界);.(int)s(线段是处于上端还是处于下端)),也就是说对于一个矩形,我们用了一种用上下两条线段来表示的方法来表示一个矩形,再把所有的线段按纵坐标从小到大的顺序排一下序,再进行接下来的处理。
因为离散化了以后那些什么什么数都按从小到大的顺序(无重复)变成了0,1,2,3...,所以我们在离散化之后就是以离散化后0~k-1.下面来看PushUp操作,因为离散化之后是把之前当作水平线段的每个矩形的上下边界当作水平线段记录下来的,接下来对每一条线段从下到上进行处理,如果是下边,那么将该边加入到线段树中,并计算目前覆盖的边的长度,最后求出当前覆盖长度,再乘以当前边与其上边的高度差,将结果加入到最终答案中;如果是上边,将其所对应的下边从线段树中删除,再进行求面积操作,最终得到的答案就是面积并了。
*周长并的问题:平面上有n个矩形,各边均平行于坐标轴,求它们覆盖形成的多边形的周长。算法类似面积并的算法,只不过由于组成周长的线段有水平的也有竖直的,线段树结点要记录的除了len以外还有一个ss,表示被线段覆盖的端点数量。另外还有lr和rr两个bool值,分别表示该线段的左端点和右端点是否被某条插入的线段覆盖。则T[x].ss = lch(T[x]).ss + rch(T[x]).ss - 2 * (lch(T[x]).rr && rch(T[x].lr)),若该线段被整体覆盖则T[x].ss=2(两端点)。最后,这次得到的T[root].len与上次得到的T[root].len之差的绝对值就是水平线段的长度,T[root].ss*纵坐标之差就是竖直线段的长度。*(引自Mato)
解决周长并的问题和面积并差不多,都是扫描线的思想,只不过记录的时候开numseg数组记录这一层里面有几个端点一遍竖线的相加,所以还要多开两个数组lbd,rbd。
贝贝侠最终救出了小龙女MM。(To be continue)

链接:

Mato    http://www.cppblog.com/MatoNo1/archive/2011/07/02/149965.html

notonlysuccess   http://www.notonlysuccess.com/index.php/segment-tree-complete/

posted @ 2012-06-27 01:30  lenohoo  阅读(639)  评论(0编辑  收藏  举报