The Skyline Problem
Sweep line + max-heap
Sweep line + ordered multiset. Compose techniques under pressure.
Problem
You're given a list of buildings, each as [left, right, height]. Return the skyline — the outline formed by the union of all buildings — as a list of key points [x, y] where the height changes.
The skyline must not contain consecutive horizontal segments of the same height.
Intuition
Think about what actually changes as you walk left to right along the x-axis: the maximum active height.
- Every building contributes two events: an entry at its left edge (height becomes active) and an exit at its right edge (height becomes inactive).
- At any x, the skyline's height is the maximum of all currently active heights.
- The skyline emits a key point whenever that maximum changes.
This reduces a 2D geometry problem to a 1D event loop over 2n events — classic sweep line.
The hard part is maintaining "max of currently active heights" under insertions and removals efficiently. The idiomatic answer is a max-heap with lazy deletion — we never explicitly remove expired buildings, we only remove them when they surface at the top of the heap, which is exactly when their expiry could matter to the answer.
Approach
Sweep Line + Max-Heap
O(n log n)O(n)- Build events. For each
[l, r, h], produce(l, -h, r)and(r, 0, 0). Negative height at entries + sort by(x, h)naturally orders entries before exits at the same x, tallest first. - Sort events.
- Sweep. Walk events; on entry push
(h, end_x)onto a max-heap. Before reading the max, lazy-prune expired entries (end_x ≤ x). Emit(x, height)when the max changes.
use std::collections::BinaryHeap;
impl Solution {
pub fn get_skyline(buildings: Vec<Vec<i32>>) -> Vec<Vec<i32>> {
let mut events: Vec<(i32, i32, i32)> = Vec::with_capacity(buildings.len() * 2);
for b in &buildings {
let (l, r, h) = (b[0], b[1], b[2]);
events.push((l, -h, r));
events.push((r, 0, 0));
}
events.sort_unstable();
let mut heap: BinaryHeap<(i32, i32)> = BinaryHeap::new();
let mut out: Vec<Vec<i32>> = Vec::new();
let mut prev_h: i32 = 0;
let mut i = 0;
while i < events.len() {
let x= events[i].0;
while i < events.len() && events[i].0= x {
let (_, h_signed, r)= events[i];
if h_signed < 0 {
heap.push((-h_signed, r));
}
i = 1;
}
while let Some(&(_, end_x))= heap.peek() {
if end_x <= x {
heap.pop();
} else {
break;
}
}
let curr_h= heap.peek().map(|&(h, _)| h).unwrap_or(0);
if curr_h = prev_h {
out.push(vec![x, curr_h]);
prev_h= curr_h;
}
}
out
}
}// Minimal max-heap on (height, endX) ordered by height desc.
class MaxHeap {
private a: [number, number][] = [];
size(): number { return this.a.length; }
peek(): [number, number] | undefined { return this.a[0]; }
push(item: [number, number]): void {
this.a.push(item);
this.up(this.a.length - 1);
}
pop(): [number, number] | undefined {
if (this.a.length === 0) return undefined;
const top = this.a[0];
const last = this.a.pop()!;
if (this.a.length > 0) {
this.a[0] = last;
this.down(0);
}
return top;
}
private up(i: number): void {
while (i > 0) {
const p = (i - 1) >> 1;
if (this.a[p][0] >= this.a[i][0]) break;
[this.a[p], this.a[i]] = [this.a[i], this.a[p]];
i = p;
}
}
private down(i: number): void {
const n = this.a.length;
while (true) {
const l = 2 * i + 1;
const r = l + 1;
let best = i;
if (l < n && this.a[l][0] > this.a[best][0]) best = l;
if (r < n && this.a[r][0] > this.a[best][0]) best = r;
if (best === i) break;
[this.a[best], this.a[i]] = [this.a[i], this.a[best]];
i = best;
}
}
}
function getSkyline(buildings: number[][]): number[][] {
const events: [number, number, number][] = [];
for (const [l, r, h] of buildings) {
events.push([l, -h, r]);
events.push([r, 0, 0]);
}
events.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
const heap = new MaxHeap();
const out: number[][] = [];
let prevH = 0;
let i = 0;
while (i < events.length) {
const x = events[i][0];
while (i < events.length && events[i][0] === x) {
const [, hSigned, r] = events[i];
if (hSigned < 0) heap.push([-hSigned, r]);
i++;
}
while (heap.size() > 0 && heap.peek()![1] <= x) heap.pop();
const currH = heap.peek()?.[0] ?? 0;
if (currH !== prevH) {
out.push([x, currH]);
prevH = currH;
}
}
return out;
}Language notes:
- Rust —
BinaryHeapis a max-heap by default; tuplesOrdlexicographically, so sorting(x, h_signed, end_x)gives the right tie-break for free.sort_unstable()uses pattern-defeating quicksort. - TypeScript — The standard library has no heap; a handful of lines gets us a typed max-heap. Alternative: sort active buildings by height at each event (
O(n² log n), fine for small inputs, not for the hard constraints).
Trace
Partial trace on [[2,9,10], [3,7,15]]:
| x | event | heap after | max h | prev h | emit |
|---|---|---|---|---|---|
| 2 | enter (10, end 9) | [(10,9)] | 10 | 0 | [2, 10] |
| 3 | enter (15, end 7) | [(15,7), (10,9)] | 15 | 10 | [3, 15] |
| 7 | exit (h=15) | prune (15,7) → [(10,9)] | 10 | 15 | [7, 10] |
| 9 | exit (h=10) | prune (10,9) → [] | 0 | 10 | [9, 0] |
Edge cases
- Buildings with identical left edges — the negative-height tiebreak ensures the tallest enters first; only it contributes a skyline point.
- Buildings fully nested inside taller ones — their entries never surface as the heap top; their exits are pruned silently.
- Touching buildings (right edge = next left edge, same height) — processing all events at a given
xbefore emitting prevents a spurious[x, 0]in the middle. - Empty input — returns
[].
Lazy-deletion requires you to peek before trusting. If you heap.pop() directly without checking expiry, you corrupt the active-height invariant. Always prune from the top before reading the max.
Takeaway
Sweep line reduces 2D geometry to 1D event processing. Every time you see "at each x, compute some aggregate over a set that changes with x," the pattern is the same: emit events at boundaries, sort by x, maintain an active-set data structure.
Choice of active-set DS (heap with lazy deletion, ordered multiset, segment tree) is a separate decision driven by which operations you need. This composition — sweep + active-set DS — appears in #850 Rectangle Area II, #715 Range Module, and broadly in computational geometry. Trading systems use the same pattern for order book aggregation across sliding time windows.
More in Timed Random Hards
Bitmask DP (TSP variant). Can you identify it within 3 minutes?
Set cover via bitmask DP. n ≤ 16 is the signal.
2D sweep line + coordinate compression. Multi-technique composition.
Ordered map interval tracking. Relevant to order book modeling.