GRAPH[1]
Graph Theory II
INDEX
Review
Shortest Paths
Spanning Tree
Review
複習
1. 術語
2. 定義
Review
Terminology
術語
\text{vertex} / \text{node}
頂點
\text{edge}
邊
\text{arc}
弧\\
(有向圖)
\text{degree}
度數\\
(相連邊數)
1
2
1
\text{in-degree}
入度
0
2
0
\text{out-degree}
出度
1
0
1
\text{path(路徑)}
1
2
3
4
5
P=(1, 2, 3, 5)
\text{length of path}
1
2
3
4
5
路徑長=4
\text{cycle}
環
Review
Definitions
定義
\text{Graph(圖)}
由一組頂點和邊組成
記為\ G=(V,E)\\V\ 是頂點集合,E\ 是邊集合
\text{Directed Graph(有向圖)}
邊具有方向
邊表示為\ (u, v)\\
表示從\ u\ 指向\ v
\text{Directed Graph(有向圖)}
邊沒有方向
邊表示為\ \{u, v\}\\
表示\ u\ 和\ v\ 之間的關聯
\text{Simple Graph(簡單圖)}
無多重邊和自環
\text{Weight Graph(加權圖)}
邊附有權重
權重是一個數值,記為\ w(u, v)
4
8
7
6
3
\text{Complete Graph(完全圖)}
每對頂點之間都有一條邊
完全圖的邊數為\ \frac{n(n-1)}{2}(\,n\ 是頂點數)
\text{Connected Graph(連通圖)}
無向圖中每一對頂點之間都有至少有一條路徑
\text{Acyclic Graph(無環圖)}
不包含任何環的圖
\text{Bipartite Graph (二分圖)}
頂點可以分為兩個不相交的子集\ U\ 和\ V\\且所有邊都連接\ U\ 和\ V\ 的頂點
Review
Unweighted Shortest Path
如何找無權圖的最短路?
用之前講過的\ \text{BFS}
\text{Directed}\\
1
3
2
4
5
1
3
2
4
5
\text{Undirected}\\
vector<vector<int>> adj(n + 1);
while (m--) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
}
vector<vector<int>> adj(n + 1);
while (m--) {
int a, b;
cin >> a >> b;
adj[a].push_back(b);
adj[b].push_back(a);
}
只差在存圖方式(以下都使用鄰接陣列)
Review
複習一下
寫過的可以試一下
Shortest Paths
最短路
1. Bellman-Ford
2. Dijkstra
3. Floyd-Warshall
Shortest Paths
Shortest Path
Weighted
\text{Bellman-Ford}
\\O(|V||E|)
如何找加權圖的最短路?
有三種方法
\text{Dijkstra}
\\O((|V| + |E| \log |E|)
\text{Floyd-Warshall}
\\O(|V|^3)
V\ 是節點數,E\ 是邊數
1
3
2
4
5
3
5
7
1
3
2
2
Shortest Paths
Bellman-Ford
找出一個點到其他點的最短距離
通過尋找能縮短路徑的邊來減少距離\\
直到無法再縮短任何距離為止
1
3
2
4
5
3
5
7
1
3
2
2
0
\infty
\infty
\infty
\infty
把起始點設為\ 0\\
且到其他點的距離為\ \infty
1
3
2
4
5
3
5
7
1
3
2
2
0
5
\infty
3
7
從\ 1\ 開始的三條邊都可以減少距離
1
3
2
4
5
3
5
7
1
3
2
2
0
5
7
3
4
2\rightarrow 5\ \text{and}\ 3\rightarrow 4\ 會縮短距離
1
3
2
4
5
3
5
7
1
3
2
2
0
5
6
3
4
4\rightarrow 5
1
3
2
4
5
3
5
7
1
3
2
2
0
5
6
3
4
沒有邊可以減少距離\\
即構成完\ 1\ 到其他點的最短距離
1
3
2
4
5
3
5
7
1
3
2
2
0
5
6
3
4
1\rightarrow 5\ 的最短路徑
Shortest Paths
Implement
for (int i = 1; i <= n; i++) distance[i] = INF;
distance[x] = 0;
for (int i = 1; i <= n - 1; i++) {
for (auto e : edges) {
int a, b, w;
tie(a, b, w) = e;
distance[b] = min(distance[b], distance[a] + w);
}
}
\text{Time Complexity: }O(|V||E|)
因為最多經過\ n - 1\ 個點就可以構成所有最短路
只需看第\ n\ 次如果可以縮短距離代表有負環存在
2
3
4
1
-7
2
1
3
5
負環
Shortest Paths
Dijkstra
一點到其他點的最短距離
貪心的選擇當前距離最短的點並且更新相鄰點\\
直到每個點都被處理過
3
2
4
1
5
2
6
5
9
2
1
\infty
\infty
\infty
\infty
0
把起始點設為\ 0\\
且到其他點的距離為\ \infty
3
2
4
1
5
2
6
5
9
2
1
\infty
9
1
5
0
選擇一個距離最短且尚未處理過的點
3
2
4
1
5
2
6
5
9
2
1
\infty
3
1
5
0
距離為\ 1\ 的\ 5
3
2
4
1
5
2
6
5
9
2
1
9
3
1
5
0
距離為\ 3\ 的\ 4
3
2
4
1
5
2
6
5
9
2
1
7
3
1
5
0
然後會發現所有的點都只會處理一次\\
最終構成最短距離
Shortest Paths
Proof
想當然貪心少不了證明
假設\ x\ 是最近點\quad distance(x) < distance(y)
若且唯若\\
distance(x) < distance(y) + w(y, ..., x)\\
則優先選擇最近點為最佳解
而滿足此條件的\ w(y, ..., x) \geq 0\Rightarrow 代表整張圖不能有負邊
不然幹嘛講\ \text{Bellman-Ford}
最短路:1\rightarrow 3\rightarrow 4
\text{Dijkstra}:1\rightarrow 2\rightarrow 4
錯了
2
3
4
3
-5
1
2
6
負邊
Shortest Paths
Implement
for (int i = 1; i <= n; i++) dist[i] = INF;
dist[start] = 0; // 起點距離為 0
pq.push({0, start}); // 預設是由大到小
while (!pq.empty()) {
long long c = pq.top().front;
int u = pq.top().second;
pq.pop();
if (-c != dist[u]) continue; // 如果該節點已處理過
for (auto [v, w] : adj[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.push({-dist[v], v}) // 將 dist 變成負數
// 由小到大排序
}
}
}
\text{Time Complexity: }O(|V| + |E| \log |E|)
鄰接陣列存圖\quad adj[a] = (b, w)
用\ priority\ queue\ 存每個點\\
根據距離由小到大排序
Shortest Paths
Floyd-Warshall
所有點之間的最短距離
維持一個二維陣列,嘗試所有中繼點以縮短距離
3
2
4
1
5
2
7
5
9
2
1
1\quad 2\quad 3\quad 4\quad 5
1\\
2\\
3\\
4\\
5
所有節點到自己的距離設為為零,且\ (a,b)\ 之間有邊的則設為其權重
0\quad 5\ \ \infty \quad 9\quad 1
5\quad 0\quad 2\ \ \infty\ \ \infty
2\quad 0\quad 7\ \ \infty
9\ \ \infty\quad 7\quad 0\quad 2
1\ \ \infty\ \ \infty\quad 2\quad 0
\infty
1\quad 2\quad 3\quad 4\quad 5
1\\
2\\
3\\
4\\
5
first,\ 用\ 1\ 作為中繼點
0\quad 5\ \ \infty \quad 9\quad 1
5\quad 0\quad 2
\infty\ \ 2\quad 0\quad 7\ \ \infty
7\quad 0\quad 2
\infty\quad 2\quad 0
14\quad 6
9
14
6
9
1\quad 2\quad 3\quad 4\quad 5
1\\
2\\
3\\
4\\
5
用\ 2\ 作為中繼點
9\quad 1
5\quad 0\quad 2
2\quad 0\quad 7
7\quad 0\quad 2
2\quad 0
14\quad 6
9
14
6
9
0\quad 5
7
7
8
8
1\quad 2\quad 3\quad 4\quad 5
1\\
2\\
3\\
4\\
5
用\ 3\ 作為中繼點
9\quad 1
5\quad 0\quad 2
2\quad 0\quad 7
7\quad 0\quad 2
2\quad 0
6
9
9
6
9
0\quad 5
7
7
8
8
9
1\quad 2\quad 3\quad 4\quad 5
1\\
2\\
3\\
4\\
5
直到完成整張表
0\quad 5\quad 7\quad 3\quad 1
5\quad 0\quad 2\quad 8\quad 6
7\quad 2\quad 0\quad 7\quad 8
3\quad 8\quad 7\quad 0\quad 2
1\quad 6\quad 8\quad 2\quad 0
1\quad 2\quad 3\quad 4\quad 5
1\\
2\\
3\\
4\\
5
2\rightarrow 4\ 的最短路是\ 8
0\quad 5\quad 7\quad 3\quad 1
5\quad 0\quad 2
7\quad 2\quad 0\quad 7\quad 8
3\quad 8\quad 7\quad 0\quad 2
1\quad 6\quad 8\quad 2\quad 0
8
6
Shortest Paths
Implement
\text{Time Complexity: }O(n^3)
使用鄰接矩陣存圖\quad adj[a][b] = w
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (i == j) distance[i][j] = 0;
else if (adj[i][j]) distance[i][j] = adj[i][j];
else distance[i][j] = INF;
}
}
尋找最短路:
for (int k = 1; k <= n; k++) { // 中繼點,記得放在最外圈
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
distance[i][j] = min(distance[i][j],
distance[i][k]+distance[k][j]);
}
}
}
忘記\ k\ 迴圈放哪沒關係,你只需要整個跑三遍他就一定會是對的
Shortest Paths
負環
在圖論正常來說有負環存在的圖找最短路徑沒意義\\
(你可以一直繞直到無窮小)
\text{Bellman-Ford}:判斷第\ n\ 次迴圈是否縮減距離
\text{Dijkstra}:不能處理負邊
\text{Floyd-Warshall}:如果\ distance[i][i]<0
而有時候你只是想要判有沒有負環
但其實上面只適用於\bf{有向圖}
我們應該很難說服自己這是個負環,但他是。
1
4
-7
不然......
Shortest Paths
Summary
小結
單源
單源
多源
任意圖
無負邊
任意圖
O(|V||E|)
O(|V| + |E| \log |E|)
O(|V|^3)
檢查負環
快速單點源
全點對
\text{Bellman-Ford}
\text{Dijkstra}
\text{Floyd-Warshall}
題單
Shortest Paths
Spanning Tree
生成樹
1. DSU
2. Kruskal's
3. Prim's
Spanning Tree
Minimum Spanning Tree
最小生成樹
生成樹是一棵\\包含圖裡面所有點和一條通過所有點的路徑的\bf{樹}
(無向聯通無環圖)
他的權重就是所有邊的權重總和
3 + 5 + 2 + 3 + 7 = 20
而最小生成樹就是權重最小的生成樹
2
5
3
6
4
6
5
2
3
9
7
1
3
5
這是張圖
2
5
3
6
4
2
3
7
1
3
5
\Rightarrow
這是他的生成樹
Spanning Tree
題目
找最小生成樹要幹嘛?
\Rightarrow 其實就是找最小生成樹的權重
有兩種方法可以構成最小生成樹
\text{Kruskal's}\quad \& \quad \text{Prim's}
n\ 個城市\ m\ 條道路,每一條路都是破損的\\
修補道路使每兩個城市間皆有一條路的最小代價為多少?
Spanning Tree
Kruskal's algorithm
將邊由小到大遍歷,如果該邊不構成環\\
把它加到生成樹裡,最終構成一個樹。
2
5
3
6
4
1
初始所有點不相連
2
5
3
6
4
1
第一個邊把\ \{5\}\ 和\ \{6\}\ 相連
2
2
5
3
6
4
1
1\!-\!2,\ 3\!-\!6\ 和\ 1\!-\!5
2
3
3
5
2
5
3
6
4
1
下兩個邊\ 2\!-\!3,\ 2\!-\!5\ 會構成環,不加入\\
最後\ 4\!-\!6\ ,構成最小生成樹
2
3
3
5
7
5
2
3
9
3
1
6
3
4
5
5
6
7
2
先把所有邊按照權重排序
edge\quad weight
2
5\!-\!6
1\!-\!2
3
3\!-\!6
3
1\!-\!5
5
2\!-\!3
5
2\!-\!5
6
4\!-\!6
7
3\!-\!4
9
Spanning Tree
Proof
這很貪心對吧
假設剛剛的圖沒有包含\ 5\!-\!6,他可能會長:
但他一定不會是最小生成樹
因為如果我們移除一個邊並且加上\ 5\!-\!6,會產生一個更小的生成樹
\Rightarrow 所以在不破壞樹性質的情況下加入最小邊則可構成最小生成樹
2
5
3
6
4
1
\Rightarrow
2
5
3
6
4
1
2
Spanning Tree
Implement
併查集(Disjoint Set Union)
要怎麼判兩點是否會構成環?
所以我們需要一個可以快速查詢和合併的結構\\
看他們是不是在同一個集合
\Rightarrow 對於每一個集合,都作一個鏈指向代表\\
就能根據代表判斷他們是否在同個集合
4
1
7
5
3
6
8
2
以上三個集合的代表為\ 4,\ 5\ 和\ 2
4
1
7
3
6
8
2
也可以讓他們合併,代表變為\ 2
Shortest Paths
Implement
for (int i = 1; i <= n; i++) link[i] = i; // 各自為代表
for (int i = 1; i <= n; i++) size[i] = 1; // 紀錄代表 i 所在集合的大小
查找
使用\ link\ 陣列紀錄對於每個點,在鏈上的下個節點\\
(如果自身是代表則指向自己)
int find(int x) {
while (x != link[x]) x = link[x];
return x;
}
合併
void unite(int a, int b) {
a = find(a);
b = find(b);
// 這是啟發式合併,可以避免惡意測資
if (size[a] < size[b]) swap(a, b);
size[a] += size[b];
link[b] = a;
}
Spanning Tree
Prim's algorithm
雖然大多數人都用\ \text{Kruskal's},但還是來介紹另外一個算法
從任意一個點開始,每次都找可加入的點中代價最少的加入\\
(跟\ \text{Dijkstra}\ 很像,但是找最小權重邊)
2
5
3
6
4
6
5
2
3
9
7
1
3
5
使用跟剛剛一樣的圖
2
5
3
6
4
1
初始沒有任何邊
2
5
3
6
4
1
隨機選擇一個邊(1)\\
加入節點\ 2
3
2
5
3
6
4
1
接下來有兩個權重為\ 5\ 的邊\\
可以選擇加入\ 3\ 或\ 5
3
5
2
5
3
6
4
1
重複直到所有節點都被加入
3
5
2
3
7
實作也跟\ \text{Dijkstra}\ 一樣可以使用\ priority\ queue
Shortest Paths
AC Code
#include <iostream>
#include <vector>
#include <algorithm>
#include <utility>
using namespace std;
// 這邊的 DSU 是用路徑壓縮,有興趣可以去查一下
struct DSU {
vector<int> boss;
DSU(int n) : boss(n + 1) {
for (int i = 1; i <= n; i++) {
boss[i] = i;
}
}
int query(int a) {
if (boss[a] == a) return a;
return boss[a] = query(boss[a]);
}
bool unite(int a, int b) {
a = query(a), b = query(b);
if (a == b) return false;
boss[a] = query(b);
return true;
}
};
int main() {
int n, m;
cin >> n >> m;
vector<pair<int, pair<int, int>>> adj;
for (int i = 0; i < m; i++) {
int a, b, c;
cin >> a >> b >> c;
adj.push_back({c, {a, b}});
}
sort(adj.begin(), adj.end());
DSU dsu(n);
long long ans = 0;
int node = 0;
for (int i = 0; i < m; i++) {
int a, b, c;
a = adj[i].second.first;
b = adj[i].second.second;
c = adj[i].first;
if (dsu.unite(a, b)) {
ans += c;
node++;
}
}
if (node != n - 1) cout << "IMPOSSIBLE\n";
else cout << ans << '\n';
}
\text{Time Complexity}\approx O(m \log n)
實際上是\ O(m \log_{2+f/n} n)
掰掰
圖論 II
By keaucucal
圖論 II
- 298