Shortest Distance After Road Addition Queries I

tags: JavaScript
category: LeetCode
description: LeetCode - Shortest Distance After Road Addition Queries I
created_at: 2024/11/27 21:00:00

cover image

題目連結: https://leetcode.com/problems/shortest-distance-after-road-addition-queries-i


前言

這一題是 2024/11/27Daily 題目,而為什麼會忽然寫這一題呢? 不是因為它的難度,而是因為今天剛好想到了一個不錯的做法,所以很想寫下來記錄一下。

這個做法應該是受到了 Union FindBellman-FordDijkstra 的啟發(?),總之是有那麼一點點感覺


題目簡介

  1. 有向無環圖
  2. 每個節點預設都和下一個數字相連
  3. 給你一個 N 代表有 N 個節點
  4. 給你一個 queries 陣列,裡面是 [u, v] 代表要把 u 連到 v
  5. 求出每個 queries 連接後的最短路徑(從頭走到尾,意思是 0N-1)
  6. 題目不會有回頭路,也就是說 u 一定比 v
    • 題外話: 一開始我不小心把題目想複雜了,老實說這次我沒有細看題目(因為今天整天有點不太舒服(可能沒睡好QQ)),所以我第一個想的 edge case 是有沒有一種可能是他交叉著連結果更短,後來敲進 test case 才發現他不會有這種情況;不過接下來的解法可以涵蓋這種情況。

我想到的解法是這樣的:

  1. 用一個 disatnces 存在每個節點到 0 的最短距離
  2. 用一個 effects 存放某個節點變化時,應該更新誰?
  3. 於是當 u 連到 v 的時候,如果有更好,我們就把 v 的距離更新成 u 的距離+1,並且更新 effects,並往前更新 disatnces。 ((這樣講比較順,雖然寫起來不是這樣,總之我們後面會細說

雖然說題目不會有回頭路,但為了進一步驗證這個解法,所以特別用有回頭路的 test case 來說明。

文字描述可能有點抽象,我們來看看圖,假設今天 N = 10

你的初始狀態是這樣的:

0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9

這時你的 distances 是這樣的:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

然後你的 effects 是這樣的:

[
    [1], // 0
    [2], // 1
    [3], // 2
    [4], // 3
    [5], // 4
    [6], // 5
    [7], // 6
    [8], // 7
    [9], // 8
    []   // 9
]

這代表當 1 有變化時,我們要更新 2,當 2 有變化時,我們要更新 3,以此類推。

接著假設有組資料長這樣: image

這邊是多了以下幾個資料:

[[3,9],[5,3],[0,5]]

以結果論來看,最短路徑是 0 -> 5 -> 3 -> 9,也就是 3

那我們來看看這個 test case 的過程:

  1. 3 連到 9 的時候,我們先更新 effects,當 3 有變化時,我們要更新 9,所以:
    [
     [1], // 0
     [2], // 1
     [3], // 2
     [4, 9], // 3
     [5], // 4
     [6], // 5
     [7], // 6
     [8], // 7
     [9], // 8
     []   // 9
    ]

接著需要去更新 distances,所以開始執行類似 Union Find 的操作:

索引  : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
更新前: [0, 1, 2, 3, 4, 5, 6, 7 ,8, 9]
更新 4: [0, 1, 2, 3, 4, 5, 6, 7 ,8, 9] (略過,4沒有更優)
更新 9: [0, 1, 2, 3, 4, 5, 6, 7 ,8, 4] (更新)

接著我們再來看 5 連到 3 的時候:

[
    [1], // 0
    [2], // 1
    [3], // 2
    [4, 9], // 3
    [5], // 4
    [6, 3], // 5
    [7], // 6
    [8], // 7
    [9], // 8
    []   // 9
]

接著一樣更新 distances:

索引  : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
更新前: [0, 1, 2, 3, 4, 5, 6, 7 ,8, 4]
更新 6: [0, 1, 2, 3, 4, 5, 6, 7 ,8, 4] (略過,6沒有更優)
更新 3: [0, 1, 2, 3, 4, 5, 6, 7 ,8, 4] (略過,3也沒有更優)

最後,我們再來看 0 連到 5 的時候:

[
    [1, 5], // 0
    [2], // 1
    [3], // 2
    [4, 9], // 3
    [5], // 4
    [6, 3], // 5
    [7], // 6
    [8], // 7
    [9], // 8
    []   // 9
]

接著一樣更新 distances:

索引  : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
更新前: [0, 1, 2, 3, 4, 5, 6, 7 ,8, 4]
更新 1: [0, 1, 2, 3, 4, 5, 6, 7 ,8, 4] (略過,1沒有更優)
更新 5: [0, 1, 2, 3, 4, 1, 6, 7 ,8, 4] (更新)
    索引  : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    更新 6: [0, 1, 2, 3, 4, 1, 2, 7 ,8, 4] (更新)
        索引  : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        更新 7: [0, 1, 2, 3, 4, 1, 2, 3 ,8, 4] (更新)
            索引  : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
            更新 8: [0, 1, 2, 3, 4, 1, 2, 3 ,4, 4] (更新)
                索引  : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
                更新 9: [0, 1, 2, 3, 4, 1, 2, 3 ,4, 4] (略過,9沒有更優)
    索引  : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    更新 3: [0, 1, 2, 2, 4, 1, 2, 3 ,4, 4] (更新)
        索引  : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        更新 4: [0, 1, 2, 2, 3, 1, 2, 3 ,4, 4] (更新)
        索引  : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        更新 9: [0, 1, 2, 2, 3, 1, 2, 3 ,4, 3] (更新)

這次的更新比較複雜,因為若有更優的話,我們就要一直往前更新,直到沒有更優為止,這樣才能保證整個 distances 是最優解。

如果有算法底子的這時可能就看出這樣的解法是 DFS,但其實這邊用 BFS 的話也可以做到一樣的結果,不過效率會有一些些差異,以這題的這個解法來說的話用 DFS 的話不只效率比較穩定,而且 Code 也比較好看(?),但不管怎麼樣,這裡你用 DFS 還是 BFS 時間複雜度都是一樣的(以最壞情況來說,不過 DFS 可能不會)。

所以選擇 DFSBFS 還是必須稍微評估一下,雖然大多數題目都是 BFS 比較快,但這題用 BFS 可能會導致前面都在做無謂的更新(queue越長越大),如果用 DFS 就可以避免這種情況。


JavaScript 實作

一樣分段來解釋,首先定義 distanceseffects,然後不要忘了最後要輸出的 output

const distances = Array.from({length: n}, (_, i) => i)
const effects = Array.from({length: n}, (_, i) => [i + 1])
const output = []

effects[n - 1] = []

接著我們需要開始讀取題目的 queries,這邊我們用 for 來讀取:

for (const [from, to] of queries) {
    effects[from].push(to)
    update(distances, effects, from)
    output.push(distances[n - 1])
}

最後我們需要實作 update,這邊我們用 DFS 來實作:

const update = (distances, effects, from) => {
    effects[from].forEach(node => {
        if (distances[node] > distances[from] + 1) {
            distances[node] = distances[from] + 1
            update(distances, effects, node)
        }
    })
}

這邊比較重要的是那個 if,這邊我們要判斷是否有更優解,如果有的話,我們就要一直往前更新,這樣不只能保證 distances 是最優解,也才不會拉垮整個時間複雜度。

最後我們就可以輸出 output 了:

return output

整個 Code 就這樣子,非常的精簡。

如果你要更精簡的話也許可以讓 update 直接使用 globaldisatnceseffects

: 使用遞迴的方式需要注意遞迴深度的問題,避免超過 Call Stack 的限制,不過這題資料不大,所以不用擔心這個問題;或者是你也可以單純使用 while + stack 來實作遞迴來避免這個問題。


最後附上 LeetCode 的截圖

100%
O(N)

然後他寫的時間複雜度是 O(N) 但其實這題最最最差的情況是 O(N^2)

(真的很差那種,要像下面這樣)

5
[[3,4],[2,3],[1,2],[0,1],[2,4],[1,3],[0,2],[1,4],[0,3],[0,4]]

漸漸的更新每一個,這樣 1 + 2 + 3 + 4 + ... + N,所以最差是 O(N^2)

不過這題的 N 最大是 500,所以這樣肯定超級快XD




最後更新時間: 2024年11月27日.