> 上次的講義: [Minimum Spanning Tree](/Bp3bb9eYRyyK0TKKbMIHZg) ## 什麼是最短路徑 在圖上,如果我們要從A節點走到B節點,通常不會只有一條路徑 而最短路徑就是**從A到B途中經過的邊權重和最小的路徑** 如下圖所示 ![](https://i.imgur.com/FVDwCqD.png =500x) 從A到B的最短路徑權重總和為 $4$ <!-- 應該要解釋一下負環不存在最短路 --> --- 不過下圖 ![](https://i.imgur.com/oZWvBfQ.png =300x) 左上到右下的點最短路是..5? 等等好像不是欸,是...4? **可以發現若且唯若一張圖存在負環則不存在最短路** ## 如何求最短路徑 要求最短路徑,最直覺的解法就是爆搜 枚舉從A開始的每條邊,直到碰到B為止 再從多條路徑之間找出最小值 i.e. ```cpp= struct edge{ int b, w; }; vector<vector<edge>> e(n); vector<bool> visited(n, false); function<int(int)> dfs = [&] (int cur) { if (visited[cur]) return 1e9; if (cur == b) return 0; int minn = 1e9; for (auto& i : e[cur]) minn = min(minn, dfs(i.b) + i.w); return minn; }; cout << dfs(a) << endl; ``` 但在完全圖中,這個演算法的複雜度高達 $O(V!)$ 就算我們利用前面學過(?)的技巧來減少可能的分支 i.e. ```cpp= struct edge{ int b, w; }; vector<vector<edge>> e(n); vector<bool> visited(n, false); int minpri = 1e9; function<void(int, int)> dfs = [&] (int cur, int d) { if (visited[cur]} || d > minpri) return; if (cur == b) {minpri = min(minpri, d); return;} for (auto& i : e[cur]) dfs(i.b, d + i.w); }; dfs(a, 0); cout << minpri << endl; ``` 還是沒辦法保證複雜度會是好的 勢必需要更快一點的演算法 ## 鬆弛 Relaxation 對於兩點之間的距離 dis(a, b),若存在一點k使得dis(a, k) + dis(k, b) < dis(a, b),我們就稱這過程為鬆弛 這是解決最短路問題裡很核心的概念 ## Floyd-Warshall 有了鬆弛的概念後,就能很容易理解這個最短路演算法了 我們重新看一下鬆弛的式子$$dis[a][b] = min(dis[a][b], dis[a][k] + dis[k][b])$$ 因為對於一個點而言,他最多能被其他|V|-1個點鬆弛,所以我們可以假設dp狀態為$$dp(k, i, j)$$ 代表從i走到j經由前k個節點鬆弛後的結果,那麼我們就能得出轉移式為$$dp(k + 1, i, j) = min\{dp(k, i, j), dp(k, i, k + 1) + dp(k, k + 1, j)\}$$ ```cpp= int dis[V][V]; for(int k = 0; k < V; ++k) for(int i = 0; i < V; ++i) for(int j = 0; j < V; ++j) dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]); ``` Time Complexity: $O(|V|^3)$ ## Bellman-Ford 由於一條最短路的長度不會超過|V|-1,所以我們可以重複|V|-1次對所有邊進行鬆弛 此演算法也可以用來檢查負環,因為一條最短路最多被鬆弛|V|-1次,若且唯若能被鬆弛|V|次則存在負環 ```cpp= struct Edge{ int u, v, w; }; int V, E; vector<Edge> edge(E); void Bellman_Ford(int sourse) { int dis[V]; for(int i = 0; i < V; ++i) dis[i] = inf; // initialize dis[sourse] = 0; // 原點設為0 // Relaxation for(int i = 0; i < V - 1; ++i) for(int j = 0; j < E; ++j) if(dis[edge[j].u] + edge[j].w < dis[edge[j].v]) dis[edge[j].v] = dis[edge[j].u] + edge[j].w; // 檢查負環 for(int i = 0; i < E; ++i) if(dis[edge[i].u] + edge[i].w < dis[edge[i].v]) cout << "A!"; } ``` Time Complexity: $O(|V||E|)$ ## SPFA (Shortest Path Faster Algorithm) SPFA基本上是Bellman-Ford的優化版 可以發現Bellman-Ford都是很無腦的嘗試鬆弛每個點,不過我們知道對於一個點而言,只有在他被鬆弛後,這個點才有可能鬆弛其他點。 因此,SPFA多維護了一個佇列來挑選可能作為鬆弛的點,直到佇列為空就代表鬆弛結束。 ```cpp= #define MEM(x) memset(x, 0, sizeof(x)); // NICE OUO const int INF = 1e9; vector<pair<int, int>> edge[V]; // edge[u] = {{v0, w0}, ...} void SPFA(int src) { int dis[V], cnt[V]; bool inqueue[V]; MEM(cnt); MEM(inqueue); fill(dis, dis + V, INF); dis[src] = 0; queue<int> que; que.push(src); inqueue[src] = 1; while(!que.empty()) { int u = que.top(); que.pop(); inqueue[u] = 0; if(++cnt[u] >= V) { // check negcycle cerr << "GG there's a neg cycle XD"; break; } for(auto [v, w] : edge[u]) if(dis[u] + w < dis[v]) { dis[v] = dis[u] + w; if(!inqueue[v]) inqueue[v] = 1, que.push(v); } } } ``` Average Time Complexity: $O(|E|)$ Worst-Case Time Complexity: $O(|V||E|)$ **雖然期望複雜度是$O(|E|)$,但很容易構造出$O(|V||E|)$的測資,所以還是要把他當$O(|V||E|)$來用** ## Dijkstra 上次上課有學過[Prim](/Bp3bb9eYRyyK0TKKbMIHZg#Prim’s-algorithm) 而Dijkstra的概念和實作都和Prim幾乎一模一樣 差別只在 * **Prim是以到MST的距離做鬆弛** * **Dijkstra是以到起始點的距離作鬆弛** ```cpp= struct edge { int b, w; friend bool operator<(edge a, edge b) { return a.w > b.w; } }; vector<vector<edge>> e(n); vector<int> d(n, 1e9); vector<bool> visited(n, false); priority_queue<edge> pq; pq.emplace(a, d[a] = 0); for (int i = 0; i != n; i++) { int a = ~0u; while (pq.size() && visited[a = pq.top()]) pq.pop(); if(!~a) break; else visited[a] = true; for (const auto& i : e[cur]) if (d[a] + i.w < d[i.b]) pq.emplace(i.b, d[i.b] = d[a] + i.w); } ``` --- ~~我想複習一下dijkstra 所以再多寫一個講解XD~~ ~~沒我好累~~