线段树总结
综述:
线段树可谓解决区间问题的神兵利器,其稳定的O(n*log(n))的优秀时间复杂度适应了OI和ACM竞赛的一般需求,而且代码简介,功能丰富,使用灵活。经过一段时间对线段树由浅入深的了解,笔者越发感受到这个数据结构的强大魅力。虽然有关介绍线段树的文章很多,其中不乏优秀作品,但是毕竟再好看也是别人的东西,只有消化吸收了才能为己所用。所以,笔者还是决定自己写一份关于线段树的总结,可能有理解有误的地方,希望大家不吝指教。
最初了解到的线段树主要用来解决RMQ(区间最值)和区间求和问题,这类问题又分为单点修改,区间询问;区间修改,单点询问以及区间修改,区间询问。先附上几段最裸的线段树模板。(代码风格采用了四倍空间的虚拟节点写法,笔者水平有限,不喜欢的朋友们可以自行忽略)。
代码1:
单点修改,区间查询。最基本的线段树操作。
//#include
//#pragma comment(linker, "/STACK:102400000,102400000")
#include
#include
#include
#include
#include
#include
#include
#include
代码2:
区间修改,单点查询。如果是区间求和问题,可以先叉分,转化为单点修改,区间查询,这里给出区间修改最值,单点查询最值模板。用处很小,这里就不给代码了。
代码3:
区间修改,区间查询。为了防止线段树时间复杂度退化到O(N2),需要另外开一个lazy数组维护。这个对于初学者来讲是个难点,先上代码,之后详解。
//#include
//#pragma comment(linker, "/STACK:102400000,102400000")
#include
#include
#include
#include
#include
#include
#include
#include
原理解析:
简单解析一下线段树的工作原理。首先线段树是一颗完全二叉树,这个性质决定了它稳定的时空复杂度。树上的每个节点都代表了一个区间,一般根节点代表整个区间,根节点的两个孩子平分父亲的区间,以此类推,叶子节点只包含单个元素。线段树主要完成动态修改和查询功能,每次修改和查询操作都是从根节点开始向下推进,因为是完全二叉树,所以最多遍历logN层。
线段树的修改和查询互为逆操作。修改的目的是将节点信息更新到树的所有相关节点上,因为线段树特殊的结构,保证了相关节点数量为logN级别。查询的目的是统计对应区间的节点信息,同样的,每个区间在线段树上可以分割之多logN次,所以查询操作的复杂度也稳定logN级别。线段树的修改和查询操作分别对应了区间的分割以及合并操作,这两个操作是线段树解决问题的关键。简而言之,凡是具备快速区间分割以及合并性质的问题都可以用线段树来解决。比如区间最值问题,正因为两个区间的最值满足快速合并属性,所以可以利用线段树结构实现区间最值的快速查询,同样因为节点的修改操作可以快速分割到子区间中,所以待区间修改的最值查询问题也可以用线段树解决。求和的道理类似,这里不再重复。
单纯靠讲解自然不够,来点例题帮助消化。
例题强化:
例题1:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1174
区间最值。当然所有可以解决RMQ问题的算法都可以解决这道题目,权当热身。
代码1:
//#include
//#pragma comment(linker, "/STACK:102400000,102400000")
#include
#include
#include
#include
#include
#include
#include
#include
例题2:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1376
把这道题目放到例题2会让人决定很奇怪,怎么比第一题难了那么多,而且不是这不是经典的DP题目吗?很多情况下,线段树只起辅助作用,但偏偏是这辅助作用至为关键,可以降低算法复杂度,从而满足了应用需求。例题2就是很好的证明,N2的动态规划算法不难想,难的是想到如何优化。
简单题解:先把元素离散化。对每个ai,求当前1~ai-1为结尾的单增序列长度的最大值和最大值的个数,用所求值更新ai的情况。用线段树维护。
同时这道题目也告诉我们,线段树在维护最值的同时可以维护最值的位置,这是由线段树和问题的属性决定的,毕竟统计最值的同时维护最值的位置并不难。
代码2:
//#include
//#pragma comment(linker, "/STACK:102400000,102400000")
#include
#include
#include
#include
#include
#include
#include
#include
例题3:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1207
例题1、2都是线段树的常规应用,同时线段树支持区间很多性质的维护,包括区间染色,颜色数量统计,最大区间和等等。例题3就是线段树的综合应用。另外放这道题目的原因就是,线段树节点的含义和最值的含义可以根据需求而定,节点可以表示此段内存是否空闲,最值可以表示当前区间最靠前的满足要求的区间左端点。为什么可以这样做呢?其实不难,还是想线段树的性质,区间分割和合并。满足这两个性质的问题就可以用线段树求解。
代码3:(暂缺)
例题4:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1206
扫描线,线段树的经典应用。可以求矩形面积并,交,以及矩形周长并,以及类似模型的衍生问题。
代码4:(暂缺)
例题5:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1208
类似扫描线的处理过程。巧妙之处在于点转化成矩形,矩形转化成点。
代码5:
//#include
//#pragma comment(linker, "/STACK:102400000,102400000")
#include
#include
#include
#include
#include
#include
#include
#include
例题6:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1461
线段树的模型题目,即修改操作是需要根据题目分析转化出来。这道题目需要从小到大枚举可行的桌子腿长度,每次枚举通过线段树优化查询可行解操作,从而使时空复杂度降低。这类题目也说明了线段树的辅助作用以及降低复杂度的神奇作用。另外,这道题目中线段树被用来统计前K小的数的和,基于提前知道所有值的相对大小确定了每个值在线段树中的位置,从而实现修改和查询操作。这个题目也说明了线段树空间静态以及二分的性质。空间静态在于节点的值以及范围需要提前确定以便在线段树中位所有节点安排对应的位置。二分性质通过从小到大安排节点可以得到和第K大有关的问题。
代码6:
//#include
//#pragma comment(linker, "/STACK:102400000,102400000")
#include
#include
#include
#include
#include
#include
#include
#include
例题7:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1199
与子树相关的线段树。实际上通过深搜遍历树,得到树的欧拉序列后,发现子树的节点都在一起,对子树的操作等价于对一段区间的操作,所以题目转化为线段树的经典题目。
代码7:
//#include
#pragma comment(linker, "/STACK:102400000,102400000")
#include
#include
#include
#include
#include
#include
#include
#include
附:划分树,主席树,树套树等也是线段树的主要应用,但是篇幅有限,笔者能力也有限,暂时总结这么多了。囧。