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

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

网络异常,图片无法展示
|


题目描述



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


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


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


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


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


天际线 应该表示为由 “关键点” 组成的列表,格式 [[x1,y1],[x2,y2],...],并按 x 坐标 进行 排序 。关键点是水平线段的左端点。列表中最后一个点是最右侧建筑物的终点,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]]
解释:
图 A 显示输入的所有建筑物的位置和高度,
图 B 显示由这些建筑物形成的天际线。图 B 中的红点表示输出列表中的关键点。
复制代码


示例 2:


输入:buildings = [[0,2,3],[2,5,3]]
输出:[[0,3],[5,0]]
复制代码


提示:


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


基本分析



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


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


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


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


扫描线



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


网络异常,图片无法展示
|


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


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


因此我们需要实时维护一个最大高度,可以使用优先队列(堆)。


实现时,我们可以先记录下 buildingsbuildings 中所有的左右端点横坐标及高度,并根据端点横坐标进行从小到大排序。


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

  • 左端点:因为是左端点,必然存在一条从右延展的边,但不一定是需要被记录的边,因为在同一矩形中,我们只需要记录最上边的边。这时候可以将高度进行入队;
  • 右端点:此时意味着之前某一条往右延展的线结束了,这时候需要将高度出队(代表这结束的线不被考虑)。


然后从优先队列中取出当前的最大高度,为了防止当前的线与前一矩形“上边”延展而来的线重合,我们需要使用一个变量 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
复制代码


  • 时间复杂度:需要处理的矩阵数量与 nn 正比,每个矩阵需要使用优先队列维护高度,其中 remove 操作需要先花费 O(n)O(n) 复杂度进行查找,然后通过 O(\log{n})O(logn) 复杂度进行移除,复杂度为 O(n)O(n)。整体复杂度为 O(n^2)O(n2)
  • 空间复杂度: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. 为什么在处理前,先往「优先队列」添加一个 00


因为题目本身要求我们把一个完整轮廓的「右下角」那个点也取到,所以需要先添加一个 00


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


网络异常,图片无法展示
|


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


由于优先队列的 remove 操作需要先经过 O(n)O(n) 的复杂度进行查找,再通过 O(\log{n})O(logn) 的复杂度进行删除。因此整个 remove 操作的复杂度是 O(n)O(n) 的,这导致了我们算法整体复杂度为 O(n^2)O(n2)


优化方式包括:使用基于红黑树的 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(nlogn)
  • 空间复杂度:O(n)O(n)


最后



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


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


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


在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

相关文章
|
17天前
|
存储 算法 调度
【复现】【遗传算法】考虑储能和可再生能源消纳责任制的售电公司购售电策略(Python代码实现)
【复现】【遗传算法】考虑储能和可再生能源消纳责任制的售电公司购售电策略(Python代码实现)
122 26
|
25天前
|
机器学习/深度学习 算法 机器人
【机器人路径规划】基于D*算法的机器人路径规划(Python代码实现)
【机器人路径规划】基于D*算法的机器人路径规划(Python代码实现)
|
25天前
|
机器学习/深度学习 算法 机器人
【机器人路径规划】基于改进型A*算法的机器人路径规划(Python代码实现)
【机器人路径规划】基于改进型A*算法的机器人路径规划(Python代码实现)
|
25天前
|
机器学习/深度学习 编解码 算法
【机器人路径规划】基于迪杰斯特拉算法(Dijkstra)的机器人路径规划(Python代码实现)
【机器人路径规划】基于迪杰斯特拉算法(Dijkstra)的机器人路径规划(Python代码实现)
142 4
|
25天前
|
机器学习/深度学习 算法 机器人
【机器人路径规划】基于A*算法的机器人路径规划研究(Python代码实现)
【机器人路径规划】基于A*算法的机器人路径规划研究(Python代码实现)
129 4
|
25天前
|
机器学习/深度学习 算法 机器人
【机器人路径规划】基于深度优先搜索(Depth-First-Search,DFS)算法的机器人路径规划(Python代码实现)
【机器人路径规划】基于深度优先搜索(Depth-First-Search,DFS)算法的机器人路径规划(Python代码实现)
106 3
|
25天前
|
算法 机器人 定位技术
【机器人路径规划】基于流场寻路算法(Flow Field Pathfinding)的机器人路径规划(Python代码实现)
【机器人路径规划】基于流场寻路算法(Flow Field Pathfinding)的机器人路径规划(Python代码实现)
机器学习/深度学习 算法 自动驾驶
179 0
|
1月前
|
算法 定位技术 调度
基于蚂蚁优化算法的柔性车间调度研究(Python代码实现)
基于蚂蚁优化算法的柔性车间调度研究(Python代码实现)
|
1月前
|
机器学习/深度学习 算法 PyTorch
【DQN实现避障控制】使用Pytorch框架搭建神经网络,基于DQN算法、优先级采样的DQN算法、DQN + 人工势场实现避障控制研究(Matlab、Python实现)
【DQN实现避障控制】使用Pytorch框架搭建神经网络,基于DQN算法、优先级采样的DQN算法、DQN + 人工势场实现避障控制研究(Matlab、Python实现)

热门文章

最新文章

  • 1
    Python零基础爬取东方财富网股票行情数据指南
    46
  • 2
    解析Python爬虫中的Cookies和Session管理
    48
  • 3
    Python日志模块配置:从print到logging的优雅升级指南
    40
  • 4
    【可视化大屏】全流程讲解用python的pyecharts库实现拖拽可视化大屏的背后原理,简单粗暴!
    40
  • 5
    (Pandas)Python做数据处理必选框架之一!(二):附带案例分析;刨析DataFrame结构和其属性;学会访问具体元素;判断元素是否存在;元素求和、求标准值、方差、去重、删除、排序...
    47
  • 6
    (Pandas)Python做数据处理必选框架之一!(一):介绍Pandas中的两个数据结构;刨析Series:如何访问数据;数据去重、取众数、总和、标准差、方差、平均值等;判断缺失值、获取索引...
    72
  • 7
    (numpy)Python做数据处理必备框架!(二):ndarray切片的使用与运算;常见的ndarray函数:平方根、正余弦、自然对数、指数、幂等运算;统计函数:方差、均值、极差;比较函数...
    42
  • 8
    (numpy)Python做数据处理必备框架!(一):认识numpy;从概念层面开始学习ndarray数组:形状、数组转置、数值范围、矩阵...
    62
  • 9
    (Python基础)新时代语言!一起学习Python吧!(四):dict字典和set类型;切片类型、列表生成式;map和reduce迭代器;filter过滤函数、sorted排序函数;lambda函数
    33
  • 10
    (Python基础)新时代语言!一起学习Python吧!(三):IF条件判断和match匹配;Python中的循环:for...in、while循环;循环操作关键字;Python函数使用方法
    54
  • 推荐镜像

    更多