# 111 選手班 - 圖論 ###### tags: `宜中資訊` `CP` 2022.08.12 ## 大綱 - 最小生成樹 - prim - kruskal - 最短路 - Bellman-Ford - Dijkstra - Floyd-Warshall - [110 Slide](https://hackmd.io/@Ccucumber12/BJGKlTRkt#/) ## 最小生成樹 ### 問題 給定一張圖 $G(V,E)$ 請找出 $V-1$ 條邊使得 - 形成一棵樹 - 邊權總和最小 ![](https://i.imgur.com/RgYZXxn.png) ### Kruskal #### 想法 - 每次選擇邊權最小的邊 - 如果沒必要拿就忽略 #### 過程 - 將圖 $G$ 上的 $E$ 條邊排序 - 對於每條邊 $(u, v, w)$ - 如果 $u$, $v$ 不在同一個聯通塊 - 把 $w$ 加進答案 - 連接 $u$, $v$ #### 並查集 - 檢查 $u$, $v$ 是否在同一個聯通塊 $\Rightarrow$ 檢查是否在同一個集合 - 連接 $u$, $v$ $\Rightarrow$ 合併兩個集合 #### 證明 - 對於某一個時刻,假設已經選了一個集合 $F$ - 假設現在最小的邊 $e(u, v, w)$ - 如果 $u$, $v$ 不在同一個聯通塊且我們不選他 - $u$, $v$ 會被其他邊($e'$) 連接且 $e'_w > e_w$ - 在最後把 $e$ 加進來會形成一個環且此時可以把 $e'$ 拔掉形成更小的 MST #### 時間複雜度 - 排序: $O(ElogE)$ - DSU: $O(E\alpha(V))$ - Total: $O(ElogE)$ ### Prim - 設任一點當根 - 找到最小的相鄰邊加進解集合 - 重複直到所有邊都被加入 #### Heap - 找到集合中最小的邊 - 把邊加入集合 #### 時間複雜度 - 找集合內最小邊: $O(ElogE)$ - 把邊加入集合: $O(ElogE)$ - 總和: $O(ElogE)$ - Fibonacci Heap優化: $O(VlogV + E)$ ## 最短路 ### 題目 - 給定一張帶權圖(有向 or 無向) - 請找到一個 $S$ 走到 $T$ 的路徑 - 使得路徑權重總和最小 - 型態: - 單源最短路 Single-Source Shortest Path - 全點對最短路 All-Pairs Shortest Path ### 戴克斯特拉演算法 - Dijkstra - 單點源最短路 - 不能有負邊 - 最佳時間複雜度 #### 鬆弛 - 設 $S$ 是目前的最短路集合 - 把所有 $S$ 連出去的邊鬆弛 - 把相鄰最小的點 $v$ 加進 $$ - 重複直到所有點都在 $S$ 內 #### Heap - 找到集合內最小的點 - 把新鬆弛的點加進集合內 - $\implies$ priority_queue #### 證明 - DP - 如果不選擇最小的點 - 之後再過去只會更慢 #### 時間複雜度 - 找到最小: $O(E\log E)$ - 插入最小的點: $O(E\log E)$ - Total: $O(E\log E)$ :::spoiler Code ```c= #include <bits/stdc++.h> using namespace std; vector<pair<int,int>> g[100010] ; // <b, w> const int INF = 0x3f3f3f3f ; int main() { int n, m, s ; cin >> n >> m >> s ; for(int i=0; i<m; ++i) { int a, b, w ; cin >> a >> b >> w ; g[a].push_back(make_pair(b, w)) ; g[b].push_back(make_pair(a, w)) ; } priority_queue<pair<int,int>> pq; // <-dist[i], i> vector<int> dist(n + 1, INF) ; dist[s] = 0 ; for(auto i:g[s]) { // <b, w> pq.push(make_pair(-(dist[s] + i.second), i.first)) ; } while(pq.size() != 0) { auto u = pq.top() ; // <-dist[i], i> pq.pop() ; if(dist[u.second] != INF) continue ; dist[u.second] = -u.first ; for(auto i:g[u.second]) { // <b, w> if(dist[i.first] == INF) pq.push(make_pair(-(dist[u.second] + i.second), i.first)) ; } } for(int i=1; i<=n; ++i) cout << dist[i] << ' ' ; cout << endl ; } ``` ::: ### 貝爾曼-福特演算法 - Bellman-Ford - 單點源最短路 - 支援負邊 - 尋找負環 #### 鬆弛 - 設 $\text{dist}(u)$ 為起點 $S$ 到點 $u$ 的最短路 - $\text{relax}(u,v)$: $\text{dist}(u) = \min(\text{dist}(u), \text{dist}(v) + \text{len}(v,u)$ - 對於每條邊 $(u,v) \rightarrow \text{relax}(u,v)$ - 重複直到無法在鬆弛任何 $dist(u)$ #### 時間複雜度 - 最多鬆弛 $V-1$ 次 - 每次最少會把一個點鬆弛成正解 - 總共只有 $V-1$ 個點要鬆弛 - $O(VE)$ #### 負環 - 第 $V$ 次還是成功鬆弛了某個點 - $\implies$ 存在負環 [負環模板題](https://www.luogu.com.cn/problem/P3385) #### SPFA - 只有相鄰的邊才要有可能需要鬆弛 - $\implies$ queue - $O(E) \sim O(VE)$ ### 弗洛伊德演算法 - Floyd-Warshall - 全點對最短路 - 實作簡單 - 找最小環 #### 鬆弛 - Relax - $\delta(s,t) = s$ 到 $t$ 的距離 - $\delta(s,t) = \min(\delta(s,t),\ \delta(s,i)+\delta(i,t))$ - 利用點 $i$ 進行鬆弛 #### DP - 設 $D_{i,j,k}$ 代表點 $i$ 到點 $j$ 利用前 $k$ 個點進行鬆弛後的最短路。 - 如果經過 $k$: $D_{i,j,k} = D_{i,k,k-1} + D_{k,j,k-1}$ - 如果沒經過 $k$: $D_{i,j,k} = D_{i,j,k-1}$ - $\implies$ $D_{i,j,k} = \min(D_{i,j,k-1}, D_{i,k,k-1} + D_{k,j,k-1})$ #### 簡化 - $dp[i][j][k] = \min(dp[i][j][k-1], dp[i][k][k-1], dp[k][j][k-1])$ - $dp[i][j] = \min(dp[i][j], dp[i][k] + dp[k][j])$ #### 時間複雜度 - $O(V^3)$ - 常數小 #### KIJ ```c= for(int k=1; k<=n; ++k) for(int i=1; i<=n; ++i) for(int j=1; j<=n; ++j) dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]) ; ``` ### 比較 | | Floyd-Warshall | Bellman-Ford | Dijkstra | | -------- | -------------- | ------------------- | ------------- | | Time | $O(V^3)$ | $O(VE)$ | $O(E\log E)$ | | Problem | All Pairs | Single Source | Single Source | | Negative | O | O | X | | Usage | Smallest cycle | Negative cycle | Fast | ## 題單 - [Contest](https://vjudge.net/contest/510093#overview) - Password: `111apcs`