题目详情
城市的 天际线 是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。给你所有建筑物的位置和高度,请返回由这些建筑物形成的 天际线 。
每个建筑物的几何信息由数组 buildings
表示,其中三元组 buildings[i] = [lefti, righti, heighti]
表示:
lefti
是第i
座建筑物左边缘的x
坐标。righti
是第i
座建筑物右边缘的x
坐标。heighti
是第i
座建筑物的高度。
你可以假设所有的建筑都是完美的矩形,在高度为 0
的绝对平坦的表面上。
天际线应该表示为由“关键点”组成的列表,格式 [[x1,y1],[x2,y2],...]
,并按 x
坐标进行排序。关键点是水平线段的左端点。列表中最后一个点是最右侧建筑物的终点,y
坐标始终为 0
,仅用于标记天际线的终点。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。
注意:输出天际线中不得有连续的相同高度的水平线。例如 [...[2,3],[4,5],[7,5],[11,5],[12,7]...]
是不正确的答案;三条高度为 5
的线应该在最终输出中合并为一个:[...[2,3],[4,5],[12,7]...]
。
示例 1:
输入:buildings = [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]]
输出:[[2,10],[3,15],[7,12],[12,0],[15,10],[20,8],[24,0]]
解释:
图 A 显示输入的所有建筑物的位置和高度,
图 B 显示由这些建筑物形成的天际线。图 B 中的红点表示输出列表中的关键点。
示例 2:
输入:buildings = [[0,2,3],[2,5,3]]
输出:[[0,3],[5,0]]
提示:
1 <= buildings.length <= 10^4
0 <= lefti < righti <= 2^31 - 1
1 <= heighti <= 2^31 - 1
buildings
按lefti
非递减排序
解题思路
本题需要高效地计算建筑物的天际线关键点,核心在于扫描线算法与延迟删除优化。以下是详细步骤:
-
事件生成:
- 每个建筑物生成两个事件:开始事件(左边缘,高度取负)和结束事件(右边缘,高度取正)。
- 例如,建筑
[left, right, height]
生成事件[left, -height]
和[right, height]
。
-
事件排序:
- 按
x
坐标升序排序,x
相同时按高度升序(保证开始事件优先处理,且同一位置高建筑先处理)。
- 按
-
扫描线处理:
- 使用最大堆维护当前活跃建筑的最大高度。
- 使用
delayed
字典记录待删除的高度及次数(延迟删除优化)。 - 初始化堆中加入
0
(表示地面高度)。
-
关键点生成:
- 遍历排序后的事件列表,对每个
x
位置处理所有事件:- 开始事件:将高度绝对值加入堆。
- 结束事件:将高度加入
delayed
字典(计数+1)。
- 清理堆顶无效高度(在
delayed
中标记过的)。 - 获取当前有效最大高度,若与前一个关键点高度不同,则记录关键点
[x, currMax]
。
- 遍历排序后的事件列表,对每个
-
亮点:
- 延迟删除:避免每次删除操作都调整堆,仅在堆顶元素无效时删除,均摊复杂度低。
- 事件批处理:同一
x
位置的事件统一处理,确保每个位置只生成一个关键点。
时间复杂度:O(n log n)
,其中 n
是建筑数量(事件数 2n
,堆操作 O(log n)
)。
空间复杂度:O(n)
,存储事件、堆和延迟删除字典。
代码实现(Java版)
class Solution {
public List<List<Integer>> getSkyline(int[][] buildings) {
// 创建事件列表:每个事件是 [x, height]
List<int[]> events = new ArrayList<>();
for (int[] building : buildings) {
int left = building[0], right = building[1], height = building[2];
events.add(new int[]{left, -height}); // 开始事件:高度取负
events.add(new int[]{right, height}); // 结束事件:高度取正
}
// 事件排序:优先按x坐标升序;x相同时按高度升序(保证开始事件优先,且高建筑优先)
Collections.sort(events, (a, b) -> {
if (a[0] != b[0]) {
return Integer.compare(a[0], b[0]); // x坐标升序
}
return Integer.compare(a[1], b[1]); // x相同则按事件高度升序
});
// 最大堆(维护当前活跃建筑的高度)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
maxHeap.offer(0); // 初始地面高度0
// 延迟删除字典:记录已被移除但仍在堆中的高度及其出现次数
Map<Integer, Integer> delayed = new HashMap<>();
List<List<Integer>> ans = new ArrayList<>();
int prevMax = 0; // 记录上一次的最大高度
int i = 0;
while (i < events.size()) {
int currentX = events.get(i)[0];
// 处理同一x位置的所有事件
while (i < events.size() && events.get(i)[0] == currentX) {
int h = events.get(i)[1];
if (h < 0) { // 开始事件:高度取绝对值加入堆
maxHeap.offer(-h);
} else { // 结束事件:记录延迟删除
delayed.put(h, delayed.getOrDefault(h, 0) + 1);
}
i++;
}
// 清理堆顶无效高度(已标记延迟删除的)
while (!maxHeap.isEmpty()) {
int top = maxHeap.peek();
if (delayed.containsKey(top)) {
// 堆顶高度已被标记删除,移除堆顶并更新延迟字典
maxHeap.poll();
int count = delayed.get(top);
if (count == 1) {
delayed.remove(top);
} else {
delayed.put(top, count - 1);
}
} else {
break; // 堆顶有效,退出清理
}
}
// 获取当前有效最大高度
int currMax = maxHeap.isEmpty() ? 0 : maxHeap.peek();
// 若最大高度变化,则记录关键点
if (currMax != prevMax) {
ans.add(Arrays.asList(currentX, currMax));
prevMax = currMax;
}
}
return ans;
}
}
代码说明
-
事件生成与排序:
- 每个建筑生成两个事件(开始和结束),开始事件高度取负以便区分。
- 事件按
x
升序排序,x
相同时按事件高度升序(确保高建筑开始事件优先处理)。
-
最大堆与延迟删除:
- 最大堆维护当前扫描线穿过的建筑高度。
delayed
字典记录已结束建筑的高度及次数,避免立即调整堆。
-
关键点生成逻辑:
- 同一
x
位置的事件批量处理,更新堆和延迟字典。 - 清理堆顶无效高度后,若当前最大高度变化,则记录关键点
[x, currMax]
。
- 同一
-
边界处理:
- 初始地面高度
0
确保堆不为空。 - 结束时堆中仅剩
0
时,自动生成[x,0]
关键点。
- 初始地面高度