大家好,歡迎來到小彭的 LeetCode 周賽解題報告。
做晚是 LeetCode 雙周賽第 102 場,你參加了嗎?這場比賽比較簡單,拼的是板子手速,繼上周掉大分后算是回了一口血 ??。
2618. 查詢網格圖中每一列的寬度(Easy)
簡單模擬題,無需解釋。
- 模擬:
2619. 一個數組所有前綴的分數(Medium)
簡單動態規劃題,簡單到像模擬題。
- 動態規劃:
2620. 二叉樹的堂兄弟節點 II(Medium)
思考過程:遞歸→DFS→BFS。由于堂兄弟節點都在同一層,發現 “遞歸地減少問題規模求解原問題” 和 DFS 都不好編碼,而 BFS 更符合 “層” 的概念。往 BFS 方向思考后,容易找到解決方法。
- BFS:
2621. 設計可以求最短路徑的圖類(Hard)
最近周賽的最短路問題非常多,印象中已經連續出現三次最短路問題。理解 Dijkstra 算法和 Floyd 算法的應用場景非常重要。
- 樸素 Dijkstra:
- Dijkstra + 最小堆:
- Floyd:
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
}
}
復雜度分析:
- 時間復雜度:
其中
和
為 grid 數組的行列大小,每個節點最多訪問 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
}
}
復雜度分析:
- 時間復雜度:
其中
為
數組的長度,每個節點最多訪問 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
}
}
復雜度分析:
- 時間復雜度:
其中 n 為二叉樹的節點總數,每個節點最多訪問 2 次(含入隊 1 次);
- 空間復雜度:
BFS 隊列空間。
相似題目:
2621. 設計可以求最短路徑的圖類(Hard)
題目地址
https://leetcode.cn/problems/design-graph-with-shortest-path-calculator/
題目描述
給你一個有 n
個節點的 有向帶權 圖,節點編號為 0
到 n - 1
。圖中的初始邊用數組 edges
表示,其中 edges[i] = [fromi, toi, edgeCosti]
表示從 fromi
到 toi
有一條代價為 edgeCosti
的邊。
請你實現一個 Graph
類:
-
Graph(int n, int[][] edges)
初始化圖有n
個節點,并輸入初始邊。 -
addEdge(int[] edge)
向邊集中添加一條邊,其中 ****edge = [from, to, edgeCost]
。數據保證添加這條邊之前對應的兩個節點之間沒有有向邊。 -
int shortestPath(int node1, int node2)
返回從節點node1
到node2
的路徑 最小 代價。如果路徑不存在,返回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]
}
}
復雜度分析:
- 時間復雜度:
其中 n 為節點數量,m 為邊數量,
為查詢次數,
為添加邊次數。建圖時間 O(m),每個節點訪問 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]
}
}
復雜度分析:
- 時間復雜度:
其中 n 為節點數量,m 為邊數量,
為查詢次數,
為添加邊次數。建圖時間
,每條邊都會訪問一次,每輪迭代取堆頂 O(lgm)。這道題邊數大于點數,樸素寫法更優。
- 空間復雜度:
圖空間 + 堆空間。
題解三(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]
}
}
復雜度分析:
- 時間復雜度:
其中
為節點數量,
為邊數量,
為查詢次數,
為添加邊次數。建圖時間
,單次查詢時間
,單次添加邊時間
;
- 空間復雜度:
圖空間。
相關題目:
近期周賽最短路問題: