Blind Plus 100 Visualized

#86 ยท Heap / Priority Queue

The Skyline Problem

Given buildings as [left, right, height] triples sorted by left edge, return the skyline: the list of key points [x, height] where the outline of the merged silhouette changes height, ending with a point on the ground.

HardLeetCode
  • Being examined
  • New / inserted
  • Kept / merged / answer
  • Removed / conflict
  • Untouched
step 1 / 1

Pointers

Variables

Hash map / table

Stack / queue

Foundations & data-structure selection

Sweep line over edge coordinates

The outline can only change where a building starts or ends. A vertical line sweeps left to right over those coordinates; at each one, the outline height is the tallest building that covers it.

Memory layout

A sorted array of distinct x coordinates, or of start / end events.

OperationCost
sorting the edgesO(n log n)

Max-heap with lazy deletion

Holds (height, right edge) for buildings entered so far. The top is the tallest one. Instead of removing a building when it ends (which a heap cannot do efficiently), expired entries are popped only when they reach the top.

Memory layout

A contiguous array where children of i are at 2i + 1 and 2i + 2; at most n entries.

OperationCost
push / popO(log n)
peekO(1)

Skyline lists and merging

A skyline is a sorted list of (x, height) key points. Two skylines merge with two pointers in linear time by tracking each side's current height and emitting max(h1, h2) whenever it changes.

Memory layout

Plain lists of pairs; merging is sequential and cache friendly.

OperationCost
mergeO(a + b)

Why it fits

  • The answer is a function of the maximum height at each x, which is exactly what a max-heap tracks while buildings enter and leave.
  • Lazy deletion works because only the top matters: a stale entry buried under taller entries cannot affect the outline.
  • Divide and conquer fits because a skyline is closed under merging: the outline of two groups is a simple pointwise maximum of their outlines.

Why alternatives degrade

Painting a height array over x:
O(total width) memory and time, and coordinates can reach 2ยณยน โˆ’ 1.
Sorted list of heights without a heap:
Removal and max are O(n) per event, giving O(nยฒ).
Segment tree over compressed coordinates:
Works (range chmax, point query) but is heavier than a heap for the same O(n log n).

Four approaches, from naive to production

1 ยท beginner

Height at every edge by scanning all buildings

time O(nยฒ)space O(n)

Collect all distinct edge coordinates. At each one, scan every building to find the tallest that covers it; whenever this height differs from the previous one, record a key point.

vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
    set<int> edges;
    for (auto& b : buildings) { edges.insert(b[0]); edges.insert(b[1]); }
    vector<int> xs(edges.begin(), edges.end());
    vector<vector<int>> result;
    int prev = 0;
    for (int x : xs) {
        int height = 0;
        for (auto& b : buildings) {
            if (b[0] <= x && x < b[1]) height = max(height, b[2]);
        }
        if (height != prev) { result.push_back({x, height}); prev = height; }
    }
    return result;
}

Flaws & bottlenecks

  • For each of up to 2n coordinates it scans all n buildings.
  • The same buildings are re-tested at neighbouring coordinates.
  • Too slow for n = 10โด.

Interviewer cross-questions

Why only edge coordinates?

Say: Between two consecutive edges the set of covering buildings does not change, so the height is constant there.

Why x < r and not x <= r?

Say: A building covers [l, r); at its right edge it has already ended, which is why the outline can drop exactly at r.

What avoids rescanning?

Say: Keep the covering buildings in a data structure with fast maximum, such as a max-heap, as the sweep advances.

2 ยท intermediate

Sorted start / end events with an active list

time O(nยฒ)space O(n)

Turn each building into a start event (l, โˆ’h) and an end event (r, +h), sort them, and maintain a list of active heights. After each event compare the current maximum with the previous one.

vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
    vector<pair<int, int>> events;
    for (auto& b : buildings) {
        events.push_back({b[0], -b[2]});
        events.push_back({b[1], b[2]});
    }
    sort(events.begin(), events.end());
    vector<int> active;
    vector<vector<int>> result;
    int prev = 0;
    for (auto& [x, h] : events) {
        if (h < 0) active.push_back(-h);
        else active.erase(find(active.begin(), active.end(), h));
        int cur = active.empty() ? 0 : *max_element(active.begin(), active.end());
        if (cur != prev) { result.push_back({x, cur}); prev = cur; }
    }
    return result;
}

Flaws & bottlenecks

  • Removing from and scanning the active list costs O(n) per event.
  • The tie-breaking order of events (taller starts first, shorter ends first) is subtle.
  • Math.max over a spread array can overflow the argument limit for huge lists.

Interviewer cross-questions

Why store starts as โˆ’h?

Say: Sorting by (x, value) then puts starts before ends at the same x, and among starts the tallest first; among ends the shortest first. That avoids spurious key points at equal x.

What if two buildings touch (one ends where another starts)?

Say: Starts are processed before ends at the same x, so the new building is active before the old one is removed and no dip to ground is emitted.

How do you speed up the maximum?

Say: Use a max-heap (with lazy deletion) or a balanced multiset so max and remove cost O(log n).

3 ยท advanced

Sweep line with a max-heap and lazy deletion

time O(n log n)space O(n)

Sweep over the distinct x coordinates. Push every building that has started into a max-heap keyed by height (storing its right edge). Pop the top while it has already ended. The top's height is the outline height.

vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
    set<int> edges;
    for (auto& b : buildings) { edges.insert(b[0]); edges.insert(b[1]); }
    vector<int> xs(edges.begin(), edges.end());
    priority_queue<pair<int, int>> heap; // (height, right edge)
    vector<vector<int>> result;
    size_t i = 0; int prev = 0;
    for (int x : xs) {
        while (i < buildings.size() && buildings[i][0] <= x) {
            heap.push({buildings[i][2], buildings[i][1]});
            i++;
        }
        while (!heap.empty() && heap.top().second <= x) heap.pop();
        int height = heap.empty() ? 0 : heap.top().first;
        if (height != prev) { result.push_back({x, height}); prev = height; }
    }
    return result;
}

Flaws & bottlenecks

  • Stale entries can sit inside the heap until they reach the top.
  • Needs the buildings sorted by left edge (given by the problem; otherwise sort first).
  • Coordinates are compared with <=, which is a classic off-by-one trap.

Interviewer cross-questions

Why is lazy deletion correct?

Say: Only the maximum matters. An expired entry below the top cannot change the outline, and an expired entry at the top is removed before reading it.

Why is each building pushed and popped at most once?

Say: The pointer i only moves forward and every pop removes an entry for good: total O(n log n).

What changes for 10โต buildings with huge coordinates?

Say: Nothing: the algorithm depends on the number of edges, not on coordinate values, so no coordinate compression is needed.

4 ยท expert

Divide and conquer: merge two skylines

time O(n log n)space O(n)

The skyline of one building is [(l, h), (r, 0)]. Recursively compute the skylines of the two halves and merge them like a merge sort, tracking each side's current height and emitting max(h1, h2) when it changes.

vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
    function<vector<vector<int>>(const vector<vector<int>>&, const vector<vector<int>>&)> merge =
        [](const vector<vector<int>>& a, const vector<vector<int>>& b) {
            vector<vector<int>> out;
            size_t i = 0, j = 0; int h1 = 0, h2 = 0;
            while (i < a.size() || j < b.size()) {
                long long xa = i < a.size() ? a[i][0] : LLONG_MAX;
                long long xb = j < b.size() ? b[j][0] : LLONG_MAX;
                long long x = min(xa, xb);
                if (xa == x) h1 = a[i++][1];
                if (xb == x) h2 = b[j++][1];
                int h = max(h1, h2);
                if (out.empty() || out.back()[1] != h) out.push_back({(int)x, h});
            }
            return out;
        };
    function<vector<vector<int>>(int, int)> solve = [&](int lo, int hi) -> vector<vector<int>> {
        if (lo == hi) return {{buildings[lo][0], buildings[lo][2]}, {buildings[lo][1], 0}};
        int mid = (lo + hi) >> 1;
        return merge(solve(lo, mid), solve(mid + 1, hi));
    };
    return buildings.empty() ? vector<vector<int>>{} : solve(0, (int)buildings.size() - 1);
}

Flaws & bottlenecks

  • Allocates new lists at every merge level.
  • Recursion depth is only O(log n), but the intermediate lists use O(n log n) allocations in total.
  • The merge must handle equal x from both sides in one step to avoid duplicate key points.

Interviewer cross-questions

Why does the merge work?

Say: The merged outline is the pointwise maximum of the two outlines, and both are step functions that change only at their own key points, so one two-pointer sweep sees every change.

What is the recurrence?

Say: T(n) = 2T(n/2) + O(n), which is O(n log n), the same as the heap solution.

When would you prefer this over the heap?

Say: When the data is distributed (merge partial skylines from workers) or you need an allocation-light, heap-free solution; it also parallelises naturally.

Synchronized code walkthrough

Showing: Height at every edge by scanning all buildings โ€” shares the player above; click any step to jump.

vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
    set<int> edges;
    for (auto& b : buildings) { edges.insert(b[0]); edges.insert(b[1]); }
    vector<int> xs(edges.begin(), edges.end());
    vector<vector<int>> result;
    int prev = 0;
    for (int x : xs) {
        int height = 0;
        for (auto& b : buildings) {
            if (b[0] <= x && x < b[1]) height = max(height, b[2]);
        }
        if (height != prev) { result.push_back({x, height}); prev = height; }
    }
    return result;
}