:+1: [2020 競技程設教材 HackMD Book](https://hackmd.io/@nckuacm/ryLIV6BYI) :+1: 2019 Week 11: Graph = 本主題會用到[第四週教材](https://hackmd.io/s/SJ7nxwRL4#Graph)的圖與樹的術語 > 術語不熟的話,底下介紹的都看不懂了 圖論最為重要觀念就是 **"連通"**, 若說此圖為連通圖,則所有點能透過邊接到任一點 (**弱連通**)[^1]。 而不只接上還能**走過去**[^directed_edge],則為**強連通**。 例如: ```graphviz digraph { 1 -> 2 -> 4; 1 -> 3 -> 4; } ``` 上圖弱連通但非強連通。 若是這樣: ```graphviz digraph { 1 -> 2 -> 4; 1 -> 3 -> 4; 4 -> 1; } ``` 則上圖為強連通。 # 點與邊的表示方式 ==以下兩個結構體,將成為本篇的慣例== - 點 ```cpp struct node { int id, w; // id := 點編號, w := 連結到此點的權重 (邊權重) bool operator<(const node &rhs) const { return w > rhs.w; // 使 priority_queue 為 min heap } }; ``` - 邊 ```cpp struct edge { int u, v, w; // u := 起端編號, v := 終端編號, w := 權重 bool operator<(const edge &rhs) const { return w > rhs.w; // 使 priority_queue 為 min heap } }; ``` 討論圖的邊,常會有 $u$ 是邊起點與 $v$ 是邊終點的慣例用符 ```graphviz digraph { rankdir="LR"; u -> v; } ``` # Minimum spanning tree (MST) 最小生成樹 (Minimum spanning tree) 簡稱 MST。 給定圖 $G=(E, V)$,使用 $E$ 子集連結所有點 (來自 $V$) 所得到的樹為**生成樹**。 > $E$ 為邊集合,$V$ 為點集合 若帶權重圖的某生成樹為所有生成樹中**權重總和最小**,則稱此為**最小生成樹** ![](https://i.imgur.com/wgUP8c1.png) [^min_span_tree] :::info 給定一個連通圖,找出一個最小生成樹 ::: ## Kruskal 演算法 Kruskal 用了兩個重要的觀念: - 樹是**無環**的**連通**圖 - 若圖中只有點沒有任何邊,那麼每個點都是一個獨立的**連通塊**。 >這有什麼可利用的呢? 若將一點連向另一點,整張圖就少了一個連通塊, 反覆操作,最終圖上只會有一個連通塊,若最終連通塊**沒有環**,那他就是樹,他就是此圖的**生成樹**。 於是在連通塊與連通塊相連這個動作,確保產生的連通塊無環就行: 連通塊 $A$ 想與連通塊 $B$ 相連,只要 $A \not= B$,那麼相連的新連通塊**保證無環**。 >怎樣產生出**最小**生成樹? 所以若在 $A$ 與 $B$ 相連時**每次**考慮**最小權重的邊**,那最終產生的一定是最小權重的生成樹 >沒錯哦,這是最優子結構 ![](https://i.imgur.com/oQ90KRq.gif) [^kruskaldemo] 實作上先將每個邊依照權重排序, ```cpp vector<edge> E; : . sort(E.begin(), E.end(), [&](edge x, edge y) { return x.w < y.w; }); ``` 接著用 DFS 判斷塊與塊是否為同個連通塊,若不是的話就能相連起來。 DFS 每次都將連通塊的邊遍歷,複雜度為 $O(|E|^2)$ 還記得 [Union-Find Forest](https://hackmd.io/s/SJ7nxwRL4#Union-Find-Forest-%E4%BD%B5%E6%9F%A5%E6%A3%AE%E6%9E%97) 嗎?**分類問題**包括連通塊的分類也能使用它: ```cpp for (auto e: E) { int a = Find(e.u), b = Find(e.v); if (a != b) { Union(e.u, e.v); cost += e.w; MST.push_back({u, v, w}); } } ``` 複雜度約為 $O(|E|\log |E|+|E|\cdot \alpha)$ 其中 $\alpha = \alpha(|E|)$ 為 Union-Find Forest 的時間成本 (通常很小) #### 範例 [UVa OJ 10369 Arctic Network](https://uva.onlinejudge.org/external/103/10369.pdf): 很直覺的,使 outpost 都能通訊的簡單方法,就是做出樹 畢竟若兩點已能通訊,加入一條形成環的邊不會更好。 而想使 D 下降的方法就是將 network 上的 distance 盡量小,也就是要求最小生成樹 而生成樹上較大的邊就用 satellite channel 來解決 先把 outpost 之間的距離算出來: ```cpp vector<edge> E; for(int v = 0; v < P; v++) { scanf("%lf%lf", &C[v].x, &C[v].y); for(int u = 0; u < v; u++) E.push_back({u, v, dist(C[u], C[v])}); } ``` 接著將最小生成樹 (network) 上最大的距離找出來: ```cpp sort(E.begin(), E.end(), [&](edge A, edge B) { return A.dist < B.dist; }); vector<double> D; for(edge e: E) { int a = Find(e.u), b = Find(e.v); if(a != b) { D.push_back(e.dist); Union(e.u, e.v); } } printf("%.2lf\n", D[P-S-1]); ``` #### 練習: [TIOJ 1211 圖論 之 最小生成樹 ](https://tioj.ck.tp.edu.tw/problems/1211) [AIZU 1280 Slim Span](http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1280) [NCPC 2018 Final Problem E Connecting Cities](https://e-tutor.itsa.org.tw/e-Tutor/mod/programming/view.php?id=45110) [TIOJ 1445 機器人組裝大賽](https://tioj.ck.tp.edu.tw/problems/1445) [TIOJ 1795 咕嚕咕嚕呱啦呱啦](https://tioj.ck.tp.edu.tw/problems/1795) --- ## Prim 演算法 Prim 維護一個**未完成的生成樹** 其精神是:每次將樹**周遭**有**最小權重**的邊接到樹上,使樹最終**成長**至最小生成樹 > 傑出的一手貪心策略 ![](https://i.imgur.com/vaV44e5.gif)[^primdemo] Prim 用了兩個重要的觀念: - 樹是**無環**的**連通**圖 - 若圖中只有點沒有任何邊,那麼每個點都是一個獨立的**連通塊**。 > 跟 Kruskal 一樣呢 首先隨便找任意點,作為那個初始的"未完成的生成樹",也就是一塊無環的連通圖 $A$ **每次**將 $A$ 能觸及到的**最小權重**的邊接上 $A$,那最終產生的一定是最小權重的生成樹 >跟 Kruskal 不同的是,這裡枚舉的是"*點*" ```cpp vector<node> E[maxn]; // maxn 為最大節點數 : . priority_queue<edge> Q; // 每次挑最小權重的邊 Q.push({1, 1, 0}); // 初始的生成樹 (只有一個點) while (!Q.empty()) { edge e = Q.top(); Q.pop(); int u = e.v; if (vis[u]) continue; // 避免出現環 vis[u] = true; cost += e.w; MST.push_back(e); for (auto v: E[u]) if(!vis[v.id]) Q.push({u, v.id, v.w}); } ``` 複雜度約 $O(|E|\log_2 |V|)$ #### 練習: [UVa OJ 908 Re-connecting Computer Sites](https://uva.onlinejudge.org/external/9/908.pdf) [ACM-ICPC 2005 Southwestern 3505 Buy or Build](https://icpcarchive.ecs.baylor.edu/external/35/3505.pdf) # A* 搜尋演算法 是路徑搜尋演算法的一個抽象觀點 A* 在節點 $n$ 有個評估函數 $f(n) = g(n) + h(n)$ 當 $f(n)$ 大於**門檻**時就剪枝 $g(n)$ 表示從起點到節點 $n$ 的成本 $h(n)$ 表示從節點 $n$ 到終點的成本 >評估函數是根據欲求解問題而設計,討論上是相當自由的 例如: - 當 $g(n)$ 是搜尋深度(或最短路步數), 即 Breadth-first Search 是 A* 的特例 - 有種搜尋法叫做 Best-first Search, $h(n)$ 為該節點 $n$ 到終點的歐幾里得距離 ## Iterative Deepening A* (IDA*) IDA* 是種 A* 的變形,會逐步地將門檻變寬 #### 範例 [AIZU 1271 Power Calculus](http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1271): 直覺的,將**所有**羃次 (`p`) 的組合全部都**試試看**,找出**最小**的操作數 (operations) 就行: ```cpp int ans = n; // 操作數至多為 n void dfs(int d) { if (p[d] == n) ans = min(ans, d); if (d == n-1) return; for (int i = 0; i <= d; i++) { p[d+1] = p[d] + p[i]; dfs(d+1); p[d+1] = p[d] - p[i]; dfs(d+1); } } ``` 執行前先設定邊界(當還未進行操作): ```cpp p[0] = 1; ``` 複雜度為 $O(n^n)$ > 複雜度頗高, $n=10$ 的時候就能感受到輸出遲緩了 解答也許在深度**還很小**的時候就已經能找到了,所以將深度給定一個上限 (`ops`): ```cpp bool dfs(int d) { // d := depth if(p[d] == n) return true; if(d == ops) return false; for(int i = 0; i <= d; i++) { p[d+1] = p[d] + p[i]; if(dfs(d+1)) return true; p[d+1] = p[d] - p[i]; if(dfs(d+1)) return true; } return false; } ``` 接著每次將深度/操作數 (`ops`) 的上限逐步提高: ```cpp while(scanf("%d", &n) && n) { for(ops = log2(n);; ops++) if(dfs(0)) break; printf("%d\n", ops); // 印出題目要求答案 } ``` 但這題還不只如此,除了 IDA* 門檻設定以外,還得額外剪枝 當剩餘的操作數**絕不可能**湊出目標羃次 (`n`) 時,就不繼續以此狀態深入搜尋: ```cpp int mx = 0; for(int i = 0; i <= d; i++) mx = max(mx, p[i]); if((mx << (ops-d)) < n) return false; ``` #### 練習: [UVa OJ 11212 Editing a Book](https://uva.onlinejudge.org/external/112/11212.pdf) [UVa OJ 12558 Egyptian Fractions (HARD version)](https://uva.onlinejudge.org/external/125/12558.pdf) [UVa OJ 1343 The Rotation Game](https://uva.onlinejudge.org/external/13/1343.pdf) [UVa OJ 11846 Finding Seats Again](https://uva.onlinejudge.org/external/118/11846.pdf) \* [UVa OJ 11376 Tilt!](https://uva.onlinejudge.org/external/113/11376.pdf) # Single-Source Shortest Paths 單源最短路徑 (Single-Source Shortest Paths) 簡稱 SSSP。 源點 (Source),就是**起點**,通常問題都只是簡單求每條路徑的最小成本。 :::info 求單源點到其他所有節點的最短路徑總權重 ::: ## Relaxation 想像自己在圖中是某一點 `v`,你看到你的鄰點們 `u`,他們告訴你:源點走到他們那裡的*最小花費成本* `cost[u]` 是多少,同時你也知道那些點們要過來你這分別要花費多少成本 `w` (i.e. 邊權重) 考慮源點到自己這的最小成本 `cost[v]` 得知: ```cpp int update = cost[u] + w; if (update < cost[v]) cost[v] = update; ``` ## Dijkstra's Algorithm Dijkstra's Algo 可看作是 BFS 的帶權重版本 >BFS (Breadth-first Search) 的詳細流程複習[第四週教材](https://hackmd.io/s/SJ7nxwRL4#BFS) > 狀態 $S(n)$ 代表**從源點**到 $n$ 的最小成本 顯然若 $(u, v) \in E$,則 $S(v) \leq S(u) + \text{weight}(u,v)$ 所以狀態轉移方程為 $$S(v) = \min_{(u, v) \in E}(S(u) + \text{weight}(u,v))$$ > 這就是 relaxation 邊界 (源點到源點最小成本) $$S(\text{source}) = 0$$ 實作上,每次欲更新 $S(v)$ 需從**已解**的 $S(u)$ 透過 $S(u) + \text{weight}(u, v)$ 做 relaxation 而不斷更新 $S(v)$,終有一刻 $S(v)$ 將**得到最優解**。此刻就是當 $S(v)$ *小於所有**未解**的* $S(p)$ 時 > 這裡 $p$ 是還未得到最優解的點 > 第十一週投影片有更進一步的解釋 ![](https://i.imgur.com/GEqS8q2.gif)[^dijkstrademo] ```cpp vector<node> E[maxn]; // 邊集合 ``` 初始源點到任意點的總成本未知,於是將之設為無限大[^unknown-cost] ```cpp memset(s, 0x3f, sizeof(s)); // 源點到任意點的成本初始為無限大 priority_queue<node> Q; // 為從未解集挑出已解的狀態 Q.push({source, s[source] = 0}); ``` 接著找出源點到任意點的最短路徑,其就是 BFS 並搭配 Relaxation ```cpp while (!Q.empty()) { node u = Q.top(); Q.pop(); for (node v: E[u.id]) { // v := 鄰點 int update = u.w + v.w; // 源點到 u 的最小成本 + u 到 v 的成本 if (update < s[v.id]) Q.push({v.id, s[v.id] = update /* relaxation */}); } } ``` 其複雜度為 $O(E\log_2 V)$ 慢著!演算法的終止條件是 `Q.empty() == true`, 也就是說,==若有個邊權重為**負的**==,則 `Q.push({v.id, update});` ==將無盡的執行下去==。 > Bellman's Algo 會提供解決之道 #### 範例 [POJ 3255 Roadblocks](http://poj.org/problem?id=3255): 對於點 `v` 每次將比最短路 `d1[v]` 還大的更新值 `ud` 交給次短路 `d2[v]` 去做 relaxation ```cpp priority_queue<node> Q; int d1[maxn], d2[maxn]; memset(d1, 0x3f, sizeof d1); memset(d2, 0x3f, sizeof d2); Q.push({1, d1[1] = 0}); while(!Q.empty()) { node u = Q.top(); Q.pop(); for(node v: E[u.id]) { int ud = u.w + v.w; // ud := update if(ud < d1[v.id]) { Q.push({v.id, ud}); swap(d1[v.id], ud); // 舊 d1[v.id] 要交給 d2[v.id] 做 relaxation } if(ud < d2[v.id] && ud > d1[v.id]) Q.push({v.id, d2[v.id] = ud}); } } ``` #### 練習: [UVa OJ 10986 Sending email](https://uva.onlinejudge.org/external/109/10986.pdf) [UVa OJ 10801 Lift Hopping](https://uva.onlinejudge.org/external/108/10801.pdf) ## Bellman-Ford's Algorithm 步驟有兩個: 1. 對每一個邊做 relaxation 2. 重複步驟 $|V|-1$ 次 ```cpp vector<edge> E; : . memset(s, 0x3f, sizeof(s)); // 源點到任意點的成本初始為無限大 s[source] = 0; for (int i = 0; i < V.size(); i++) // 共 V.size() 個點 for (edge e: E) s[e.v] = min(s[e.v], s[e.u] + e.w); ``` > 找單源最短路徑就是這麼簡單 :+1: ![](https://i.imgur.com/VkQ5U6p.gif) Bellman-Ford's Algo 可以簡單處理權重是**負的**情況: - 對每邊做至多 $|V|-1$ 次 relaxation 後, - 要是某邊還能 relaxation,就表示有個負權重的邊能使路徑**成本一直降低**。 #### 範例 [UVa OJ 558 Wormholes](https://uva.onlinejudge.org/external/5/558.pdf): 先做 $|V|-1$ 次的每邊 relaxation ```cpp fill(s, s+n, INF); s[0] = 0; // source for(int i = 0; i < n-1; i++) for(int j = 0; j < m; j++) s[y[j]] = min(s[y[j]], s[x[j]] + t[j]); // relaxtion ``` 接著就能判斷是否能回到過去 ```cpp for(int j = 0; j < m; j++) if(s[x[j]] + t[j] < s[y[j]]) return true; // 還能做 relaxation return false; ``` #### 練習: [POJ 2387 Til the Cows Come Home](http://poj.org/problem?id=2387) [UVa OJ 10000 Longest Paths](https://uva.onlinejudge.org/external/100/10000.pdf) ## Shortest Path Fast Algorithm (SPFA) Bellman-Ford’s Algorithm 的進化版本, 其動機是: 若邊被 relax 過,則它的**鄰點**可以直接進行 relaxation ```cpp memset(s, 0x3f, sizeof(s)); s[source] = 0; memset(in_que, false, sizeof in_que); // in_que := is it in queue? queue<int> Q; Q.push(source); in_que[source] = true; while (!Q.empty()) { int u = Q.front(); Q.pop(); in_que[u] = false; for (node v: E[u]) if (s[v.id] > s[u] + v.w) { s[v.id] = s[u] + v.w; // relaxation if (!in_que[v.id]) Q.push(v.id), in_que[v.id] = true; // 下次直接讓 v.id 的鄰點做 relaxation } } ``` > 長得非常像 Dijkstra’s Algo 最壞複雜度 $O(|V|\cdot |E|)$ >平均而言 $O(k \cdot |E|)$, SPFA 提出者認為 $k$ 很小:accept: #### 練習: [UVa OJ 658 It’s not a Bug, it’s a Feature!](https://uva.onlinejudge.org/external/6/658.pdf) # All-Pairs Shortest Paths 全點對最短路徑 (All-Pairs Shortest Paths) 簡稱 APSP。 顧名思義,可以得知所有點與點之間的最短路徑,同樣的一般只需求最小花費成本而已。 ## Floyd-Warshall's Algorithm 設定狀態 $S(i, j, k)$ 為 $i$ 到 $j$ 只許以 $\{1, .., k\}$ 為**中間點**的最小總成本 而 $i$ 到 $j$ 間 $$ S(i, j, k) = \begin{cases} S(i, \underline{k}, k-1) + S(\underline{k}, j, k-1) &\text{若有經過點 } k \\ S(i, j, k-1) &\text{若不經過點 } k \end{cases} $$ 所以狀態轉移方程: $$S(i, j, k) = \min(S(i, j, k-1),S(i, k, k-1) + S(k, j, k-1))$$ 邊界: $$ S(i, j, 0) = \begin{cases} 0 &\text{if } i = j \\ \text{weight}(i, j) &\text{if } (i, j) \in E \\ \infty &\text{if } (i, j) \notin E \end{cases} $$ 經由**滾動陣列**優化方法,可使狀態三維降至二維: > 同學自行研究,這很簡單的 ```cpp int s[maxn][maxn]; for (int i = 1; i <= N; i++) for (int j = 1; j <= N; j++) s[i][j] = G[i][j]; for (int k = 1; k <= N; k++) for (int i = 1; i <= N; i++) for (int j = 1; j <= N; j++) s[i][j] = min(s[i][j], s[i][k] + s[k][j]); ``` #### 範例 [UVa OJ 117 The Postal Worker Rings Once](https://uva.onlinejudge.org/external/1/117.pdf): 根據[歐拉圖](https://zh.wikipedia.org/wiki/%E4%B8%80%E7%AC%94%E7%94%BB%E9%97%AE%E9%A2%98),若有兩點為 odd degree,則終點與卡車並非同一點 也就是說,遇到這個狀況需要算出送件終點與卡車間的最短距離 因為**輸入規模很小**,直接使用 Floyd-Warshall’s Algo: > Floyd-Warshall’s Algo 很好寫 ```cpp #include<bits/stdc++.h> using namespace std; int const maxn = 27; string s; int G[maxn][maxn], deg[maxn]; int main() { while(1) { memset(deg, 0, sizeof deg); memset(G, 0x3f, sizeof G); for(int u = 0; u < maxn; u++) G[u][u] = 0; int dist = 0; while(cin >> s && s != "deadend") { int u = s.front() - 'a', v = s.back() - 'a'; dist += s.length(); G[u][v] = G[v][u] = min(G[v][u], (int)s.length()); deg[u]++, deg[v]++; } if(cin.eof()) return 0; vector<int> odd; for(int u = 0; u < maxn; u++) if(deg[u] & 1) odd.push_back(u); if(!odd.empty()) { for(int k = 0; k < maxn; k++) for(int u = 0; u < maxn; u++) for(int v = 0; v < maxn; v++) G[u][v] = min(G[u][v], G[u][k] + G[k][v]); dist += G[odd[0]][odd[1]]; } cout << dist << endl; } return 0; } ``` #### 練習: [CODEFORCES 320B Ping-Pong (Easy Version)](https://codeforces.com/contest/320/problem/B) [TIOJ 1096 E.漢米頓的麻煩](https://tioj.ck.tp.edu.tw/problems/1096) [Google Code Jam Kickstart Round F 2018 B Specializing Villages](https://code.google.com/codejam/contest/3314486/dashboard#s=p1): Small dataset [UVa OJ 10724 Road Construction](https://uva.onlinejudge.org/external/107/10724.pdf) [UVa OJ 125 Numbering Paths](https://uva.onlinejudge.org/external/1/125.pdf) [Google Code Jam Round 1B 2017 C Pony Express](https://code.google.com/codejam/contest/8294486/dashboard#s=p2) [^1]: 即任意兩點必有至少一條道路 [^unknown-cost]: 也能設為其他具代表性的數字作為未知的總成本 [^directed_edge]: u -> v 表示 u 能**走過去**到 v; 而 v 不能走到 u [^min_span_tree]: [Wikipedia/ Minimum_spanning_tree.svg](https://en.wikipedia.org/wiki/Minimum_spanning_tree#/media/File:Minimum_spanning_tree.svg) [^kruskaldemo]: [Wikipedia/ A demo for Kruskal's algorithm based on Euclidean distance.](https://en.wikipedia.org/wiki/Kruskal%27s_algorithm#/media/File:KruskalDemo.gif) [^primdemo]: [Wikipedia/ A demo for Prim's algorithm based on Euclidean distance.](https://en.wikipedia.org/wiki/Prim%27s_algorithm#/media/File:PrimAlgDemo.gif) [^dijkstrademo]: [Wikipedia/ Dijkstra's algorithm runtime](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm#/media/File:Dijkstra_Animation.gif)