There are N
network nodes, labelled 1
to N
.
Given times
, a list of travel times as directededges times[i] = (u, v, w)
, where u
is the source node, v
is the target node, and w
is the time it takes for a signal to travel from source to target.
Now, we send a signal from a certain node K
. How long will it take for all nodes to receive the signal? If it is impossible, return -1
.
Example 1:
Input: times = [[2,1,1],[2,3,1],[3,4,1]], N = 4, K = 2 Output: 2
Note:
N
will be in the range[1, 100]
.K
will be in the range[1, N]
.- The length of
times
will be in the range[1, 6000]
. - All edges
times[i] = (u, v, w)
will have1 <= u, v <= N
and0 <= w <= 100
.
這道題給了我們一些有向邊,又給了一個結點K,問至少需要多少時間才能從K到達任何一個結點。這實際上是一個有向圖求最短路徑的問題,求出K點到每一個點到最短路徑,然后取其中最大的一個就是需要的時間了。可以想成從結點K開始有水流向周圍擴散,當水流到達最遠的一個結點時,那么其他所有的結點一定已經流過水了。最短路徑的常用解法有迪傑斯特拉算法 Dijkstra Algorithm, 弗洛伊德算法 Floyd-Warshall Algorithm, 和貝爾曼福特算法 Bellman-Ford Algorithm,其中,Floyd 算法是多源最短路徑,即求任意點到任意點到最短路徑,而 Dijkstra 算法和 Bellman-Ford 算法是單源最短路徑,即單個點到任意點到最短路徑。這里因為起點只有一個K,所以使用單源最短路徑就行了。這三種算法還有一點不同,就是 Dijkstra 算法處理有向權重圖時,權重必須為正,而另外兩種可以處理負權重有向圖,但是不能出現負環,所謂負環,就是權重均為負的環。為啥呢,這里要先引入松弛操作 Relaxtion,這是這三個算法的核心思想,當有對邊 (u, v) 是結點u到結點v,如果 dist(v) > dist(u) + w(u, v),那么 dist(v) 就可以被更新,這是所有這些的算法的核心操作。Dijkstra 算法是以起點為中心,向外層層擴展,直到擴展到終點為止。根據這特性,用 BFS 來實現時再好不過了,注意 while 循環里的第一層 for 循環,這保證了每一層的結點先被處理完,才會進入進入下一層,這種特性在用 BFS 遍歷迷宮統計步數的時候很重要。對於每一個結點,都跟其周圍的結點進行 Relaxtion 操作,從而更新周圍結點的距離值。為了防止重復比較,需要使用 visited 數組來記錄已訪問過的結點,最后在所有的最小路徑中選最大的返回,注意,如果結果 res 為 INT_MAX,說明有些結點是無法到達的,返回 -1。普通的實現方法的時間復雜度為 O(V2),基於優先隊列的實現方法的時間復雜度為 O(E + VlogV),其中V和E分別為結點和邊的個數,這里多說一句,Dijkstra 算法這種類貪心算法的機制,使得其無法處理有負權重的最短距離,還好這道題的權重都是正數,參見代碼如下:
解法一:
class Solution { public: int networkDelayTime(vector<vector<int>>& times, int N, int K) { int res = 0; vector<vector<int>> edges(101, vector<int>(101, -1)); queue<int> q{{K}}; vector<int> dist(N + 1, INT_MAX); dist[K] = 0; for (auto e : times) edges[e[0]][e[1]] = e[2]; while (!q.empty()) { unordered_set<int> visited; for (int i = q.size(); i > 0; --i) { int u = q.front(); q.pop(); for (int v = 1; v <= 100; ++v) { if (edges[u][v] != -1 && dist[u] + edges[u][v] < dist[v]) { if (!visited.count(v)) { visited.insert(v); q.push(v); } dist[v] = dist[u] + edges[u][v]; } } } } for (int i = 1; i <= N; ++i) { res = max(res, dist[i]); } return res == INT_MAX ? -1 : res; } };
下面來看基於 Bellman-Ford 算法的解法,時間復雜度是 O(VE),V和E分別是結點和邊的個數。這種算法是基於 DP 來求全局最優解,原理是對圖進行 V - 1 次松弛操作,這里的V是所有結點的個數(為啥是 V-1 次呢,因為最短路徑最多只有 V-1 條邊,所以只需循環 V-1 次),在重復計算中,使得每個結點的距離被不停的更新,直到獲得最小的距離,這種設計方法融合了暴力搜索之美,寫法簡潔又不失優雅。之前提到了,Bellman-Ford 算法可以處理負權重的情況,但是不能有負環存在,一般形式的寫法中最后一部分是檢測負環的,如果存在負環則報錯。不能有負環原因是,每轉一圈,權重和都在減小,可以無限轉,那么最后的最小距離都是負無窮,無意義了。沒有負環的話,V-1 次循環后各點的最小距離應該已經收斂了,所以在檢測負環時,就再循環一次,如果最小距離還能更新的話,就說明存在負環。這道題由於不存在負權重,所以就不檢測了,參見代碼如下:
解法二:
class Solution { public: int networkDelayTime(vector<vector<int>>& times, int N, int K) { int res = 0; vector<int> dist(N + 1, INT_MAX); dist[K] = 0; for (int i = 1; i < N; ++i) { for (auto e : times) { int u = e[0], v = e[1], w = e[2]; if (dist[u] != INT_MAX && dist[v] > dist[u] + w) { dist[v] = dist[u] + w; } } } for (int i = 1; i <= N; ++i) { res = max(res, dist[i]); } return res == INT_MAX ? -1 : res; } };
下面這種解法是 Bellman Ford 解法的優化版本,由熱心網友旅葉提供。之所以能提高運行速度,是因為使用了隊列 queue,這樣對於每個結點,不用都松弛所有的邊,因為大多數的松弛計算都是無用功。優化的方法是,若某個點的 dist 值不變,不去更新它,只有當某個點的 dist 值被更新了,才將其加入 queue,並去更新跟其相連的點,同時還需要加入 HashSet,以免被反復錯誤更新,這樣的時間復雜度可以優化到 O(E+V)。Java 版的代碼在評論區三樓,旅葉聲稱可以 beat 百分之九十多,但博主改寫的這個 C++ 版本的卻只能 beat 百分之二十多,hmm,因缺斯汀。不過還是要比上面的解法二快很多,博主又仔細看了看,發現很像解法一和解法二的混合版本哈,參見代碼如下:
解法三:
class Solution { public: int networkDelayTime(vector<vector<int>>& times, int N, int K) { int res = 0; unordered_map<int, vector<pair<int, int>>> edges; vector<int> dist(N + 1, INT_MAX); queue<int> q{{K}}; dist[K] = 0; for (auto e : times) edges[e[0]].push_back({e[1], e[2]}); while (!q.empty()) { int u = q.front(); q.pop(); unordered_set<int> visited; for (auto e : edges[u]) { int v = e.first, w = e.second; if (dist[u] != INT_MAX && dist[u] + w < dist[v]) { dist[v] = dist[u] + w; if (visited.count(v)) continue; visited.insert(v); q.push(v); } } } for (int i = 1; i <= N; ++i) { res = max(res, dist[i]); } return res == INT_MAX ? -1 : res; } };
討論:最后再來說說這個 Floyd 算法,這也是一種經典的動態規划算法,目的是要找結點i到結點j的最短路徑。而結點i到結點j的走法就兩種可能,一種是直接從結點i到結點j,另一種是經過若干個結點k到達結點j。所以對於每個中間結點k,檢查 dist(i, k) + dist(k, j) < dist(i, j) 是否成立,成立的話就松弛它,這樣遍歷完所有的結點k,dist(i, j) 中就是結點i到結點j的最短距離了。時間復雜度是 O(V3),處處透露着暴力美學。除了這三種算法外,還有一些很類似的優化算法,比如 Bellman-Ford 的優化算法- SPFA 算法,還有融合了 Bellman-Ford 和 Dijkstra 算法的高效的多源最短路徑算法- Johnson 算法,這里就不過多贅述了,感興趣的童鞋可盡情的 Google 之~
Github 同步地址:
https://github.com/grandyang/leetcode/issues/743
參考資料:
https://leetcode.com/problems/network-delay-time/description/
https://leetcode.com/problems/network-delay-time/discuss/109982/C++-Bellman-Ford