# 多源最短路徑(All pairs shortest paths/ multiple source shortest paths) 在Dijkstra後,應該了解了圖論的基本內容,所以就能夠繼續延伸幾個常見的圖論相關演算法。 更準確來說Dijkstra是<span style="color:red;">單源最短路徑</span>,也就是說每次使用Dijkstra,通常都是針對<span style="color:red;">某個起點</span>,那如果我們想要一次"處理多個起點和終點"呢? 所以可以簡單歸類為以下幾種 1. single source shortest path without negative cycle 2. multiple source shortest path without negative cycle (All pairs) 3. single source shortest path with negative cycle 那為甚麼沒有"全對全"有負環? ## 核心概念只有五行的演算法 - Floyd-Warshall 演算法 ![image](https://hackmd.io/_uploads/rJxzBVwOgx.png) 我們現在要找的是任意兩個點之間的最短路徑,也稱作「多源最短路徑」。 先將圖的資訊存到二維矩陣中。 | | 1 | 2 | 3 | 4 | |---|----|----|----|----| | 1 | 0 | 2 | 6 | 4 | | 2 | ∞ | 0 | 3 | ∞ | | 3 | 7 | ∞ | 0 | 1 | | 4 | 5 | ∞ | 12 | 0 | 而這邊就要提到一個很重要的概念-<span style="color:red;">鬆弛</span>了 如果我們要求任意兩個點的最短距離,但有些點不能直達,勢必要有經過中間點才行。 我們來看看以下的分析: ``` 4 → 3 為 12 4 → 1 → 3 為 11 4 → 1 → 2 → 3 為 10 ``` - 這表示當兩點之間沒有經過第三點時,兩點間的初始距離就是最短路徑。 - 但若有經過中轉,可能不只一個,能讓總路徑變得更短。 我們只要透過多次的中轉,和初始的路徑比較,如果能讓兩點之間的路徑變短,就更新。好比 4 到 3 的原始距離為 12,4 到 1 的距離為 5,加上 1 到 3 的距離為 6 共 11,比原始 12 小,我們就讓二為矩陣中 4 到 3 的距離更新為 11 。因為我們只看最短路徑,不討論最少的中轉次數,所以可以這麼做。 所以只要「逐步嘗試把每個點 k 當作中繼點,看看經過它會不會更短」 在嘗試全部點後,得出的肯定就是最短路徑。 先來看以下程式碼 ```clike FloydWarshallAlgorithm(){ for k in 0 to n - 1{ for i in 0 to n - 1{ for j in 0 to n - 1{ if (distance[i][j] > distance[i][k] + distance[k][j]){ distance[i][j] = distance[i][k] + distance[k][j] }}}}} ``` 最外層就表示 - 表示「現在嘗試的中繼點」 - 第 k 次迭代後,代表我們已經考慮過 <span style="color:red;"> 0 ~ k </span> 這些點,看看能不能經過它們讓距離變短。 我們就來練習看看吧 [Leetcode 1334. Find the City With the Smallest Number of Neighbors at a Threshold Distance](https://leetcode.com/problems/find-the-city-with-the-smallest-number-of-neighbors-at-a-threshold-distance/description/) ```CPP class Solution { public: int findTheCity(int n, vector<vector<int>>& edges, int distanceThreshold) { vector<vector<int>> dist(n, vector<int>(n, INT_MAX)); // 自己到自己的距離 for (int i = 0; i < n; i++) dist[i][i] = ; // TODO for (int i = 0; i < edges.size(); i++) { int u = edges[i][0], v = edges[i][1], w = edges[i][2]; dist[u][v] = ; // TODO dist[v][u] = ; // TODO } // Floyd–Warshall for (int k = 0; k < n; k++) { for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { // 如果使用中繼點的路線比目前的還小,就要更新 if () { // TODO } } } } /* 現在你已經有了所有的最短路徑矩陣了,只要遍歷全部的起點i, 計算每個起點在distanceThreshold能達到的城市數量,更新 最小值就能返回了 */ int bestCity = -1, bestCnt = INT_MAX; // TODO return bestCity; } }; ``` 課後練習 [Leetcode 1462. Course Schedule IV](https://leetcode.com/problems/course-schedule-iv/description/?envType=problem-list-v2&envId=9idenloe) ## Bellman-Ford Algorithm 然而Dijkstra不是萬能的,在處理負邊時就會遇到問題,我們來思考以下圖片如果用Dijkstra會怎麼更新,假設從0開始遍歷所有點 ![image](https://hackmd.io/_uploads/rkX7H3tFxl.png) ```javascript Dijkstra(G, w, s): # G: graph, w(u,v): edge weight, s: source for each vertex v in G.V: dist[v] ← ∞ prev[v] ← NIL dist[s] ← 0 Q ← all vertices in G # min-priority queue (by dist) while Q is not empty: u ← Extract-Min(Q) # vertex with smallest dist for each neighbor v of u: if dist[v] > dist[u] + w(u, v): dist[v] ← dist[u] + w(u, v) prev[v] ← u Decrease-Key(Q, v, dist[v]) return dist[], prev[] ``` 首先一開始會將所有點都設為無窮大 | 節點 | 0 | 1 | 2 |-------------|---|---|--- | **Step 0** | 0 | ∞ | ∞ 接著Dijkstra更新完這個點的所有邊後,每次都是去遍歷下一個距離最小的點,所以下一個會是從2出發 | 節點 | 0 | 1 | 2 |-------------|---|---|--- | **Step 0** | 0 | 7 | 5 由於2沒有相鄰節點因此不更新 | 節點 | 0 | 1 | 2 |-------------|---|---|--- | **Step 0** | 0 | 7 | 5 ![image](https://hackmd.io/_uploads/SykCMhtYge.png) 而bellman-ford algorithm如下 ![image](https://hackmd.io/_uploads/BktJfTtKel.png) ![image](https://hackmd.io/_uploads/r1v7GTtYex.png) ![image](https://hackmd.io/_uploads/rJ4UMaYKge.png)