Leetcode(218)——天际线问题

Leetcode(218)——天际线问题

题目

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

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

  • lefti 是第 i 座建筑物左边缘的 x 坐标。
  • righti 是第 i 座建筑物右边缘的 x 坐标。
  • heighti 是第 i 座建筑物的高度。

你可以假设所有的建筑都是完美的长方形,在高度为 0 的绝对平坦的表面上。

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

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

Leetcode(218)——天际线问题_第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 <= 104
  • 0 <= lefti < righti <= 231 - 1
  • 1 <= heighti <= 231 - 1
  • buildings 按 lefti 非递减排序

题解

关键:如何维护各个区间的最值

方法一:暴力枚举

思路

​​  循环每栋建筑并维护每个点的最大值,最后取连续相同的若干点(最小个数为1)中 x 最小的为关键点。

  1. 找出建筑群的最左 x 坐标和最右的 x 坐标(即确定遍历的范围,比如例1的范围为[2,24])并全部赋值y为0
  2. 循环遍历每一个三元组,并为每一个 x 坐标的 y 坐标的赋值。比如 [2,9,10],[3,7,15],[5,12,12],先遍历[2,9,10],注意只修改[2,9)的 y 坐标,这很重要,为了避免重复。赋值后 [2,10] [3,10] [4,10] [5,10] [6,10] [7,10] [8,10] [9,10] 。以此类推,最后赋值完是[2,10] [3,15] [4,15] [5,15] [6,15] [7,15] [8,12] [9,12] [10,12] [11,12] [12,0]。
  3. 当全部建筑都访问完后,如果有若干连续相同的 y 坐标,只取 x 最小的那一个坐标。
代码实现
class Solution {
public:
    vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
        int n = buildings.size();
        vector<vector<int>> ans, tmp;
        for(auto & build : buildings){
            int l = build[0], r = build[1], h = build[2];
            int lhigh = h, rhigh = 0;
            for(auto & t : buildings){
                if(t[0] <= l && l < t[1]){
                    lhigh = max(lhigh, t[2]);
                    }
                if(t[0] <= r && r < t[1]){
                    rhigh = max(rhigh, t[2]);
                    }
                }
            tmp.push_back({l, lhigh});
            tmp.push_back({r, rhigh});
            }

        sort(tmp.begin(), tmp.end(), [](vector<int>&a, vector<int>&b){
            return a[0] < b[0];
        });

        int y = -1; // 当前的纵坐标
        for(auto & d : tmp){
            if(d[1] == y) continue;
            ans.emplace_back(d);
            y = d[1];
            }

        return ans;
    }
};
复杂度分析

时间复杂度 O ( n 2 ) O(n^2) O(n2) ,其中 n n n 为建筑数量。 O ( n ) O(n) O(n) 地枚举建筑的每一个边缘作为关键点的横坐标,过程中我们 O ( n ) O(n) O(n) 地检查每一座建筑是否「包含该横坐标」,找到最大高度,即为该关键点的纵坐标。
空间复杂度 O ( m ) O(m) O(m) ,其中 m m m 为建筑群的占地面积(即最小x与最大x的差值)。

方法二:扫描线算法+优先队列(priority_queue)+延迟删除

思路

​​  这一题用线段树做时间复杂度有点高,可以用 扫描线算法+优先队列+延迟删除 的方法。

延迟删除:需要保证本节点对后续关键点没有影响后才能出队列(这是延迟思想的关键)。—— pair (关键值对)通过给要保存的值 加标记以确定删除的时机。

  1. 判断 高度最高的矩阵当前矩阵(即新来的矩阵) 是否重合,如果最高矩阵的右端点大于等于当前矩阵的左端点那么两个矩阵有重合,因为最高矩阵的左端点一定小于等于当前矩阵(即新来的矩阵)的左端点;
  2. 使用优先级队列存储矩阵高度,当矩阵重合时,方便选择端点的最高高度;
  3. 按照一个矩阵一个矩阵的来处理问题当遇到重合的矩阵时,处理当前矩阵的左端点,否则处理之前重合矩阵的右端点直到优先队列为空
  4. 处理左端点时,都将当前矩阵的高度以 (高度,右端点) 的形式入优先队列,然后选择当前矩阵的左端点和优先队列中的最高高度组成左天际线(即存入结果数组中);
  5. 处理右端点时(即处理优先队列中的点时),选择最高高度的矩阵的右端点,及其右侧的重合的矩阵(不包括本矩阵)最高高度,组成右天际线(即存入结果数组中)。

算法实现的细节:
Leetcode(218)——天际线问题_第2张图片

  • 比如处理优先队列的上图前三个重叠矩阵时,优先队列中从低到高为 [10,9] [12,12] [15,7] 。每次都处理最大值。
  • 先处理 [15,7] ,选择最高高度的矩阵的右端点(7),及其右侧的重合的矩阵(不包括本矩阵)最高高度(12),组成左天际线 (7,15) 存入数组。
  • 然后处理 [12,12] ,选择最高高度的矩阵的右端点(12),及其右侧的重合的矩阵(不包括本矩阵)最高高度(0),组成左天际线 (12,0) 存入数组。——因为是最高高度的矩阵的右端点(12)的右侧重叠矩阵,所以优先队列中的 [10,9] 的9小于12,证明其不在该矩阵的右侧,所以优先队列会一步步将最大值删除,因为其不在最高矩阵的右侧,所以会被之前的最高矩阵挡住。
代码实现
class Solution {
public:
    vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
        vector<vector<int>> ans;
        priority_queue<pair<int, int>> max_heap;	// 使用 pair ,利用数据成员 second 来实现延迟删除
        int i = 0, len = buildings.size();
        int cur_x, cur_h;
        while (i < len || !max_heap.empty()) {
            // 如果最高的矩阵和当前矩阵重合,处理左端点
            // 最高矩阵的右边缘x坐标大于等于当前矩阵的左边缘则代表重合,因为最高矩阵的左边缘x坐标一定小于等于当前矩阵的左边缘x
            if (max_heap.empty() || i < len && buildings[i][0] <= max_heap.top().second) {  
                cur_x = buildings[i][0];	// 选择当前矩阵的左端点
                // 相同的左端点全部入队,选择最高高度,按高度排序,相同高度按右边缘排序
                while (i < len && cur_x == buildings[i][0]) {
                    max_heap.emplace(buildings[i][2], buildings[i][1]); // 将左端点入优先队列,格式为 (高度,右边缘x坐标)
                    // 遍历矩阵
                    ++i;
                } 
            } else {	// 如果最高的矩阵和当前矩阵不重合,处理之前重合矩阵的右端点
                cur_x = max_heap.top().second;	// 选择最高高度的矩阵的右端点
                // 选择其右侧重合的矩阵(不包括本矩阵)最高高度
                while (!max_heap.empty() && cur_x >= max_heap.top().second) {// 找到优先队列中 cur_x 的右侧重合矩阵
                    max_heap.pop();
                } 
            }
            cur_h = (max_heap.empty()) ? 0 : max_heap.top().first;
            if (ans.empty() || cur_h != ans.back()[1]) {
                ans.push_back({cur_x, cur_h});	// 组成左天际线或右天际线
            }
        }
        return ans;
    }
};
复杂度分析

时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn) ,其中 n n n 为建筑数量。每座建筑至多只需要将(高度,右端点)入/出队一次,单次时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)
空间复杂度 O ( n ) O(n) O(n) ,其中 n n n 为建筑数量。优先队列中最多存储 n n n 个(高度,右端点)

方法三:扫描线算法 + 平衡二叉排序树(multiset)

思路

​​  如果是 C++,则可以用 扫描线算法+平衡二叉排序树(multiset) 的方法。大致思路与方法二类似,只是使用 multiset 替换了 priority_queue。
  所以可以在满足了优先队列无法实现的删除指定元素的能力同时,还可以维护元素的顺序,且有较快的取最值的能力(虽然比不过常数级别的优先队列)。
  所以可以修改方法二的一些细节:

  • 左边界相同时可能会加入多个相同的高度。综合考虑 multiset 是一个很好的选择。
  • 向 multiset 中加入高度之前,需要先对原数组进行处理,就是要先排序。先根据 x坐标,再根据高度进行排序。这里有个小trick,可以把左边界对应的高度设为负值,用来处理左边界的相同情况,pair 默认的排序规则已经满足逻辑。具体见代码注释。
  • 最后遍历 multiset,每遇到一个左边界,加入当前高度,遇到右边界,删除当前高度。然后判断进行此操作之后,有没有改变天际线的最大高度。由于 multiset 本身就是排好序的,所以可以直接得到当前的最大高度。
  • 这里我们用两个变量 pre 和 cur 用来表示当前操作前后的最大高度,一旦发生变化,则需要往数组中添加一个新的“关键点”。
代码实现
class Solution {
public:
    vector<vector<int>> getSkyline(vector<vector<int>>& buildings) {
        vector<pair<int, int>> height;
        for (auto &b : buildings) {
            // 正负用于判别是左边界还是右边界,同时保证排序后:
            // 左边界相同时,最高的楼排在前面,insert的一定是相同左边界中最大的高度
            // 右边界相同时,最低的楼排在前面,erase的时候不会改变最大高度
            height.push_back({b[0], -b[2]}); // 左边界
            height.push_back({b[1], b[2]});  // 右边界
        }
        sort(height.begin(), height.end());
        // 维护当前最大高度
        multiset<int> heap;
        heap.insert(0);
        vector<vector<int>> res;
        // pre 表示遇到一个边界之前的最大高度
        // cur 表示遇到一个边界之后的当前最大高度
        int pre = 0, cur = 0;
        for (auto &h : height) {
            if (h.second < 0) { // 左边界
                heap.insert(-h.second);
            } else { // 右边界
                heap.erase(heap.find(h.second));
            }
            
            cur = *heap.rbegin();
            // 最大高度发生改变,一定是一个 key point,即一个水平线段的左端点
            if (cur != pre) {
                res.push_back({h.first, cur});
                pre = cur;
            }
        }
        return res;
    }
};
复杂度分析

时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn) ,其中 n n n 为建筑数量。每座建筑只需要将(高度,右端点)入/出队2次,单次时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)
空间复杂度 O ( n ) O(n) O(n) ,其中 n n n 为建筑数量

你可能感兴趣的:(Leetcode,leetcode,算法,图论)