在问 · 算法题解

218. 天际线问题

困难栈与堆扫描线 + 优先队列(延迟删除)力扣原题 ↗

题目描述

从远处看一座城市,所有建筑合在一起形成的外轮廓就是天际线。输入数组 buildings,第 i 座建筑记为 buildings[i],它是三元组 [lefti,righti,heighti]:左边缘横坐标为 lefti,右边缘横坐标为 righti,高度为 heighti。

每座建筑都是立在高度为 0 的平地上的长方形。两座建筑之间空着的地面也算作天际线的一部分。

用关键点列表 [[x1,y1],[x2,y2],...] 表示天际线,按 x 升序排列。每个关键点是一段水平线的左端点,表示从横坐标 x 开始轮廓高度变为 y。最后一个关键点位于最右侧建筑的右端,其 y 为 0,用来标明天际线结束。

相邻的两个关键点高度不能相同。例如 [...,[2,3],[4,5],[7,5],[11,5],[12,7],...] 不合法,三段高度为 5 的线要合成一段,写成 [...,[2,3],[4,5],[12,7],...]。

示例 1
输入: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]]

解释:x = 2 时轮廓升到 10,x = 3 升到 15,x = 7 降到 12,x = 12 落到地面;之后在 15、20 处变为 10、8,x = 24 回到 0。

示例 2
输入:buildings = [[0,2,3],[2,5,3]]
输出:[[0,3],[5,0]]

解释:两座建筑同高且相接,合并成一段高度为 3 的轮廓,从 0 延伸到 5。

数据范围

  • 1 <= buildings.length <= 10^4
  • 0 <= lefti < righti <= 2^31 - 1
  • 1 <= heighti <= 2^31 - 1
  • buildings 按 lefti 非递减排列。

解题思路:扫描线 + 优先队列(延迟删除)

轮廓高度只可能在某栋建筑的左边或右边发生变化,所以把每栋楼拆成“左端点进入”和“右端点离开”两个事件,按 x 从左到右扫描。用最大堆(存 (-h, r))维护当前覆盖扫描线的建筑,堆顶就是此刻的轮廓高度。处理事件时先把右端点 r <= x 的过期楼从堆顶弹掉(延迟删除),再把新楼压入堆,最后若堆顶高度与上一个关键点高度不同,就记录关键点 [x, 堆顶高度]。

为什么正确

处理完坐标 x 上的事件后,堆顶的高度等于 x 右侧紧邻处所有覆盖建筑的最大高度;堆里可以残留已过期的楼,但它们只要不在堆顶就不影响答案。

复杂度分析

时间 O(n log n)(排序与堆操作),空间 O(n)。

易错点

同一 x 上既有楼结束又有楼开始时,必须先处理完该 x 的所有事件再看高度,否则会产生 [x,0] 紧跟 [x,h] 的多余关键点;排序时把开始事件(-h 为负)排在结束事件(0)前面、高楼排在矮楼前面,就能自然避免。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def getSkyline(self, buildings: List[List[int]]) -> List[List[int]]:
        events = []
        for l, r, h in buildings:
            events.append((l, -h, r))
            events.append((r, 0, 0))
        events.sort()
        res = [[0, 0]]
        heap = [(0, float('inf'))]
        for x, negH, r in events:
            while heap[0][1] <= x:
                heapq.heappop(heap)
            if negH:
                heapq.heappush(heap, (negH, r))
            if res[-1][1] != -heap[0][0]:
                res.append([x, -heap[0][0]])
        return res[1:]

载入编辑器,自己跑一遍 →

其他解法

扫描线从左到右处理所有左右边界,用堆维护当前最高的建筑;分治把问题拆成两个子问题,合并时只需要取两条天际线的较高者。两者都是 O(n log n)。

解法 2:分治(合并天际线) 时间 O(n log n),空间 O(n)

把建筑分成左右两半,分别递归求出天际线,再合并。只有一栋建筑 [l, r, h] 时,天际线就是 [[l, h], [r, 0]]。合并两条天际线时像归并排序一样用两个指针按 x 从小到大处理关键点,分别记录两条线当前的高度 h1、h2;每处理一个 x(两边 x 相同就一起处理),合并后的高度就是 max(h1, h2);只有高度和上一个关键点不同时才输出新的关键点。

不需要堆和事件排序,合并过程和归并排序完全对应,也容易证明正确。需要递归和频繁创建临时数组,合并时 x 相等、高度去重等细节需要小心。

from typing import List

class Solution:
    def getSkyline(self, buildings: List[List[int]]) -> List[List[int]]:
        def merge(a, b):
            res = []
            i = j = h1 = h2 = 0

            def add(x, h):
                if not res or res[-1][1] != h:
                    res.append([x, h])

            while i < len(a) and j < len(b):
                if a[i][0] < b[j][0]:
                    x, h1 = a[i]
                    i += 1
                elif a[i][0] > b[j][0]:
                    x, h2 = b[j]
                    j += 1
                else:
                    x, h1 = a[i]
                    h2 = b[j][1]
                    i += 1
                    j += 1
                add(x, max(h1, h2))
            for x, h in a[i:] + b[j:]:
                add(x, h)
            return res

        def solve(lo, hi):
            if lo == hi:
                l, r, h = buildings[lo]
                return [[l, h], [r, 0]]
            mid = (lo + hi) // 2
            return merge(solve(lo, mid), solve(mid + 1, hi))

        return solve(0, len(buildings) - 1)

栈与堆 专题的其他题目

查看 栈与堆 全部题目 →