# 【資工考研】DSA: Graph (1) BFS & DFS
1. Graph Representations
2. BFS & DFS
3. Shortest Path Problem
4. Minimum Spanning Tree
5. Articulation Point
6. Flow Network
## About Graph
一個圖 (Graph) $G$ 定義為 $G = (V, E)$,其中 $V$ 代表頂點 (vertices) 之集合; $E$ 代表邊 (edges) 之集合
兩頂點 $v_i$, $v_j$ 若相連,表示為 $(v_i, v_j)$
圖中邊集合 $E$ 中元素若為無序對,則稱 $G$ 為無向圖 (Undirected Graph);反之,若為有序對,則稱 $G$ 為有向圖 (Directed Graph):
- 無向圖中與某頂點 $v$ 相連的邊數稱為該頂點之 degree ($\mathrm{deg}(v)$)
- 有向途中自某頂點 $v$ 出發與其他頂點相連之邊數稱為該頂點之 out-degree;自其他頂點出發與 $v$ 相連之邊數稱為該頂點之 in-degree
我們定義一條自 $v_0$ 到 $v_k$ 之 path 為一序列 $v_0, v_1,\cdots, v_k$
此序列中任相鄰之兩頂點 $v_i$, $v_j$ 均存在邊 $(v_i, v_j)$ 使之相連
- 若在 path 中每一個頂點均只出現一次,則此 path 為一 simple path
- Cycle 為起終點相同頂點之 path
給定 $G = (V,E)$,稱兩頂點 $v_i, v_j \in V$ 為 reachable 若且唯若存在一條從 $v_i$ 到 $v_j$ 之 path
- 若 $G$ 中任取兩頂點均 reachable 則稱此圖為連通的 (connected)
- 若 $G$ 中任取兩頂點均存在兩條以上互斥的 path 使之 reachable 則稱此圖為雙連通 (biconnected)
- 若移除 $G$ 中某頂點 $v$ 可使得 $G$ 不連通,則稱 $v$ 為切點 (cut point)
$G = (V,E)$ 之子圖 $H = (V',E')$ 亦為一圖,滿足 $V'\subseteq V$ 且 $E'\subseteq E$
## Graph Representations
1. Adjacency Matrix
2. Adjacency List
3. Adjacency multi-list
4. Incidence Matrix
5. Index + Array
給定 $G = (V,E)$ 為有 $n$ 個頂點、 $m$ 個邊之圖,那麼寫程式時該如何表達圖呢?
### Adjacency Matrix
用一個 $n\times n$ 的二維陣列 `A`,若 $v_i$, $v_j$ 為 reachable 則 `A[i,j] == 1`;反之,則 `A[i,j] == 0`
若把二維陣列看作矩陣,則無向圖之 adjacency matrix 必為 symmetric matrix
Adjacency matrix 需要 $O(n^2)$ 的空間,適用於邊數多的圖 (dense graph)
```cpp!
std::vector<std::vector<int>> adjMatrix;
```
### Adjacency List
用一個長度 `n` 的陣列,其中每格代表 $G$ 中對應的頂點,陣列元素裝該與頂點相連的頂點所構成的 linked list
需要 $O(n + m)$ 的空間,適合邊少的圖 (sparse graph)
```cpp!
std::vector<std::list<int>> adjList;
```
雖然我上面 adjacency matrix 是給 `vector<std::vector<int>> ` 而 adjacency list 我是給 `vector<std::list<int>>`
但其實通常在 C++ 中要表示 adjacency list 還是更習慣使用 `vector<std::vector<int>>`
畢竟每列 vector 長度可變,我們只要裝填元素的邏輯是清楚的就好
### Adjacency Multilist
很像 adjacency list ,不過我們對長度 `n` 的陣列裝的東西有所不同,改試著裝邊 (邊節點):
<table style="margin-left: auto;margin-right: auto;">
<tr>
<td>Edge Node (可有可無)</td>
<td>V1</td>
<td>V2</td>
<td>Link for V1</td>
<td>Link for V2</td>
</tr>
</table>
```cpp!
// Forward declaration
struct VertexNode;
struct EdgeNode;
struct EdgeNode
{
int v1;
int v2;
EdgeNode* linkV1;
EdgeNode* linkV2;
EdgeNode(int v1, int v2) : v1(v1), v2(v2), linkV1(nullptr), linkV2(nullptr) {}
};
struct VertexNode
{
int vertex;
EdgeNode* edge;
VertexNode(int v) : vertex(v), edge(nullptr) {}
};
std::vector<VertexNode*> adjMultilist
```
### Incidence Matrix
有向圖 $G$ 的 incidence matrix $M$ 是一個 $n \times m$ 的矩陣滿足:
$$
M[i][j] =
\begin{cases}
1 & \text{if vertex } v_i \text{ is incident to edge } e_j \text{ and the edge is directed away from } v_i, \\
-1 & \text{if vertex } v_i \text{ is incident to edge } e_j \text{ and the edge is directed toward } v_i, \\
0 & \text{if vertex } v_i \text{ is not incident to edge } e_j.
\end{cases}
$$
如果是無向圖:
$$
M[i][j] =
\begin{cases}
1 & \text{if vertex } v_i \text{ is incident to edge } e_j, \\
0 & \text{if vertex } v_i \text{ is not incident to edge } e_j.
\end{cases}
$$
### Index + Array
陣列前 $n$ 項是 index 用來表示陣列中從那開始是記錄與該頂點相鄰的頂點,後面則全都紀錄相鄰頂點
很少考,但考出來你要記得上述規則,不然你會看不出圖長怎樣
## BFS & DFS
接下來是圖論中相當頻繁使用的兩個演算法,用在圖之遍歷:
1. Breadth-First Search (廣度優先搜尋)
2. Depth-First Search (深度優先搜尋)
在演算法中,因為它有要說明別的東西,所以多了些標示及定義,實際上像是 LeetCode 刷題時,則如資料結構中那樣直接寫
### BFS
假設 $G = (V,E)$ 為一個 graph, $s\in V$ 為一個 source vertex ,則 $\forall u\in V$:
1. $u.\pi$ 為 $u$ 之 predecessor (breadth-first tree 中之 parent),$s.\pi =$ `nil` (在 BFS 中, $u$ 是被 $u.\pi$ 所發現的)
2.
$$
u.\mathrm{color} =
\begin{cases}
\mathrm{white}\text{, if } u \text{ is undiscovered.} \\
\mathrm{gray}\text{, if } u \text{ is discovered, but } \exists v \text{, a neighbor of } u \text{, which is undiscovered.} \\
\mathrm{black}\text{, if } u \text{ and all of its neighbors are discovered.} \\
\end{cases}
$$
3. $u.\mathrm{d}$ 為 BFS 所求出,自 $s$ 到 $u$ 之距離
4. $\delta (s,v)$ 為自 $s$ 到 $v$ 所需經過之最少邊數,$\forall v \in V$
5. $\mathrm{adj}\lbrack u \rbrack = \left\{ v | (u,v)\in E \right\}$
```cpp!
enum class Color
{
WHITE,
GRAY,
BLACK,
};
struct Graph
{
int num;
struct Vertex
{
int d;
Vertex* pi;
Color color;
};
std::vector<std::vector<int>> adjList;
std::vector<Vertex> vertices;
Graph(int n) : num(n), adjList(n), vertices(n) {}
void addEdge(int u, int v)
{
adjList[u].push_back(v);
adjList[v].push_back(u); // Remove this for directed graphs
}
const std::vector<int>& neighbors(int u) const noexcept
{
return adjList[u];
}
};
void BFS(Graph &g, int s)
{
for (int i = 0; i < g.num; ++i)
{
g.vertices[i].d = INT_MAX;
g.vertices[i].pi = nullptr;
g.vertices[i].color = Color::WHITE;
}
g.vertices[s].d = 0;
g.vertices[s].pi = nullptr;
g.vertices[s].color = Color::GRAY;
std::queue<int> q;
q.push(s);
while (!q.empty())
{
const int u = q.front();
q.pop();
for (int v : g.neighbors(u))
{
// If v is undiscovered
if (g.vertices[v].color == Color::WHITE)
{
g.vertices[v].color == Color::GRAY;
g.vertices[v].d = g.vertices[u].d + 1;
g.vertices[v].pi = &g.vertices[u];
q.push(v);
}
}
// After processing all neighbors, mark u as fully explored (black)
g.vertices[u].color = Color::BLACK;
}
}
```
Time complexity: $O(\left| V \right| + \left| E \right|)$
($\because$ 我們使用 adjacency list 來表示圖,如果是 adjacency matrix,則會是 $O(\left| V \right|^2)$)
BFS 只能找出所有與 $s$ 為 reachable 的頂點,即 $s$ 所在之 component 中的所有頂點
所以要注意,依此定義不見得能遍歷圖中所有頂點
也因此, BFS 可以用來判斷 $G$ 是否為連通圖
#### Diameter
假設 $u$, $v$ 為 $G$ 中距離最遠的兩頂點,則該兩點之距離即為 diameter ($\max\limits_{u,v\in V}\delta (u,v)$)
在這邊,可以延伸到兩件事:
1. 在 tree 上找 diameter (即 tree 上找 longest path) 存在 linear time algorithm 可解
2. 在一般圖上找 longest path 為 NP-Complete problem ,並不好解
關於 tree 上找 diameter 的演算法可參考下列:
```cpp!
int treeDiameter(Graph &tree)
{
// Perform BFS from an arbitrary vertex (e.g. vertex 0)
BFS(tree, 0);
auto compare = [](const Graph::Vertex& a, const Graph::Vertex& b)
{
return a.d < b.d;
};
// Find the vertex with the largest d value
int t = std::distance(tree.vertices.begin(), std::max_element(tree.vertices.begin(), tree.vertices.end(), compare));
BFS(tree, t);
int u = std::distance(tree.vertices.begin(), std::max_element(tree.vertices.begin(), tree.vertices.end(), compare));
return tree.vertices[u].d;
}
```
### DFS
與 BFS 不同的是,對於搜尋到的頂點 $u$ ,我們會紀錄其發現時間 $u.\mathrm{d}$ ,接著對其在遞迴樹上的 child 繼續遞迴搜尋,直到後代都被搜尋到才結束對 $u$ 的的搜尋,並且紀錄該結束時間 $u.\mathrm{f}$
$$
u.\mathrm{color} =
\begin{cases}
\mathrm{white}\text{, if } u \text{ is undiscovered.} \\
\mathrm{gray}\text{, if } u \text{ is discovered, but } \exists v \text{, a descendant of } u \text{, which is undiscovered.} \\
\mathrm{black}\text{, if } u \text{ and all of its descendants are discovered.} \\
\end{cases}
$$
也由此可知,在此定義下的 DFS 能遍歷 $G$ 中所有的頂點
```cpp!
enum class Color
{
WHITE,
GRAY,
BLACK,
};
struct Graph
{
int num;
struct Vertex
{
int d;
int f;
Vertex* pi;
Color color;
};
std::vector<std::vector<int>> adjList;
std::vector<Vertex> vertices;
Graph(int n) : num(n), adjList(n), vertices(n) {}
void addEdge(int u, int v)
{
adjList[u].push_back(v);
adjList[v].push_back(u); // Remove this for directed graphs
}
};
// Forward declaration
void DFSVisit(Graph &g, int u, int &t);
void DFS(Graph &g)
{
for (auto &u : g.vertices)
{
u.color = Color::WHITE;
u.pi = nullptr;
u.d = 0;
u.f = 0;
}
int t = 0;
for (int u = 0; u < g.num; ++u)
{
if (g.vertices[u].color == Color::WHITE)
DFSVisit(g, u, t);
}
}
void DFSVisit(Graph &g, int u, int &t)
{
g.vertices[u].color = Color::GRAY;
g.vertices[u].d = ++t;
for (int v : g.adjList[u])
{
if (g.vertices[v].color == Color::WHITE)
{
g.vertices[v].pi = &g.vertices[u];
DFSVisit(g, v, t);
}
}
g.vertices[u].color = Color::BLACK;
g.vertices[u].f = ++t;
}
```
Time complexity: $O(\left| V \right| + \left| E \right|)$
> [!NOTE]
> 雖然上面的 code 是基於無向圖,但我們接著的討論會先基於有向圖
以此定義執行完 DFS ,我們可以根據 depth-first forest 將 $G$ 中所有 $(u, v)\in E$ 分為以下四類:
1. Tree edge: if $v.\pi = u$, then $(u, v)$ is a tree edge
2. Back edge: if $v$ is an ancestor of $u$ in the depth-first forest (including self-loops)
3. Forward edge: if $v$ is a descendant of $u$ in the depth-first forest, but $v.\pi \neq u$
4. Cross edge: otherwise
依據我們 DFS 執行完後的 $G$ ,此時顏色就派上用場了
$$
\forall (u, v)\in E
\begin{cases}
\text{If } v.\mathrm{color} = \mathrm{white} \text{, then it's a tree edge} \\
\text{If } v.\mathrm{color} = \mathrm{gray} \text{, then it's a back edge} \\
\text{If } v.\mathrm{color} = \mathrm{black} \text{ and } u.\mathrm{d} \lt v.\mathrm{d} \text{, then it's a forward edge} \\
\text{If } v.\mathrm{color} = \mathrm{black} \text{ and } u.\mathrm{d} \gt v.\mathrm{d} \text{, then it's a cross edge} \\
\end{cases}
$$
此外,
- forward edge 中因為 $v$ 為 $u$ 之 descendant ,所以 $u.\mathrm{d} \lt v.\mathrm{d} \lt v.\mathrm{f} \lt u.\mathrm{f}$
- cross edge 中因為 $v$ 不為 $u$ 之 descendant ,所以 $v.\mathrm{d} \lt v.\mathrm{f} \lt u.\mathrm{d} \lt u.\mathrm{f}$
如果 $G$ 是無向圖,則想想會發現,forward edge 可以想成 back edge 的反向而 cross edge 可以想成 tree edge 的反向
故如果 $G$ 為無向圖則 $G$ 中僅含 tree edge 與 back edge
此外,不難想像,$G$ is acyclic $\Leftrightarrow G$ (經過 DFS 後)不含 back edge
acyclic 表示 $G$ 沒有 cycle,讀者如果想像不出來,可以試著用反證法去證明看看
常考的應用:
1. 判斷 $G$ 是否連通並找出所有 connected component
2. 判斷 $G$ 是否 acyclic
3. 在 directed acyclic graph (DAG) 上找出一個 topological sort
4. 在有向圖中找出所有 strongly connected component
5. 在無向圖中找出 biconnected component 與 acticulation point
#### 判斷 $G$ 是否 acyclic
```cpp!
bool detectCycleUtil(Graph &g, int u)
{
g.vertices[u].color = Color::GRAY;
for (int v : g.adjList[u])
{
if (g.vertices[v].color == Color::WHITE)
{
if (detectCycleUtil(g, v))
return true;
}
else if (g.vertices[v].color == Color::GRAY) // (u, v) is a back edge
return true;
}
g.vertices[u].color = Color::BLACK;
return false;
}
bool detectCycle(Graph &g)
{
for (auto &u : g.vertices)
{
u.color = Color::WHITE;
}
for (int u = 0; u < g.num; ++u)
{
if (g.vertices[u].color == Color::WHITE)
{
if (detectCycleUtil(g, u))
return true;
}
}
return false;
}
```
> [!IMPORTANT]
> 值得注意的是,上面的 algo 在有向圖不會有問題,但在無向圖中,判斷是否為 back edge 還要多一個條件是 $v$ 並非 $u$ 之 parent (`g.vertices[v].color == Color::GRAY && g.vertices[u].pi != &g.vertices[v]`)
> [!Tip]
> 對無向圖 $G$ 做 DFS 的過程中如果偵測到 back edge ,則 $G$ 含有 cycle
> 若走完 $\left| V \right| - 1$ 個邊後尚有邊未被走訪,表示 $G$ 中含有 cycle
> 也因此對於無向圖,偵測是否含有 cycle 的演算法之時間複雜度為 $O(\left| V \right|)$,與 $\left| E \right|$ 無關
#### 找出 topological sort
給定一個有向圖 $G = (V, E)$ ,若 $L$ 為 $V$ 中元素之一種排序方式滿足:若 $(u, v)\in E$ 則在 $L$ 中 $u$ 出現在 $v$ 的前面,則稱 $L$ 為一個 topological sort
由此定義可知對於有向圖 $G$ ,存在 $G$ 之 topological sort 若且唯若 $G$ is acyclic
所以,給定 **DAG** $G$ ,對其執行 DFS 後,無論 $(u, v)$ 為 tree edge, forward edge 還是 cross edge ,皆可推得 $v.\mathrm{f} \lt u.\mathrm{f}$
故我們找出 topological sort 的演算法只需要對 DFS 後所有頂點的 finishing time 做排序即可
```cpp!
void DFSVisit(Graph &g, int u, std::list<int> &l)
{
g.vertices[u].color = Color::GRAY;
for (int v : g.adjList[u])
{
if (g.vertices[v].color == Color::WHITE)
DFSVisit(g, v, l);
}
g.vertices[u].color = Color::BLACK; // Finished
l.push_front(u); // Insert vertex at the front of the list (topological order)
}
void DFS(Graph &g, std::list<int> &l)
{
for (auto &u : g.vertices)
{
u.color = Color::WHITE;
}
for (int u = 0; u < g.num; ++u)
{
if (g.vertices[u].color == Color::WHITE)
DFSVisit(g, u, l);
}
}
std::list<int> topologicalSort(Graph &g)
{
std::list<int> l;
DFS(g, l);
return l;
}
```
#### 找出所有 strongly connected component
給定有向圖 $G = (V, E)$ ,若 $C\subseteq V$ 為 maximal 滿足: $\forall u, v \in C, \exists$ path 自 $u$ 走到 $v$ 且亦 $\exists$ path 自 $v$ 走到 $u$
則稱 $C$ 為一個 strongly connected component (SCC) of $G$
如果給定有向圖 $G$ ,建立一個新圖 $G' = (V', E')$ 稱之為 component graph
顧名思義, $V'$ 中元素對應 $G$ 中的 components
則 $(v_i, v_j) \in E' \Leftrightarrow \exists u\in C_i, w\in C_j$ 使得 $G$ 中存在自 $u$ 到 $w$ 的 path
這樣的 component graph 必為 DAG
此時離想出演算法只差最後的巧思:我們對 $G$ 中所有邊取反向建成新的 $G^T = (V, E^T)$
我們先對 $G$ 做 DFS 再對 $G^T$ 做 DFS,過程中依照第一次 DFS 求出的 finishing time 由大到小選點,則最後所得的 depth-first forest 中每棵 tree 即一個 SCC
這便是 Kosaraju's algorithm
```cpp!
void DFSVisit(Graph &g, int u, std::vector<int> &finishStack)
{
g.vertices[u].color = Color::GRAY;
for (int v : g.adjList[u])
{
if (g.vertices[v].color == Color::WHITE)
DFSVisit(g, v, finishStack);
}
g.vertices[u].color = Color::BLACK;
finishStack.push_back(u);
}
void DFS(Graph &g, std::vector<int> &finishStack)
{
for (auto &u : g.vertices)
{
u.color = Color::WHITE;
}
for (int u = 0; u < g.num; ++u)
{
if (g.vertices[u].color == Color::WHITE)
DFSVisit(g, u, finishStack)
}
}
Graph getTransposeGraph(Graph &g)
{
Graph gT(g.num);
for (int u = 0; u< g.num; ++u)
{
for (int v : g.adjList[u])
{
gT.addEdge(v, u);
}
}
return gT;
}
void DFSOnTranspose(Graph &gT, int u, std::vector<int> &component)
{
gT.vertices[u].color = Color::GRAY;
component.push_back(u);
for (int v : gT.adjList[u])
{
if (gT.vertices[v].color == Color::WHITE)
DFSOnTranspose(gT, v, component);
}
}
std::vector<std::vector<int>> SCC(Graph &g)
{
std::vector<int> finishStack;
finishStack.reserve(g.num);
DFS(g, finishStack);
Graph gT = getTransposeGraph(g);
for (auto &vertex : gT.vertices)
{
vertex.color = Color::WHITE;
}
std::vector<std::vector<int>> scc;
while (!finishStack.empty())
{
const int u = finishStack.back();
finishStack.pop_back();
if (gT.vertices[u].color == Color::WHITE)
{
std::vector<int> component;
DFSOnTranspose(gT, u, component);
scc.push_back(component);
}
}
return scc;
}
```
## 【資工考研】DSA: Graph 全系列
- [【資工考研】DSA: Graph (2)](https://hackmd.io/@RyoukiWei/graph_2)
- [【資工考研】DSA: Graph (3)](https://hackmd.io/@RyoukiWei/graph_3)