Graph[0]
algorithm[9] 22527 Brine
Index
Glossary
圖論的基本名詞
什麼是「圖」
唯利是圖
什麼是「圖」
唯利是圖
1
2
0
5
3
4
所以什麼是圖?
圖的表示
- 頂點(vertex / vertices)
- 常用 \(v\) 來表示
- 頂點的集合為 \(V\)
- 邊(edge)
- 常用 \( e = (u,\ v) \) 表示
- 邊的集合為 \(E\)
- 圖(graph)
- 圖就是點和邊的集合
- 通常用 \( G(V,\ E) \) 表示
邊的屬性
- 權重(weight)
- 方向(direction)
- 有向/無向(directed / undirected)
- 如果 \((u,\ v)\) 和 \((v,\ u)\) 所代表的是不同的邊,則說這兩個邊有向
- 自環(loop)
- 起點終點相同的邊,即 \((v, v)\)
- 重邊(parallel edges)
- 兩個一模一樣的邊
點的屬性
- 權重(weight)
- 度(degree)
- 一個點連到的邊的數量
- 入度/出度(indegree / outdegree)
- 在有向圖之中分為進去和出去的邊
路徑相關
- 路徑(path)
- 一個頂點和邊交錯的序列,每條邊都要有連到左右兩個點
- 以 \((v_{start},\ e_1,\ v_1,\ e_2,\ v_2,...,\ e_n,\ v_{end})\) 表示
- 行跡(trace)
- 沒有重複
邊
的路徑
- 沒有重複
- 簡單路徑(track)
- 沒有重複頂點的路徑
- 迴路(circuit)
- 起點和終點為相同頂點的路徑
- 環(cycle)
- 只有起點和終點為相同頂點的迴路
路徑
0
1
3
2
4
7
6
5
行跡
0
1
3
2
4
7
6
5
簡單路徑
0
1
3
2
4
7
6
5
迴路
0
1
3
2
4
7
6
5
環
0
1
3
2
4
7
6
5
無向圖/有向圖
- 只有無向邊或有向邊
0
1
3
2
4
0
1
3
2
4
帶點權/帶邊權
- 也可以都帶(?
0
5
1
7
2
1
3
2
4
2
0
1
3
2
4
9
8
4
7
簡單圖 Simple Graph
- 沒有重邊
- 沒有自環
0
1
3
2
4
0
1
3
2
4
連通圖 Connected Graph
- 如果所有點都是無向邊是否所有點都可以到任意一個點
0
1
3
2
4
0
1
3
2
4
弱連通/強連通 WC/SC
- weakly / strongly connected
- 是否要變成無向圖才能所有點都能抵達其他點
0
1
3
2
4
0
1
3
2
4
完全圖 Complete Graph
- 每個可能的邊都存在的簡單圖
0
1
3
2
子圖 Subgraph
0
1
3
2
\forall v_i, e_i \in G_{sub}(V,\ E),\ v_i, e_i \in G(V,\ E)
- 若某圖的所有邊和點都屬於另一圖,則其為該圖之子圖
0
1
3
2
補圖 Complement
0
1
3
2
- 兩圖點集相同
- 邊集無交集且聯集等於所有可能的邊
0
1
3
2
樹 Tree
- 沒有環的無向連通圖
0
1
3
2
4
- 會有多少邊?
- 森林 forest
7
6
5
二分圖 Bipartite Graph
- 可以分為兩個點集,沒有連接兩點集內點的邊
0
1
3
2
4
7
6
5
0
1
3
2
4
7
6
5
有向無環圖 DAG
- directed acyclic graph
0
1
3
2
4
稀疏圖/稠密圖
|E|\le|V|\log|V|
|V|\log|V| \le |E| \le |V|^2
- sparse / dense
Graph Storage
圖的儲存
鄰接矩陣 Adjacency Matrix
- 將所有邊的可能用 \(\mathcal{O}(|V|^2)\) 的陣列表示
G[i][j] = \begin{cases}
1,\ if\ (i, j) \in E
\\
0,\ if\ (i, j) \notin E
\end{cases}
- 將所有邊的可能用 \(\mathcal{O}(|V|^2)\) 的陣列表示
G[i][j] = \begin{cases}
1,\ if\ (i, j) \in E
\\
0,\ if\ (i, j) \notin E
\end{cases}
0
1
3
2
4
\longrightarrow \begin{bmatrix}
0 & 1 & 0 & 0 & 0\\
1 & 0 & 1 & 1 & 0\\
0 & 1 & 0 & 1 & 1\\
0 & 1 & 1 & 0 & 0\\
0 & 0 & 1 & 0 & 0
\end{bmatrix}
鄰接矩陣 Adjacency Matrix
鄰接矩陣實作
- 之後前幾行可能會被我省略掉喔
int main() {
int vertexCount, edgeCount;
cin >> vertexCount >> edgeCount;
vector< vector<bool> > graph;
graph.assign(vertexCount, vector<bool>(vertexCount));
int u, v;
for (int i = 0; i < edgeCount; i++) {
cin >> u >> v;
graph[u][v] = graph[v][u] = 1;
}
}
鄰接串列 Adjacency List
- 將每個點有連到的點用記錄在一個清單上
鄰接串列 Adjacency List
- 將每個點有連到的點用記錄在一個清單上
0
1
3
2
4
0:\text{\{1\}}
1:\text{\{0, 2, 3\}}
2:\text{\{1, 3, 4\}}
3:\text{\{1, 2\}}
4:\text{\{2\}}
\longrightarrow
空間複雜度?
鄰接串列實作
int main() {
int vertexCount, edgeCount;
cin >> vertexCount >> edgeCount;
vector< vector<int> > graph(vertexCount);
int u, v;
for (int i = 0; i < edgeCount; i++) {
cin >> u >> v;
graph[u].push_back(v);
graph[v].push_back(u);
}
}
有向圖儲存
就少存一邊就好啊
cin >> u >> v;
graph[u].push_back(v);
graph[v].push_back(u);
cin >> u >> v;
graph[u].push_back(v);
cin >> u >> v;
graph[u][v] = 1;
graph[v][u] = 1;
cin >> u >> v;
graph[u][v] = 1;
帶權圖儲存
-
會用 pair<int, int> 是因為有內建比較函式
typedef pair<int, int> Edge;
int main() {
int vertexCount, edgeCount;
cin >> vertexCount >> edgeCount;
vector< vector<Edge> > graph(vertexCount);
int u, v, w; // w is for weight
for (int i = 0; i < edgeCount; i++) {
cin >> u >> v >> w;
graph[u].push_back({w, v});
graph[v].push_back({w, u});
}
}
如果不想用 pair?
-
p.first 和 p.second 不好打?
-
用 priority_queue 會有問題,但很難寫
struct E {
int to;
int w;
};
int main() {
priority_queue<E, vector<E>, function<bool(E, E)> > pq(
[](const E& a, const E& b) {
return (a.w != b.w ? a.w < b.w : a.to < b.to);
}
);
}
比較
- 在稠密圖用鄰接矩陣,稀疏圖用鄰接串列!
項目 | 鄰接矩陣 | 鄰接串列 |
---|---|---|
空間複雜度 | ||
插入邊 | ||
查詢邊 | ||
枚舉邊 |
\mathcal O(|V|^2)
\mathcal O(1)
\mathcal O(1)
\mathcal O(|V| + |E|)
\mathcal O(1)
\mathcal O(|E|)
\mathcal O(|V|^2)
\mathcal O(|V| + |E|)
Other Methods
其他存圖的方式
網格座標(?
- 我不知道這個叫什麼,但反正就是有這種東西
- 我之後可能就叫他網格座標喔
#include <iostream>
using namespace std;
int main() {
int height, length;
cin >> height >> length;
vector<string> graph(height);
for (auto& s: graph) cin >> s;
}
h = 5, l = 4\\
\begin{bmatrix}
0000\\
0100\\
0010\\
0000\\
0100
\end{bmatrix}
分開輸入
- 可以外面包一圈邊界比較好做事
- 把不能走的也改成邊界
int main() {
int height, length;
cin >> height >> length;
vector<vector<int>> graph(height + 2,
vector<int>(length + 2, -1));
for (int i = 1; i <= height; i++) {
for (int j = 1; j <= length; j++) {
cin >> graph[i][j];
if (graph[i][j]) graph[i][j] = -1;
}
}
}
#include <bits/stdc++.h>
using namespace std;
int main() {
int vertexCount, edgeCount;
cin >> vertexCount >> edgeCount;
vector< pair<int, int> > edgeList(edgeCount);
for (auto& [u, v]: edgeList) {
cin >> u >> v;
}
}
邊串列 Edge List
- 把邊丟到集合中
存樹
- 指標
- 陣列
1
3
2
4
7
6
5
例題
- TIOJ 1807 簡單圖判斷
Graph Traversal
圖的遍歷
我們現在有某個點突然有很多的水的一張圖,而我們現在想要計算出這張圖中的點何時會被水淹到,該怎麼做呢?
淹水 Floodfill
我們現在有某個點突然有很多的水的一張圖,而我們現在想要計算出這張圖中的點何時會被水淹到,該怎麼做呢?
4
0
2
3
2
7
3
8
5
1
9
6
淹水 Floodfill
我們現在有某個點突然有很多的水的一張圖,而我們現在想要計算出這張圖中的點何時會被水淹到,該怎麼做呢?
0
0
4
2
3
2
7
3
8
5
1
9
6
淹水 Floodfill
我們現在有某個點突然有很多的水的一張圖,而我們現在想要計算出這張圖中的點何時會被水淹到,該怎麼做呢?
4
1
2
7
3
1
8
1
5
1
1
1
9
6
0
0
淹水 Floodfill
我們現在有某個點突然有很多的水的一張圖,而我們現在想要計算出這張圖中的點何時會被水淹到,該怎麼做呢?
4
1
2
2
7
3
1
8
1
5
1
1
1
9
2
6
2
0
0
淹水 Floodfill
淹水 Floodfill
我們現在有某個點突然有很多的水的一張圖,而我們現在想要計算出這張圖中的點何時會被水淹到,該怎麼做呢?
4
1
2
2
7
3
3
1
8
1
5
1
1
1
9
2
6
2
0
0
暴力作法
- 每次確定每一格有沒有被淹到,有的話就更新他所有鄰居點
- 每輪做 \(\mathcal O(|V|)\) 次檢查
- 最多檢查 \(\mathcal O(|V|)\) 次(why?)
- 時間複雜度 \(\mathcal O(|V|^2)\)!
- 花太多時間確定誰有資格溢出了!
小觀察
- 只有剛被淹過的點有需要去往下淹
- 每個點只會當最外圍一次
- 可以開個下回合要溢出的點的清單!
- 用個 queue 來記錄就好了
- 這就是廣度優先搜尋/遍歷(Breadth-First Search / traversal)
- 通常簡稱 BFS
複雜度?
- 剛剛我們想了一個不錯的方法來處理這個問題
- 那這個演算法的時間複雜度是什麼?
- 所有點都只會進去/出來 queue 一次
- \(\mathcal O(|V|)\) 完成所有操作?
- 每個點都會有不同的邊數,怎麼估計複雜度?
- 每個邊會被走到兩次
- 用這條邊淹到一個點時
- 下次操作淹回去失敗時
- \(\mathcal O(|E|)\) 完成所有邊的操作
- 每個邊會被走到兩次
- 總複雜度 \(\mathcal O(|V| + |E|)\)
- 額外空間複雜度(存圖以外的)?
BFS code
- 記得一淹到某個點就要記錄!
vector<int> bfs(int source, vector< vector<int> >& graph) {
queue<int> q;
q.push(source);
vector<int> distance(graph.size(), INT32_MAX);
distance[source] = 0;
while (!q.empty()) { // while (q.size())
int& current = q.front();
for (auto& n: graph[current]) {
if (distance[n] != INT32_MAX) continue;
distance[n] = distance[current] + 1;
q.push(n);
}
q.pop();
}
distance[source] = 0;
return distance;
}
怎麼在網格座標圖 BFS
-
四個方向的
while (!q.empty()) { // while (q.size())
auto& [x, y] = q.front();
// queue< pair<int, int> > q;
if (graph[x][y + 1] == INT32_MAX) {
graph[x][y + 1] = graph[x][y] + 1;
q.push({x, y + 1});
}
if (graph[x][y - 1] == INT32_MAX) {
graph[x][y - 1] = graph[x][y] + 1;
q.push({x, y - 1});
}
if (graph[x + 1][y] == INT32_MAX) {
graph[x + 1][y] = graph[x][y] + 1;
q.push({x + 1, y});
}
if (graph[x - 1][y] == INT32_MAX) {
graph[x - 1][y] = graph[x][y] + 1;
q.push({x - 1, y});
}
q.pop();
}
看起來還好?
- 作為一個聰明人,你不喜歡程式有重複結構吧
- :那我喜歡怎麼辦
- :那現在水可以朝八個方向淹
- :沒關係
- :那現在空間變三維的,往 26 個方向擴散
將重複部分換成迴圈
const int dx[] = {1,-1, 0, 0};
const int dy[] = {0, 0, 1,-1};
while (!q.empty()) {
auto& [x, y] = q.front();
for (int i = 0; i < 4; i++) {
auto& nextPosition = graph[x + dx[i]][y + dy[i]];
if (nextPosition == INT32_MAX) {
nextPosition = graph[x][y] + 1;
q.push({x + dx[i], y + dy[i]});
}
}
q.pop();
}
BFS 的功用
- 用某種方式走完整張圖
- 淹水模擬
- 無權圖最短路徑
- 帶權的會發生什麼事情?
如果把 queue 換掉?
- 換成 stack 會長什麼樣子呢?
6
1
4
2
3
0
5
9
7
8
如果把 queue 換掉?
- 換成 stack 會長什麼樣子呢?
6
1
4
2
3
0
0
5
9
7
8
\\
\\
\\
0\\
如果把 queue 換掉?
- 換成 stack 會長什麼樣子呢?
6
1
4
2
1
3
0
0
5
9
7
8
\\
\\
\\
0\\
2
如果把 queue 換掉?
- 換成 stack 會長什麼樣子呢?
6
1
2
4
2
1
3
0
0
5
9
7
8
\\
\\
\\
0\\
2
1
如果把 queue 換掉?
- 換成 stack 會長什麼樣子呢?
6
3
1
2
4
2
1
3
0
0
5
9
7
8
\\
\\
\\
0\\
2
1
6
如果把 queue 換掉?
- 換成 stack 會長什麼樣子呢?
6
3
1
2
4
2
1
3
0
0
9
7
8
5
3
\\
\\
\\
0\\
2
1
5
如果把 queue 換掉?
- 換成 stack 會長什麼樣子呢?
6
3
1
2
2
1
3
0
0
9
7
8
5
3
\\
\\
\\
0\\
4
4
\\
1
2
1
5
如果把 queue 換掉?
- 換成 stack 會長什麼樣子呢?
6
3
1
2
2
1
3
0
0
7
8
5
3
4
1
9
\\
2
\\
\\
\\
0\\
4
9
如果把 queue 換掉?
- 換成 stack 會長什麼樣子呢?
6
3
1
2
2
1
3
0
0
7
5
3
4
1
9
2
8
2
\\
\\
\\
0\\
4
8
如果把 queue 換掉?
- 換成 stack 會長什麼樣子呢?
6
3
1
2
2
1
3
0
0
5
3
4
1
9
2
8
2
7
3
\\
\\
\\
0\\
4
8
7
如果把 queue 換掉?
- 換成 stack 會長什麼樣子呢?
6
3
1
2
2
1
3
0
0
5
3
4
1
9
2
8
2
7
3
\\
\\
\\
0\\
4
8
7
如果把 queue 換掉?
- 換成 stack 會長什麼樣子呢?
6
3
1
2
2
1
3
0
0
5
3
4
1
9
2
8
2
7
3
\\
\\
\\
0\\
4
8
7
如果把 queue 換掉?
- 換成 stack 會長什麼樣子呢?
6
3
1
2
2
1
0
0
5
3
4
1
9
2
8
2
7
3
3
1
\\
\\
\\
0\\
4
8
7
\\
\\
\\
3\\
如果把 queue 換掉?
- 換成 stack 會長什麼樣子呢?
6
3
1
2
2
1
0
0
5
3
4
1
9
2
8
2
7
3
3
1
\\
\\
\\
0\\
4
8
7
\\
\\
\\
3\\
剛剛發生了什麼事?
- 如果一個點可以走,我就往那個點走,把他寫上標記
- 走了之後就從那個點繼續往下
- 我們把這個動作稱為深度優先搜尋(Depth-First Search)
- DFS
用 stack 實作
- 除了在 stack 裡面紀錄點的編號,還要記錄這個點已經看過的邊
- 如果不記錄會有什麼不好的?
- 有沒有可以不記錄的情況?
- 有沒有辦法可以不用記錄編號呢?
- 是不是用遞迴維護 stack 就好了!
- 除非因為資料太大導致遞迴 stack overflow,否則用遞迴就好
DFS code
- 函式裡面塞太多東西不太舒服?
void dfs(int current, vector< vector<int> >& graph, vector<bool>& visited) {
visited[current] = 1;
for (auto& n: graph[current]) {
if (visited[n]) continue;
dfs(n, graph, visited);
}
}
int main() {
int vC, eC;
cin >> vC >> eC;
vector< vector<int> > graph(vC);
int u, v;
for (int i = 0; i < eC; i++) {
cin >> u >> v;
graph[u].push_back(v);
}
vector<bool> visited(vC);
dfs(0, graph, visited);
}
使用 lambda
- 這裡有我之前做的簡報,如果不懂可以看看
vector<bool> visited(vC);
function<void(int)> dfs = [&](int current) {
visited[current] = 1;
for (auto& n: graph[current]) {
if (!visited[n]) dfs(n);
}
};
- 也可以用全域變數就好,但我不喜歡
- 要用 lambda 遞迴必須要標註型別不能用 auto
前/中/後序遍歷
- preorder / inorder / postorder
- 僅限二元樹
vector< pair<int, int> > tree(vC);
int leftChild, rightChild;
for (int i = 0; i < vC; i++) {
cin >> leftChild >> rightChild;
tree[i] = {leftChild, rightChild};
}
vector<int> preorder, inorder, postorder;
function<void(int)> dfs = [&](int current) {
auto [l, r] = tree[current];
preorder.push_back(current);
if (l >= 0) dfs(l);
inorder.push_back(current);
if (r >= 0) dfs(r);
postorder.push_back(current);
};
DFS 的功能
- 用某種方式走完整張圖
- 蛤?就這樣?
- DFS 功能看起來較少,但
- 通常 DFS 空間需求較低
- 相對好寫
- 之後會用 DFS 來作其他事喔
- DFS 功能看起來較少,但