# 求有權圖最短路徑 (Dijkstra's, Bellman-Ford, Floyd Warshall) ## Dijkstra's 演算法 * 介紹影片:https://www.youtube.com/watch?v=JLARzu7coEs --- ### 解釋: :::warning * 假設你有一個有權圖,叫你求一點到其他點的最短距離 * **very important: 權重不能有負的,不然就要用bellmen-ford** * **只能求一點到其他點的最段路徑,否則就要用Floyd-Warshall** ::: 1. 將1到n的距離設成dist[n],並將dist全部設成無限 (dist[1]=0) ![pic 1](https://hackmd.io/_uploads/SkXs0vGCJe.png) * 目標:從目前dist最小的點,不斷更新他附近(沒走過)的點,並且將可更新的點存到 一個將 **距離由小到大** 的priority_queue (來處存dist最小的點) * pq裡有可能有重複的點,則如果此點已經經過的話,則直接跳過 (因為比較小的距離之前就處裡過了) * 若pq是空的就停掉 * (走過的點為綠色,正在更新的點為紅色,無法更新為紫色) 2. A連接B和C,可將dist[2]更新為4,dist[3]更新為8(4<inf, 8<inf) 並將B,C點丟到pq ![pic 2](https://hackmd.io/_uploads/BksjRPGAyg.png) 3. B在pq最上層,所以對B做一樣的事 (因為C的8小於4+11,所以不用更新,也不用加到pq) ![pic 3](https://hackmd.io/_uploads/SJH2RvG01g.png) 4. C在pq的最上方 ![pic 4](https://hackmd.io/_uploads/SJJaAPfCJg.png) 5. F在pq的最上方 ![pic 5](https://hackmd.io/_uploads/Skhp0PGA1e.png) 6. I在pq的最上方 ![pic 6](https://hackmd.io/_uploads/rJ2RCvMRJe.png) 7. D在pq的最上方 ![pic 7](https://hackmd.io/_uploads/rkWyyOGRJl.png) 8. E在pq的最上方 ![pic 8](https://hackmd.io/_uploads/B1S1y_GA1e.png) 9. H在pq的最上方 ![pic 9](https://hackmd.io/_uploads/rJtk1dfRyx.png) 10. J在pq的最上方 ![pic 10](https://hackmd.io/_uploads/ByTJJ_M01l.png) * 答案就會是dist[n] --- ### 題目: #### CSES - Shortest Routes I > https://cses.fi/problemset/task/1671 ```cpp #include <bits/stdc++.h> #define endl '\n' #define maxn 100005 #define inf LLONG_MAX #define int long long using namespace std; int n, m, dist[maxn]; vector<pair<int, int>> f[maxn]; bool vis[maxn]; priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq; //f[i]存{j, i到j的距離} //pq存{0到i到距離, i} signed main(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> n >> m; for (int i=1; i<=m; ++i){ int a, b, c; cin >> a >> b >> c; f[a].push_back({b, c}); } for (int i=1; i<=n; ++i) dist[i] = inf; dist[1] = 0; pq.push({0, 1}); while (!pq.empty()){ int p = pq.top().second; pq.pop(); //我們只需要pq最上面的點,距離是用來sort的 if (vis[p] == 1) continue; //因為有可能有重複的點被放到pq,我們只需要處裡距離最小的, //且pq的距離是由小排到大的,所以我們若造訪過就不用更新旁邊的了 vis[p] = 1; //設為造訪過 for (int i=0; i<f[p].size(); ++i){ int v=f[p][i].first; //要更新的點 int d=f[p][i].second; //現在點到要更新的點的距離 //若沒有參訪過,且新的距離小於原本的 if (vis[v] == 0 && dist[p]+d < dist[v]){ dist[v] = dist[p]+d; //更新dist pq.push({dist[v], v}); //推進pq } } } for (int i=1; i<=n; ++i){ if (i==n) cout << dist[n] << endl; else cout << dist[i] << " "; } } ``` --- #### 求路徑: * 只要更新dist[i]同時,存到i點過來的點,最後從最後一點找回第一點就好了 (剛剛的程式) ```cpp #include <bits/stdc++.h> #define endl '\n' #define maxn 100005 #define inf LLONG_MAX #define int long long using namespace std; int n, m, dist[maxn], path[maxn]; vector<pair<int, int>> f[maxn]; bool vis[maxn]; vector<int> final_path; priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq; signed main(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> n >> m; for (int i=1; i<=m; ++i){ int a, b, c; cin >> a >> b >> c; f[a].push_back({b, c}); } for (int i=1; i<=n; ++i) dist[i] = inf; dist[1] = 0; pq.push({0, 1}); while (!pq.empty()){ int p = pq.top().second; pq.pop(); if (vis[p] == 1) continue; vis[p] = 1; path[1] = 0; for (int i=0; i<f[p].size(); ++i){ int v=f[p][i].first; int d=f[p][i].second; if (vis[v] == 0 && dist[p]+d < dist[v]){ dist[v] = dist[p]+d; path[v] = p; //加這一行 pq.push({dist[v], v}); } } } //輸出1到n點最短距離和路徑 cout << dist[n] << endl; int x=n; //從n往回找 while (x != 0){ //path[1] = 0 final_path.push_back(x); x = path[x]; } //反向的輸出 for (int i=final_path.size()-1; i>=0; --i){ if (i==0) cout << final_path[i] << endl; else cout << final_path[i] << " "; } } ``` --- #### E - Rush Hour 2 > https://atcoder.jp/contests/abc204/tasks/abc204_e :::info ## 求等待時間的最佳值: 假設我們從出發點到一點的時間為 $t+a+C~i~+\lfloor \frac{D~i~}{t+1+a}\ \rfloor$ t = 前面花的時間 a = 等待時間 (要求最佳值的) ### 我們要求的就是 上述函式 的min值 --- 1.先把常數拿掉 $a+\lfloor \frac{D~i~}{t+1+a}\ \rfloor$ 2.floor拿掉(之後處理),讓函式變連續的 $a+ \frac{D~i~}{t+1+a}$ 3.微分求極植 $f(a) = a+ \frac{D~i~}{t+1+a}$ $f(a)' = 1- \frac{D~i~}{(t+1+a)^2}$ $let\ f(a)' = 0$ $\frac{D~i~}{(t+1+a)^2} = 1$ $a = \sqrt{D~i~}-t-1$ --- ### 得出解果: $a = \sqrt{D~i~}-t-1$ 時,時間會是最小的。 ::: :::spoiler 為什麼要微分,power rule,加法律 ### 微分? 因為我們這一題是要找極值,並且$f(a)$微之後只有一個最低點 (凸的), 而導數有點像找$f(a)$每一點的斜率,所以$f(a)'$為0時就是我們要找的答案。 --- ### power rule: $let\ f(x) = x^{n} + 2x^{n-1}$ $then\ f(x)' = nx^{n-1} + 2(n-1)x^{n-2}$ --- ### 加法律: $let\ h(x) = f(x) + g(x)$ $then\ h(x)' = f(x)' + g(x)'$ --- ### 剩下自己看lol: https://m.gamer.com.tw/home/creationDetail.php?sn=4950044 ::: :::success ## 微分小教室:連鎖律 * 假設我們有一個$f(g(x))$要對$x$微分,則先將$f(g(x))$對$g(x)$微分,再將$g(x)$對$x$微分,最後再相乘,就會是答案了: ## $\frac{df(g(x))}{dx} = \frac{df(g(x))}{dg(x)} * \frac{dg(x)}{dx}$ * 以我們上面題目舉例子: 原式: $f(a) = a+ \frac{D~i~}{t+1+a}$ 標示成這樣比較好看: $f(a) = a+ D~i~*(t+1+a)^{-1}$ 先微a:(加法律) $f(a)' = 1+ \frac{d(D~i~*(t+1+a)^{-1})}{dx}$ 再微後面的: $f(a)' = 1+ -D~i~*(t+1+a)^{-2} * 1$ 最後得到答案: $f(a)' = 1- \frac{D~i~}{(t+1+a)^2}$ ::: ```cpp #include <bits/stdc++.h> #define maxn 100005 #define inf LLONG_MAX #define int long long #define double long double #define endl '\n' using namespace std; struct edge{ int v, w, d; } temp; int n, m, dist[maxn]; vector<edge> e[maxn]; bool vis[maxn]; priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq; signed main(){ ios::sync_with_stdio(0); cin.tie(0); cin >> n >> m; for (int i=1; i<=m; ++i){ int a, b, c, d; cin >> a >> b >> c >> d; e[a].push_back({b, c, d}); e[b].push_back({a, c, d}); } for (int i=1; i<=n; ++i) dist[i] = inf; dist[1] = 0; pq.push({0, 1}); while (!pq.empty()){ int v = pq.top().second; pq.pop(); if (vis[v]) continue; vis[v] = 1; for (edge k:e[v]){ int u = k.v, c = k.w, d = k.d, floora = inf; if (vis[u]) continue; double a = sqrt(d)-1; if (a<dist[v]) floora = dist[v]+c+ d/(dist[v]+1); else{ for (int i=a-5; i<=a+5; ++i){ if (i>=dist[v]) floora = min(floora, i+c+ d/(i+1) ); } } if (dist[u] > floora){ dist[u] = floora; pq.push({floora, u}); } } } if (dist[n] == inf) cout << -1 << endl; else cout << dist[n] << endl; } ``` #### TIOJ 2379 > https://tioj.ck.tp.edu.tw/problems/2379 * 太難了 我放棄 :( * 會的教我一下 謝謝 :) --- ## Bellmam-Ford 演算法 ### 解說: * 最短路徑所經過的邊數,最多為 V-1 條 * 並且重複 V-1 次,每次將全部的邊兩點去更新 * (若這次更新沒有任何點被更新,則也代表結束) :::success 偵測負環: 若更新到第 V 次的時候,卻還是有被更新的話,那就代表有負環 ::: --- ### 題目: #### CSES - High Score > https://cses.fi/problemset/task/1673 ```cpp #include <bits/stdc++.h> #define endl '\n' #define maxn 5005 #define int long long #define inf 1e18 using namespace std; struct edge{ int u, v, w; }; int n, m, dist[maxn]; vector<edge> e; bool flag, nag; vector<int> f[maxn], nedge; bool vis[maxn]; bool dfs(int x){ if (x == n) return true; vis[x] = 1; for (int i:f[x]){ if (vis[i]) continue; if (dfs(i)) return true; } return false; } signed main(){ ios::sync_with_stdio(0); cin.tie(0); cin >> n >> m; for (int i=1; i<=m; ++i){ int a, b, c; cin >> a >> b >> c; e.push_back({a, b, -c}); //要找最高的距離,則將輸入變成相反數,找最小的就是答案了 f[a].push_back(b); } for (int i=1; i<=n; ++i) dist[i] = inf; dist[1] = 0; for (int i=1; i<=n; ++i){ flag = false; for (int j=0; j<m; ++j){ if (dist[e[j].u] == inf) continue; if (dist[e[j].u] + e[j].w < dist[e[j].v]){ flag = true; dist[e[j].v] = dist[e[j].u] + e[j].w; if (i==n) nedge.push_back(e[j].v); } } if (!flag) break; if (flag && i==n) nag = true; } if (!nag) cout << -dist[n] << endl; else{ //dfs to check if negative round connects to n bool found=false; for (int i:nedge){ if (dfs(i)){ found = true; break; } } if (found) cout << -1 << endl; else cout << -dist[n] << endl; } } ``` --- #### CSES - Cycle Finding > https://cses.fi/problemset/task/1673 ```cpp #include <bits/stdc++.h> #define endl '\n' #define maxn 2505 #define inf 1e18 #define int long long using namespace std; struct coord{ int u, v, d; }; int n, m, dist[maxn], w[maxn], x; vector<coord> e; bool found; signed main(){ ios::sync_with_stdio(0); cin.tie(0); cin >> n >> m; for (int i=1; i<=m; ++i){ int a, b, c; cin >> a >> b >> c; e.push_back({a, b, c}); } for (int k=1; k<=n; ++k){ for (int i=1; i<=n; ++i) dist[i] = inf; dist[k] = 0; for (int i=1; i<=n; ++i){ bool flag = false; for (int j=0; j<m; ++j){ int u = e[j].u, v = e[j].v, d = e[j].d; if (dist[u] + d < dist[v]){ flag = true; dist[v] = dist[u] + d; w[v] = u; if (i==n) x = v; } } if (!flag) break; if (i==n && flag) found = true; } if (found) break; } if (found){ cout << "YES" << endl; for (int i=1; i<=n; ++i) x = w[x]; //make x go inside the cycle vector<int> cycle; int v = w[x]; cycle.push_back(x); while (v != x){ cycle.push_back(v); v = w[v]; } cycle.push_back(x); reverse(cycle.begin(), cycle.end()); for (int i=0; i<cycle.size(); ++i){ if (i==cycle.size()-1) cout << cycle[i] << endl; else cout << cycle[i] << " "; } }else cout << "NO" << endl; } ``` --- ## Floyd-Warshall 演算法 ### 解說: dis~k,i,j~ = min(dis~k,i,j~, dis~k−1,i,k~ + dis~k−1,k,j~) * 每次多考慮一個可以經過的點 (k) * 將 k 當作中繼點,更新 i 到 j 的距離 --- ### 題目: #### CSES - shortest route II > https://cses.fi/problemset/task/1672 ```cpp #include <bits/stdc++.h> #define endl '\n' #define maxn 505 #define inf 1e18 #define int long long using namespace std; int n, m, q, dist[maxn][maxn]; signed main(){ ios::sync_with_stdio(0); cin.tie(0); cin >> n >> m >> q; //把dist[i][j] 預設到inf for (int i=1; i<=n; ++i){ for (int j=1; j<=n; ++j){ if (i!=j) dist[i][j] = inf; } } //輸入到dist (輸入有可能重複) for (int i=1; i<=m; ++i){ int u, v, w; cin >> u >> v >> w; dist[u][v] = min(dist[u][v], w); dist[v][u] = min(dist[v][u], w); } //主要程式 for (int k=1; k<=n; ++k){ for (int i=1; i<=n; ++i){ for (int j=1; j<=n; ++j){ dist[i][j] = min(dist[i][j], dist[i][k]+dist[k][j]); } } } //i點到j點的最短距離為 dist[i][j] for (int i=1; i<=q; ++i){ int a, b; cin >> a >> b; if (dist[a][b] != inf) cout << dist[a][b] << endl; else cout << -1 << endl; } } ``` #### Ex - Directed Graph and Query > https://atcoder.jp/contests/abc287/tasks/abc287_h ```cpp #include <bits/stdc++.h> #define endl '\n' #define maxn 2005 #define maxq 10005 #define inf 1e9 using namespace std; int n, m, q, ans[maxq]; pair<int, int> p[maxq]; bitset<maxn> bt[maxn]; //bt[i][j] 存是否可以從i到j int main(){ ios::sync_with_stdio(0); cin.tie(0); cin >> n >> m; for (int i=1; i<=m; ++i){ int u, v; cin >> u >> v; bt[u][v] = 1; } cin >> q; for (int i=1; i<=q; ++i){ int a, b; cin >> a >> b; p[i] = {a, b}; //處存要找的點 ans[i] = -1; //default } for (int k=1; k<=n; ++k){ for (int i=1; i<=n; ++i){ if (bt[i][k]) bt[i] |= bt[k]; //若i可以到達k,則k可以到達的i也可以到達 (取or) } for (int i=1; i<=q; ++i){ //試著去更新我們要找的點 if (ans[i] == -1 && bt[p[i].first][p[i].second]){ //若可以到達的話,則去試著更新答案 //因為要找ans[i]的min值 -> k的min值 //因為k是由小到大去找的,所以第一次遇到可以的k最會是k的min值 ans[i] = max({p[i].first, p[i].second, k}); } } } for (int i=1; i<=q; ++i) cout << ans[i] << endl; } ``` --- ok bye