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)

掰掰

Made with Slides.com