【每日算法】掃描線算法基本思路 & 優先隊列維護當前最大高度 |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 原題連接和其餘優選題解。

相關文章
相關標籤/搜索