This commit is contained in:
krahets
2026-07-19 03:42:10 +08:00
parent cb5235bc3f
commit 2b8612bc41
238 changed files with 7477 additions and 3 deletions
+110
View File
@@ -0,0 +1,110 @@
---
comments: true
hide: [edit]
---
<!-- 此檔案由 utils/exercises/publish_exercises.py 根據 exercises.yaml 自動生成,請勿直接修改。 -->
# 9.5 &nbsp; 練習
## 9.5.1 &nbsp; 知識鞏固
### 1. &nbsp; 用兩種方式表示同一張圖
一個無向圖有 4 個頂點 `A、B、C、D`,邊為
`A-B、A-C、B-C、C-D`
<!-- numbered-subquestions -->
1. 寫出它的鄰接表;
2. 填寫只含 0 和 1 的鄰接矩陣;
3. 如果要判斷 `A``D` 是否直接相連,哪種圖的表示方法只需檢視一個儲存條目?
4. 如果圖的頂點很多但邊很少,哪種表示通常更節省空間?
??? success "參考答案"
1. 鄰接表為:
```text
A: B, C
B: A, C
C: A, B, D
D: C
```
2. 鄰接矩陣為:
| | A | B | C | D |
| --- | --- | --- | --- | --- |
| A | 0 | 1 | 1 | 0 |
| B | 1 | 0 | 1 | 0 |
| C | 1 | 1 | 0 | 1 |
| D | 0 | 0 | 1 | 0 |
3. 鄰接矩陣可直接檢視第 `A` 行、第 `D` 列,因此很適合判斷任意兩點是否直接相連。
4. 頂點很多但邊很少時,鄰接表只記錄實際存在的邊,通常比為每一對頂點都預留位置的鄰接矩陣更節省空間。
### 2. &nbsp; 廣度優先與深度優先訪問順序
一個無向圖的頂點為 `A、B、C、D、E`,邊為
`A-B、A-C、B-D、C-D、D-E`。
從 A 出發,並規定遇到多個未訪問鄰接頂點時按字母順序選擇:
<!-- numbered-subquestions -->
1. 寫出廣度優先走訪(BFS)的訪問順序;
2. 寫出遞迴深度優先走訪(DFS)的訪問順序;
3. 為什麼兩種走訪都需要記錄已經訪問過的頂點?
??? success "參考答案"
1. BFS 的訪問順序為 `A, B, C, D, E`。它先訪問離 A 一條邊的 B、C,
再訪問更遠的 D、E。
2. DFS 的訪問順序為 `A, B, D, C, E`。它依次進入當前頂點的未訪問鄰接頂點,
因此先走過 `A → B → D → C`;C 沒有新的鄰接頂點時回到 D,再訪問 E。
3. 圖中存在環,例如 `A-B-D-C-A`。如果不記錄已訪問頂點,
走訪可能反覆沿環訪問同一批頂點,無法正常結束。
### 3. &nbsp; 一次 BFS 能訪問整張圖嗎
一個無向圖有頂點 `A、B、C、D、E、F`,邊只有
`A-B、B-C、D-E`。
<!-- numbered-subquestions -->
1. 從 A 開始進行一次 BFS,可以訪問哪些頂點?
2. 根據第 1 問,這一次 BFS 是否已經訪問圖中的所有頂點?為什麼?
3. 若按字母順序掃描所有頂點,每遇到一個未訪問頂點就重新開始 BFS,
各次 BFS 的起點是什麼?這個圖被分成幾個互不連通的部分(連通分量)?
??? success "參考答案"
1. 從 A 出發只能訪問 `A、B、C`。
2. 沒有訪問所有頂點。`D、E` 組成另一個互相連通的部分,F 是單獨的頂點;
它們與 A 之間都沒有路徑,因此從 A 出發無法到達。
3. 三次 BFS 的起點依次為 `A、D、F`,分別訪問
`{A, B, C}`、`{D, E}` 和 `{F}`。因此這個圖有 3 個連通分量。
## 9.5.2 &nbsp; 程式設計練習
### 1. &nbsp; 判斷無向圖中是否存在路徑
給定一個含有 $n$ 個頂點的無向圖,頂點編號為 $0$ 到 $n-1$。陣列 `edges` 中的每一項 `[u, v]` 表示頂點 `u` 和 `v` 之間有一條無向邊。
再給定起點 `source` 和終點 `destination`。請先根據 `edges` 建立鄰接表,再使用 BFS 或 DFS
判斷是否存在一條從 `source` 到 `destination` 的路徑:存在則返回 `true`,否則返回 `false`。
圖中可能有環,也可能不連通。
??? tip "解題提示"
1. 每條無向邊需要同時加入兩個方向
2. 圖中可能有環,必須記錄已經訪問過的節點
3. 從 source 出發,遇到 destination 返回 true,走訪結束仍未遇到則返回 false
[LeetCode](https://leetcode.cn/problems/find-if-path-exists-in-graph/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
+103
View File
@@ -0,0 +1,103 @@
---
comments: true
---
# 9.1 &nbsp; 圖
<u>圖(graph</u>是一種非線性資料結構,由<u>頂點(vertex</u>和<u>邊(edge</u>組成。我們可以將圖 $G$ 抽象地表示為一組頂點 $V$ 和一組邊 $E$ 的集合。以下示例展示了一個包含 5 個頂點和 7 條邊的圖。
$$
\begin{aligned}
V & = \{ 1, 2, 3, 4, 5 \} \newline
E & = \{ (1,2), (1,3), (1,5), (2,3), (2,4), (2,5), (4,5) \} \newline
G & = \{ V, E \} \newline
\end{aligned}
$$
如果將頂點看作節點,將邊看作連線各個節點的引用(指標),我們就可以將圖看作一種從鏈結串列拓展而來的資料結構。如圖 9-1 所示,**相較於線性關係(鏈結串列)和分治關係(樹),網路關係(圖)的自由度更高**,因而更為複雜。
![鏈結串列、樹、圖之間的關係](graph.assets/linkedlist_tree_graph.png){ class="animation-figure" }
<p align="center"> 圖 9-1 &nbsp; 鏈結串列、樹、圖之間的關係 </p>
## 9.1.1 &nbsp; 圖的常見型別與術語
根據邊是否具有方向,可分為<u>無向圖(undirected graph</u>和<u>有向圖(directed graph</u>,如圖 9-2 所示。
- 在無向圖中,邊表示兩頂點之間的“雙向”連線關係,例如微信或 QQ 中的“好友關係”。
- 在有向圖中,邊具有方向性,即 $A \rightarrow B$ 和 $A \leftarrow B$ 兩個方向的邊是相互獨立的,例如微博或抖音上的“關注”與“被關注”關係。
![有向圖與無向圖](graph.assets/directed_graph.png){ class="animation-figure" }
<p align="center"> 圖 9-2 &nbsp; 有向圖與無向圖 </p>
根據所有頂點是否連通,可分為<u>連通圖(connected graph</u>和<u>非連通圖(disconnected graph</u>,如圖 9-3 所示。
- 對於連通圖,從某個頂點出發,可以到達其餘任意頂點。
- 對於非連通圖,從某個頂點出發,至少有一個頂點無法到達。
![連通圖與非連通圖](graph.assets/connected_graph.png){ class="animation-figure" }
<p align="center"> 圖 9-3 &nbsp; 連通圖與非連通圖 </p>
我們還可以為邊新增“權重”變數,從而得到如圖 9-4 所示的<u>有權圖(weighted graph</u>。例如在《王者榮耀》等手遊中,系統會根據共同遊戲時間來計算玩家之間的“親密度”,這種親密度網路就可以用有權圖來表示。
![有權圖與無權圖](graph.assets/weighted_graph.png){ class="animation-figure" }
<p align="center"> 圖 9-4 &nbsp; 有權圖與無權圖 </p>
圖資料結構包含以下常用術語。
- <u>鄰接(adjacency</u>:當兩頂點之間存在邊相連時,稱這兩頂點“鄰接”。在圖 9-4 中,頂點 1 的鄰接頂點為頂點 2、3、5。
- <u>路徑(path</u>:從頂點 A 到頂點 B 經過的邊構成的序列被稱為從 A 到 B 的“路徑”。在圖 9-4 中,邊序列 1-5-2-4 是頂點 1 到頂點 4 的一條路徑。
- <u>度(degree</u>:一個頂點擁有的邊數。對於有向圖,<u>入度(in-degree</u>表示有多少條邊指向該頂點,<u>出度(out-degree</u>表示有多少條邊從該頂點指出。
## 9.1.2 &nbsp; 圖的表示
圖的常用表示方式包括“鄰接矩陣”和“鄰接表”。以下使用無向圖進行舉例。
### 1. &nbsp; 鄰接矩陣
設圖的頂點數量為 $n$ <u>鄰接矩陣(adjacency matrix</u>使用一個 $n \times n$ 大小的矩陣來表示圖,每一行(列)代表一個頂點,矩陣元素代表邊,用 $1$ 或 $0$ 表示兩個頂點之間是否存在邊。
如圖 9-5 所示,設鄰接矩陣為 $M$、頂點串列為 $V$ ,那麼矩陣元素 $M[i, j] = 1$ 表示頂點 $V[i]$ 到頂點 $V[j]$ 之間存在邊,反之 $M[i, j] = 0$ 表示兩頂點之間無邊。
![圖的鄰接矩陣表示](graph.assets/adjacency_matrix.png){ class="animation-figure" }
<p align="center"> 圖 9-5 &nbsp; 圖的鄰接矩陣表示 </p>
鄰接矩陣具有以下特性。
- 在簡單圖中,頂點不能與自身相連,此時鄰接矩陣主對角線元素沒有意義。
- 對於無向圖,兩個方向的邊等價,此時鄰接矩陣關於主對角線對稱。
- 將鄰接矩陣的元素從 $1$ 和 $0$ 替換為權重,則可表示有權圖。
使用鄰接矩陣表示圖時,我們可以直接訪問矩陣元素以獲取邊,因此增刪查改操作的效率很高,時間複雜度均為 $O(1)$ 。然而,矩陣的空間複雜度為 $O(n^2)$ ,記憶體佔用較多。
### 2. &nbsp; 鄰接表
<u>鄰接表(adjacency list</u>使用 $n$ 個鏈結串列來表示圖,鏈結串列節點表示頂點。第 $i$ 個鏈結串列對應頂點 $i$ ,其中儲存了該頂點的所有鄰接頂點(與該頂點相連的頂點)。圖 9-6 展示了一個使用鄰接表儲存的圖的示例。
![圖的鄰接表表示](graph.assets/adjacency_list.png){ class="animation-figure" }
<p align="center"> 圖 9-6 &nbsp; 圖的鄰接表表示 </p>
鄰接表僅儲存實際存在的邊,而邊的總數通常遠小於 $n^2$ ,因此它更加節省空間。然而,在鄰接表中需要透過走訪鏈結串列來查詢邊,因此其時間效率不如鄰接矩陣。
觀察圖 9-6 ,**鄰接表結構與雜湊表中的“鏈式位址”非常相似,因此我們也可以採用類似的方法來最佳化效率**。比如當鏈結串列較長時,可以將鏈結串列轉化為 AVL 樹或紅黑樹,從而將時間效率從 $O(n)$ 最佳化至 $O(\log n)$ ;還可以把鏈結串列轉換為雜湊表,從而將時間複雜度降至 $O(1)$ 。
## 9.1.3 &nbsp; 圖的常見應用
如表 9-1 所示,許多現實系統可以用圖來建模,相應的問題也可以約化為圖計算問題。
<p align="center"> 表 9-1 &nbsp; 現實生活中常見的圖 </p>
<div class="center-table" markdown>
| | 頂點 | 邊 | 圖計算問題 |
| -------- | ---- | -------------------- | ------------ |
| 社交網路 | 使用者 | 好友關係 | 潛在好友推薦 |
| 地鐵線路 | 站點 | 站點間的連通性 | 最短路線推薦 |
| 太陽系 | 星體 | 星體間的萬有引力作用 | 行星軌道計算 |
</div>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+22
View File
@@ -0,0 +1,22 @@
---
comments: true
icon: material/graphql
---
# 第 9 章 &nbsp; 圖
![](../assets/covers/chapter_graph.jpg){ class="cover-image" }
!!! abstract
在生命旅途中,我們就像是一個個節點,被無數看不見的邊相連。
每一次的相識與相離,都在這張巨大的網路圖中留下獨特的印記。
## 本章內容
- [9.1 &nbsp; 圖](graph.md)
- [9.2 &nbsp; 圖基礎操作](graph_operations.md)
- [9.3 &nbsp; 圖的走訪](graph_traversal.md)
- [9.4 &nbsp; 小結](summary.md)
- [9.5 &nbsp; 練習](exercises.md)
+35
View File
@@ -0,0 +1,35 @@
---
comments: true
---
# 9.4 &nbsp; 小結
### 1. &nbsp; 重點回顧
- 圖由頂點和邊組成,可以表示為一組頂點和一組邊構成的集合。
- 相較於線性關係(鏈結串列)和分治關係(樹),網路關係(圖)具有更高的自由度,因而更為複雜。
- 有向圖的邊具有方向性,連通圖中的任意頂點均可達,有權圖的每條邊都包含權重變數。
- 鄰接矩陣利用矩陣來表示圖,每一行(列)代表一個頂點,矩陣元素代表邊,用 $1$ 或 $0$ 表示兩個頂點之間有邊或無邊。鄰接矩陣在增刪查改操作上效率很高,但空間佔用較多。
- 鄰接表使用多個鏈結串列來表示圖,第 $i$ 個鏈結串列對應頂點 $i$ ,其中儲存了該頂點的所有鄰接頂點。鄰接表相對於鄰接矩陣更加節省空間,但由於需要走訪鏈結串列來查詢邊,因此時間效率較低。
- 當鄰接表中的鏈結串列過長時,可以將其轉換為紅黑樹或雜湊表,從而提升查詢效率。
- 從演算法思想的角度分析,鄰接矩陣體現了“以空間換時間”,鄰接表體現了“以時間換空間”。
- 圖可用於建模各類現實系統,如社交網路、地鐵線路等。
- 樹是圖的一種特例,樹的走訪也是圖的走訪的一種特例。
- 圖的廣度優先走訪是一種由近及遠、層層擴張的搜尋方式,通常藉助佇列實現。
- 圖的深度優先走訪是一種優先走到底、無路可走時再回溯的搜尋方式,常基於遞迴來實現。
### 2. &nbsp; Q & A
**Q**:路徑的定義是頂點序列還是邊序列?
維基百科上不同語言版本的定義不一致:英文版是“路徑是一個邊序列”,而中文版是“路徑是一個頂點序列”。以下是英文版原文:In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.
在本文中,路徑被視為一個邊序列,而不是一個頂點序列。這是因為兩個頂點之間可能存在多條邊連線,此時每條邊都對應一條路徑。
**Q**:非連通圖中是否會有無法走訪到的點?
在非連通圖中,從某個頂點出發,至少有一個頂點無法到達。走訪非連通圖需要設定多個起點,以走訪到圖的所有連通分量。
**Q**:在鄰接表中,“與該頂點相連的所有頂點”的頂點順序是否有要求?
可以是任意順序。但在實際應用中,可能需要按照指定規則來排序,比如按照頂點新增的次序,或者按照頂點值大小的順序等,這樣有助於快速查詢“帶有某種極值”的頂點。