Graph Theory

Lecturer: 22527 Brine

我是誰

22527 鄭竣陽
Brine
BrineTW#7355
  • 建中資訊參伍 學術長
  • 建中電研肆貳 學術
  • 地科讀書會 正則人
  • 為了排版走火入魔
  • 吃語法糖吃到蛀牙
  • 想學圖論所以當圖論講師
  • 絕對會忘記你是誰
  • 絕對不記得摯友有誰
  • 絕對不會放棄你除非你很機掰
Index[0]
Index[1]
Index[2]
Index[3]

警告!

  • 這次的講師很弱
    • 在講的時候可能不太會表達
    • 也有可能自己觀念有點奇怪
    • 在寫範例程式碼時可能疏忽
  • 如果上課時有不理解的地方,可以跟講師說
    • 他會努力講到讓你聽懂
  • 回家寫題目遇到問題可以丟到讀書會伺服器或是私訊講師

參考資料

Glossary

圖論的基本名詞

什麼是「圖」

  • 唯利是圖

什麼是「圖」

  • 唯利是圖
11
1
22
2
00
0
55
5
33
3
44
4

所以什麼是圖?

圖的表示

  • 頂點(vertex / vertices)
    • 常用 vv 來表示
    • 頂點的集合為 VV
  • 邊(edge)
    • 常用 e=(u, v) e = (u,\ v) 表示
    • 邊的集合為 EE
  • 圖(graph)
    • 圖就是點和邊的集合
    • 通常用 G(V, E) G(V,\ E) 表示

邊的屬性

  • 權重(weight)
  • 方向(direction)
    • 有向/無向(directed / undirected)
    • 如果 (u, v)(u,\ v)(v, u)(v,\ u) 所代表的是不同的邊,則說這兩個邊有向
  • 自環(loop)
    • 起點終點相同的邊,即 (v,v)(v, v)
  • 重邊(parallel edges)
    • 兩個一模一樣的邊

點的屬性

  • 權重(weight)
  • 度(degree)
    • 一個點連到的邊的數量
    • 入度/出度(indegree / outdegree)
      • 在有向圖之中分為進去和出去的邊

路徑相關

  • 路徑(path)
    • 一個頂點和邊交錯的序列,每條邊都要有連到左右兩個點
    • (vstart, e1, v1, e2, v2,..., en, vend)(v_{start},\ e_1,\ v_1,\ e_2,\ v_2,...,\ e_n,\ v_{end}) 表示
    • 行跡(trace)
      • 沒有重複的路徑
    • 簡單路徑(track)
      • 沒有重複頂點的路徑
    • 迴路(circuit)
      • 起點和終點為相同頂點的路徑
      • 環(cycle)
        • 只有起點和終點為相同頂點的迴路

路徑

00
0
11
1
33
3
22
2
44
4
77
7
66
6
55
5

行跡

00
0
11
1
33
3
22
2
44
4
77
7
66
6
55
5

簡單路徑

00
0
11
1
33
3
22
2
44
4
77
7
66
6
55
5

迴路

00
0
11
1
33
3
22
2
44
4
77
7
66
6
55
5

00
0
11
1
33
3
22
2
44
4
77
7
66
6
55
5

無向圖/有向圖

  • 只有無向邊或有向邊
00
0
11
1
33
3
22
2
44
4
00
0
11
1
33
3
22
2
44
4

帶點權/帶邊權

  • 也可以都帶(?
00
0
55
5
11
1
77
7
22
2
11
1
33
3
22
2
44
4
22
2
00
0
11
1
33
3
22
2
44
4
99
9
88
8
44
4
77
7

簡單圖 Simple Graph

  • 沒有重邊
  • 沒有自環
00
0
11
1
33
3
22
2
44
4
00
0
11
1
33
3
22
2
44
4

連通圖 Connected Graph

  • 如果所有點都是無向邊是否所有點都可以到任意一個點
00
0
11
1
33
3
22
2
44
4
00
0
11
1
33
3
22
2
44
4

弱連通/強連通 WC/SC

  • weakly / strongly connected
  • 是否要變成無向圖才能所有點都能抵達其他點
00
0
11
1
33
3
22
2
44
4
00
0
11
1
33
3
22
2
44
4

完全圖 Complete Graph

  • 每個可能的邊都存在的簡單圖
00
0
11
1
33
3
22
2

子圖 Subgraph

00
0
11
1
33
3
22
2
vi,eiGsub(V, E), vi,eiG(V, E)\forall v_i, e_i \in G_{sub}(V,\ E),\ v_i, e_i \in G(V,\ E)
\forall v_i, e_i \in G_{sub}(V,\ E),\ v_i, e_i \in G(V,\ E)
  • 若某圖的所有邊和點都屬於另一圖,則其為該圖之子圖
00
0
11
1
33
3
22
2

補圖 Complement

00
0
11
1
33
3
22
2
  • 兩圖點集相同
  • 邊集無交集且聯集等於所有可能的邊
00
0
11
1
33
3
22
2

Tree

  • 沒有環的無向連通圖
00
0
11
1
33
3
22
2
44
4
  • 會有多少邊?
  • 森林 forest
77
7
66
6
55
5

二分圖 Bipartite Graph

  • 可以分為兩個點集,沒有連接兩點集內點的邊
00
0
11
1
33
3
22
2
44
4
77
7
66
6
55
5
00
0
11
1
33
3
22
2
44
4
77
7
66
6
55
5

有向無環圖 DAG

  • directed acyclic graph
00
0
11
1
33
3
22
2
44
4

稀疏圖/稠密圖

EVlogV|E|\le|V|\log|V|
|E|\le|V|\log|V|
VlogVEV2|V|\log|V| \le |E| \le |V|^2
|V|\log|V| \le |E| \le |V|^2
  • sparse / dense

例題

Graph Storage

圖的儲存

鄰接矩陣 Adjacency Matrix

  • 將所有邊的可能用 O(V2)\mathcal{O}(|V|^2) 的陣列表示
G[i][j]={1, if (i,j)E0, if (i,j)EG[i][j] = \begin{cases} 1,\ if\ (i, j) \in E \\ 0,\ if\ (i, j) \notin E \end{cases}
G[i][j] = \begin{cases} 1,\ if\ (i, j) \in E \\ 0,\ if\ (i, j) \notin E \end{cases}
  • 將所有邊的可能用 O(V2)\mathcal{O}(|V|^2) 的陣列表示
G[i][j]={1, if (i,j)E0, if (i,j)EG[i][j] = \begin{cases} 1,\ if\ (i, j) \in E \\ 0,\ if\ (i, j) \notin E \end{cases}
G[i][j] = \begin{cases} 1,\ if\ (i, j) \in E \\ 0,\ if\ (i, j) \notin E \end{cases}
00
0
11
1
33
3
22
2
44
4
[0100010110010110110000100]\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}
\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

  • 將每個點有連到的點用記錄在一個清單上
00
0
11
1
33
3
22
2
44
4
0:{1}0:\text{\{1\}}
0:\text{\{1\}}
1:{0, 2, 3}1:\text{\{0, 2, 3\}}
1:\text{\{0, 2, 3\}}
2:{1, 3, 4}2:\text{\{1, 3, 4\}}
2:\text{\{1, 3, 4\}}
3:{1, 2}3:\text{\{1, 2\}}
3:\text{\{1, 2\}}
4:{2}4:\text{\{2\}}
4:\text{\{2\}}
\longrightarrow
\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);
        }
    );
}

比較

  • 在稠密圖用鄰接矩陣,稀疏圖用鄰接串列!
項目 鄰接矩陣 鄰接串列
空間複雜度
插入邊
查詢邊
枚舉邊
O(V2)\mathcal O(|V|^2)
\mathcal O(|V|^2)
O(1)\mathcal O(1)
\mathcal O(1)
O(1)\mathcal O(1)
\mathcal O(1)
O(V+E)\mathcal O(|V| + |E|)
\mathcal O(|V| + |E|)
O(1)\mathcal O(1)
\mathcal O(1)
O(E)\mathcal O(|E|)
\mathcal O(|E|)
O(V2)\mathcal O(|V|^2)
\mathcal O(|V|^2)
O(V+E)\mathcal O(|V| + |E|)
\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[00000100001000000100]h = 5, l = 4\\ \begin{bmatrix} 0000\\ 0100\\ 0010\\ 0000\\ 0100 \end{bmatrix}
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

  • 把邊丟到集合中

存樹

  • 指標
  • 陣列
11
1
33
3
22
2
44
4
77
7
66
6
55
5
  • 樹相關的會有更電的來講

例題

Graph Traversal

圖的遍歷

  我們現在有某個點突然有很多的水的一張圖,而我們現在想要計算出這張圖中的點何時會被水淹到,該怎麼做呢?

淹水 Floodfill

  我們現在有某個點突然有很多的水的一張圖,而我們現在想要計算出這張圖中的點何時會被水淹到,該怎麼做呢?

44
4
00
0
22
2
33
3
22
2
77
7
33
3
88
8
55
5
11
1
99
9
66
6

淹水 Floodfill

  我們現在有某個點突然有很多的水的一張圖,而我們現在想要計算出這張圖中的點何時會被水淹到,該怎麼做呢?

00
0
00
0
44
4
22
2
33
3
22
2
77
7
33
3
88
8
55
5
11
1
99
9
66
6

淹水 Floodfill

  我們現在有某個點突然有很多的水的一張圖,而我們現在想要計算出這張圖中的點何時會被水淹到,該怎麼做呢?

44
4
11
1
22
2
77
7
33
3
11
1
88
8
11
1
55
5
11
1
11
1
11
1
99
9
66
6
00
0
00
0

淹水 Floodfill

  我們現在有某個點突然有很多的水的一張圖,而我們現在想要計算出這張圖中的點何時會被水淹到,該怎麼做呢?

44
4
11
1
22
2
22
2
77
7
33
3
11
1
88
8
11
1
55
5
11
1
11
1
11
1
99
9
22
2
66
6
22
2
00
0
00
0

淹水 Floodfill

淹水 Floodfill

  我們現在有某個點突然有很多的水的一張圖,而我們現在想要計算出這張圖中的點何時會被水淹到,該怎麼做呢?

44
4
11
1
22
2
22
2
77
7
33
3
33
3
11
1
88
8
11
1
55
5
11
1
11
1
11
1
99
9
22
2
66
6
22
2
00
0
00
0

暴力作法

  • 每次確定每一格有沒有被淹到,有的話就更新他所有鄰居點
    • 每輪做 O(V)\mathcal O(|V|) 次檢查
    • 最多檢查 O(V)\mathcal O(|V|) 次(why?)
    • 時間複雜度 O(V2)\mathcal O(|V|^2)
  • ​花太多時間確定誰有資格溢出了!

小觀察

  • 只有剛被淹過的點有需要去往下淹
  • 每個點只會當最外圍一次
  • 可以開個下回合要溢出的點的清單!
  • 用個 queue 來記錄就好了
  • 這就是廣度優先搜尋/遍歷(Breadth-First Search / traversal)
  • 通常簡稱 BFS

複雜度?

  • 剛剛我們想了一個不錯的方法來處理這個問題
  • 那這個演算法的時間複雜度是什麼?
  • 所有點都只會進去/出來 queue 一次
    • O(V)\mathcal O(|V|) 完成所有操作?
  • 每個點都會有不同的邊數,怎麼估計複雜度?
    • 每個邊會被走到兩次
      • 用這條邊淹到一個點時
      • 下次操作淹回去失敗時
    • O(E)\mathcal O(|E|) 完成所有邊的操作
  • ​總複雜度 O(V+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 會長什麼樣子呢?
66
6
11
1
44
4
22
2
33
3
00
0
55
5
99
9
77
7
88
8

如果把 queue 換掉?

  • 換成 stack 會長什麼樣子呢?
66
6
11
1
44
4
22
2
33
3
00
0
00
0
55
5
99
9
77
7
88
8
0\\ \\ \\ 0\\
\\ \\ \\ 0\\

如果把 queue 換掉?

  • 換成 stack 會長什麼樣子呢?
66
6
11
1
44
4
22
2
11
1
33
3
00
0
00
0
55
5
99
9
77
7
88
8
0\\ \\ \\ 0\\
\\ \\ \\ 0\\
22
2

如果把 queue 換掉?

  • 換成 stack 會長什麼樣子呢?
66
6
11
1
22
2
44
4
22
2
11
1
33
3
00
0
00
0
55
5
99
9
77
7
88
8
0\\ \\ \\ 0\\
\\ \\ \\ 0\\
22
2
11
1

如果把 queue 換掉?

  • 換成 stack 會長什麼樣子呢?
66
6
33
3
11
1
22
2
44
4
22
2
11
1
33
3
00
0
00
0
55
5
99
9
77
7
88
8
0\\ \\ \\ 0\\
\\ \\ \\ 0\\
22
2
11
1
66
6

如果把 queue 換掉?

  • 換成 stack 會長什麼樣子呢?
66
6
33
3
11
1
22
2
44
4
22
2
11
1
33
3
00
0
00
0
99
9
77
7
88
8
55
5
33
3
0\\ \\ \\ 0\\
\\ \\ \\ 0\\
22
2
11
1
55
5

如果把 queue 換掉?

  • 換成 stack 會長什麼樣子呢?
66
6
33
3
11
1
22
2
22
2
11
1
33
3
00
0
00
0
99
9
77
7
88
8
55
5
33
3
0\\ \\ \\ 0\\
\\ \\ \\ 0\\
44
4
44
4
1\\ 1
\\ 1
22
2
11
1
55
5

如果把 queue 換掉?

  • 換成 stack 會長什麼樣子呢?
66
6
33
3
11
1
22
2
22
2
11
1
33
3
00
0
00
0
77
7
88
8
55
5
33
3
44
4
11
1
99
9
2\\ 2
\\ 2
0\\ \\ \\ 0\\
\\ \\ \\ 0\\
44
4
99
9

如果把 queue 換掉?

  • 換成 stack 會長什麼樣子呢?
66
6
33
3
11
1
22
2
22
2
11
1
33
3
00
0
00
0
77
7
55
5
33
3
44
4
11
1
99
9
22
2
88
8
22
2
0\\ \\ \\ 0\\
\\ \\ \\ 0\\
44
4
88
8

如果把 queue 換掉?

  • 換成 stack 會長什麼樣子呢?
66
6
33
3
11
1
22
2
22
2
11
1
33
3
00
0
00
0
55
5
33
3
44
4
11
1
99
9
22
2
88
8
22
2
77
7
33
3
0\\ \\ \\ 0\\
\\ \\ \\ 0\\
44
4
88
8
77
7

如果把 queue 換掉?

  • 換成 stack 會長什麼樣子呢?
66
6
33
3
11
1
22
2
22
2
11
1
33
3
00
0
00
0
55
5
33
3
44
4
11
1
99
9
22
2
88
8
22
2
77
7
33
3
0\\ \\ \\ 0\\
\\ \\ \\ 0\\
44
4
88
8
77
7

如果把 queue 換掉?

  • 換成 stack 會長什麼樣子呢?
66
6
33
3
11
1
22
2
22
2
11
1
33
3
00
0
00
0
55
5
33
3
44
4
11
1
99
9
22
2
88
8
22
2
77
7
33
3
0\\ \\ \\ 0\\
\\ \\ \\ 0\\
44
4
88
8
77
7

如果把 queue 換掉?

  • 換成 stack 會長什麼樣子呢?
66
6
33
3
11
1
22
2
22
2
11
1
00
0
00
0
55
5
33
3
44
4
11
1
99
9
22
2
88
8
22
2
77
7
33
3
33
3
11
1
0\\ \\ \\ 0\\
\\ \\ \\ 0\\
44
4
88
8
77
7
3\\ \\ \\ 3\\
\\ \\ \\ 3\\

如果把 queue 換掉?

  • 換成 stack 會長什麼樣子呢?
66
6
33
3
11
1
22
2
22
2
11
1
00
0
00
0
55
5
33
3
44
4
11
1
99
9
22
2
88
8
22
2
77
7
33
3
33
3
11
1
0\\ \\ \\ 0\\
\\ \\ \\ 0\\
44
4
88
8
77
7
3\\ \\ \\ 3\\
\\ \\ \\ 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 來作其他事喔

例題

Topological Sort

拓樸排序

什麼是「拓樸」

  • 我不會
  • 在圖論中
    • 只關心點的連接關係
    • 不關心邊的權重等
  • 有錯的話請電神糾正我 QQ
00
0
11
1
22
2
00
0
11
1
22
2

什麼是拓樸排序

  • 在有向圖中,我們會想要把點作輩分關係的排序
  • 準確點說一個拓樸順序(topological order)是:
    • 一個圖 G(V,E)G(V, E) 的頂點的排序 P=(p1,p2,...,pV)P = (p_1, p_2, ..., p_{|V|})
    • 且對於所有有向邊 (pi,pj)(p_i, p_j) 滿足 i<ji < j
    • 而做這件事情就叫做拓樸排序(topological sort)
  • 一張有向圖一定能被拓樸排序嗎?
    • 不一定!
    • 只要圖上有環,就沒辦法被拓樸排序!

為什麼要拓樸排序?

  • 有些時候我們在解決問題的時候需要關注點的先後順序
    • 從源頭往下做來推算累積下來的狀態
    • 從結尾回溯推出遊戲起始盤面的結果
    • ……
  • 透過獲得 VV 的拓樸順序,我們就可以確保轉移狀態時沒有問題!

如何拓樸排序?

  • 能夠被選為第一個點的點,入度一定為 0
  • 選完第一個元素之後,選所有他連到的點當下一個一定都沒有問題
    • 把這個元素的出度都視為不存在
  • 剩下的點不管選誰當下一個都和第一個沒有關係了
  • 將剩下的圖視為新的圖來解決!

演算法執行概念

  • 對於一張有向圖 G(V,E)G(V, E)
    1. 找到任意一個入度為 0 的點 pp
    2. 將其放入排序的第一格
    3. pp 和其出度移除,得到 GG'
    4. 求出 GG' 的拓樸順序,放在 pp 後面
  • 遞迴?
    • 真的需要嗎?

BFS 寫法

  1. 計算所有點的入度
  2. 並把入度為 0 的點全部推入一個 queue
  3. 從 queue 中取出首位元素 pp 放入排序結果
  4. pp 連到的點的入度減 1
    • 如果該點入度變為 0,將其推入 queue
  5. 重複直到結束

BFS 版程式碼

  • 這份程式的回傳值是對的嗎?
vector<int> topologicalSort(vector< vector<int> >& graph) {
    vector<int> indegree(graph.size());
    for (auto& v: graph) for (auto& n: v) indegree[n]++;

    vector<int> order;
    order.reserve(graph.size());

    queue<int> q;
    for (int i = 0; i < graph.size(); i++) {
        if (indegree[i] == 0) q.push(i);
    }

    while (!q.empty()) {
        auto& current = q.front();

        order.push_back(current);
        for (auto& n: graph[current]) {
            if (--indegree[n] == 0) q.push(n);
        }
    }

    return order;
}

如何判斷正確性

  • 如果 P<V|P| < |V|,答案顯然有錯
    • 為什麼會發生這種狀況呢?
      • 剩下的點入度皆不為 0
    • 怎麼會發生的?
      • 寫爛了
      • 有環存在於此有向圖
  • 如果 P=V|P| = |V| 呢?
    • ​只要符合條件一定合法
      • ​沒有環存在於此有向圖
    • ​拓樸順序有解 \longleftrightarrow 此圖有向無環

另外一種想法

  • 接下來會介紹另外一種拓樸排序的方法
    • 此方法用到了叫做「時間戳記」(time stamp)的概念
  • 只要會一種就能拓樸排序了我幹嘛學?
    • 之後會用到

時間戳記

  • 講完 BFS,接下來想當然就是要用 DFS 啊
  • 在 DFS 的過程中
    1. 進入一個點
    2. 對這個點進行 DFS
    3. 離開這個點
  • 如果紀錄進入和離開每一個點的時機?
00
0
(?,?)(?,?)
(?,?)
11
1
(?,?)(?,?)
(?,?)
44
4
(?,?)(?,?)
(?,?)
22
2
(?,?)(?,?)
(?,?)
55
5
(?,?)(?,?)
(?,?)
33
3
(?,?)(?,?)
(?,?)

時間戳記

  • 講完 BFS,接下來想當然就是要用 DFS 啊
  • 在 DFS 的過程中
    1. 進入一個點
    2. 對這個點進行 DFS
    3. 離開這個點
  • 如果紀錄進入和離開每一個點的時機?
00
0
(0,?)(0,?)
(0,?)
11
1
(?,?)(?,?)
(?,?)
44
4
(?,?)(?,?)
(?,?)
22
2
(?,?)(?,?)
(?,?)
55
5
(?,?)(?,?)
(?,?)
33
3
(?,?)(?,?)
(?,?)

時間戳記

  • 講完 BFS,接下來想當然就是要用 DFS 啊
  • 在 DFS 的過程中
    1. 進入一個點
    2. 對這個點進行 DFS
    3. 離開這個點
  • 如果紀錄進入和離開每一個點的時機?
00
0
(0,?)(0,?)
(0,?)
11
1
(1,?)(1,?)
(1,?)
44
4
(?,?)(?,?)
(?,?)
22
2
(?,?)(?,?)
(?,?)
55
5
(?,?)(?,?)
(?,?)
33
3
(?,?)(?,?)
(?,?)

時間戳記

  • 講完 BFS,接下來想當然就是要用 DFS 啊
  • 在 DFS 的過程中
    1. 進入一個點
    2. 對這個點進行 DFS
    3. 離開這個點
  • 如果紀錄進入和離開每一個點的時機?
00
0
(0,?)(0,?)
(0,?)
11
1
(1,?)(1,?)
(1,?)
44
4
(?,?)(?,?)
(?,?)
22
2
(2,?)(2,?)
(2,?)
55
5
(?,?)(?,?)
(?,?)
33
3
(?,?)(?,?)
(?,?)

時間戳記

  • 講完 BFS,接下來想當然就是要用 DFS 啊
  • 在 DFS 的過程中
    1. 進入一個點
    2. 對這個點進行 DFS
    3. 離開這個點
  • 如果紀錄進入和離開每一個點的時機?
00
0
(0,?)(0,?)
(0,?)
11
1
(1,?)(1,?)
(1,?)
44
4
(?,?)(?,?)
(?,?)
22
2
(2,3)(2,3)
(2,3)
55
5
(?,?)(?,?)
(?,?)
33
3
(?,?)(?,?)
(?,?)

時間戳記

  • 講完 BFS,接下來想當然就是要用 DFS 啊
  • 在 DFS 的過程中
    1. 進入一個點
    2. 對這個點進行 DFS
    3. 離開這個點
  • 如果紀錄進入和離開每一個點的時機?
00
0
(0,?)(0,?)
(0,?)
11
1
(1,4)(1,4)
(1,4)
44
4
(?,?)(?,?)
(?,?)
22
2
(2,3)(2,3)
(2,3)
55
5
(?,?)(?,?)
(?,?)
33
3
(?,?)(?,?)
(?,?)

時間戳記

  • 講完 BFS,接下來想當然就是要用 DFS 啊
  • 在 DFS 的過程中
    1. 進入一個點
    2. 對這個點進行 DFS
    3. 離開這個點
  • 如果紀錄進入和離開每一個點的時機?
00
0
(0,5)(0,5)
(0,5)
11
1
(1,4)(1,4)
(1,4)
44
4
(?,?)(?,?)
(?,?)
22
2
(2,3)(2,3)
(2,3)
55
5
(?,?)(?,?)
(?,?)
33
3
(?,?)(?,?)
(?,?)

時間戳記

  • 講完 BFS,接下來想當然就是要用 DFS 啊
  • 在 DFS 的過程中
    1. 進入一個點
    2. 對這個點進行 DFS
    3. 離開這個點
  • 如果紀錄進入和離開每一個點的時機?
00
0
(0,5)(0,5)
(0,5)
11
1
(1,4)(1,4)
(1,4)
44
4
(?,?)(?,?)
(?,?)
22
2
(2,3)(2,3)
(2,3)
55
5
(?,?)(?,?)
(?,?)
33
3
(6,?)(6,?)
(6,?)

時間戳記

  • 講完 BFS,接下來想當然就是要用 DFS 啊
  • 在 DFS 的過程中
    1. 進入一個點
    2. 對這個點進行 DFS
    3. 離開這個點
  • 如果紀錄進入和離開每一個點的時機?
00
0
(0,5)(0,5)
(0,5)
11
1
(1,4)(1,4)
(1,4)
44
4
(?,?)(?,?)
(?,?)
22
2
(2,3)(2,3)
(2,3)
55
5
(7,?)(7,?)
(7,?)
33
3
(6,?)(6,?)
(6,?)

時間戳記

  • 講完 BFS,接下來想當然就是要用 DFS 啊
  • 在 DFS 的過程中
    1. 進入一個點
    2. 對這個點進行 DFS
    3. 離開這個點
  • 如果紀錄進入和離開每一個點的時機?
00
0
(0,5)(0,5)
(0,5)
11
1
(1,4)(1,4)
(1,4)
44
4
(?,?)(?,?)
(?,?)
22
2
(2,3)(2,3)
(2,3)
55
5
(7,8)(7,8)
(7,8)
33
3
(6,?)(6,?)
(6,?)

時間戳記

  • 講完 BFS,接下來想當然就是要用 DFS 啊
  • 在 DFS 的過程中
    1. 進入一個點
    2. 對這個點進行 DFS
    3. 離開這個點
  • 如果紀錄進入和離開每一個點的時機?
00
0
(0,5)(0,5)
(0,5)
11
1
(1,4)(1,4)
(1,4)
44
4
(?,?)(?,?)
(?,?)
22
2
(2,3)(2,3)
(2,3)
55
5
(7,8)(7,8)
(7,8)
33
3
(6,9)(6,9)
(6,9)

時間戳記

  • 講完 BFS,接下來想當然就是要用 DFS 啊
  • 在 DFS 的過程中
    1. 進入一個點
    2. 對這個點進行 DFS
    3. 離開這個點
  • 如果紀錄進入和離開每一個點的時機?
00
0
(0,5)(0,5)
(0,5)
11
1
(1,4)(1,4)
(1,4)
44
4
(10,?)(10,?)
(10,?)
22
2
(2,3)(2,3)
(2,3)
55
5
(7,8)(7,8)
(7,8)
33
3
(6,9)(6,9)
(6,9)

時間戳記

  • 講完 BFS,接下來想當然就是要用 DFS 啊
  • 在 DFS 的過程中
    1. 進入一個點
    2. 對這個點進行 DFS
    3. 離開這個點
  • 如果紀錄進入和離開每一個點的時機?
00
0
(0,5)(0,5)
(0,5)
11
1
(1,4)(1,4)
(1,4)
44
4
(10,11)(10,11)
(10,11)
22
2
(2,3)(2,3)
(2,3)
55
5
(7,8)(7,8)
(7,8)
33
3
(6,9)(6,9)
(6,9)

時間戳記

  • 講完 BFS,接下來想當然就是要用 DFS 啊
  • 在 DFS 的過程中
    1. 進入一個點
    2. 對這個點進行 DFS
    3. 離開這個點
  • 如果紀錄進入和離開每一個點的時機?
00
0
(0,5)(0,5)
(0,5)
11
1
(1,4)(1,4)
(1,4)
44
4
(10,11)(10,11)
(10,11)
22
2
(2,3)(2,3)
(2,3)
55
5
(7,8)(7,8)
(7,8)
33
3
(6,9)(6,9)
(6,9)

觀察一下

  • 剛剛的動畫中,值得注意的是「離開戳記」
  • 對於兩個點 u,vu, v
    • uu 可以單向走到 vv,且 uu 先被拜訪
      • 離開 vv 後才會離開 uuuu 離開戳記較大
    • uu 可以單向走到 vv,且 vv 先被拜訪
      • 離開 vv 後才會碰到 uuuu 離開戳記較大
    • uuvv 可以互相走到
      • ​誰先走到誰的離開戳記就較大
  • ​透過離開戳記判斷順序!
00
0
(0,5)(0,5)
(0,5)
11
1
(1,4)(1,4)
(1,4)
55
5
(7,8)(7,8)
(7,8)
33
3
(6,9)(6,9)
(6,9)
44
4
(10,11)(10,11)
(10,11)
22
2
(2,3)(2,3)
(2,3)

DFS 程式碼

  • 這份程式碼其實是爛的!
    • 你能看出為什麼嗎?
vector<int> topologicalSort(vector< vector<int> >& graph) {
    vector<int> order;
    order.reserve(graph.size());

    vector<int> visited(graph.size());
    bool cyclic = false;
    function<void(int, int&)> dfs = [&](int now, int& s) {
        if (cyclic) return;

        visited[now] = s;
        for (auto& n: graph[now]) {
            if (visited[n] == s) {
                cyclic = true;
                return;
            }
            if (!visited[n]) dfs(n, s);
        }

        order.push_back(now);
    };

    for (int i = 0; i < graph.size(); i++) dfs(i, i);

    if (cyclic) return vector<int>(0);

    reverse(order.begin(), order.end());
    return order;
}

小細節

  • 不要耍毒瘤就不會有這種問題了!
function<void(int, const int&)> dfs
= [&](int now, const int s) {
    if (cyclic || visited[now]) return;

    visited[now] = s;
    for (auto& n: graph[now]) {
        if (visited[n] == s) {
            cyclic = true;
            return;
        }
        if (!visited[n]) dfs(n, s);
    }

    order.push_back(now);
};

for (int i = 0; i < graph.size(); i++) dfs(i, i+1);

例題

Disjoint Set

並查集

查詢和合併連通分量

  • 今天我希望有一個資料結構,支援以下操作:
    • 查詢元素 ee 所在的集合
    • 合併兩個給定的集合
  • 假設今天這是一個圖論的問題,那上述操作即為:
    • 查詢點 vv 所在的連通分量
    • 將兩連通分量連起來

怎麼做?

  • 查詢時,回傳該集合「代表元素」的編號
  • 只要找到代表元素就回傳

 

  • q(i)q(i) 回傳元素 ii 所在集合的代表元素
  • 假設一開始所有元素都是獨自形成一個集合,就是 q(i)=iq(i) = i
  • 合併集合時,從兩集合各選出一個元素 a,ba, b
  •  q(a)=bq(a) = b 就好?

修改   的定義

  • 剛剛那樣會爆掉,那我們該怎麼辦
  • 既然我們剛剛說初始條件 q(i)=iq(i) = i,那不如我們試試看照著做
    • 如果要「確定」找到的代表元素正確(唯一)要怎麼做
      • 剛才的方法可以使得連通分量中 q(i)=iq(i) = i 的元素唯一嗎?
        • 可以的話就這樣做吧!
q(i)q(i)
q(i)
vector<int> master(vertexCount);

int query(int i) {
    if (master[i] == i) return i;
    
    return query(master[i]);
}

合併集合

  • 看起來是什麼樣子?
00
0
11
1
33
3
22
2
44
4

合併集合

  • 看起來是什麼樣子?
00
0
11
1
33
3
22
2
44
4
  • 合併只要 O(1)\mathcal O(1)
    • 那查詢呢?

鍊狀的情況

  • 如果照我們的想法,但合併時變出一條鍊會怎樣?
00
0
11
1
33
3
22
2
44
4

鍊狀的情況

  • 如果照我們的想法,但合併時變出一條鍊會怎樣?
00
0
11
1
33
3
22
2
44
4
  • O(n)\mathcal O(n) 查詢,不太妙
    • 怎麼辦?

啟發式合併

  • 我們想要使樹的高度(深度)越低越好
  • 可以從合併的方法下手!
    • 如果我們開個紀錄「自己是根時樹的高度」的陣列呢
      • 合併時比較兩棵樹的高度
      • 將比較矮的接到比較高的下面
      • 這樣有多好?
    • 複雜度長怎樣
      • 讓最大樹高變成 hh 至少需要 2h12^h-1 次合併
        • 樹高不可能超過 logV\lceil \log|V| \rceil
      • 查詢的複雜度現在只剩 O(logV)\mathcal O(\log |V|)

複雜度分析

  • 是不是有人看不懂剛剛在幹嘛
00
0
11
1
22
2
33
3

複雜度分析

  • 是不是有人看不懂剛剛在幹嘛
00
0
11
1
33
3
22
2

複雜度分析

  • 是不是有人看不懂剛剛在幹嘛
00
0
11
1
33
3
22
2
44
4

複雜度分析

  • 是不是有人看不懂剛剛在幹嘛
00
0
11
1
33
3
22
2
44
4

複雜度分析

  • 是不是有人看不懂剛剛在幹嘛
00
0
11
1
33
3
22
2
44
4
55
5
77
7
66
6
88
8

複雜度分析

  • 是不是有人看不懂剛剛在幹嘛
00
0
11
1
33
3
22
2
44
4
55
5
77
7
66
6
88
8

複雜度分析

  • 是不是有人看不懂剛剛在幹嘛
00
0
11
1
33
3
22
2
44
4
55
5
77
7
66
6
88
8
  • 需要兩個高度為 hh 的樹合併,高度才會增加 1!
  • 任何兩不同高度的樹合併都不增加最大深度
  • VV 合併的深度屬於 O(logV)\mathcal O(\log|V|)

路徑壓縮

  • q(i)q(i) 現在是「找到代表元素為自己的元素後回傳」
    • 那我們能不能在遞迴時順便做些事情呢?
  • 在查詢時順便把路徑上每個元素的代表元素改成終點聽起來不錯吧
int query(int i) {
    return (master[i] == i ? i : query(master[i]));
}
int query(int i) {
    if (master[i] == i) return i;
    
    master[i] = query(master[i]);
    return master[i];
}

圖例

  • 剛才那樣有點抽象對吧
00
0
11
1
33
3
22
2
44
4
55
5

圖例

  • 剛才那樣有點抽象對吧
00
0
11
1
33
3
22
2
44
4
55
5

圖例

  • 剛才那樣有點抽象對吧
00
0
11
1
33
3
22
2
44
4
55
5

圖例

  • 剛才那樣有點抽象對吧
00
0
11
1
33
3
22
2
44
4
55
5

圖例

  • 剛才那樣有點抽象對吧
00
0
11
1
33
3
22
2
44
4
55
5

圖例

  • 剛才那樣有點抽象對吧
00
0
11
1
33
3
22
2
44
4
55
5

圖例

  • 剛才那樣有點抽象對吧
00
0
11
1
33
3
22
2
44
4
55
5

圖例

  • 剛才那樣有點抽象對吧
00
0
11
1
33
3
22
2
44
4
55
5

圖例

  • 剛才那樣有點抽象對吧
00
0
11
1
33
3
22
2
44
4
55
5
  • 查詢時每次多一個修改的動作,但以後查詢變輕鬆了!

快,還要更快

  • 光是剛才的路徑壓縮,就快到不行了
    • 查詢複雜度為 O(log(2+QV)V)\mathcal O(\log_{(2 + \frac{Q}{|V|})}|V|)
    • 我不會證明
  • 但其實還可以更快?!
    • 記得剛才的「啟發式合併」嗎?
    • 配合路徑壓縮的話複雜度可以再更低
    • 啟發式合併 + 路徑壓縮的複雜度只有 O(α(V))\mathcal O(\alpha(|V|))

Disjoint Set code

  • 只寫路徑壓縮通常就夠快了啦
struct DisjointSet {
    vector<int> master, depth;

    disjointSet(int vertexCount) {
        master.resize(vertexCount);
        depth.resize(vertexCount);

        for (int i = 0; i < vertexCount; i++) {
            master[i] = i;
            depth[i] = 1;
        }
    }

    int query(int i) {
        if (master[i] == i) return i;

        return master[i] = query(master[i]);
    }

    void merge(int a, int b) {
        a = query(a);
        b = query(b);

        if (a == b) return;

        if (depth[a] < depth[b]) swap(a, b);
        master[b] = a;
        depth[a] += depth[b];
    }
}; // 記得分號

例題

Lowest Common Ancestor

最低共同祖先

我發現樹論會講,這裡先講其中一種作法就好了

什麼是最低共同祖先

  • 要研究兩生物的親緣關係,我們會從演化樹找到兩生物分化的地方
D\text D
\text D
C\text C
\text C
B\text B
\text B
A\text A
\text A
LCA(A, B)\text{LCA(A, B)}
\text{LCA(A, B)}
LCA(A, D)\text{LCA(A, D)}
\text{LCA(A, D)}
LCA(C, D)\text{LCA(C, D)}
\text{LCA(C, D)}
  • 可是這樣看起來是最高欸
  • 但我們的根應該要在最上面,全資訊圈的樹都是那樣長的

資訊學的 LCA

  • 在一個有根樹上,我們一定可以找到任意兩點的 LCA

資訊學的 LCA

  • 在一個有根樹上,我們一定可以找到任意兩點的 LCA
00
0
55
5
11
1
44
4
22
2
77
7
66
6
33
3

資訊學的 LCA

  • 在一個有根樹上,我們一定可以找到任意兩點的 LCA
00
0
55
5
11
1
44
4
22
2
77
7
66
6
33
3

資訊學的 LCA

  • 在一個有根樹上,我們一定可以找到任意兩點的 LCA
00
0
55
5
11
1
44
4
22
2
77
7
66
6
33
3

資訊學的 LCA

  • 在一個有根樹上,我們一定可以找到任意兩點的 LCA
00
0
55
5
11
1
44
4
22
2
77
7
66
6
33
3

資訊學的 LCA

  • 在一個有根樹上,我們一定可以找到任意兩點的 LCA
00
0
55
5
11
1
44
4
22
2
77
7
66
6
33
3

資訊學的 LCA

  • 在一個有根樹上,我們一定可以找到任意兩點的 LCA
00
0
55
5
11
1
44
4
22
2
77
7
66
6
33
3
  • 要處理樹上兩點的問題很常需要用到 LCA
    • 像是兩點間的距離
  • 如何查詢 LCA?
    • 倍增法!

倍增法 binary lifting

  • 要用多少的砝碼才能夠湊出 [0,1000]Z[0, 1000]\cap\mathbb{Z} 的所有值呢
    • 只要十個就好!
    • 怎麼做?
      • 考慮二進制
        • (1000)10=(1111101000)2(1000)_{10} = (1111101000)_{2}
        • (225)10=(0011100001)2(225)_{10} = (0011100001)_{2}
      • 只需要 log2x\lceil \log_2 x\rceil 個砝碼就可以表示 [0,x]Z[0, x]\cap\mathbb{Z} 的所有值!
  • ​​找自己的 nn 倍祖先也是一樣
    • 只要 log2n\lceil \log_2 n\rceil2k2^k 倍祖先就可以 O(logn)\mathcal O(\log n) 查詢

要怎麼建祖先表

  • 我們不難發現,自己 kk 倍祖先的 kk 倍祖先就是自己的 2k2k 祖先
  • 我們只要有所有人的 2j2^j 倍祖先,就可以有所有人的 2j+12^{j + 1} 倍祖先
vector< vector<int> > ancestor;

for (int j = 1; j <= __lg(maxDepth); j++) {
    for (int i = 0; i < vertexCount; i++) {
        ancestor[i][j] = ancestor[ancestor[i][j - 1]][j - 1];
    }
}

查詢共同祖先

  • 我們現在有祖先表了,要怎麼用他?
  • 如果我們要查詢兩點 u,vu, v 的共同祖先,我們要:
    • 假設他們的共同祖先是他們的第 kk 祖先
    • 想辦法用二進制把 kk 表達出來?
    • 好像哪裡怪怪的?

查詢共同祖先

  • 我們現在有祖先表了,要怎麼用他?
  • 如果我們要查詢兩點 u,vu, v 的共同祖先,我們要:
    • 假設他們的共同祖先是他們的第 kk 祖先
    • 想辦法用二進制把 kk 表達出來?
    • 好像哪裡怪怪的?
    • 如果兩者深度不同,假設不成立!
  • 先讓深度較深的先向上走,走到同深度
  • 這樣就假設就成立了
  • 所以我們應該還要記錄節點的深度
00
0
55
5
11
1
44
4
22
2
77
7
66
6
33
3

DFS 邊紀錄深度

  • 把深度當成 visited 來用
vector<int> depth(vC);
function<void(int, int)> dfs = [&](int current, int d = 1) {
    depth[current] = d++;
    for (auto& n: graph[current]) {
        if (depth[n]) continue;

        dfs(n, d);
    }
};

LCA code

  • 把前面的東西也補上就完成了!
function<int(int, int)> lca = [&](int u, int v) {
    if (depth[u] < depth[v]) swap(u, v);
    if (depth[u] != depth[v]) {
        int dif = depth[u] - depth[v];

        for (int i = 0; dif > 0; i++) {
            if (dif & 1) u = ancestor[u][i];
            dif >>= 1;
        }
    }

    for (int i = __lg(maxDepth); i >= 0; i--) {
        if (ancestor[u][i] != ancestor[v][i]) {
            u = ancestor[u][i];
            v = ancestor[v][i];
        }
    }

    return ancestor[u][0];
};

完整的 LCA

  • 把剛剛所有東西接在一起
#include <bits/stdc++.h>

using namespace std;

int main() {
    int vertexCount, query;
    cin >> vertexCount >> query;

    vector< vector<int> > graph(vertexCount);
    int a, b;
    for (int i = 1; i < vertexCount; i++) {
        cin >> a >> b;
        graph[a].push_back(b);
        graph[b].push_back(a);
    }


    vector< vector<int> > ancestor(vertexCount, vector<int>(1));
    vector<int> depth(vertexCount);
    function<void(int, int)> dfs = [&](int current, int d) {
        depth[current] = d++;
        for (auto& n: graph[current]) {
            if (depth[n]) continue;
            
            ancestor[n][0] = current;
            dfs(n, d);
        }
    };

    dfs(0, 1);

    int maxDepth = 0;
    for (auto& n: depth) maxDepth = max(maxDepth, n);
    for (auto& v: ancestor) v.resize(__lg(maxDepth) + 1);

    for (int j = 1; j <= __lg(maxDepth); j++) {
        for (int i = 0; i < vertexCount; i++) {
            ancestor[i][j] = ancestor[ancestor[i][j - 1]][j - 1];
        }
    }

    function<int(int, int)> lca = [&](int u, int v) {
        if (depth[u] < depth[v]) swap(u, v);
        if (depth[u] != depth[v]) {
            int dif = depth[u] - depth[v];

            for (int i = 0; dif > 0; i++) {
                if (dif & 1) u = ancestor[u][i];
                dif >>= 1;
            }
        }

        if (u == v) return u;

        for (int i = __lg(maxDepth); i >= 0; i--) {
            if (ancestor[u][i] != ancestor[v][i]) {
                u = ancestor[u][i];
                v = ancestor[v][i];
            }
        }

        return ancestor[u][0];
    };

    while (query--) {
        cin >> a >> b;
        cout << lca(a, b) << '\n';
    }
}

其他的 LCA 求法

  • 在之後的樹論讀書會中:
    • 倍增法(複習)
    • 樹壓平
    • 樹鏈剖分
  • 在之後的離線讀書會中:
    • Tarjan's LCA 演算法

例題

Spanning Tree

生成樹

邊權和最小的連通子圖

  • 今天有一張連通圖 G(V,E)G(V, E),我們想要一張子圖 G(V,E)G'(V', E') 符合:
    • 點集 V=VV = V'
    • 邊權和 eEwe\displaystyle \sum_{e \in E'} w_e 最小
    • GG' 仍然連通
  • 在正常情況下(eE,we>0\forall e \in E, w_e > 0),該圖一定是一棵樹
    • 我們稱這棵樹為最小生成樹,Minimum Spanning Tree,MST
    • 什麼情況下可以不是樹?
    • 最小生成樹唯一嗎

Cycle Property

  • 環性質
  • 對於任何圖上的環 CC,邊權嚴格最大的邊 ee 不屬於最小生成樹
  • 用反證法來證明此性質:
    • 假設 ee 屬於最小生成樹
    • 刪除 ee 會使最小生成樹分裂成兩個子樹
    • 但環 CC 上剩下的邊必定存在一個 ee' 能連接兩個子樹
    • we>wew_e > w_{e'},此 MST 並非最小權重,矛盾!
  • 這個可以拿來幹嘛?
    • 等一下就知道了
00
0
55
5
11
1
44
4
22
2
66
6
33
3

Cut property

  • 🈹性質
  • 什麼是割?

Cut property

  • 🈹性質
  • 什麼是割?
00
0
11
1
33
3
22
2
44
4

Cut property

  • 🈹性質
  • 什麼是割?
00
0
11
1
33
3
22
2
44
4

Cut property

  • 🈹性質
  • 什麼是割?
00
0
11
1
33
3
22
2
44
4

Cut property

  • 🈹性質
  • 什麼是割?
00
0
11
1
33
3
22
2
44
4

Cut property

  • 🈹性質
  • 什麼是割?
    • 把一張圖的點分割為兩子集
    • 一個割會形成一個割集(cut-set)
    • 割集為跨越兩子點集的邊集
  • 割集中權重嚴格最小的邊屬於 MST
    • 跟剛才一樣,反證法
    • 若它不屬於,換成它一定更好
00
0
11
1
33
3
22
2
44
4

不嚴格怎麼辦

  • 如果剛才環或是割中,沒有邊權嚴格大於/小於所有邊的邊呢
    • 這個時候就是「平手」,選誰都一樣
    • 對於 cycle property,就是兩個邊都一定不會存在於某個 MST
      • 言下之意就是,會有超過一個 MST 存在而至少有一個沒有其中一個邊
    • 對於 cut property,就是兩個邊都一定會存在於某個 MST 中
00
0
55
5
11
1
44
4
22
2
66
6
33
3
77
7
77
7
11
1
11
1
55
5
33
3
44
4
99
9
11
1
22
2

不嚴格怎麼辦

  • 如果剛才環或是割中,沒有邊權嚴格大於/小於所有邊的邊呢
    • 這個時候就是「平手」,選誰都一樣
    • 對於 cycle property,就是兩個邊都一定不會存在於某個 MST
      • 言下之意就是,會有超過一個 MST 存在,而至少有一個沒有其中一個邊
    • 對於 cut property,就是兩個邊都一定能存在於某個 MST 中
00
0
55
5
11
1
44
4
22
2
66
6
33
3
77
7
77
7
11
1
11
1
55
5
33
3
44
4
99
9
11
1
22
2

不嚴格怎麼辦

  • 如果剛才環或是割中,沒有邊權嚴格大於/小於所有邊的邊呢
    • 這個時候就是「平手」,選誰都一樣
    • 對於 cycle property,就是兩個邊都一定不會存在於某個 MST
      • 言下之意就是,會有超過一個 MST 存在,而至少有一個沒有其中一個邊
    • 對於 cut property,就是兩個邊都一定能存在於某個 MST 中
00
0
55
5
11
1
44
4
22
2
66
6
33
3
77
7
77
7
11
1
11
1
55
5
33
3
44
4
99
9
11
1
22
2

不嚴格怎麼辦

  • 如果剛才環或是割中,沒有邊權嚴格大於/小於所有邊的邊呢
    • 這個時候就是「平手」,選誰都一樣
    • 對於 cycle property,就是兩個邊都一定不會存在於某個 MST
      • 言下之意就是,會有超過一個 MST 存在,而至少有一個沒有其中一個邊
    • 對於 cut property,就是兩個邊都一定能存在於某個 MST 中
00
0
55
5
11
1
44
4
22
2
66
6
33
3
77
7
77
7
11
1
11
1
55
5
33
3
44
4
99
9
11
1
22
2

不嚴格怎麼辦

  • 如果剛才環或是割中,沒有邊權嚴格大於/小於所有邊的邊呢
    • 這個時候就是「平手」,選誰都一樣
    • 對於 cycle property,就是兩個邊都一定不會存在於某個 MST
      • 言下之意就是,會有超過一個 MST 存在,而至少有一個沒有其中一個邊
    • 對於 cut property,就是兩個邊都一定能存在於某個 MST 中
00
0
55
5
11
1
44
4
22
2
66
6
33
3
77
7
77
7
11
1
11
1
55
5
33
3
44
4
99
9
11
1
22
2

不嚴格怎麼辦

  • 如果剛才環或是割中,沒有邊權嚴格大於/小於所有邊的邊呢
    • 這個時候就是「平手」,選誰都一樣
    • 對於 cycle property,就是兩個邊都一定不會存在於某個 MST
      • 言下之意就是,會有超過一個 MST 存在,而至少有一個沒有其中一個邊
    • 對於 cut property,就是兩個邊都一定能存在於某個 MST 中
00
0
55
5
11
1
44
4
22
2
66
6
33
3
77
7
77
7
11
1
11
1
55
5
33
3
44
4
99
9
11
1
22
2

不嚴格怎麼辦

  • 如果剛才環或是割中,沒有邊權嚴格大於/小於所有邊的邊呢
    • 這個時候就是「平手」,選誰都一樣
    • 對於 cycle property,就是兩個邊都一定不會存在於某個 MST
      • 言下之意就是,會有超過一個 MST 存在,而至少有一個沒有其中一個邊
    • 對於 cut property,就是兩個邊都一定能存在於某個 MST 中
00
0
55
5
11
1
44
4
22
2
66
6
33
3
77
7
77
7
11
1
11
1
55
5
33
3
44
4
99
9
11
1
22
2

不嚴格怎麼辦

  • 如果剛才環或是割中,沒有邊權嚴格大於/小於所有邊的邊呢
    • 這個時候就是「平手」,選誰都一樣
    • 對於 cycle property,就是兩個邊都一定不會存在於某個 MST
      • 言下之意就是,會有超過一個 MST 存在,而至少有一個沒有其中一個邊
    • 對於 cut property,就是兩個邊都一定能存在於某個 MST 中
00
0
55
5
11
1
44
4
22
2
66
6
33
3
77
7
77
7
11
1
11
1
55
5
33
3
44
4
99
9
11
1
22
2
  • 從最小的邊開始,依次嘗試加入 MST
  • 如果加入後不會形成環,它就屬於 MST
  • 為什麼這是對的?
    • 分類討論!

加入邊後形成環

  • 我們剛才說「從邊權小到邊權大開始試」
    • 那最後一個加入的邊所形成的環中一定是它邊權最大
    • 根據 cycle property,它一定不屬於某個 MST
      • 既然它不屬於某一個,我們就讓它不屬於我們這個吧!

加入邊後不形成環

  • 加入邊後不形成環,代表它使兩個連通分量連通
    • 它連接了兩個被分割的點集
    • 根據剛才的「由小到大」,它是屬於某個割集中最小權者
    • 根據 cut property,它一定屬於某一個 MST
      • 既然它屬於某一個,我們就讓它屬於我們這個吧!

我們證明它是對的了!

  • 那要怎麼實作?
  • 我們需要一個可以「判斷兩點是否連通」的資料結構
  • 使用剛剛學到的 disjoint set!
  • 它的複雜度會是什麼呢?
    • 需要對邊排序,複雜度 O(ElogE)\mathcal O(|E|\log|E|)
    • 使用並查集 E|E| 次,複雜度 O(Eα(E))\mathcal O(|E|\cdot \alpha(|E|))
    • 總複雜度 O(ElogE)\mathcal O(|E|\log|E|)

Kruskal Code

  • 先別急,等等還有另外一種方法
typedef pair<int, int> pii;
typedef pair<int, pii> pi_ii;

struct DisjointSet {
    vector<int> master;

    DisjointSet(int vertexCount) {
        master.resize(vertexCount);
        iota(master.begin(), master.end(), 0);
    }

    int query(int a) {
        if (a == master[a]) return a;

        return master[a] = query(master[a]);
    }

    bool connected(int a, int b) {
        return query(a) == query(b);
    }

    void merge(int a, int b) {
        master[query(a)] = master[b];
    }
};

int kruskal(vector<pi_ii>& minEdge, DisjointSet& ds) {
    int cost = 0;
    
    for (auto& [w, uv]: minEdge) {
        auto& [u, v] = uv;

        if (ds.connected(u, v)) continue;

        ds.merge(u, v);
        cost += w;
    }

    return cost;
}

int main() {
    ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

    int vertexCount, edgeCount;
    cin >> vertexCount >> edgeCount;
    DisjointSet ds(vertexCount);

    vector<pi_ii> minEdge(edgeCount);
    for (auto& [w, uv]: minEdge) {
        auto& [u, v] = uv;
        cin >> u >> v >> w;
	}
    
    sort(minEdge.begin(), minEdge.end());

    cout << kruskal(minEdge, ds) << '\n';
}
  • 一般是稱為 Prim's algorithm,但是是 Jarník 先發現的
  • 步驟:
    1. 選擇任意一點開始
    2. 選擇目前點集連出去所有邊中最小者
    3. 將該邊的另外一端加入當前點集
    4. 重複直到所有點都被加入當前點集

為什麼是對的

  • 🈹性質!

為什麼是對的

  • 🈹性質!
88
8
11
1
33
3
22
2
77
7
55
5
66
6
00
0
44
4
99
9

為什麼是對的

  • 🈹性質!
88
8
11
1
33
3
22
2
77
7
55
5
66
6
00
0
44
4
99
9
1313
13
22
2
22
2
55
5
22
2
77
7
99
9
22
2
66
6
88
8
22
2
33
3
44
4
77
7
11
1

為什麼是對的

  • 🈹性質!
88
8
11
1
33
3
22
2
77
7
55
5
66
6
00
0
44
4
99
9
1313
13
22
2
22
2
55
5
22
2
77
7
99
9
22
2
66
6
88
8
22
2
33
3
44
4
77
7
11
1

為什麼是對的

  • 🈹性質!
88
8
11
1
33
3
22
2
77
7
55
5
66
6
00
0
44
4
99
9
1313
13
22
2
22
2
55
5
22
2
77
7
99
9
22
2
66
6
88
8
22
2
33
3
44
4
77
7
11
1

為什麼是對的

  • 🈹性質!
88
8
11
1
33
3
22
2
77
7
55
5
66
6
00
0
44
4
99
9
1313
13
22
2
22
2
55
5
22
2
77
7
99
9
22
2
66
6
88
8
22
2
33
3
44
4
77
7
11
1

為什麼是對的

  • 🈹性質!
88
8
11
1
33
3
22
2
77
7
55
5
66
6
00
0
44
4
99
9
1313
13
22
2
22
2
55
5
22
2
77
7
99
9
22
2
66
6
88
8
22
2
33
3
44
4
77
7
11
1

為什麼是對的

  • 🈹性質!
88
8
11
1
33
3
22
2
77
7
55
5
66
6
00
0
44
4
99
9
1313
13
22
2
22
2
55
5
22
2
77
7
99
9
22
2
66
6
88
8
22
2
33
3
44
4
77
7
11
1

為什麼是對的

  • 🈹性質!
88
8
11
1
33
3
22
2
77
7
55
5
66
6
00
0
44
4
99
9
1313
13
22
2
22
2
55
5
22
2
77
7
99
9
22
2
66
6
88
8
22
2
33
3
44
4
77
7
11
1

為什麼是對的

  • 🈹性質!
88
8
11
1
33
3
22
2
77
7
55
5
66
6
00
0
44
4
99
9
1313
13
22
2
22
2
55
5
22
2
77
7
99
9
22
2
66
6
88
8
22
2
33
3
44
4
77
7
11
1

為什麼是對的

  • 🈹性質!
88
8
11
1
33
3
22
2
77
7
55
5
66
6
00
0
44
4
99
9
1313
13
22
2
22
2
55
5
22
2
77
7
99
9
22
2
66
6
88
8
22
2
33
3
44
4
77
7
11
1

為什麼是對的

  • 🈹性質!
88
8
11
1
33
3
22
2
77
7
55
5
66
6
00
0
44
4
99
9
1313
13
22
2
22
2
55
5
22
2
77
7
99
9
22
2
66
6
88
8
22
2
33
3
44
4
77
7
11
1

為什麼是對的

  • 🈹性質!
88
8
11
1
33
3
22
2
77
7
55
5
66
6
00
0
44
4
99
9
1313
13
22
2
22
2
55
5
22
2
77
7
99
9
22
2
66
6
88
8
22
2
33
3
44
4
77
7
11
1
  • 每次皆可視為以選取和未選取兩點集
    • 這個割的割集符合割性質!

Prim Code

  • 這樣的複雜度是 O(ElogE)\mathcal O(|E|\log|E|)
  • 可以用 Fibonacci heap 做到 O(E+VlogV)\mathcal O(|E| + |V|\log|V|)
typedef pair<int, int> pii;

int prim(vector< vector<pii> >& graph) {
    priority_queue<pii, vector<pii>, greater<pii> > pq;
    vector<int> currentMinCost(graph.size(), INT32_MAX);
    vector<bool> inside(graph.size(), false);
    int sum = 0;

    pq.push({0, 0});
    currentMinCost[0] = 0;

    while (!pq.empty()) {
        auto [w, u] = pq.top();
        pq.pop();

        if (inside[u]) continue;
        inside[u] = true;
        sum += w;

        for (auto& [w, v]: graph[u]) {
            if (!inside[v] && currentMinCost[v] > w) {
                currentMinCost[v] = w;
                pq.push({w, v});
            }
        }
    }

    return sum;
}

int main() {
    ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);

    int vertexCount, edgeCount;
    cin >> vertexCount >> edgeCount;

    vector< vector<pii> > graph(vertexCount);
    int u, v, w;
    for (int i = 0; i < edgeCount; i++) {
        cin >> u >> v >> w;
        graph[u].push_back({w, v});
        graph[v].push_back({w, u});
    }

    cout << prim(graph) << '\n';
}

例題

Shortest Path

最短路徑

最短路徑

  • 在生活中,我們常常遇到交通上的問題
  • 所以這大概是整堂資讀裡面跟生活最相關的了
  • 討論最短路徑的時候,會有不同的限制條件,例如:
    • 帶權/無權
    • 有負邊/無負邊
    • 有負環/無負環
    • 單點源/全點對
    • ……
  • 待會會介紹一些常見的最短路徑演算法

無權單點源最短路徑

  • 之前講過了!
  • 就是 BFS
  • 通常是類似走迷宮的題目
  • 如果是樹的話只要 DFS 就好
  • 複雜度 O(V+E)\mathcal O(|V| + |E|)

全點對最短路徑

  • 今天我們想要把整張圖的所有點對互相抵達的最短距離都算出來
    • 全點對最短路徑
  • 要怎麼做才好
  • 動態規劃!
    • dp[k][i][j]dp[k][i][j] 為使用前 kk 個點中繼,從 iijj 的最短距離
    • dp[k+1][i][j]=min(dp[k][i][j],dp[k][i][k+1]+dp[k][k+1][j])dp[k+1][i][j] = min(dp[k][i][j], dp[k][i][k+1] + dp[k][k+1][j])
    • 可以滾動,所以 kk 不用寫在狀態裡面
  • ​時間複雜度:O(V3)\mathcal O(|V|^3)
  • 好寫,但不能處理負邊
vector< vector<int> > d(vC, vector<int>(vC, 1e9 + 225));
for (int i = 0; i < vC; i++) d[i][i] = 0;
int u, v, w;
for (int i = 0; i < eC; i++) {
    cin >> u >> v >> w;
    d[u][v] = min(d[u][v], w);
}

for (int k = 0; k < vC; k++) {
    for (int i = 0; i < vC; i++) {
        for (int j = 0; j < vC; j++) {
            d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
        }
    }
}

單點源最短路徑

  • 講到單點源(single-source)最短路徑,就一定要提到一個名詞
  • 鬆弛(relaxation)
    • 定義從起點 ss 到點 ii 的最短距離為 did_i
    • 有什麼條件時可以使 dud_u 降低?
      • 存在一個邊 e=(u,v)e = (u, v)
      • du>dv+wed_u > d_v + w_e

單點源最短路徑

ss
s
vv
v
uu
u
22
2
2525
25
  • 講到單點源(single-source)最短路徑,就一定要提到一個名詞
  • 鬆弛(relaxation)
    • 定義從起點 ss 到點 ii 的最短距離為 did_i
    • 有什麼條件時可以使 dud_u 降低?
      • 存在一個邊 e=(u,v)e = (u, v)
      • du>dv+wed_u > d_v + w_e

單點源最短路徑

  • 講到單點源(single-source)最短路徑,就一定要提到一個名詞
  • 鬆弛(relaxation)
    • 定義從起點 ss 到點 ii 的最短距離為 did_i
    • 有什麼條件時可以使 dud_u 降低?
      • 存在一個邊 e=(u,v)e = (u, v)
      • du>dv+wed_u > d_v + w_e
  • 不覺得比較適合叫他「繃緊」嗎?
ss
s
vv
v
uu
u
22
2
2525
25
22
2
  • 剛才我們定義了 did_i
  • 只要存在一個邊 e=(u,v)e = (u, v) 滿足 dv>du+ewd_v > d_u + e_w 就得更新 dvd_v
  • 怎麼更新 dvd_v
  • 暴力來!
    • 先將所有點和原點距離設為 \infty(一個很大的數)
    • 只要遍歷 EE,將需要鬆弛的點鬆弛
      • 遍歷一次?
      • 要遍歷 EE 幾次才會是對的?

遍歷 EE 的次數

  • 剛剛我們已經知道遍歷 EE 若干次會是好的了
    • 總有一天他會是對的
    • 跑個 while 迴圈,沒辦法鬆弛就結束?
      • 不是不行
      • 那這樣做的複雜度會是?
  • 如果觀察一下,會發現在一張無負環的簡單圖:
    • 為什麼要「無負環」?
    • 一條簡單路徑最多只有 V1|V| - 1 條邊
      • 只要超過 V1|V| - 1 條邊就會有環
    • 最多只要鬆弛 V1|V| - 1 次!
    • 時間複雜度 O(VE)\mathcal O(|V||E|)

Bellman-Ford Code

  • 看起來超笨
  • 其實只要發現沒有鬆弛就可以跳出
struct Edge {
    int u, v, w;
};

vector<int> Bellman_Ford(int vertexCount, int source, vector<Edge>& edge) {
    vector<int> distance(vertexCount, INT32_MAX);

    distance[source] = 0;

    for (int i = 1; i < vertexCount; i++) {
        for (auto& [u, v, w]: edge) {
            distance[u] = min(distance[u], distance[v] + w);
            distance[v] = min(distance[v], distance[u] + w);
        }
    }

    return distance;
}

佇列最佳化

  • 一直比較不能再鬆弛的點好笑嗎
  • 只要跑上次有被鬆弛到的點就好了!
  • 這個做法在中國被叫做 Shortest Path Faster Algorithm
    • 謝囉
  • ​​​所以要怎麼實作「只嘗試鬆弛上次鬆弛過的點」
    • 把起點放到 queue 中
    1. 從 queue 中取出 uu
    2. 用有 uu 的邊去鬆弛其他點
    3. 有被鬆弛到的全部丟入 queue
    4. 重複直到 queue 沒有元素
  • 這個做法得用鄰接串列!

SPFA 的複雜度

  • 這個演算法的複雜度感覺小很多了呢
    • 究竟進步了多少呢
    • O(VE)O(VE)\mathcal O(|V||E|) \longrightarrow \mathcal O(|V||E|)
  • 不是,這不是完全沒有進步嗎?
  • 實際上運行起來可不是這樣
    • 對於一個隨機生成的圖,他的期望複雜度是 O(V+E)\mathcal O(|V|+|E|)
    • 快的跟鬼一樣
    • 不過注意,這裡指的是「隨機生成」時
    • 現在比賽已經有開始卡這種做法的趨勢,不一定能用了

SPFA Code

  • 雖然他已經沒了,但還是看一下
typedef pair<int, int> pii;

vector<int> SPFA(vector< vector<pii>& graph, int source) {
    vector<int> distance(graph.size(), INT32_MAX);
    distance[source] = 0;

    queue<int> q;
    vector<bool> inQueue(graph.size(), 0);
    q.push(source);

    while (!q.empty()) {
        auto v = q.front();
        q.pop();
        inQueue[v] = 0;

        for (auto& [u, w]: graph[v]) {
            distance[u] = min(distance[u], distance[v] + w);

            if (inQueue[u]) continue;
            q.push(u);
            inQueue[u] = 1;
        }
    }

    return distance;
}
  • 假設圖上沒有負邊
    • 沒有負邊可以有什麼特性?
    • 如果 dud_u 已經被最小化?
      • uu 來鬆弛別人的結果 dvdud'_v \ge d_u
    • 取當前距離最小的點 uu 來鬆弛所有人!
  • 演算法長怎樣?
    1. 從圖上距離最小的點 uu 開始,標記已使用
    2. 將所有該點連到的點鬆弛
    3. 以未被使用過最小的點繼續,直到沒有點

複雜度分析

  • 可以把 Dijkstra 的過程分成兩個部分
    • 找到目前距離最小且未被用過的點
    • 修改該點連到的點之距離
  • 複雜度為 O(ETmodify+VTgetmin)\mathcal O(|E| \cdot T_{modify} + |V| \cdot T_{get \min})
  • 幾種常見實作
    • 自平衡二元樹 O((E+V)logE)\mathcal O((|E| + |V|) \log |E|)
    • 暴力做線性搜 O(E+V2)\mathcal O(|E| + |V|^2)
    • Fibonacci heap O(E+VlogV)\mathcal O(|E| + |V| \log |V|)

Dijkstra Code

typedef pair<int, int> pii;

vector<int> Dijkstra(vector< vector<pii> >& graph, int source) {
    vector<int> distance(graph.size(), INT32_MAX);
    distance[source] = 0;

    vector<bool> visited(graph.size(), 0);
    priority_queue<pii, vector<pii>, greater<pii> > pq;
    pq.push({0, source});

    while (!pq.empty()) {
        auto [w, u] = pq.top();
        pq.pop();

        if (visited[u]) continue;
        visited[u] = 1;

        for (auto& [w, v]: graph[u]) {
            if (distance[v] <= distance[u] + w) continue;

            distance[v] = distance[u] + w;
            pq.push({distance[v], v});
        }
    }

    return distance;
}

例題

Connectivity

連通性

連通性

  • 如何判斷一張圖是否連通
    • 簡單的 BFS / DFS 都能處理
  • 但除了連通與否,連通的強度也是一大重點

連通性

  • 如何判斷一張圖是否連通
    • 簡單的 BFS / DFS 都能處理
  • 但除了連通與否,連通的強度也是一大重點
  • 如果中間的關鍵點沒了就不連通了
    • 越高的容錯率越好
  • 點連通度
    • 最少要拔掉多少點使得圖不連通
  • 邊連通度
    • 最少要拔掉多少邊使得圖不連通

連通度的重要性

  • 現實生活中,我們常常需要連通度不太差的網路
  • 如果供電系統壞掉一條電線或電線杆就停電,非常不好
    • 一般情況下我們會希望能建構出一個連通度 2\ge 2 的網路

連通度的重要性

  • 現實生活中,我們常常需要連通度不太差的網路
  • 如果供電系統壞掉一條電線或電線杆就停電,非常不好
    • 一般情況下我們會希望能建構出一個連通度 2\ge 2 的網路

連通度的重要性

  • 現實生活中,我們常常需要連通度不太差的網路
  • 如果供電系統壞掉一條電線或電線杆就停電,非常不好
    • 一般情況下我們會希望能建構出一個連通度 2\ge 2 的網路

連通度的重要性

  • 現實生活中,我們常常需要連通度不太差的網路
  • 如果供電系統壞掉一條電線或電線杆就停電,非常不好
    • 一般情況下我們會希望能建構出一個連通度 2\ge 2 的網路

連通度的重要性

  • 現實生活中,我們常常需要連通度不太差的網路
  • 如果供電系統壞掉一條電線或電線杆就停電,非常不好
    • 一般情況下我們會希望能建構出一個連通度 2\ge 2 的網路

連通度的重要性

  • 現實生活中,我們常常需要連通度不太差的網路
  • 如果供電系統壞掉一條電線或電線杆就停電,非常不好
    • 一般情況下我們會希望能建構出一個連通度 2\ge 2 的網路
  • 但有時候就是會遇到連通度只有 11 的網路
    • 每個點或每個邊被拔掉都會使圖不連通嗎?
    • 誰被拔掉會使圖不連通?

割點 Cut Vertex

  • 還記得什麼是「割」嗎
    • 使點集分割為兩個子集
    • 割點也是一樣的意思
      • 把割點拔掉就可以使圖不連通

割點 Cut Vertex

  • 還記得什麼是「割」嗎
    • 使點集分割為兩個子集
    • 割點也是一樣的意思
      • 把割點拔掉就可以使圖不連通

割點 Cut Vertex

  • 還記得什麼是「割」嗎
    • 使點集分割為兩個子集
    • 割點也是一樣的意思
      • 把割點拔掉就可以使圖不連通

割點 Cut Vertex

  • 還記得什麼是「割」嗎
    • 使點集分割為兩個子集
    • 割點也是一樣的意思
      • 把割點拔掉就可以使圖不連通
  • 又被稱為關節點 articulation vertex
  • 要怎麼求出所有的割點呢?

暴力作法

  • 要怎麼求出一個點 vv 是不是割點呢?
    • 根據定義,把他去掉後圖片不連通的話,該點即為割點
    • 那我們就照定義做
      • 選一個點 vv,拔除之
      • 對圖做一次 DFS,判斷沒有經過 vv 能否遍歷整張圖
      • 對所有點做以上操作
    • O(V)\mathcal O(|V|) 窮舉點,O(V+E)\mathcal O(|V| + |E|) DFS
      • 總複雜度 O(V2+VE)\mathcal O(|V|^2 + |V||E|)

DFS Tree

  • 在討論連通度時,我們有一個強大的工具,DFS Tree(DFST)
  • 在 DFS 走過所有能走的點時,碰到的邊可以分為以下幾種:
    • 樹邊(tree edge)
      • DFS 時候碰到新的點用的邊
      • 從祖先連到其子代
    • 返祖邊(back edge):從子孫連到祖先
    • 前向邊(forward edge):從祖先連到非子代的子孫
    • 交錯邊(cross edge):跨越兩顆樹的邊

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧
  • 那如果換成無向圖呢?

DFS Tree 如何分類邊

  • 我們就照剛才的定義跑,然後為那些邊著色吧
  • 那如果換成無向圖呢?
    • 無向圖只有樹邊跟返祖邊,我們先來做無向圖吧

如何判斷誰是割點

  • 可以看得出來,這些點是割點

如何判斷誰是割點

  • 這樣好像看不出來什麼性質,移動一下好了
  • 可以看得出來,這些點是割點

如何判斷誰是割點

  • 一棵樹中除了葉都是割點
    • 拔除 vv 使圖被分成 deg(v)\deg(v)
  • 在 DFS 樹中呢?
    • vv 的任意子孫不能連回祖先
    • 沒有返祖邊

DFS 樹的根節點 rr

  • 只要子孫都能連回祖先,自己就不是割點
  • 對於 rr,自己沒有祖先可言
    • 子孫不可能連回祖先
  • 只要 deg(r)>1\deg(r) > 1rr 即為割點

利用割點性質

  • 只要符合剛才性質的,就是割點
  • 那我們照定義,對每個節點的子孫(O(V)\mathcal O(|V|)
    • 嘗試不經過該節點回到該節點的祖先(O(V+E)\mathcal O(|V| + |E|)
    • 單點複雜度 O(V2+VE)\mathcal O(|V|^2 + |V||E|)
      • 比剛剛還糟糕
      • 勢必得要加速演算法不然我教幹嘛

一位傳奇人物

  • Robert Endre Tarjan
    • 1986 圖靈獎
  • 貢獻
    • 並查集
    • 離線 LCA 演算法
    • 連通性演算法
    • splay tree
    • Fibonacci heap
  • 你今晚的夢魘

Tarjan 的想法

  • 剛剛我們的問題有:
    • 每次判定子節點連回祖先太久
    • 每個節點的子節點太多
  • 所以他就想到我們可以:
    • 定義一個函數 low(v)\text{low}(v) 回傳 vv 不經過親代能走到的最低深度
    • 只要 low(v)<depth(u)\text{low}(v) < \text{depth}(u) 就代表 vv 不通過 uu 也能走回祖先
    • 如果維護成功,就可以均攤複雜度
  • 除此之外,在無向圖中:
    • uu 的子代 vv 的子樹只需存在一點 ww 使得 low(w)<depth(u)\text{low}(w) < \text{depth}(u)
    • 只要查詢 vv 就好,均攤只要 O(1)\mathcal O(1)

如何計算 low(v)\text{low}(v)

  • 一個點 vv 如果能不靠祖先連到深度為 dd 的點,代表:
    • 自己能有邊能連到深度為 dd 的點
    • 自己的子孫能連到深度為 dd 的點
  • low(v)\text{low}(v) 能從子代轉移!
    • ​DFS 時記錄然後遞迴完子代後比較就好!
    • 複雜度只有 O(V+E)\mathcal O(|V| + |E|)

Tarjan's Cut Vertex

  • 記錄深度太麻煩了,不如我們記錄 DFS 入點時的順序吧!
vector<int> low(vC);
vector<int> preorder(vC);
vector<bool> isCutVertex(vC);

int counter = 0;
function<void(int, int)> dfs = [&](int u, int last) {
    low[u] = preorder[u] = ++counter;

    int child = 0;

    for (auto& v: graph[u]) {
        if (v == last) continue;

        if (preorder[v]) { // is back edge
            low[u] = min(low[u], preorder[v]);
        } else {
            dfs(v, u);

            if (last >= 0 && low[v] >= preorder[u]) {
                isCutVertex[u] = true;
            }

            child++;
            low[u] = min(low[u], low[v]);
        }
    }

    if (last < 0 && child > 1) isCutVertex[u] = true;
};

dfs(0, -1);

為什麼 order(i)\text{order(i)} 是好的

  • 相信他就好你管那麼多
  • 在建 DFS Tree 的時候,有以下兩種情況:
    • 往下遍歷時,碰到還沒有走訪過的點
      • 一般的樹邊
      • 連到的全部都是自己的子代
      • 該點的前序 (進入戳記) 一定大於自己
    • 往下遍歷時,碰到已經被走訪過的點
      • 一定是連到自己直系祖先的返祖邊
        • 如果連到不是自己的直系祖先?

Bridge

  • 我們已經討論完關鍵點了
  • 現在我們想要探討一個連通圖的關鍵邊
  • 拔掉就會使圖不連通的邊

Bridge

  • 我們已經討論完關鍵點了
  • 現在我們想要探討一個連通圖的關鍵邊
  • 拔掉就會使圖不連通的邊

Bridge

  • 我們已經討論完關鍵點了
  • 現在我們想要探討一個連通圖的關鍵邊
  • 拔掉就會使圖不連通的邊
  • 跟割點有什麼關係?

Bridge

  • 我們已經討論完關鍵點了
  • 現在我們想要探討一個連通圖的關鍵邊
  • 拔掉就會使圖不連通的邊
  • 跟割點有什麼關係?
  • 還是不知道?

還是 Tarjan 的想法

  • 我們把剛才那張圖變成它的 DFST
  • 我們把剛才那張圖變成它的 DFST

還是 Tarjan 的想法

還是 Tarjan 的想法

  • 我們把剛才那張圖變成它的 DFST
  • 對於一個點 vv
    • 若其子樹裡沒有返祖邊能連到 vv 的祖先 uu 或以上
    • (u,v)(u, v) 是一座橋
  • 一樣用 low(v)\text{low}(v) 函數處理
    • 改成 low(v)order(u)\text{low}(v) \le \text{order}(u)

Tarjan's Bridge

  • 重邊需要另外看,如果不是簡單圖要記得喔
vector<int> low(vC);
vector<int> order(vC);
set<pii> isBridge;

int counter = 0;
function<void(int, int)> dfs = [&](int current, int last) {
    low[current] = order[current] = ++counter;

    for (auto& v: graph[current]) {
        if (v == last) continue;

        if (order[v]) { // is back edge
            low[current] = min(low[current], order[v]);
            // a back edge always forms a cycle
        } else {
            dfs(v, current);
            low[current] = min(low[current], low[v]);

            if (low[v] > order[current]) {
                isBridge.insert({current, v});
            }
        }
    }
};

dfs(0, -1);

例題

Components

連通分量

連通分量的定義

  • 若在一張圖 GG 上存在一個連通子圖 GG',則 GG'GG 的連通分量

連通分量的定義

  • 若在一張圖 GG 上存在一個連通子圖 GG',則 GG'GG 的連通分量

連通分量的定義

  • 若在一張圖 GG 上存在一個連通子圖 GG',則 GG'GG 的連通分量

連通分量的定義

  • 若在一張圖 GG 上存在一個連通子圖 GG',則 GG'GG 的連通分量

連通分量的定義

  • 若在一張圖 GG 上存在一個連通子圖 GG',則 GG'GG 的連通分量
  • 若連通分量無法再連到其他點,則此連通分量極大(maximal)
  • 通常我們預設討論的連通分量皆為極大連通分量

雙連通分量

  • 我們剛剛在把圖上的割點和橋找出來了
  • 現在我們可以把圖分隔成若干個區塊,中間以割點或橋相隔
  • 這些區塊被稱為雙連通分量(biconnected components, BCC)
    • 有些時候是:
      • 點雙連通分量稱為 biconnected components
        • 拔掉任何邊、點仍連通的連通分量
      • 邊雙連通分量稱為 bridge connected components
        • 拔掉任何邊仍連通的連通分量
    • 或是 2-vertex / edge-connected components
  • 雙連通分量有什麼用?

把圖「縮起來」

  • 邊雙連通分量

把圖「縮起來」

  • 邊雙連通分量
88
8
11
1
33
3
22
2
77
7
66
6
55
5
00
0
44
4
99
9

把圖「縮起來」

  • 邊雙連通分量
88
8
11
1
33
3
22
2
77
7
66
6
55
5
00
0
44
4
99
9

把圖「縮起來」

  • 邊雙連通分量
88
8
11
1
33
3
22
2
77
7
66
6
55
5
00
0
44
4
99
9

把圖「縮起來」

  • 邊雙連通分量
66
6
55
5
00
0
12391239
1239
487487
487

把圖「縮起來」

  • 點雙連通分量

把圖「縮起來」

  • 點雙連通分量
88
8
11
1
33
3
22
2
77
7
66
6
55
5
44
4
99
9
00
0

把圖「縮起來」

  • 點雙連通分量
88
8
11
1
33
3
22
2
77
7
66
6
55
5
44
4
99
9
00
0

把圖「縮起來」

  • 點雙連通分量
88
8
11
1
33
3
22
2
77
7
66
6
55
5
00
0
44
4
99
9

把圖「縮起來」

  • 點雙連通分量
88
8
11
1
33
3
22
2
77
7
66
6
55
5
00
0
44
4
99
9

把圖「縮起來」

  • 點雙連通分量
  • 看起來沒有縮欸
22
2
55
5
44
4
99
9
12391239
1239
00
0
66
6
487487
487

縮完點之後可以幹嘛

  • 有的時候我們不會喜歡一張有環的圖,複雜的圖很難被處理
  • 但是如果今天要處理的是一棵樹呢?
    • 只要把任何一張圖縮起來,他就會變成一棵樹(或森林)!
  • 我們可以透過找出連通分量來使問題變得可做
    • 要用割點或橋來分割就看題目要求
  • 要怎麼找連通分量呢?

邊雙連通分量

  • 找邊雙連通分量要做的事情就是不能走橋
  • 那我們就把橋都標記起來,然後 DFS
    • DFS 的時候若該邊是橋就不往下走
  • 成功了欸
    • 標記然後 DFS 應該不用教吧
  • 那點雙連通分量也是一樣嗎?

點雙連通分量

  • 橋雙連通分量是「由點為主體的集合」
  • 點雙連通分量是「由邊為主體的集合」
    • 這兩者有什麼最大的區別?

點雙連通分量

  • 橋雙連通分量是「由點為主體的集合」
  • 點雙連通分量是「由邊為主體的集合」
    • 這兩者有什麼最大的區別?
11
1
33
3
22
2
55
5
00
0
44
4

點雙連通分量

  • 橋雙連通分量是「由點為主體的集合」
  • 點雙連通分量是「由邊為主體的集合」
    • 這兩者有什麼最大的區別?
22
2
44
4
12341234
1234
00
0
55
5

點雙連通分量

  • 橋雙連通分量是「由點為主體的集合」
  • 點雙連通分量是「由邊為主體的集合」
    • 這兩者有什麼最大的區別?
  • 點雙連通分量不能輕鬆把點標記成不可通行
    • 因為一個點可以存在於兩個點雙連通分量
      • 誰?
      • 割點!
  • 這樣的話還能用跟剛才類似的方法嗎?
    • 可以!
    • 但很麻煩
    • 既然這樣,我們不如來想想別的作法吧

人生而懶惰

  • 我們做了一次 DFS,把 DFS 樹找出來
  • 接著我們對某些點 DFS,算出所有點雙連通分量
    • 可是做兩次 DFS 好麻煩,要寫不一樣的函式
    • 也許只要做一次?
  • 我們其實可以只做一次 DFS 來求得所有的點雙連通分量!
    • 在 DFS 離開一個點時做一點判定
    • 若該點是割點,把其子孫中沒有 BCC 的邊指派一個 BCC

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

這樣是對的嗎

  • 相信他就對了?
    • 根不管是否為割點

Vertex BCC Code

struct Edge {
    int a, b;

    Edge(int u = -1, int v = -1): a(u), b(v) {}
};

vector< vector<int> > graph;
vector<int> low, preorder;
vector<int> isCutVertex;
stack<Edge> s;

vector<int> bccId;
vector< vector<int> > bcc;

int counter = 0;
int bccCounter = 0;

void dfs(int u, int p) {
    low[u] = preorder[u] = ++counter;
    int child = 0;

    for (auto& v: graph[u]) {
        Edge e(u, v);

        if (v == p) continue;

        s.push(e);

        if (preorder[v]) {
            low[u] = min(low[u], preorder[v]);
            continue;
        }

        child++;
        dfs(v, u);
        low[u] = min(low[u], low[v]);

        if (low[v] >= preorder[u]) {
            isCutVertex[u] = 1;
            ++bccCounter;
            Edge temp;

            do {
                temp = s.top(), s.pop();

                if (bccId[temp.a] != bccCounter) bcc[bccCounter].push_back(temp.a);
                if (bccId[temp.b] != bccCounter) bcc[bccCounter].push_back(temp.b);

                bccId[temp.a] = bccId[temp.b] = bccCounter;
            } while (temp.a != u || temp.b != v);
        }

    }

    if (p <= 0 && child == 1) {
        isCutVertex[u] = 0;
    }
}

怎麼縮圖?

  • 其實直接照定義縮可能會長出很麻煩的東西
  • 有個資料結構叫做封鎖割樹圓方樹(block-cut tree)
    • 把同一個點雙連通分量改成:
      • 所有分量內的邊消失
      • 全部連到一個虛點(自己創的點),也就是方點
    • 這樣的話就一樣有樹的性質了
    • 看要求再把資訊放在圓點或方點上
  • 自己研究,我懶得放 code

回到邊雙連通分量

  • 剛剛我們討論完點雙連通分量一次求完的方法
  • 會發現其實邊雙連通分量也是一樣的概念
  • 而且其實邊雙連通比較好求?
  • 把 stack 裡面存的東西從邊換成點,判割點換成判定橋

Edge BCC Code

vector< vector<int> > graph;
vector<int> low, preorder;
stack<int> s;

vector<int> bccId;
vector< vector<int> > bcc;

int counter = 0;
int bccCounter = 0;

void dfs(int u, int p) {
    low[u] = preorder[u] = ++counter;
    s.push(u);

    for (auto& v: graph[u]) {
        if (v == p) continue;

        if (preorder[v]) {
            low[u] = min(low[u], preorder[v]);
            continue;
        }

        dfs(v, u);
        low[u] = min(low[u], low[v]);

        if (low[v] > preorder[u]) {
            ++bccCounter;
            int w;

            do {
                w = s.top(), s.pop();
                bccId[w] = bccCounter;
            } while (w != u);
        }

    }

    if (p < 0) {
        ++bccCounter;
        int w;
        while (!s.empty()) {
            w = s.top(), s.pop();
            bccId[w] = bccCounter;
        }
    }
}

有向圖上的連通分量

  • 我們終於要來講我們逃離很久的有向圖了!
  • 我們在討論有向圖的連通性的時候,會討論以下兩種分量
    • 強連通分量(strongly connected component)
      • 對於任意兩點 u,vu, v,存在 uvu \rightarrow v路徑
      • 注意不用「拔掉一個點或邊仍然成立」,不用雙連通
    • 弱連通分量(weakly connected component)
      • 對於任意兩點 u,vu, v,存在 uvu \rightarrow v vuv \rightarrow u 的路徑
      • 注意弱連通分量的定義和弱連通圖不太一樣
        • ​為什麼?
        • 我的解釋:「這種定義應用比較多」

求強連通分量的意義

  • 一個強連通分量會使我們沒辦法好好轉移狀態

求強連通分量的意義

  • 一個強連通分量會使我們沒辦法好好轉移狀態

求強連通分量的意義

  • 一個強連通分量會使我們沒辦法好好轉移狀態

求強連通分量的意義

  • 一個強連通分量會使我們沒辦法好好轉移狀態

求強連通分量的意義

  • 一個強連通分量會使我們沒辦法好好轉移狀態
    • 縮點之後變成一張有向無環圖(DAG)!
    • 只要是 DAG 就可以拓樸排序了

Tarjan's SCC

  • 對,還是他,每次都是他
  • 應該知道要用什麼了吧
    • 沒錯!就是 DFS 樹
    • 但是現在,我們要面對 DFS 樹的每一種邊了!
      • 樹邊
      • 返祖邊
      • 前向邊
      • 交錯邊
    • 怎麼辦?
      • 不怎麼辦,就正常處理多判一點條件就好了

樹邊 vs. 前向邊

  • 戳到樹邊就是一定代表進入點時為首次入點嗎?
    • 不是,有可能前向邊先戳到了
      • 那怎麼辦?
      • 不怎麼辦!
    • uu 先被樹邊戳到,low(u)\text{low}(u) 已經轉移了
      • 就把它用 visited 判定,然後正常來就好
    • uu 先被前向邊戳到呢?
      • ​在樹邊再次戳到時,一樣轉移就好
    • ​樹邊和前向邊不會產生影響!

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

返祖邊 vs. 交錯邊

  • 返祖邊跟交錯邊同樣有機會戳到比自己輩分大的點
    • 怎麼區分兩者?
    • 交錯邊會從祖先另外一個子樹戳過來
      • 如果他們不是同一個 SCC,那就不轉移
        • 如果戳到的點已經被分派 SCC 了,那它一定是交錯邊

Tarjan's SCC Code

  • 用 scc[i] 判斷交錯邊是否要更新
#include <bits/stdc++.h>

using namespace std;

vector<int> tarjan(vector< vector<int> >& graph) {
    vector<int> preorder(graph.size());
    vector<int> low(graph.size());
    vector<int> scc(graph.size());
    stack<int> s;

    int counter = 0;
    int sccId = 0;
    function<void(int)> dfs = [&](int u) {
        preorder[u] = low[u] = ++counter;
        s.push(u);

        for (auto& v: graph[u]) {
            if (scc[v]) continue;

            if (preorder[v]) low[u] = min(low[u], preorder[v]);
            if (!preorder[v]) {
                dfs(v);
                low[u] = min(low[u], low[v]);
            }
        }

        if (low[u] == preorder[u]) {
            ++sccId;
            int v;
            do {
                v = s.top(), s.pop();
                scc[v] = sccId;
            } while (u != v);
        }
    };

    for (int i = 0; i < graph.size(); i++) if (!preorder[i]) dfs(i);

    return scc;
}

另一位連通超人

  • Sambasiva Rao Kosaraju
  • 這個人想到了另外一種算強連通的方法
    • 太神奇了,他不是 Tarjan 欸
    • 但是他沒有發表該論文
    • 但他不是 Tarjan 欸
  • 讓我們來看看他怎麼想的吧!

把邊倒過來的圖

  • 在談 Kosaraju 的算法的時候,就要先談一個重要的東西
    • 反圖(transpose / converse / reverse graph)
      • 對於一個圖 G(V,E)G(V, E),若有一個圖:
        • 和它有相同的點集
        • 所有的邊 (u,v)(u, v) 都有能一一對應的邊 (v,u)(v, u)
      • 則稱這張圖為 GG 的反圖,記為 GG'GT,GRG^T, G^R \cdots
    • 反圖可以拿來幹嘛?

反圖的性質

  • 我們把一張圖和它的反圖放在一起,看看它們之間的關係吧

反圖的性質

  • 我們把一張圖和它的反圖放在一起,看看它們之間的關係吧

反圖的性質

  • 我們把一張圖和它的反圖放在一起,看看它們之間的關係吧

反圖的性質

  • 我們把一張圖和它的反圖放在一起,看看它們之間的關係吧
    • 反圖和原圖具有相同的 SCC!
    • 若在原圖中,存在一條 uuvv 的路徑
      • 反圖也存在一條 uuvv 的路徑 \leftrightarrow u,vu, v 在同一個 SCC 中

Kosaraju's SCC

  • 對一張有向圖做「偽」拓樸排序
    • 也就是「將圖 DFS 一次,並記錄其後序(離開戳記)」
  • 依照偽拓樸排序的結果,將點由後序大到小在反圖上 DFS
    • 給所有當前點開始走沒有 SCC 的點同一個新的 SCC
  • 做完了!
    • 真的假的?
    • 對,就這樣,超級好寫
    • 但是時間執行起來比 Tarjan 稍微久了一點點
  • 對於沒辦法馬上了解 Tarjan 的人,學習 Kosaraju 比較方便
  • 甚至有些人只記 Kosaraju's SCC,確保自己會 SCC

Kosaraju's SCC Code

  • 就跟你說了,短的跟鬼一樣
void dfs(int current, vector<int>& postorder, vector<bool>& visited, vector< vector<int> >& graph) {
    visited[current] = 1;

    for (auto& v: graph[current]) {
        if (visited[v]) continue;
        dfs(v, postorder, visited, graph);
    }

    postorder.push_back(current);
}

void sfd(int current, vector<int>& scc, const int id, vector< vector<int> >& hparg) {
    scc[current] = id;

    for (auto& v: hparg[current]) {
        if (scc[v]) continue;
        sfd(v, scc, id, hparg);
    }
}

把東西包成函式處理

  • 你會覺得比較長是因為我把建反圖放在裡面
vector<int> kosaraju(vector< vector<int> >& graph) {
    vector<bool> visited(graph.size());
    vector<int> postorder;
    postorder.reserve(graph.size());

    function<void(int)> dfs = [&](int u) {
        if (visited[u]) return;

        for (auto& v: graph[u]) {
            visited[v] = true;
            dfs(v);
        }

        postorder.push_back(u);
    };

    for (int u = 0; u < graph.size(); u++) dfs(u);


    vector< vector<int> > hparg(graph.size());
    for (int u = 0; u < graph.size(); u++) {
        for (auto& v: graph[u]) hparg[v].push_back(u);
    }

    vector<int> scc(graph.size());
    int sccCounter = 0;

    function<void(int)> sfd = [&](int u) {
        scc[u] = sccCounter;

        for (auto& v: hparg[u]) {
            if (scc[v]) continue;
            sfd(v);
        }
    };
	
    reverse(postorder.begin(), postorder.end());
    for (auto& u: postorder) {
        if (!scc[u]) ++sccCounter, sfd(u);
    }

    return scc;
}

Kosaraju 的特色

  • 比較好寫
  • 相對直觀
  • 不是 Tarjan
  • 常數比較大
  • 不是 Tarjan

2SAT2-\text{SAT}

  • 在資訊科學中,有個著名的問題,kk 可滿足性問題
    • 布林變數(boolean variable):
      • v=1¬v=0v = 1 \leftrightarrow \neg v = 0
      • v=0¬v=1v = 0 \leftrightarrow \neg v = 1
    • 文字(literal):l=v / ¬vl = v\ /\ \neg v
    • 子句(clause):c=(l1l2l3lk)c = (l_1 \vee l_2 \vee l_3 \vee \cdots \vee l_k)
    • 合取範式(conjunctive normal form, CNF):
      • 符合形如 c1c2cnc_1 \land c_2 \land \cdots \land c_n 的布林算式
    • 析取範式(disjunctive normal form, DNF):
      • \vee\land 互換的 CNF
  • ​​kSATk-\text{SAT} 就是求解子句長度皆為 kk 之 CNF 的問題

一開始

  • 我們先從 1SAT1-\text{SAT} 開始吧
  • 子句長度為 11 的 CNF 是什麼?
    • 形如 l1l2lnl_1 \land l_2 \land \cdots \land l_n
    • 例: a¬bc¬d\ a \land \neg b \land c \land \neg d
  • 不就是把所有東西都設成 true 就好嗎
    • 還有檢查有沒有 li¬lil_i \land \neg l_i 存在
    • 沒了,就這樣
  • 阿你是在期待什麼

回到 2SAT2-\text{SAT}

  • 所以這要怎麼解
    • 我都把它放在圖論了,應該跟圖論有點關係吧?
  • 我們先想一下在一個子句 c=(ab)c=(a \vee b) 中,有什麼性質吧
    • c=1c = 1 時,b=1b = 1¬a=1\neg a = 1 的必要條件
    • b=1b = 1¬a=1\neg a = 1 的充分條件嗎?
      • ​不是!
    • 如果 c1c2cn=1c_1 \land c_2 \land \cdots \land c_n = 1,則 ci=1c_i = 1
      • ci=(ai,bi)c_i = (a_i,\,b_i)
        • ¬ab\neg a \rightarrow b
        • ¬ba\neg b \rightarrow a

把狀態化成圖

  • 剛剛狀態都化成箭頭了,不要不知道這是有向圖欸

把狀態化成圖

  • 剛剛狀態都化成箭頭了,不要不知道這是有向圖欸
  • 然後呢?
  • 多一些狀態好了,例如 (a¬b)(bc)(¬a¬c)(b¬d)(a \vee \neg b)\land(b \vee c)\land(\neg a \vee \neg c)\land(b \vee \neg d)
aa
a
bb
b
¬a\neg a
\neg a
¬b\neg b
\neg b

把狀態化成圖

  • 剛剛狀態都化成箭頭了,不要不知道這是有向圖欸
  • 然後呢?
  • 多一些狀態好了,例如 (a¬b)(bc)(¬a¬c)(c¬d)(a \vee \neg b)\land(b \vee c)\land(\neg a \vee \neg c)\land(c \vee \neg d)
aa
a
bb
b
¬a\neg a
\neg a
¬b\neg b
\neg b
cc
c
dd
d
¬c\neg c
\neg c
¬d\neg d
\neg d

什麼情況會無解

  • aa¬a\neg a 同時需要成立的時候

什麼情況會無解

  • aa¬a\neg a 同時需要成立的時候
  • (a¬b)(b¬c)(¬a¬b)(bc)(a \vee \neg b)\land(b \vee \neg c)\land(\neg a \vee \neg b)\land(b \vee c)
aa
a
¬a\neg a
\neg a
bb
b
¬b\neg b
\neg b
cc
c
¬c\neg c
\neg c

什麼情況會無解

  • aa¬a\neg a 同時需要成立的時候
  • (a¬b)(b¬c)(¬a¬b)(bc)(a \vee \neg b)\land(b \vee \neg c)\land(\neg a \vee \neg b)\land(b \vee c)
    • ​當 aa¬a\neg a 在同一個 SCC 中時!
aa
a
¬a\neg a
\neg a
bb
b
¬b\neg b
\neg b
cc
c
¬c\neg c
\neg c

如果不是強連通?

  • 不在同一個強連通分量的話,就一定可以構造解

如果不是強連通?

  • 不在同一個強連通分量的話,就一定可以構造解
  • (a¬b)(b¬c)(ab)(¬b¬c)(a \vee \neg b)\land(b \vee \neg c)\land(a \vee b)\land(\neg b \vee \neg c)
aa
a
¬a\neg a
\neg a
bb
b
¬b\neg b
\neg b
cc
c
¬c\neg c
\neg c

如果不是強連通?

  • 不在同一個強連通分量的話,就一定可以構造解
  • (a¬b)(b¬c)(ab)(¬b¬c)(a \vee \neg b)\land(b \vee \neg c)\land(a \vee b)\land(\neg b \vee \neg c)
  • 從沒有被需求的開始
    • 反過來拓樸排序!
aa
a
¬a\neg a
\neg a
bb
b
¬b\neg b
\neg b
cc
c
¬c\neg c
\neg c

課後練習

  • 都會 1-SAT 和 2-SAT 了吧
    • 相信大家都會數學歸納法
  • 請回家解 3-SAT,寫完跟我對答案
  • 補充參考資料

例題

Thank you

但我不是你們的偶像

建中資讀 5th 圖論

By Brine

建中資讀 5th 圖論

迷圖羔陽

  • 1,186