题目:
城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。现在,假设您获得了城市风光照片(图A)上显示的所有建筑物的位置和高度,请编写一个程序以输出由这些建筑物形成的天际线(图B)。
每个建筑物的几何信息用三元组 [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], ...]
思路:
光看题目描述似乎觉得很难,但是学习过线段树就会觉得这道题变得很容易。不信,来试试看下面的图:
我们可以把输入列表作为一个顶点,按照输入列表的长度选取中间的值,建议使用这个方式: mid := l + (r-l)/2
选择中间值,然后进行分治算法。
直到当前输入列表的长度为1,说明不能再分了,在这个地方作为结束条件,然后返回到另外路径划分其它的输入列表。
例如我们划分到 [[2 9 10]] 的时候,当前输入列表的长度为1,不能再进行分治了。
[[2 9 10]]表示的是一个建筑物,分别是左右边缘的横坐标和高度。题目已经将天际线定义为水平线左端点的集合,如[[2 9 10]]关键点集合为[[2 10] [9 0]],分别是一个建筑物上的左上端点和右下端点。
同理,[[3 7 15]]的关键点集合为[[3 15] [7 0]]。
关键的一点来了,我们得到了[[2 9 10]] 和 [[3 7 15]] 两个集合之后,要求在满足题目天际线情况下,怎么把这两个集合进行合并呢?意思是合并之后的集合,也是满足天际线的,如下面合并的过程:
其实我们在题目标签看到了Line Sweep,[ 线扫描或扫描线 ] ,扫描线可以想象成一条向右扫过平面的竖直线,也是一个算法,一般是玩图形学的。
接着上面的步骤,可以通过扫描线算法将两个关键点集合进行合并。
如下图,扫描线从两个集合的起始点,同时向右移动,接触到第一个关键点,则判断这一个关键点是不是满足天际线的,如果是,则将这个关键点添加到“父”集合中;如果不是,则继续同时移动到下一个关键点。
但如何判断是否是属于“父”集合中的关键点呢?可以创建两个集合(“子”)的目前高度,然后多方角度找到满足关键点的条件。
扫描线移到[2 10]关键点时,10要大于rpre的,可以满足;
扫描线移到[3 15]关键点时,lpre此时目前的高度为10,而15要大于10的,可以满足;
扫描线移到[7 10]关键点时,rpre大于lpre可以满足,反之就不满足;
接着有一个集合已经遍历完了,剩下的集合的关键点肯定是满足的,因为没有其它的集合可以阻挡到这个集合,所以直接就是满足
java代码:
class Solution {
// 线段树
public List<List<Integer>> getSkyline(int[][] buildings) {
int len = buildings.length;
if (len == 0) return new ArrayList<>();
return segment(buildings, 0, len - 1);
}
private List<List<Integer>> segment(int[][] buildings, int l, int r) {
// 创建返回值
List<List<Integer>> res = new ArrayList<>();
// 找到树底下的结束条件 -> 一个建筑物
if (l == r) {
res.add(Arrays.asList(buildings[l][0], buildings[l][2])); // 左上端坐标
res.add(Arrays.asList(buildings[l][1], 0)); // 右下端坐标
return res;
}
int mid = l + (r - l) / 2; // 取中间值
// 左边递归
List<List<Integer>> left = segment(buildings, l, mid);
// 右边递归
List<List<Integer>> right = segment(buildings, mid + 1, r);
// 左右合并
// 创建left 和 right 的索引位置
int m = 0, n = 0;
// 创建left 和 right 目前的高度
int lpreH = 0, rpreH = 0;
// 两个坐标
int leftX, leftY, rightX, rightY;
while (m < left.size() || n < right.size()) {
// 当有一边完全加入到res时,则加入剩余的那部分
if (m >= left.size()) res.add(right.get(n++));
else if (n >= right.size()) res.add(left.get(m++));
else { // 开始判断left 和 right
leftX = left.get(m).get(0); // 不会出现null,可以直接用int类型
leftY = left.get(m).get(1);
rightX = right.get(n).get(0);
rightY = right.get(n).get(1);
//看我这两个矩形谁靠左
if (leftX < rightX) {
//左面还比以前高,就加左面
if (leftY > rpreH) res.add(left.get(m));
//左面比右面高,我要加入左面点的以及以前右面的的高度,因为我马上就有新高度了2,10
else if (lpreH > rpreH) res.add(Arrays.asList(leftX, rpreH));
// 用我左面的高替换我以前右面的高
lpreH = leftY;
m++;
} else if (leftX > rightX) {
if (rightY > lpreH) res.add(right.get(n));
else if (rpreH > lpreH) res.add(Arrays.asList(rightX, lpreH));
rpreH = rightY;
n++;
} else { // left 和 right 的横坐标相等
if (leftY >= rightY && leftY != (lpreH > rpreH ? lpreH : rpreH)) res.add(left.get(m));
else if (leftY <= rightY && rightY != (lpreH > rpreH ? lpreH : rpreH)) res.add(right.get(n));
lpreH = leftY;
rpreH = rightY;
m++;
n++;
}
}
}
return res;
}
}