城市的 天际线 是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。给你所有建筑物的位置和高度,请返回 由这些建筑物形成的 天际线 。
每个建筑物的几何信息由数组 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], …]
输入: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)中 x 最小的为关键点。
class Solution {
public:
vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
int n = buildings.size();
vector<vector<int>> ans, tmp;
for(auto & build : buildings){
int l = build[0], r = build[1], h = build[2];
int lhigh = h, rhigh = 0;
for(auto & t : buildings){
if(t[0] <= l && l < t[1]){
lhigh = max(lhigh, t[2]);
}
if(t[0] <= r && r < t[1]){
rhigh = max(rhigh, t[2]);
}
}
tmp.push_back({l, lhigh});
tmp.push_back({r, rhigh});
}
sort(tmp.begin(), tmp.end(), [](vector<int>&a, vector<int>&b){
return a[0] < b[0];
});
int y = -1; // 当前的纵坐标
for(auto & d : tmp){
if(d[1] == y) continue;
ans.emplace_back(d);
y = d[1];
}
return ans;
}
};
时间复杂度: O ( n 2 ) O(n^2) O(n2) ,其中 n n n 为建筑数量。 O ( n ) O(n) O(n) 地枚举建筑的每一个边缘作为关键点的横坐标,过程中我们 O ( n ) O(n) O(n) 地检查每一座建筑是否「包含该横坐标」,找到最大高度,即为该关键点的纵坐标。
空间复杂度: O ( m ) O(m) O(m) ,其中 m m m 为建筑群的占地面积(即最小x与最大x的差值)。
这一题用线段树做时间复杂度有点高,可以用 扫描线算法+优先队列+延迟删除 的方法。
延迟删除:需要保证本节点对后续关键点没有影响后才能出队列(这是延迟思想的关键)。—— pair (关键值对)通过给要保存的值 加标记以确定删除的时机。
(高度,右端点)
的形式入优先队列,然后选择当前矩阵的左端点和优先队列中的最高高度组成左天际线(即存入结果数组中);(7,15)
存入数组。(12,0)
存入数组。——因为是最高高度的矩阵的右端点(12)的右侧重叠矩阵,所以优先队列中的 [10,9] 的9小于12,证明其不在该矩阵的右侧,所以优先队列会一步步将最大值删除,因为其不在最高矩阵的右侧,所以会被之前的最高矩阵挡住。class Solution {
public:
vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
vector<vector<int>> ans;
priority_queue<pair<int, int>> max_heap; // 使用 pair ,利用数据成员 second 来实现延迟删除
int i = 0, len = buildings.size();
int cur_x, cur_h;
while (i < len || !max_heap.empty()) {
// 如果最高的矩阵和当前矩阵重合,处理左端点
// 最高矩阵的右边缘x坐标大于等于当前矩阵的左边缘则代表重合,因为最高矩阵的左边缘x坐标一定小于等于当前矩阵的左边缘x
if (max_heap.empty() || i < len && buildings[i][0] <= max_heap.top().second) {
cur_x = buildings[i][0]; // 选择当前矩阵的左端点
// 相同的左端点全部入队,选择最高高度,按高度排序,相同高度按右边缘排序
while (i < len && cur_x == buildings[i][0]) {
max_heap.emplace(buildings[i][2], buildings[i][1]); // 将左端点入优先队列,格式为 (高度,右边缘x坐标)
// 遍历矩阵
++i;
}
} else { // 如果最高的矩阵和当前矩阵不重合,处理之前重合矩阵的右端点
cur_x = max_heap.top().second; // 选择最高高度的矩阵的右端点
// 选择其右侧重合的矩阵(不包括本矩阵)最高高度
while (!max_heap.empty() && cur_x >= max_heap.top().second) {// 找到优先队列中 cur_x 的右侧重合矩阵
max_heap.pop();
}
}
cur_h = (max_heap.empty()) ? 0 : max_heap.top().first;
if (ans.empty() || cur_h != ans.back()[1]) {
ans.push_back({cur_x, cur_h}); // 组成左天际线或右天际线
}
}
return ans;
}
};
时间复杂度: O ( n log n ) O(n\log n) O(nlogn) ,其中 n n n 为建筑数量。每座建筑至多只需要将(高度,右端点)入/出队一次,单次时间复杂度为 O ( log n ) O(\log n) O(logn)。
空间复杂度: O ( n ) O(n) O(n) ,其中 n n n 为建筑数量。优先队列中最多存储 n n n 个(高度,右端点)
如果是 C++,则可以用 扫描线算法+平衡二叉排序树(multiset) 的方法。大致思路与方法二类似,只是使用 multiset 替换了 priority_queue。
所以可以在满足了优先队列无法实现的删除指定元素的能力同时,还可以维护元素的顺序,且有较快的取最值的能力(虽然比不过常数级别的优先队列)。
所以可以修改方法二的一些细节:
class Solution {
public:
vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
vector<pair<int, int>> height;
for (auto &b : buildings) {
// 正负用于判别是左边界还是右边界,同时保证排序后:
// 左边界相同时,最高的楼排在前面,insert的一定是相同左边界中最大的高度
// 右边界相同时,最低的楼排在前面,erase的时候不会改变最大高度
height.push_back({b[0], -b[2]}); // 左边界
height.push_back({b[1], b[2]}); // 右边界
}
sort(height.begin(), height.end());
// 维护当前最大高度
multiset<int> heap;
heap.insert(0);
vector<vector<int>> res;
// pre 表示遇到一个边界之前的最大高度
// cur 表示遇到一个边界之后的当前最大高度
int pre = 0, cur = 0;
for (auto &h : height) {
if (h.second < 0) { // 左边界
heap.insert(-h.second);
} else { // 右边界
heap.erase(heap.find(h.second));
}
cur = *heap.rbegin();
// 最大高度发生改变,一定是一个 key point,即一个水平线段的左端点
if (cur != pre) {
res.push_back({h.first, cur});
pre = cur;
}
}
return res;
}
};
时间复杂度: O ( n log n ) O(n\log n) O(nlogn) ,其中 n n n 为建筑数量。每座建筑只需要将(高度,右端点)入/出队2次,单次时间复杂度为 O ( log n ) O(\log n) O(logn)
空间复杂度: O ( n ) O(n) O(n) ,其中 n n n 为建筑数量