[Hard] 218. The Skyline Problem
[Hard] 218. The Skyline Problem
A city’s skyline is the outer contour of the silhouette formed by all the buildings in that city when viewed from a distance. Given the locations and heights of all the buildings, return the skyline formed by these buildings collectively.
The geometric information of each building is given in the array buildings where buildings[i] = [lefti, righti, heighti]:
leftiis the x coordinate of the left edge of the ith building.rightiis the x coordinate of the right edge of the ith building.heightiis the height of the ith building.
You may assume all buildings are perfect rectangles grounded on an absolutely flat surface at height 0.
The skyline should be represented as a list of “key points” sorted by their x-coordinate in the form [[x1,y1],[x2,y2],...]. Each key point is the left endpoint of some horizontal segment in the skyline except the last point in the list, which always has a y-coordinate 0 and is used to mark the skyline’s termination where the rightmost building ends. Any ground between the leftmost and rightmost buildings should be part of the skyline contour.
Note: There must be no consecutive horizontal lines of equal height in the output skyline. For instance, [...[2 3],[4 5],[7 5],[11 5],[12 7]...] is not acceptable; the three lines of height 5 should be merged into one: [...[2 3],[4 5],[12 7]...]
Examples
Example 1:
Input: buildings = [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]]
Output: [[2,10],[3,15],[7,12],[12,0],[15,10],[20,8],[24,0]]
Explanation:
Figure A shows the buildings of the input.
Figure B shows the skyline formed by those buildings. The red points in figure B represent the key points in the output list.
Example 2:
Input: buildings = [[0,2,3],[2,5,3]]
Output: [[0,3],[5,0]]
Constraints
1 <= buildings.length <= 10^40 <= lefti < righti <= 2^31 - 11 <= heighti <= 2^31 - 1buildingsis sorted byleftiin non-decreasing order.
Approach
This is a classic sweep line algorithm problem. The key insight is to process all building edges (start and end points) in sorted order and maintain the current maximum height at each position.
There are several approaches to solve this problem:
- Coordinate Compression + Brute Force: Compress coordinates and update heights for each building
- Sweep Line with Map: Use events and maintain height counts
- Sweep Line with Priority Queue: Use priority queue to track active buildings
- Sweep Line with Two Priority Queues: Separate queues for active and past heights
- Union Find Optimization: Use Union Find to optimize the coordinate compression approach
Solution 1: Coordinate Compression + Brute Force
class Solution {
public:
vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
set<int> edgeSet;
for(auto& building: buildings) {
int left = building[0], right = building[1];
edgeSet.insert(left);
edgeSet.insert(right);
}
vector<int> edges(edgeSet.begin(), edgeSet.end());
map<int, int> edgeIdxMap;
for(int i = 0; i < edges.size(); i++) {
edgeIdxMap[edges[i]] = i;
}
vector<int> heights(edges.size());
for(auto& building :buildings) {
int left = building[0], right = building[1], height = building[2];
int leftIdx = edgeIdxMap[left], rightIdx = edgeIdxMap[right];
for(int idx = leftIdx; idx < rightIdx; idx++) {
heights[idx] = max(heights[idx], height);
}
}
vector<vector<int>> rtn;
for(int i = 0; i < heights.size(); i++) {
int curHeight = heights[i], curPos = edges[i];
if(i == 0 || curHeight != heights[i - 1]) {
rtn.push_back({curPos, curHeight});
}
}
return rtn;
}
};
Time Complexity: O(n²) - For each building, we update all positions it covers Space Complexity: O(n) - Edge set and height array
How it works:
- Collect all unique x-coordinates (building edges)
- Create mapping from coordinate to index
- For each building, update heights in the range [left, right)
- Generate skyline by checking height changes
Solution 2: Sweep Line with Map
class Solution {
public:
vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
vector<pair<int, int>> pairs;
for(auto& b: buildings) {
int left = b[0], right = b[1], height = b[2];
pairs.emplace_back(left, -height);
pairs.emplace_back(right, height);
}
sort(pairs.begin(), pairs.end(), [](const pair<int, int>& a, const pair<int, int>& b) {
if(a.first != b.first) return a.first < b.first;
else return a.second < b.second;
});
vector<vector<int>> rtn;
map<int, int> height_map;
height_map[0] = 1;
int pre = 0;
for(auto& [x, h]: pairs) {
if(h < 0) height_map[-h]++;
else {
height_map[h]--;
if(height_map[h] == 0) height_map.erase(h);
}
int cur = height_map.rbegin()->first;
if(cur != pre) {
rtn.push_back({x, cur});
pre = cur;
}
}
return rtn;
}
};
Time Complexity: O(n log n) - Sorting + map operations Space Complexity: O(n) - Map and pairs vector
How it works:
- Create events: Start events with negative height, end events with positive height
- Sort events by x-coordinate, then by height (negative heights first)
- Process events: Add/remove heights from map
- Track maximum height and add skyline points when it changes
Solution 3: Sweep Line with Priority Queue
class Solution {
public:
vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
vector<vector<int>> edges;
for(int i = 0; i < buildings.size(); i++) {
edges.push_back({buildings[i][0], i});
edges.push_back({buildings[i][1], i});
}
sort(edges.begin(), edges.end());
priority_queue<pair<int, int>> live;
vector<vector<int>> rtn;
int idx = 0;
while(idx < edges.size()) {
int cur = edges[idx][0];
while(idx < edges.size() && edges[idx][0] == cur) {
int b = edges[idx][1];
if(buildings[b][0] == cur) {
int right = buildings[b][1];
int height = buildings[b][2];
live.push({height, right});
}
idx += 1;
}
while(!live.empty() && live.top().second <= cur) live.pop();
int curHeight = live.empty() ? 0: live.top().first;
if(rtn.empty() || rtn[rtn.size() - 1][1] != curHeight) {
rtn.push_back({cur, curHeight});
}
}
return rtn;
}
};
Time Complexity: O(n log n) - Sorting + priority queue operations Space Complexity: O(n) - Priority queue and edges vector
How it works:
- Create edge events with building indices
- Sort edges by x-coordinate
- Process events: Add buildings to priority queue when they start
- Remove expired buildings from priority queue
- Track maximum height from active buildings
Solution 4: Sweep Line with Two Priority Queues
class Solution {
public:
vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
vector<vector<int>> edges;
for(auto& b: buildings) {
edges.push_back({b[0], b[2]});
edges.push_back({b[1], -b[2]});
}
sort(edges.begin(), edges.end(), [](const vector<int>& a, const vector<int>& b) {
if (a[0] != b[0]) return a[0] < b[0];
return a[1] > b[1];
});
priority_queue<int> live;
priority_queue<int> past;
vector<vector<int>> rtn;
int idx = 0;
while(idx < edges.size()) {
int cur = edges[idx][0];
while(idx < edges.size() && edges[idx][0] == cur) {
int height = edges[idx][1];
if(height > 0) live.push(height);
else past.push(-height);
idx += 1;
}
while(!live.empty() && !past.empty() && live.top() == past.top()) {
live.pop();
past.pop();
}
int curHeight = live.empty() ? 0: live.top();
if(rtn.empty() || rtn[rtn.size() - 1][1] != curHeight) {
rtn.push_back({cur, curHeight});
}
}
return rtn;
}
};
Time Complexity: O(n log n) - Sorting + priority queue operations Space Complexity: O(n) - Two priority queues
How it works:
- Create events: Start with positive height, end with negative height
- Sort events by x-coordinate, then by height (descending)
- Use two priority queues: One for active heights, one for past heights
- Remove matching heights from both queues
- Track maximum active height
Solution 5: Union Find Optimization
class UnionFind {
private:
vector<int> root;
public:
UnionFind(int n): root(n) {
iota(root.begin(), root.end(), 0);
}
int find(int x) {
if(root[x] != x) return find(root[x]);
return root[x];
}
void merge(int x, int y) {
root[find(x)] = find(y);
}
};
class Solution {
public:
vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
sort(buildings.begin(), buildings.end(), [](auto& a, auto& b) {
return a[2] > b[2];
});
set<int> edgeSet;
for(auto& b: buildings) {
edgeSet.insert(b[0]);
edgeSet.insert(b[1]);
}
vector<int> edges(edgeSet.begin(), edgeSet.end());
unordered_map<int, int> edgeIdxMap;
for(int i = 0; i < edges.size(); i++) {
edgeIdxMap[edges[i]] = i;
}
UnionFind uf(edges.size());
vector<int> heights(edges.size());
for(auto& b: buildings) {
int left = b[0], right = b[1], height = b[2];
int leftIdx = uf.find(edgeIdxMap[left]);
int rightIdx = edgeIdxMap[right];
while(leftIdx < rightIdx) {
heights[leftIdx] = height;
uf.merge(leftIdx, rightIdx);
leftIdx = uf.find(++leftIdx);
}
}
vector<vector<int>> rtn;
for(int i = 0; i < edges.size(); i++) {
if(i == 0 || heights[i] != heights[i - 1]) {
rtn.push_back({edges[i], heights[i]});
}
}
return rtn;
}
};
Time Complexity: O(n²) in worst case, but optimized with Union Find Space Complexity: O(n) - Union Find and height arrays
How it works:
- Sort buildings by height (descending)
- Process buildings in height order
- Use Union Find to skip already processed positions
- Update heights only for unprocessed positions
Step-by-Step Example (Solution 2)
Let’s trace through buildings = [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]]:
Create Events:
(2, -10), (9, 10) // Building 1: height 10
(3, -15), (7, 15) // Building 2: height 15
(5, -12), (12, 12) // Building 3: height 12
(15, -10), (20, 10) // Building 4: height 10
(19, -8), (24, 8) // Building 5: height 8
Sort Events:
(2, -10), (3, -15), (5, -12), (7, 15), (9, 10), (12, 12), (15, -10), (19, -8), (20, 10), (24, 8)
Process Events:
- x=2: Add height 10 → max=10 → add [2,10]
- x=3: Add height 15 → max=15 → add [3,15]
- x=5: Add height 12 → max=15 → no change
- x=7: Remove height 15 → max=12 → add [7,12]
- x=9: Remove height 10 → max=12 → no change
- x=12: Remove height 12 → max=0 → add [12,0]
- x=15: Add height 10 → max=10 → add [15,10]
- x=19: Add height 8 → max=10 → no change
- x=20: Remove height 10 → max=8 → add [20,8]
- x=24: Remove height 8 → max=0 → add [24,0]
Result: [[2,10],[3,15],[7,12],[12,0],[15,10],[20,8],[24,0]]
Algorithm Analysis
Solution Comparison:
| Solution | Time Complexity | Space Complexity | Approach | Pros | Cons |
|---|---|---|---|---|---|
| Coordinate Compression | O(n²) | O(n) | Brute Force | Simple logic | Inefficient |
| Sweep Line + Map | O(n log n) | O(n) | Event Processing | Clean, efficient | Map overhead |
| Sweep Line + PQ | O(n log n) | O(n) | Priority Queue | Intuitive | PQ cleanup needed |
| Two Priority Queues | O(n log n) | O(n) | Dual PQ | Handles duplicates | More complex |
| Union Find | O(n²) worst | O(n) | Optimization | Skips processed | Complex implementation |
Key Insights:
- Event Processing: Convert buildings to start/end events
- Height Tracking: Maintain current maximum height efficiently
- Change Detection: Only add skyline points when height changes
- Coordinate Handling: Process events in sorted order
- Data Structure Choice: Map vs Priority Queue trade-offs
Common Mistakes
- Not handling height changes correctly - Only add points when height changes
- Incorrect event sorting - Must handle ties properly
- Memory management - Clean up expired buildings from priority queue
- Edge cases - Handle empty buildings, single building scenarios
- Coordinate precision - Handle large coordinate values
Edge Cases
- Single building:
[[1,2,3]]→[[1,3],[2,0]] - Overlapping buildings: Same height buildings
- Adjacent buildings: Buildings touching at edges
- Large coordinates: Integer overflow considerations
- Empty input: Return empty skyline
Related Problems
- 253. Meeting Rooms II
- 56. Merge Intervals
- 57. Insert Interval
- 715. Range Module
- 850. Rectangle Area II
Conclusion
The Skyline Problem is a classic sweep line algorithm that tests understanding of:
- Event Processing: Converting 2D problems to 1D events
- Data Structures: Choosing appropriate structures for height tracking
- Algorithm Design: Efficiently processing sorted events
- Edge Case Handling: Managing height changes and duplicates
Recommended Solution: Solution 2 (Sweep Line with Map) offers the best balance of efficiency, clarity, and correctness for most scenarios.
The key insight is recognizing that skyline changes only occur at building edges, and we need to efficiently track the maximum height at each position while processing events in order.