620 likes | 793 Views
线段树. 例题: Picture. 题目大意:墙上贴着一些海报、照片等矩形,所有的边都为垂直或水平。每个矩形可以被其它矩形部分或完全遮盖,所有矩形合并成区域的边界周长称为轮廓周长。要求编写程序计算轮廓周长。 数据量限制: 0≤ 矩形数目 <5000 ; 坐标数值为整数,范围是 [-10000 , 10000] 。. 例题: Picture. 例:下图的三个矩形轮廓周长为 30 :. 例题: Picture. 轮廓的定义: 1 、轮廓由有限条线段组成,线段是矩形边或者矩形边的一部分。 2 、组成矩形边的线段不应被任何矩形遮盖。下面分别是遮盖的两种情况。.
E N D
例题:Picture • 题目大意:墙上贴着一些海报、照片等矩形,所有的边都为垂直或水平。每个矩形可以被其它矩形部分或完全遮盖,所有矩形合并成区域的边界周长称为轮廓周长。要求编写程序计算轮廓周长。 • 数据量限制: • 0≤矩形数目<5000; • 坐标数值为整数,范围是[-10000,10000]。
例题:Picture • 例:下图的三个矩形轮廓周长为30 :
例题:Picture • 轮廓的定义: • 1、轮廓由有限条线段组成,线段是矩形边或者矩形边的一部分。 • 2、组成矩形边的线段不应被任何矩形遮盖。下面分别是遮盖的两种情况。 AB被遮盖 CD被遮盖
例题:Picture • 元线段: • 由于坐标都是整数,所以可以把平面看成是一个网格,连接相邻网格顶点的基本线段,称之为“元线段”。 • 矩形边一定在网格线上,所以矩形的边完全由元线段组成。
例题:Picture • 超元线段: • 元线段的数目过多,因此引入超元线段。 • 根据每个矩形纵向边的横坐标纵向地对平面进行2*N次切割、根据矩形横向边的纵坐标横向地对矩形进行2*N次切割(N为矩形个数)。显然,经过切割后的平面被分成了(2*N+1)^2个区域,如下图所示: 其中像横向边AB、纵向边CD这样的线段就是“超元线段”。超元线段与元线段有着相似的性质,也是组成轮廓的基本单位。所不同的是,超元线段的数目较少,一般为4*N条左右,只和矩形数目有关。
例题:Picture • 离散化: • 算法的研究对象是超元线段,但这并不等于逐一枚举,那样耗时过大,而整体考虑又使得问题无从下手。有一种考虑方法是折中的,即既不研究每一条超元线段,也不同时研究所有的超元线段,而是再进一步优化问题的离散化,即将超元线段分组研究。夹在两条纵向分割边的超元线段自然地分为一组,它们的共同点是长度相同,并且端点的横坐标相同。纵向线段也可以进行类似的离散化。
例题:Picture • 这样的离散化处理后,使得问题规模降低,以此为基础,算法的框架可以基本确定为: • 对平面进行分割。 • 累加器ans 0。 • 研究每组超元线段,检测其中属于轮廓的部分的长度,并把这一长度累加入ans。 • 输出ans的值。
例题:Picture • 映射结构的建立(对平面进行分割): • 算法的基础是问题的离散化,要进行平面“分割”,一般需要记录分割点,通常采用映射来记录分割点。直观的做法是采用一维数组形式,下标表示分割点的编号,数组元素表示分割点的坐标。利用下标与数组元素的自然对应,实现映射。应该说,这样表示是比较自然的,实现也比较方便。数组的优点主要是存取方便,且可以在O(NlogN)时间内排序。
例题:Picture • 确定每组超元线段中有多少条属于轮廓: • 累计扫描的方法:以一组水平方向的超元线段为例,把一组超元线段按照从上到下的顺序排序。如果第i条超元线段属于某个矩形的上边界,那么令数组元素a[i]=1;如果是下边界,那么令数组元素a[i]=-1。同时,设立累加器add,从上至下扫描超元线段,一条超元线段I属于轮廓的情况有两种: • A[I]≠0且扫描到该超元线段未累加时add = 0; • A[I]≠0且扫描到该超元线段累加之后add = 0。
例题:Picture • 使用线段树: • 线段树是描述单个或若干区间并的树形结构,属于平衡树的一种。使用线段树要求知道所描述的区间端点可能取到的值。换句话说,设A[1..N]是从小到大排列的区间端点集合,对于任意一个待描述的闭区间P=[x,y],存在1≤i≤j≤N使得x=a[i]且y=a[j],这里i, j称为x,y的编号。可以看到,即使是实数坐标,在线段树中也只有整数含义。以下所说的区间[x, y]如无特殊说明,x、y均是整数,即原始区间顶点坐标的编号。
例题:Picture • 线段树是一棵二叉树,将数轴划分成一系列的初等区间[i, i+1] 。每个初等区间对应于线段树的一个叶结点。
例题:Picture • 线段[3, 6]在线段树中的表示:
例题:Picture 线段树节点的数据域: • Count:区间被完全覆盖的次数; • 测度M:区间被线段覆盖的长度; • 连续段数:区间可以被分为多少个连续的区间。
例题:Picture 节点[i, j]的测度: • 如果count[i][j] > 0:M = a[i] – a[j]; • 如果count[i][j] = 0: • 如果是叶节点:M = 0 • 如果不是叶节点:M值为左右子树的M值之和。
例题:Picture 节点[i, j]的连续段数: • 需要另外两个Bool变量lbd和rbd,分别表示节点所表示区间的左右端点是否被覆盖。 • 该节点count>0,那么lbd = true • 该节点count=0 • 该节点是叶节点,那么lbd = fauth • 该节点不是叶节点,那么lbd = 左子节点的lbd
例题:Picture 节点[i, j]的连续段数l: • 如果该节点Count > 0,那么l = 1; • 如果该节点Count = 0: • 如果左子树的rbd and 右子树的lbd为真:那么该节点的l=左子树的l+右子树的l-1 • 否则,该节点的l=左子树的l+右子树的l
例题:Picture • 如图所示:超元线段CD与EF被矩形AGHB遮盖,不属于轮廓;而与之相邻DD’与FF’则“摆脱”了矩形的遮盖,属于轮廓的一部分。可以这样分析:从左往右,CD、EF首先被遮盖,但随着BF的出现,对DD’、FF’的遮盖自然消失。这一点,正是相邻超元线段组的内在联系。用线性结构无法表示出这一联系,因为各组的累计扫描过程是独立的。现在我们用树形结构来表示将较好地解决这一问题,因为线段树支持插入与删除及动态维护,可以有机联系各组超元线段的状态。
例题:Picture • 我们把“从左往右”当作一个扫描的过程,若将其严格地描述,可以得到一个称为线段扫描的过程: • 设立扫描线段L。 • L从左往右扫描,停留在每一超元线段组上。 • L的状态用线段树来表示,每一条纵向的矩形边看作一个待合并区间。线段树的连续段数*2表示该组超元线段属于轮廓的线段数目。 • 扫描过程中动态地维护L的状态。
例题:Picture • 维护线段树的状态: • 线段树L初始化为空,即线段树刚建好时的情形。 • 扫描时,遇到矩形左边,将其插入(Insert)线段树。 • 扫描时,遇到矩形右边,将其从线段树中删除(Delete)。由于从左往右扫描,事先插入了该矩形的左边,所以删除合法。
例一:苹果树 • 题目大意:有一棵包含n个节点的树,这些节点用1到n这n个整数编号,其中根节点的编号一定是1。在最初时刻,每个节点都有一个苹果;此后,会进行两种操作: • C(i):如果第i个节点此时有苹果,那么把苹果拿走;如果这个节点此时没有苹果,则在这个节点放上一个苹果。 • Q(i):问此时以第i个节点为根的子树上共有多少个苹果。要求对每一个Q(i)操作输出正确的结果。
例一:苹果树 • 输入:输入数据的第一行包含一个整数n(n ≤100000),表示树所包含的节点数;接下来有n-1行,每行包含两个整数v和u,表示节点v和节点u之间有一条边。然后是一个整数m(m≤100000),表示操作的个数。接下来有m行,每一行描述一个操作。 • 输出:输出每一个Q(i)操作的正确的结果。
例一:线段树 • 样例输入: 样例输出: • 3 3 • 1 2 2 • 1 3 • 3 • Q 1 • C 2 • Q 1
例一:线段树 • 样例输入所对应的树为: • 1(1) • 2(1) 3(1) • 样例输入的每一步操作对树的影响: • Q1:(输出:3) C2(无输出) Q1(输出:2) • 1(1) 1(1) 1(1) • 2(1) 3(1) 2(0) 3(1) 2(0) 3(1)
例一:苹果树 • 分析:最容易想到的方法是模拟。首先按照题目描述建树,每一个节点保存以这个节点为根的子树中的苹果数目,此时这个节点是否有苹果,以及该节点的父节点。这样,对于每一个Q(i)操作,可以在O(1)的时间内直接得到结果;而对于C(i)操作,由于要修改节点i以及它的所有祖先,所以这个操作的复杂度与节点i的深度有关。在最坏情况下,每一次修改的复杂度可能都是O(n)的,因此模拟算法的复杂度大约为O(mn),是不可行的。
例一:苹果树 C 2
例一:苹果树 C 5
例一:苹果树 • 分析:怎样通过略微提高Q(i)操作的复杂度来降低进行C(i)操作时的复杂度?
例一:苹果树 • 深度优先搜索(DFS)的括号定理: • 在对一个(有向或无向)图G=(V, E)的任何深度优先搜索中,对于图中的任意两个定点u和v,下述三个条件只有一个成立: • 区间[d[u], f[u]]和区间[d[v], f[v]]是完全不相交的,且在深度优先森林中,u或v都不是对方的后裔。 • 区间[d[u], f[u]]完全包含于区间[d[v], f[v]]中,且在深度优先树中,u是v的后裔。 • 区间[d[v], f[v]]完全包含与区间[d[u], f[u]]中,且在深度优先树中,v是u的后裔。
例一:苹果树 • 根据括号定理,可以发现,如果把树中的节点按照深度优先搜索发现的顺序排列,那么任意一棵子树上的所有节点在这个序列中必然是连续的。
例一:苹果树 1 按照DFS的发现顺序对节点排序: 1 2 4 7 8 3 5 6 9 10 2 3 6 5 4 8 10 7 9
例一:苹果树 1 按照DFS的发现顺序对节点排序: 1 2 4 7 8 3 5 6 9 10 2 3 6 5 4 8 10 7 9
例一:苹果树 1 按照DFS的发现顺序对节点排序: 1 2 4 7 8 3 5 6 9 10 2 3 6 5 4 8 10 7 9
例一:苹果树 • 分析:如果记录下每棵子树所对应的区间,然后用线段树维护区间内苹果数的总和。就可以在O(lgn)的时间复杂度内完成Q(i)和C(i)操作。 • 线段树的使用:线段树的每个节点对应了按DFS顺序排序后的一段区间,每个节点设一个变量表示该区间数字之和。
例一:苹果树 • 由DFS访问序列1 2 4 7 8 3 5 6 9 10建立的线段树: R:[1, 10] S:10 R:[1, 5] S:5 R:[6, 10] S:5 R:[1, 3] S:3 R:[4, 5] S:2 R:[6, 8] S:3 R:[9, 10] S:2 R:[1, 2] S:2 R:[3, 3] S:1 R:[4, 4] S:1 R:[5, 5] S:1 R:[6, 7] S:2 R:[8, 8] S:1 R:[9, 9] S:1 R:[9, 10] S:1 R:[1, 1] S:1 R:[2, 2] S:1 R:[6, 6] S:1 R:[7, 7] S:1
例一:苹果树 • 由DFS访问序列1 2 4 7 8 3 5 6 9 10建立的线段树: R:[1, 10] S:9 R:[1, 5] S:4 R:[6, 10] S:5 R:[1, 3] S:3 R:[4, 5] S:1 R:[6, 8] S:3 R:[9, 10] S:2 R:[1, 2] S:2 R:[3, 3] S:1 R:[4, 4] S:0 R:[5, 5] S:1 R:[6, 7] S:2 R:[8, 8] S:1 R:[9, 9] S:1 R:[9, 10] S:1 R:[1, 1] S:1 R:[2, 2] S:1 R:[6, 6] S:1 R:[7, 7] S:1
例一:苹果树 • 线段树中Sum值的维护:线段树的叶节点表示的是苹果树中一个节点出的苹果个数,这个值是可以直接得到的;而线段树的内部节点的Sum值等于它的两个子节点Sum值之和。
例二:Harmony Forever • 题目大意:给定一个集合S(初始为空),以及两种操作: • B(x):如果正整数x目前不在集合S中,把x加入集合S。 • A(y):输出目前集合S中除以正整数y所得余数最小的元素是第几个被加入集合的。如果有多个这样的元素,输出最后被加入集合的那个。如果没有,输出-1。 • 每组输入数据的时间限制为5秒。
例二:Harmony Forever • 输入:第一行包含一个正整数t(t≤40000),表示要进行操作的数目。接下来有t行,来描述依次出现的每一个操作,这些操作的格式为"B X"或"A Y",其中X, Y是正整数并且满足:1≤X, Y≤500000 • 输出:对于每一个"A Y"命令,输出一个整数,是集合中模y结果最小的元素是第几个被加入集合的。如果有多个满足条件的元素,输出最后加入集合的那个,如果没有这样的元素,输出-1。
例二:Harmony Forever • 样例输入: 样例输出: • 5 1 • B 1 2 • A 5 1 • B 10 • A 5 • A 40
例二:Harmony Forever 初始状态: B 1: B 10: A 5: 此时集合中10≡0(mod 5)最小,而 10是集合中的第二个元素,所以输 出2。
例二:Harmony Forever • 最简单的方法仍然是模拟的方法:用一个栈来表示集合。遇到添加元素的操作时,就把要添加的元素插入栈中;遇到查询操作时,扫描整个栈来找到需要的结果。采用这种方法,每次插入操作的时间复杂度为O(1),而查询操作的复杂度是O(n),要对栈中的每个元素取模,这里的n是进行这步查询操作时栈中元素的个数。
例二:Harmony Forever • 例:在集合{5, 2, 13, 6, 15, 19}中找模8最小的元素。 • 先把集合中的元素划分为三类: • 第一类中的元素满足0≤x < 8:{2, 6, 5}; • 第二类中的元素满足8≤x < 16:{13, 15}; • 第三类中的元素满足16≤x < 24:{19}。 • 然后找出每一类中的最小元素:2,13,19 • 最后在这些最小元素中寻找模8所得结果最小的,就是最终结果:2≡2(mod 8),13≡5(mod 8),19≡3(mod 8)。所以集合中模8最小的元素是2。
例二:Harmony Forever • 可以使用线段树查找指定区间内并且已经在集合中的最小元素: • 令线段树的每个节点表示一个整数区间,另外用一个变量表示这个区间中已经被插入集合的最小元素。
例二:Harmony Forever • 由1-10这十个整数构成的线段树,并且假设集合为:{1, 3, 7} R:[1, 10] Min:1 R:[1, 5] Min:1 R:[6, 10] Min:7 R:[1, 3] Min:1 R:[4, 5] Min:∞ R:[6, 8] Min:7 R:[9, 10] Min: ∞ R:[1, 2] Min:1 R:[3, 3] Min: 3 R:[4, 4] Min: ∞ R:[5, 5] Min: ∞ R:[6, 7] Min:7 R:[8, 8] Min: ∞ R:[9, 9] Min: ∞ R:[9, 10] Min: ∞ R:[1, 1] Min:1 R:[2, 2] Min:2 R:[6, 6] Min: ∞ R:[7, 7] Min:7
例二:Harmony Forever • 线段树中Min值的维护:叶子节点的Min值可以由这个整数是否在集合中直接确定,如果在那么Min值就是这个叶子节点表示的那个整数,如果不在,Min值就是正无穷;而内部节点的B值可以由两个叶子节点的B值比较大小运算得到。 • 在查找指定区间的最小值时,只有左子树上Min值是正无穷或者与指定区间没有交集时,才需要进入右子树。
例二:Harmony Forever • 假设集合为{1, 2, 7},并且给定的区间为[2,9],在下面这棵线段树中查找最小值时所经过的节点为: R:[1, 10] Min:1 R:[1, 5] Min:1 R:[6, 10] Min:7 R:[1, 3] Min:1 R:[4, 5] Min:∞ R:[6, 8] Min:7 R:[9, 10] Min: ∞ R:[1, 2] Min:1 R:[3, 3] Min: ∞ R:[4, 4] Min: ∞ R:[5, 5] Min: ∞ R:[6, 7] Min:7 R:[8, 8] Min: ∞ R:[9, 9] Min: ∞ R:[9, 10] Min: ∞ R:[1, 1] Min:1 R:[2, 2] Min:2 R:[6, 6] Min: ∞ R:[7, 7] Min:7
例二:Harmony Forever • 由于给出了输入数据的范围[1, 500000],所以需要建立一棵根节点表示区间为[1,500000]的线段树。每加入一个新元素的时间复杂度为O(lg500000);而每一次查找操作与B Y中的Y有关,一次查找需要在线段树中搜索(500000/Y)次,每次搜索的复杂度是O(lg500000),所以总的复杂度大约为:O(lg500000) * 500000/Y。容易发现Y越大,这种方法复杂度越低。当Y很小时,复杂度比较高。
例二:Harmony Forever • 于是,将问题分两种情况解决:当Y比较小时,采用直接的模拟方法。当Y比较大时,由于取模操作的开销比较大,采用线段树的方法解决。
例三:K-th Number • 题目大意:给定n个正整数组成的集合A[1…N],要求设计一个在线算法,对于询问Q(i, j, k)返回A[i…j]的第k大元素。数组中的元素不超过100000个,元素大小不超过1000000000,询问次数不超过5000,每组输入数据的时间限制为2秒。