圖論 - 2
By 王政祺
還記得「圖」嗎?
來試試以下這些名詞吧
無向圖、連通圖
路徑、簡單路徑
鄰接矩陣、鄰接串列
生成樹
最短路徑
給定一張\(V\)個點、\(E\)條邊的圖
試求 \((x, y)\) 兩點之間經過最少條邊的路徑
但要是邊有權重呢?
前提: 不能有負環!!!
單點源最短路徑
鬆弛(relaxation)
對於任意兩個點 \(u, v\),如果起點到他們的距離分別為 \(d_u, d_v\) 且滿足 \(d_u+w_{u,v} < d_v\),那我們就可以把 \(d_v\) 更新為 \(d_u+w_{u,v}\)
Bellman-Ford Algorithm
\(O(E)\) 枚舉邊來進行鬆弛,鬆弛 \(V-1\) 次
最短路徑長度最大\(v-1\),若鬆弛\(v-1\)次後還能再鬆弛就表示存在負環
複雜度 \(O(VE)\)
tf = 0; // tf 表示是否有負環
dis[1] = 0;
for (int i = 2; i <= N; i++) dis[i] = INF; // 起點到 i 的最短距離,初始設為無限大
for (int i = 1; i < N; i++) { // 跑 v-1 次
for (int j = 0; j < M; j++) { // 窮舉邊
a = path[j].ff, b = path[j].ss;
if (dis[a] != INF && dis[a] + c[j] < dis[b]) { // 如果可以 relax 的話
dis[b] = dis[a] + c[j];
}
}
}
for (int i = 0; i < M; i++) {
a = path[i].ff, b = path[i].ss;
if (dis[a] + c[i] < dis[b]) { // 如果還能 relax 的話就表示有負環
tf = 1;
}
}
利用Bellman-Ford實作負環判斷
好像有點慢(?
來一點優化吧
SPFA
每次只枚舉有被鬆弛到的點的邊
期望複雜度 \(O(V+E)\),最差複雜度 \(O(VE)\)
能不能再快一點???
如果沒有負邊的話...?
最短路徑樹
子節點的最短路徑在沒有負邊的情況下一定不比父節點小
Dijkstra’s Algorithm
每次選一個不在樹上且距離最近的點
加進樹中並用它來鬆弛其他點
用 priority_queue 維護!
複雜度 \(O((E+V)\log E)\)
vector < pii > path[N+1]; // adjacency list
priority_queue < pii , vector < pii >, greater < pii > > pq;
pii cur;
fill(dis, dis+N, INF);
dis[1] = 0;
pq.push({0, 1});
//每 個 節 點 只 會 被 更 新 一 次
for(int i = 0; i < N; i++){
//將 已 更 新 的 節 點 從 清 單 移 除(Lazy Deletion)
do cur = pq.top(), pq.pop();
while(cur.ff > dis[cur.ss]);
for(auto e : path[cur.ss]) // Relax
if(dis[e.ss] > cur.ff+e.ff)
dis[e.ss] = cur.ff+e.ff,
pq.push({dis[e.ss], e.ss});
}
Dijkstra
全點對最短路徑
用 Dijkstra 的話需要 \(O(V*(V+E) log E)\)
感覺有點糟QQ
一個路徑除了起點終點
剩下的都是中繼點
有沒有一種 DP 的感覺
Floyd-Warshall Algorithm
把「由 \(i\) 點中途經過前 \(k\) 點抵達 \(j\) 點的最短路徑」作為 DP 狀態
\(dp_{i,j,k}= min(dp_{i,j,k-1}, dp_{i,k,k-1}+dp_{k,j,k-1})\)
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
dis[i][j] = INF; // 初始化
if(i==j) dis[i][j] = 0;
}
}
//cin
for (int k = 1; k <= n; k++)// 窮舉 k 來看看能不能 Relax dis(i, j)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (dis[i][k] + dis[k][j] < dis[i][j])
dis[i][j] = dis[i][k] + dis[k][j];
Floyd-Warshall 實作
最小生成樹
一張有邊權的連通無向圖
求一個邊權總和最小的生成樹
一個點 = 自己的最小生成樹
有點玄?但還算可以理解嗎(?
兩個最小生成樹要怎麼合併
加上連接兩者之中最小的那條邊就對了
三個呢?
反正不管怎樣挑最短的那條來連就對了
Kruskal’s algorithm
每次找當前邊權最小的邊,檢查他的兩邊是否在同一個集合(生成樹)中
不在的話,就把它連起來吧
所以說有人記得曾經有個叫做並查集的東西嗎?
Disjoint Sets
int dsu[MAXN+5];
int qry(int now) {
if (dsu[now] != now) dsu[now] = qry(dsu[now]);
return dsu[now];
}
void Union(int a, int b) {
dsu[qry(a)] = qry(b);
}
Kruskal's Algorithm
sort(path, path+m, cmp); // 讓 path 陣列依照邊權值大小排序
for (int i = 0; i < m; i++) {
if (qry(path[i].a) == qry(path[i].b)) continue;
ans += path[k].c; // 將答案加上權重
Union(path[k].a, path[k].b);
}
Prim’s algorithm
其實就是類似 dijkstra 啦
最短路徑樹 ~ 最小生成樹
把記錄的當前最短路徑改為「與當前最小生成樹的距離」
資訊讀書會 圖論-2
By CasperWang
資訊讀書會 圖論-2
- 949