本主題會用到第四週教材的圖與樹的術語
術語不熟的話,底下介紹的都看不懂了
圖論最為重要觀念就是 "連通",
若說此圖為連通圖,則所有點能透過邊接到任一點 (弱連通)[1]。
而不只接上還能走過去[2],則為強連通。
例如:
上圖弱連通但非強連通。
若是這樣:
則上圖為強連通。
以下兩個結構體,將成為本篇的慣例
struct node {
int id, w; // id := 點編號, w := 連結到此點的權重 (邊權重)
bool operator<(const node &rhs) const {
return w > rhs.w; // 使 priority_queue 為 min heap
}
};
struct edge {
int u, v, w; // u := 起端編號, v := 終端編號, w := 權重
bool operator<(const edge &rhs) const {
return w > rhs.w; // 使 priority_queue 為 min heap
}
};
討論圖的邊,常會有
最小生成樹 (Minimum spanning tree) 簡稱 MST。
給定圖
為邊集合, 為點集合
若帶權重圖的某生成樹為所有生成樹中權重總和最小,則稱此為最小生成樹
給定一個連通圖,找出一個最小生成樹
Kruskal 用了兩個重要的觀念:
這有什麼可利用的呢?
若將一點連向另一點,整張圖就少了一個連通塊,
反覆操作,最終圖上只會有一個連通塊,若最終連通塊沒有環,那他就是樹,他就是此圖的生成樹。
於是在連通塊與連通塊相連這個動作,確保產生的連通塊無環就行:
連通塊
怎樣產生出最小生成樹?
所以若在
沒錯哦,這是最優子結構
實作上先將每個邊依照權重排序,
vector<edge> E;
:
.
sort(E.begin(), E.end(), [&](edge x, edge y) { return x.w < y.w; });
接著用 DFS 判斷塊與塊是否為同個連通塊,若不是的話就能相連起來。
DFS 每次都將連通塊的邊遍歷,複雜度為
還記得 Union-Find Forest 嗎?分類問題包括連通塊的分類也能使用它:
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});
}
}
複雜度約為
其中
很直覺的,使 outpost 都能通訊的簡單方法,就是做出樹
畢竟若兩點已能通訊,加入一條形成環的邊不會更好。
而想使 D 下降的方法就是將 network 上的 distance 盡量小,也就是要求最小生成樹
而生成樹上較大的邊就用 satellite channel 來解決
先把 outpost 之間的距離算出來:
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) 上最大的距離找出來:
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 圖論 之 最小生成樹
AIZU 1280 Slim Span
NCPC 2018 Final Problem E Connecting Cities
TIOJ 1445 機器人組裝大賽
TIOJ 1795 咕嚕咕嚕呱啦呱啦
Prim 維護一個未完成的生成樹
其精神是:每次將樹周遭有最小權重的邊接到樹上,使樹最終成長至最小生成樹
傑出的一手貪心策略
Prim 用了兩個重要的觀念:
跟 Kruskal 一樣呢
首先隨便找任意點,作為那個初始的"未完成的生成樹",也就是一塊無環的連通圖
每次將
跟 Kruskal 不同的是,這裡枚舉的是"點"
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});
}
複雜度約
UVa OJ 908 Re-connecting Computer Sites
ACM-ICPC 2005 Southwestern 3505 Buy or Build
是路徑搜尋演算法的一個抽象觀點
A* 在節點
當
評估函數是根據欲求解問題而設計,討論上是相當自由的
例如:
IDA* 是種 A* 的變形,會逐步地將門檻變寬
直覺的,將所有羃次 (p
) 的組合全部都試試看,找出最小的操作數 (operations) 就行:
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);
}
}
執行前先設定邊界(當還未進行操作):
p[0] = 1;
複雜度為
複雜度頗高,
的時候就能感受到輸出遲緩了
解答也許在深度還很小的時候就已經能找到了,所以將深度給定一個上限 (ops
):
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
) 的上限逐步提高:
while(scanf("%d", &n) && n) {
for(ops = log2(n);; ops++) if(dfs(0)) break;
printf("%d\n", ops); // 印出題目要求答案
}
但這題還不只如此,除了 IDA* 門檻設定以外,還得額外剪枝
當剩餘的操作數絕不可能湊出目標羃次 (n
) 時,就不繼續以此狀態深入搜尋:
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
UVa OJ 12558 Egyptian Fractions (HARD version)
UVa OJ 1343 The Rotation Game
UVa OJ 11846 Finding Seats Again
* UVa OJ 11376 Tilt!
單源最短路徑 (Single-Source Shortest Paths) 簡稱 SSSP。
源點 (Source),就是起點,通常問題都只是簡單求每條路徑的最小成本。
求單源點到其他所有節點的最短路徑總權重
想像自己在圖中是某一點 v
,你看到你的鄰點們 u
,他們告訴你:源點走到他們那裡的最小花費成本 cost[u]
是多少,同時你也知道那些點們要過來你這分別要花費多少成本 w
(i.e. 邊權重)
考慮源點到自己這的最小成本 cost[v]
得知:
int update = cost[u] + w;
if (update < cost[v]) cost[v] = update;
Dijkstra's Algo 可看作是 BFS 的帶權重版本
BFS (Breadth-first Search) 的詳細流程複習第四週教材
狀態
顯然若
所以狀態轉移方程為
這就是 relaxation
邊界 (源點到源點最小成本)
實作上,每次欲更新
而不斷更新
這裡
是還未得到最優解的點
第十一週投影片有更進一步的解釋
vector<node> E[maxn]; // 邊集合
初始源點到任意點的總成本未知,於是將之設為無限大[7]
memset(s, 0x3f, sizeof(s)); // 源點到任意點的成本初始為無限大
priority_queue<node> Q; // 為從未解集挑出已解的狀態
Q.push({source, s[source] = 0});
接著找出源點到任意點的最短路徑,其就是 BFS 並搭配 Relaxation
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 */});
}
}
其複雜度為
慢著!演算法的終止條件是 Q.empty() == true
,
也就是說,若有個邊權重為負的,則 Q.push({v.id, update});
將無盡的執行下去。
Bellman's Algo 會提供解決之道
對於點 v
每次將比最短路 d1[v]
還大的更新值 ud
交給次短路 d2[v]
去做 relaxation
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
UVa OJ 10801 Lift Hopping
步驟有兩個:
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);
找單源最短路徑就是這麼簡單
Image Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Bellman-Ford's Algo 可以簡單處理權重是負的情況:
先做
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
接著就能判斷是否能回到過去
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
UVa OJ 10000 Longest Paths
Bellman-Ford’s Algorithm 的進化版本,
其動機是: 若邊被 relax 過,則它的鄰點可以直接進行 relaxation
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
最壞複雜度
平均而言
, SPFA 提出者認為 很小 Image Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
UVa OJ 658 It’s not a Bug, it’s a Feature!
全點對最短路徑 (All-Pairs Shortest Paths) 簡稱 APSP。
顧名思義,可以得知所有點與點之間的最短路徑,同樣的一般只需求最小花費成本而已。
設定狀態
而
所以狀態轉移方程:
邊界:
經由滾動陣列優化方法,可使狀態三維降至二維:
同學自行研究,這很簡單的
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]);
根據歐拉圖,若有兩點為 odd degree,則終點與卡車並非同一點
也就是說,遇到這個狀況需要算出送件終點與卡車間的最短距離
因為輸入規模很小,直接使用 Floyd-Warshall’s Algo:
Floyd-Warshall’s Algo 很好寫
#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)
TIOJ 1096 E.漢米頓的麻煩
Google Code Jam Kickstart Round F 2018 B Specializing Villages: Small dataset
UVa OJ 10724 Road Construction
UVa OJ 125 Numbering Paths
Google Code Jam Round 1B 2017 C Pony Express
即任意兩點必有至少一條道路 ↩︎
u -> v 表示 u 能走過去到 v; 而 v 不能走到 u ↩︎
Wikipedia/ A demo for Kruskal's algorithm based on Euclidean distance. ↩︎
Wikipedia/ A demo for Prim's algorithm based on Euclidean distance. ↩︎
也能設為其他具代表性的數字作為未知的總成本 ↩︎