Hard#21812 min readSweep LineHeapOrdered MapLeetCode

The Skyline Problem

Sweep line + max-heap

Sweep line + ordered multiset. Compose techniques under pressure.

Published Apr 15, 2026

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.

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]]

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

TimeO(n log n)
SpaceO(n)
Recommended
  1. 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.
  2. Sort events.
  3. 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
    }
}
Rust · runnable

Language notes:

  • RustBinaryHeap is a max-heap by default; tuples Ord lexicographically, 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( log n), fine for small inputs, not for the hard constraints).

Trace

Partial trace on [[2,9,10], [3,7,15]]:

xeventheap aftermax hprev hemit
2enter (10, end 9)[(10,9)]100[2, 10]
3enter (15, end 7)[(15,7), (10,9)]1510[3, 15]
7exit (h=15)prune (15,7) → [(10,9)]1015[7, 10]
9exit (h=10)prune (10,9) → []010[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 x before emitting prevents a spurious [x, 0] in the middle.
  • Empty input — returns [].
⚠️
Warning

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