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

例題

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 來作其他事喔

例題