【每日算法】扫描线算法基本思路 & 优先队列维护当前最大高度 |Python 主题月

本文正在参加「Python主题月」,详情查看 活动连接html

题目描述

这是 LeetCode 上的 218. 天际线问题 ,难度为 困难git

Tag : 「扫描线问题」、「优先队列」github

城市的天际线是从远处观看该城市中全部建筑物造成的轮廓的外部轮廓。给你全部建筑物的位置和高度,请返回由这些建筑物造成的 天际线 。算法

每一个建筑物的几何信息由数组 buildings 表示,其中三元组 buildings[i] = [lefti, righti, heighti] 表示:数组

  • left[i] 是第 i 座建筑物左边缘的 x 坐标。
  • right[i] 是第 i 座建筑物右边缘的 x 坐标。
  • height[i] 是第 i 座建筑物的高度。

天际线 应该表示为由 “关键点” 组成的列表,格式 [[x1,y1],[x2,y2],...],并按 x 坐标 进行 排序 。关键点是水平线段的左端点。列表中最后一个点是最右侧建筑物的终点,y 坐标始终为 0 ,仅用于标记天际线的终点。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。markdown

注意:输出天际线中不得有连续的相同高度的水平线。例如 [...[2 3], [4 5], [7 5], [11 5], [12 7]...] 是不正确的答案;三条高度为 5 的线应该在最终输出中合并为一个:[...[2 3], [4 5], [12 7], ...]app

示例 1: oop

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

解释:
图 A 显示输入的全部建筑物的位置和高度,
图 B 显示由这些建筑物造成的天际线。图 B 中的红点表示输出列表中的关键点。
复制代码

示例 2:post

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

输出:[[0,3],[5,0]]
复制代码

提示:优化

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

基本分析

这是一题特别的扫描线问题 🤣🤣🤣

既不是求周长,也不是求面积,是求轮廓中的全部的水平线的左端点 🤣🤣🤣

因此这不是一道必须用「线段树」来解决的扫描线问题(由于不须要考虑区间查询问题)。

扫描线的核心在于 将不规则的形状按照水平或者垂直的方式,划分红若干个规则的矩形。

扫描线

对于本题,对应的扫描线分割形状如图:

image.png

不难发现,由相邻两个横坐标以及最大高度,能够肯定一个矩形。

题目要咱们 输出每一个矩形的“上边”的左端点,同时跳过可由前一矩形“上边”延展而来的那些边。

所以咱们须要实时维护一个最大高度,可使用优先队列(堆)。

实现时,咱们能够先记录下 b u i l d i n g s buildings 中全部的左右端点横坐标及高度,并根据端点横坐标进行从小到大排序。

在从前日后遍历处理时(遍历每一个矩形),根据当前遍历到的点进行分状况讨论:

  • 左端点:由于是左端点,必然存在一条从右延展的边,但不必定是须要被记录的边,由于在同一矩形中,咱们只须要记录最上边的边。这时候能够将高度进行入队;

  • 右端点:此时意味着以前某一条往右延展的线结束了,这时候须要将高度出队(表明这结束的线不被考虑)。

而后从优先队列中取出当前的最大高度,为了防止当前的线与前一矩形“上边”延展而来的线重合,咱们须要使用一个变量 prev 记录上一个记录的高度。

Java 代码:

class Solution {
    public List<List<Integer>> getSkyline(int[][] bs) {
        List<List<Integer>> ans = new ArrayList<>();
        
        // 预处理全部的点,为了方便排序,对于左端点,令高度为负;对于右端点令高度为正
        List<int[]> ps = new ArrayList<>();
        for (int[] b : bs) {
            int l = b[0], r = b[1], h = b[2];
            ps.add(new int[]{l, -h});
            ps.add(new int[]{r, h});
        }

        // 先按照横坐标进行排序
        // 若是横坐标相同,则按照左端点排序
        // 若是相同的左/右端点,则按照高度进行排序
        Collections.sort(ps, (a, b)->{
            if (a[0] != b[0]) return a[0] - b[0];
            return a[1] - b[1];
        });
        
        // 大根堆
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->b-a);
        int prev = 0;
        q.add(prev);
        for (int[] p : ps) {
            int point = p[0], height = p[1];
            if (height < 0) {
                // 若是是左端点,说明存在一条往右延伸的可记录的边,将高度存入优先队列
                q.add(-height);
            } else {
                // 若是是右端点,说明这条边结束了,将当前高度从队列中移除
                q.remove(height);
            }

            // 取出最高高度,若是当前不与前一矩形“上边”延展而来的那些边重合,则能够被记录
            int cur = q.peek();
            if (cur != prev) {
                List<Integer> list = new ArrayList<>();
                list.add(point);
                list.add(cur);
                ans.add(list);
                prev = cur;
            }
        }
        return ans;
    }
}
复制代码

Python 3 代码:

from sortedcontainers import SortedList

class Solution:
    def getSkyline(self, buildings: List[List[int]]) -> List[List[int]]:
        ans = []

        # 预处理全部的点,为了方便排序,对于左端点,令高度为负;对于右端点令高度为正
        ps = []
        for l, r, h in buildings:
            ps.append((l, - h))
            ps.append((r, h))
        # 先按照横坐标进行排序
        # 若是横坐标相同,则按照左端点排序
        # 若是相同的左/右端点,则按照高度进行排序
        ps.sort()

        prev = 0
        # 有序列表充当大根堆
        q = SortedList([prev])

        for point, height in ps:
            if height < 0:
                # 若是是左端点,说明存在一条往右延伸的可记录的边,将高度存入优先队列
                q.add(-height)
            else:
                # 若是是右端点,说明这条边结束了,将当前高度从队列中移除
                q.remove(height)
            
            # 取出最高高度,若是当前不与前一矩形“上边”延展而来的那些边重合,则能够被记录
            cur = q[-1]
            if cur != prev:
                ans.append([point, cur])
                prev = cur

        return ans
复制代码
  • 时间复杂度:须要处理的矩阵数量与 n n 正比,每一个矩阵须要使用优先队列维护高度,其中 remove 操做须要先花费 O ( n ) O(n) 复杂度进行查找,而后经过 O ( log n ) O(\log{n}) 复杂度进行移除,复杂度为 O ( n ) O(n) 。总体复杂度为 O ( n 2 ) O(n^2)
  • 空间复杂度: O ( n ) O(n)

答疑

1. 将左端点的高度存成负数再进行排序是什么意思?

这里只是为了方便,因此采起了这样的作法,固然也可以多使用一位来代指「左右」。

只要最终能够达到以下的排序规则便可:

  1. 先严格按照横坐标进行「从小到大」排序
  2. 对于某个横坐标而言,可能会同时出现多个点,应当按照以下规则进行处理:
    1. 优先处理左端点,再处理右端点
    2. 若是一样都是左端点,则按照高度「从大到小」进行处理(将高度增长到优先队列中)
    3. 若是一样都是右端点,则按照高度「从小到大」进行处理(将高度从优先队列中删掉)

代码:

class Solution {
    public List<List<Integer>> getSkyline(int[][] bs) {
        List<List<Integer>> ans = new ArrayList<>();
        List<int[]> ps = new ArrayList<>();
        for (int[] b : bs) {
            int l = b[0], r = b[1], h = b[2];
            ps.add(new int[]{l, h, -1});
            ps.add(new int[]{r, h, 1});
        }
        /** * 先严格按照横坐标进行「从小到大」排序 * 对于某个横坐标而言,可能会同时出现多个点,应当按照以下规则进行处理: * 1. 优先处理左端点,再处理右端点 * 2. 若是一样都是左端点,则按照高度「从大到小」进行处理(将高度增长到优先队列中) * 3. 若是一样都是右端点,则按照高度「从小到大」进行处理(将高度从优先队列中删掉) */
        Collections.sort(ps, (a, b)->{
            if (a[0] != b[0]) return a[0] - b[0];
            if (a[2] != b[2]) return a[2] - b[2];
            if (a[2] == -1) {
                return b[1] - a[1];
            } else {
                return a[1] - b[1];
            }
        });
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->b-a);
        int prev = 0;
        q.add(prev);
        for (int[] p : ps) {
            int point = p[0], height = p[1], flag = p[2];
            if (flag == -1) {
                q.add(height);
            } else {
                q.remove(height);
            }

            int cur = q.peek();
            if (cur != prev) {
                List<Integer> list = new ArrayList<>();
                list.add(point);
                list.add(cur);
                ans.add(list);
                prev = cur;
            }
        }
        return ans;
    }
}
复制代码

2. 为何在处理前,先往「优先队列」添加一个 0 0

由于题目自己要求咱们把一个完整轮廓的「右下角」那个点也取到,因此须要先添加一个 0 0

也就是下图被圈出来的那些点:

image.png

3. 优先队列的 remove 操做成为了瓶颈,如何优化?

因为优先队列的 remove 操做须要先通过 O ( n ) O(n) 的复杂度进行查找,再经过 O ( log n ) O(\log{n}) 的复杂度进行删除。所以整个 remove 操做的复杂度是 O ( n ) O(n) 的,这致使了咱们算法总体复杂度为 O ( n 2 ) O(n^2)

优化方式包括:使用基于红黑树的 TreeMap 代替优先队列;或是使用「哈希表」记录「执行了删除操做的高度」及「删除次数」,在每次使用前先检查堆顶高度是否已经被标记删除,若是是则进行 poll 操做,并更新删除次数,直到遇到一个没被删除的堆顶高度。

代码:

class Solution {
    public List<List<Integer>> getSkyline(int[][] bs) {
        List<List<Integer>> ans = new ArrayList<>();
        List<int[]> ps = new ArrayList<>();
        for (int[] b : bs) {
            int l = b[0], r = b[1], h = b[2];
            ps.add(new int[]{l, h, -1});
            ps.add(new int[]{r, h, 1});
        }
        /** * 先严格按照横坐标进行「从小到大」排序 * 对于某个横坐标而言,可能会同时出现多个点,应当按照以下规则进行处理: * 1. 优先处理左端点,再处理右端点 * 2. 若是一样都是左端点,则按照高度「从大到小」进行处理(将高度增长到优先队列中) * 3. 若是一样都是右端点,则按照高度「从小到大」进行处理(将高度从优先队列中删掉) */
        Collections.sort(ps, (a, b)->{
            if (a[0] != b[0]) return a[0] - b[0];
            if (a[2] != b[2]) return a[2] - b[2];
            if (a[2] == -1) {
                return b[1] - a[1];
            } else {
                return a[1] - b[1];
            }
        });
        // 记录进行了删除操做的高度,以及删除次数
        Map<Integer, Integer> map = new HashMap<>();
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->b-a);
        int prev = 0;
        q.add(prev);
        for (int[] p : ps) {
            int point = p[0], height = p[1], flag = p[2];
            if (flag == -1) {
                q.add(height);
            } else {
                map.put(height, map.getOrDefault(height, 0) + 1);
            }

            while (!q.isEmpty()) {
                int peek = q.peek();
                if (map.containsKey(peek)) {
                    if (map.get(peek) == 1) map.remove(peek);
                    else map.put(peek, map.get(peek) - 1);
                    q.poll();
                } else {
                    break;
                }
            }

            int cur = q.peek();
            if (cur != prev) {
                List<Integer> list = new ArrayList<>();
                list.add(point);
                list.add(cur);
                ans.add(list);
                prev = cur;
            }
        }
        return ans;
    }
}
复制代码
  • 时间复杂度: O ( n log n ) O(n\log{n})
  • 空间复杂度: O ( n ) O(n)

最后

这是咱们「刷穿 LeetCode」系列文章的第 No.218 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,咱们将先把全部不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路之外,还会尽量给出最为简洁的代码。若是涉及通解还会相应的代码模板。

为了方便各位同窗可以电脑上进行调试和提交代码,我创建了相关的仓库:github.com/SharingSour…

在仓库地址里,你能够看到系列文章的题解连接、系列文章的相应代码、LeetCode 原题连接和其余优选题解。

相关文章
相关标签/搜索