分治算法--天际线问题
分治算法–天际线问题
题目描述:
每个建筑物的几何信息用三元组 [Li,Ri,Hi] 表示,其中 Li 和 Ri 分别是第 i 座建筑物左右边缘的 x 坐标,Hi
是其高度。可以保证 0 ≤ Li, Ri ≤ INT_MAX, 0 < Hi ≤ INT_MAX 和 Ri - Li >
0。您可以假设所有建筑物都是在绝对平坦且高度为 0 的表面上的完美矩形。例如,图A中所有建筑物的尺寸记录为:[ [2 9 10], [3 7 15], [5 12 12], [15 20 10], [19 24
8] ] 。输出是以 [ [x1,y1], [x2, y2], [x3, y3], … ]
格式的“关键点”(图B中的红点)的列表,它们唯一地定义了天际线。关键点是水平线段的左端点。请注意,最右侧建筑物的最后一个关键点仅用于标记天际线的终点,并始终为零高度。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。例如,图B中的天际线应该表示为:[ [2 10], [3 15], [7 12], [12 0], [15 10], [20 8],
[24, 0] ]。说明:
任何输入列表中的建筑物数量保证在 [0, 10000] 范围内。 输入列表已经按左 x 坐标 Li 进行升序排列。 输出列表必须按 x
位排序。 输出天际线中不得有连续的相同高度的水平线。例如 […[2 3], [4 5], [7 5], [11 5], [12
7]…] 是不正确的答案;三条高度为 5 的线应该在最终输出中合并为一个:[…[2 3], [4 5], [12 7], …]来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/the-skyline-problem
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
算法思路:
官方给的分支算法的解法,看了半天,自己也写了一遍,什么没懂,归并地方一直出错,索性自己写得了。(还事先重新写了一遍归并排序,抄官方的代码抄到我怀疑我数据结构白学了。)
我的做法:第一步大致都一样,就是分治,具体代码是:
1 | def getSkyline(self, buildings: List[List[int]]) -> List[List[int]]: |
归并的思想就是将N个大楼分成两部分,分别对左右两部分求解,大问题化小问题,最后如果左边部分没有建筑物,返回空List对象[],如果只有一栋建筑,返回这栋建筑的天际线(这个就简单了),比如题目中的个大楼[2, 9, 10]指的就是x坐标2到9,高度为10的大楼,那么它的天际线就是[[2, 10][9, 0]](只需记录高度变化的那个点)
这样就获得了以上的递归函数,之后的问题就是如果合并俩个天际线left_skyline和right_skyline。下面几个解答我都没看懂,所以就自己想了一个,前两种想法最后都失败了,都是按照别人的解答“临摹”的算法,所以最后浏览器一关,自己想吧。
我想的是,假设有两个无限长的直线(我称之为current_l和current_r),分别记录左边部分天际线和右边部分的上次访问时的高度,一开始都设定为0,然后开始遍历图(List)中的点。
如果遍历到左边部分的点,会出现两种情况,这个点对我们的天际线产生了影响,或者没有任何影响。以下以遍历到左边部分的点left_dot作说明:
1 | ---------- current_r ---------- |
以上我把点也延伸成一条直线,那么如果我想要对当前的天际线产生影响,那我必须对于上一次的高度current_l产生变化,比current_l高的话,那说明这个点升高了,那么怎么判断我这个点有没有把整个天际线“撑”高呢?
那就要看left_dot对于current_r的位置了,也就是说,我之前访问过的右边部分的高度如果还没我现在这个点高的话,那么天际线必然被撑高了,即上面的情况,在这种情况下,我的目前访问到的这个点产生的高度变化被current_r覆盖了。
所以产生变化的情况一如下:
1 | ---------- left_dot ---------- |
那么点下降的情况呢?
我实验得到的结果是,只有当left_dot比current_l低,且不能比current_r还要低(即left_dot >= current_r)时,left_dot的下降变化才会对天际线产生影响。因为,如果left_dot下降到比上次访问时两部分的高度还要低时,这个变化会被右边部分覆盖。
1 | ---------- current_l ---------- |
此外还有一种下降的情况(这是在第二次提交时,解答出错时才看到的情况,一开始根本没考虑到),就是点在下降过程中被另一部分“拦截”了(这是left_dot比current_l和current_r都低时的特例,要求current_l > current_r > left_dot)
1 | ---------- current_l ---------- |
简而言之,左边部分要影响天际线(即左边点要放入结果集中),左边部分要么升高到足以覆盖右边部分,要么下降到仍然可以覆盖右边部分的位置,亦或是,在下降的过程中被右边部分截住了(其实把两部分的交点看作一个新的点),因为两部分始终处于覆盖和被覆盖的关系中。
所以一开始我决定先进行一遍归并排序,这样可以获得点排序好的List,但是写完归并后再去处理合并好的result时,发现写法和归并是一样的,然后发现这两部分可以合并在一起,因为归并每次排序时都是从两个数组中选一个最小的点出来,那么这一步其实已经可以知道当前点来自左边还是右边,所以可以一边归并排序(实则并没有排序),一边绘制天际线(这也是解答中我看不懂的地方)
具体代码实现如下:
1 | def merge_building(self, left: List[List], right: List[List]) -> List[List]: |
其中部分代码:
1 | if left_x == right_x: |
是考虑两栋大楼边界靠在一起的情况,这也是提交时官方的测试用例里我没考虑到情况,例如:
buildings_3 = [ [1, 2, 1], [1, 2, 2], [1, 2, 3] ]
我想了一下这里要考虑四条边(上次高度两条边,当前点的两条边)的高低情况,所以想了很久怎么减少一些情况(减少if语句),我的做法是计算当前两个点的高度相对变化率,如果变化率大(下降的情况对应负变化率),那么这个变化率对高度会产生影响。其实变化率很好算,因为我们记录了上一次的高度current_l和crrent_r,所以只需要用当前点的高度减去上一次的高度就是变化率了。
最后写了一天终于通过了,呵。