LeetCode 雙周賽 102,模擬 / BFS / Dijkstra / Floyd

大家好,歡迎來到小彭的 LeetCode 周賽解題報告。

做晚是 LeetCode 雙周賽第 102 場,你參加了嗎?這場比賽比較簡單,拼的是板子手速,繼上周掉大分后算是回了一口血 ??。


2618. 查詢網格圖中每一列的寬度(Easy)

簡單模擬題,無需解釋。

  • 模擬:O(nm)

2619. 一個數組所有前綴的分數(Medium)

簡單動態規劃題,簡單到像模擬題。

  • 動態規劃:O(n)

2620. 二叉樹的堂兄弟節點 II(Medium)

思考過程:遞歸→DFS→BFS。由于堂兄弟節點都在同一層,發現 “遞歸地減少問題規模求解原問題” 和 DFS 都不好編碼,而 BFS 更符合 “層” 的概念。往 BFS 方向思考后,容易找到解決方法。

  • BFS:O(n)

2621. 設計可以求最短路徑的圖類(Hard)

最近周賽的最短路問題非常多,印象中已經連續出現三次最短路問題。理解 Dijkstra 算法和 Floyd 算法的應用場景非常重要。

  • 樸素 Dijkstra:O(m + q_1·n^2 + q_2)
  • Dijkstra + 最小堆:O(m + q_1·nlgm+q_2)
  • Floyd:O(m + n^3 + q_1 + q_2·n^2)

2618. 查詢網格圖中每一列的寬度(Easy)

題目地址

https://leetcode.cn/problems/find-the-width-of-columns-of-a-grid/description/

題目描述

給你一個下標從 0 開始的 m x n 整數矩陣 grid 。矩陣中某一列的寬度是這一列數字的最大 字符串長度

  • 比方說,如果 grid = [[-10], [3], [12]] ,那么唯一一列的寬度是 3 ,因為 10 的字符串長度為 3

請你返回一個大小為 n 的整數數組 ans ,其中 ans[i] 是第 i 列的寬度。

一個有 len 個數位的整數 x ,如果是非負數,那么 字符串長度len ,否則為 len + 1

題解(模擬)

class Solution {
    fun findColumnWidth(grid: Array<IntArray>): IntArray {
        val m = grid.size
        val n = grid[0].size
        val ret = IntArray(n)
        for (column in 0 until n) {
            for (row in 0 until m) {
                ret[column] = Math.max(ret[column], "${grid[row][column]}".length)
            }
        }
        return ret
    }
}

復雜度分析:

  • 時間復雜度:O(nm) 其中 nm 為 grid 數組的行列大小,每個節點最多訪問 1 次;
  • 空間復雜度:O(1) 不考慮結果數組。

2619. 一個數組所有前綴的分數(Medium)

題目地址

https://leetcode.cn/problems/find-the-score-of-all-prefixes-of-an-array/description/

題目描述

定義一個數組 arr轉換數組 conver 為:

  • conver[i] = arr[i] + max(arr[0..i]),其中 max(arr[0..i]) 是滿足 0 <= j <= i 的所有 arr[j] 中的最大值。

定義一個數組 arr分數arr 轉換數組中所有元素的和。

給你一個下標從 0 開始長度為 n 的整數數組 nums ,請你返回一個長度為 n 的數組 **ans ,其中 ans[i]是前綴 nums[0..i] 的分數。

題解(動態規劃)

簡單動態規劃題,容易發現遞歸關系:

  • conver[i] = max{maxNum, arr[i]}
  • dp[i] = dp[i-1] + conver[i]
class Solution {
    fun findPrefixScore(nums: IntArray): LongArray {
        val n = nums.size
        val ret = LongArray(n)
        // 初始狀態
        ret[0] = 2L * nums[0]
        var maxNum = nums[0]
        // DP
        for (i in 1 until n) {
            maxNum = Math.max(maxNum, nums[i])
            ret[i] = ret[i - 1] + (0L + nums[i] + maxNum)
        }
        return ret
    }
}

復雜度分析:

  • 時間復雜度:O(n) 其中 narr 數組的長度,每個節點最多訪問 1 次;
  • 空間復雜度:O(1) 不考慮結果數組。

2620. 二叉樹的堂兄弟節點 II(Medium)

題目地址

https://leetcode.cn/problems/cousins-in-binary-tree-ii/description/

題目描述

給你一棵二叉樹的根 root ,請你將每個節點的值替換成該節點的所有 堂兄弟節點值的和

如果兩個節點在樹中有相同的深度且它們的父節點不同,那么它們互為 堂兄弟

請你返回修改值之后,樹的根 **root **。

注意,一個節點的深度指的是從樹根節點到這個節點經過的邊數。

題解(BFS)

分析 1 - 遞歸:嘗試分解左右子樹求解問題,發現左右子樹不獨立,不再考慮此思路;

分析 2 - DFS / BFS:由于堂兄弟節點都在同一層,而 BFS 更符合 “層” 的概念,往 BFS 方向思考后,容易找到解決方法:在處理每一層的節點時,第一輪遍歷先累計下一層節點的和,在第二輪遍歷時更新下一層節點(取出自己和兄弟節點的值)。

/**
 * Example:
 * var ti = TreeNode(5)
 * var v = ti.`val`
 * Definition for a binary tree node.
 * class TreeNode(var `val`: Int) {
 *     var left: TreeNode? = null
 *     var right: TreeNode? = null
 * }
 */
class Solution {
    fun replaceValueInTree(root: TreeNode?): TreeNode? {
        if (null == root) return root
        // BFS
        val queue = LinkedList<TreeNode>()
        queue.offer(root)
        root.`val` = 0
        while (!queue.isEmpty()) {
            val size = queue.size
            // 計算下一層的和
            var nextLevelSum = 0
            for (i in 0 until size) {
                val node = queue[i]
                if (null != node.left) nextLevelSum += node.left.`val`
                if (null != node.right) nextLevelSum += node.right.`val`
            }
            for (count in 0 until size) {
                val node = queue.poll()
                // 減去非堂兄弟節點
                var nextLevelSumWithoutNode = nextLevelSum
                if (null != node.left) nextLevelSumWithoutNode -= node.left.`val`
                if (null != node.right) nextLevelSumWithoutNode -= node.right.`val`
                // 入隊
                if (null != node.left) {
                    queue.offer(node.left)
                    node.left.`val` = nextLevelSumWithoutNode
                }
                if (null != node.right) {
                    queue.offer(node.right)
                    node.right.`val` = nextLevelSumWithoutNode
                }
            }
        }
        return root
    }
}

復雜度分析:

  • 時間復雜度:O(n) 其中 n 為二叉樹的節點總數,每個節點最多訪問 2 次(含入隊 1 次);
  • 空間復雜度:O(n) BFS 隊列空間。

相似題目:


2621. 設計可以求最短路徑的圖類(Hard)

題目地址

https://leetcode.cn/problems/design-graph-with-shortest-path-calculator/

題目描述

給你一個有 n 個節點的 有向帶權 圖,節點編號為 0n - 1 。圖中的初始邊用數組 edges 表示,其中 edges[i] = [fromi, toi, edgeCosti] 表示從 fromitoi 有一條代價為 edgeCosti 的邊。

請你實現一個 Graph 類:

  • Graph(int n, int[][] edges) 初始化圖有 n 個節點,并輸入初始邊。
  • addEdge(int[] edge) 向邊集中添加一條邊,其中 ****edge = [from, to, edgeCost] 。數據保證添加這條邊之前對應的兩個節點之間沒有有向邊。
  • int shortestPath(int node1, int node2) 返回從節點 node1node2 的路徑 最小 代價。如果路徑不存在,返回 1 。一條路徑的代價是路徑中所有邊代價之和。

問題分析

這道題勉強能算 Floyd 算法或 Dijkstra 算法的模板題,先回顧一下最短路問題解決方案:

  • Dijkstra 算法(單源正權最短路):
    • 本質上是貪心 + BFS;
    • 負權邊會破壞貪心策略的選擇,無法處理含負權問題;
    • 稀疏圖小頂堆的寫法更優,稠密圖樸素寫法更優。
  • Floyd 算法(多源匯正權最短路)
  • Bellman Ford 算法(單源負權最短路)
  • SPFA 算法(單源負權最短路)

由于這道題需要支持多次查詢操作,而 Floyd 算法能夠緩存最短路結果,理論上 Floyd 算法是更優的選擇。不過,我們觀察到題目的數據量非常非常小,所以樸素 Dijkstra 算法也能通過。

題解一(樸素 Dijkstra)

這道題的查詢操作是求從一個源點到目標點的最短路徑,并且這條路徑上沒有負權值,符合 Dijkstra 算法的應用場景,在處理添加邊時,只需要動態的修改圖數據結構。

Dijkstra 算法的本質是貪心 + BFS,我們需要將所有節點分為 2 類,在每一輪迭代中,我們從 “候選集” 中選擇距離起點最短路長度最小的節點,由于該點不存在更優解,所以可以用該點來 “松弛” 相鄰節點。

  • 1、確定集:已確定(從起點開始)到當前節點最短路徑的節點;
  • 2、候選集:未確定(從起點開始)到當前節點最短路徑的節點。

技巧:使用較大的整數 0x3F3F3F3F 代替整數最大值 Integer.MAX_VALUE 可以減少加法越界判斷。

class Graph(val n: Int, edges: Array<IntArray>) {

    private val INF = 0x3F3F3F3F

    // 帶權有向圖(臨接矩陣)
    private val graph = Array(n) { IntArray(n) { INF } }

    init {
        // i 自旋的路徑長度
        for (i in 0 until n) {
            graph[i][i] = 0
        }
        // i 直達 j 的路徑長度
        for (edge in edges) {
            addEdge(edge)
        }
    }

    fun addEdge(edge: IntArray) {
        graph[edge[0]][edge[1]] = edge[2]
    }

    fun shortestPath(node1: Int, node2: Int): Int {
        // Dijkstra

        // 最短路
        val dst = IntArray(n) { INF }
        dst[node1] = 0
        // 確定標記
        val visited = BooleanArray(n)
        // 迭代 n - 1 次
        for (count in 0 until n - 1) {
            // 尋找候選集中最短路長度最短的節點
            var x = -1
            for (i in 0 until n) {
                if (!visited[i] && (-1 == x || dst[i] < dst[x])) x = i
            }
            // start 可達的節點都訪問過 || 已確定 node1 -> node2 的最短路
            if (-1 == x || dst[x] == INF || x == node2) break
            visited[x] = true
            // 松弛相鄰節點
            for (y in 0 until n) {
                dst[y] = Math.min(dst[y], dst[x] + graph[x][y])
            }
        }
        return if (INF == dst[node2]) -1 else dst[node2]
    }
}

復雜度分析:

  • 時間復雜度:O(m + q_1·n^2 + q_2) 其中 n 為節點數量,m 為邊數量,q_1 為查詢次數,q_2 為添加邊次數。建圖時間 O(m),每個節點訪問 n 次;
  • 空間復雜度:O(n^2 + n) 圖空間 + 最短路數組

題解二(Dijkstra + 最小堆)

這道題是稠密圖,樸素 Dijkstra 由于 Dijkstra + 最小堆。

樸素 Dijkstra 的每輪迭代中需要遍歷 n 個節點尋找候選集中的最短路長度。事實上,這 n 個節點中有部分是 ”確定集“,有部分是遠離起點的邊緣節點,每一輪都遍歷顯得沒有必要。我們使用小頂堆記錄候選集中最近深度的節點。

class Graph(val n: Int, edges: Array<IntArray>) {

    private val INF = 0x3F3F3F3F

    // 帶權有向圖(臨接矩陣)
    private val graph = Array(n) { IntArray(n) { INF } }

    init {
        // i 自旋的路徑長度
        for (i in 0 until n) {
            graph[i][i] = 0
        }
        // i 直達 j 的路徑長度
        for (edge in edges) {
            addEdge(edge)
        }
    }

    fun addEdge(edge: IntArray) {
        graph[edge[0]][edge[1]] = edge[2]
    }

    fun shortestPath(node1: Int, node2: Int): Int {
        // Dijkstra + 最小堆

        // 最短路
        val dst = IntArray(n) { INF }
        dst[node1] = 0
        val heap = PriorityQueue<Int>() { i1, i2 ->
            dst[i1] - dst[i2]
        }
        heap.offer(node1)
        while (!heap.isEmpty()) {
            // 使用 O(lgm) 時間找出最短路長度
            var x = heap.poll()
            // 松弛相鄰節點
            for (y in 0 until n) {
                if (dst[x] + graph[x][y] < dst[y]) {
                    dst[y] = dst[x] + graph[x][y]
                    heap.offer(y)
                }
            }
        }
        return if (INF == dst[node2]) -1 else dst[node2]
    }
}

復雜度分析:

  • 時間復雜度:O(m + q_1·nlgm+q_2) 其中 n 為節點數量,m 為邊數量,q_1 為查詢次數,q_2 為添加邊次數。建圖時間 O(m),每條邊都會訪問一次,每輪迭代取堆頂 O(lgm)。這道題邊數大于點數,樸素寫法更優。
  • 空間復雜度:O(n^2 + n) 圖空間 + 堆空間。

題解三(Floyd)

Fload 算法的本質是貪心 + BFS,我們需要三層循環枚舉中轉點 i、枚舉起點 j 和枚舉終點 k,如果 dst[i][k] + dst[k][j] < dst[i][j],則可以松弛 dst[i][j]。

這道題的另一個關鍵點在于支持調用 addEdge() 動態添加邊,所以使用 Floyd 算法時要考慮如何更新存量圖。

class Graph(val n: Int, edges: Array<IntArray>) {

    val INF = 0x3F3F3F3F

    // 路徑長度(帶權有向圖)
    val graph = Array(n) { IntArray(n) { INF } }

    init {
        // i 自旋的路徑長度
        for (i in 0 until n) {
            graph[i][i] = 0
        }
        // i 直達 j 的路徑長度
        for (edge in edges) {
            graph[edge[0]][edge[1]] = edge[2]
        }
        // Floyd 算法
        // 枚舉中轉點
        for (k in 0 until n) {
            // 枚舉起點
            for (i in 0 until n) {
                // 枚舉終點
                for (j in 0 until n) {
                    // 比較 <i to j> 與 <i to p> + <p to j>
                    graph[i][j] = Math.min(graph[i][j], graph[i][k] + graph[k][j])
                }
            }
        }
    }

    fun addEdge(edge: IntArray) {
        val (x, y, cost) = edge
        // 直達
        graph[x][y] = Math.min(graph[x][y], cost)
        // 枚舉中轉點
        for (k in intArrayOf(x, y)) {
            // 枚舉起點
            for (i in 0 until n) {
                // 枚舉終點
                for (j in 0 until n) {
                    // 比較 <i to j> 與 <i to k> + <k to j>
                    graph[i][j] = Math.min(graph[i][j], graph[i][k] + graph[k][j])
                }
            }
        }
    }

    fun shortestPath(node1: Int, node2: Int): Int {
        return if (graph[node1][node2] == INF) -1 else graph[node1][node2]
    }
}

復雜度分析:

  • 時間復雜度:O(m + n^3 + q_1 + q_2·n^2) 其中 n 為節點數量,m 為邊數量,q_1 為查詢次數,q_2 為添加邊次數。建圖時間 O(m + n^3),單次查詢時間 O(1),單次添加邊時間 O(n^2)
  • 空間復雜度:O(n^2) 圖空間。

相關題目:

近期周賽最短路問題:

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容