文章目錄
- 圖論理論基礎(1)
- 1. 圖的基本概念
- 1.1 基本術語
- 2. 圖的分類
- 2.1 有向圖 vs 無向圖
- 2.2 加權圖 vs 無權圖
- 2.3 度(Degree)
- 3. 圖的連通性
- 3.1 連通圖 vs 非連通圖
- 3.2 強連通性(有向圖)
- 3.3 連通分量(無向圖)
- 4. 圖的存儲方式
- 4.1 鄰接矩陣
- 4.2 鄰接表
- 4.3 邊列表
- 5. 圖的遍歷算法
- 5.1 深度優先搜索(DFS)
- 5.2 廣度優先搜索(BFS)
- 6. 圖論算法分類
- 6.1 圖的存儲與遍歷
- 6.2 拓撲排序
- 6.3 並查集
- 6.4 最短路徑算法
- 6.5 最小生成樹
- 7. 時間複雜度總結
- 8. DFS與BFS對比
- 8.1 算法對比
- 8.2 DFS搜索過程
- 8.3 DFS代碼框架
- 9. BFS詳解
- 9.1 BFS使用場景
- 9.2 BFS搜索過程
- 9.3 BFS代碼框架
圖論理論基礎(1)
1. 圖的基本概念
**圖(Graph)**是由頂點(Vertex/Node)和邊(Edge)組成的數據結構,用來表示事物之間的關係。
1.1 基本術語
- 頂點(Vertex):圖中的節點,表示事物
- 邊(Edge):連接兩個頂點的線,表示關係
- 路徑(Path):從一個頂點到另一個頂點經過的邊的序列
- 環(Cycle):起點和終點相同的路徑
2. 圖的分類
2.1 有向圖 vs 無向圖
- 無向圖:邊沒有方向,A-B 和 B-A 是同一條邊
- 有向圖:邊有方向,A→B 和 B→A 是不同的邊
2.2 加權圖 vs 無權圖
- 無權圖:邊沒有權重,只表示連接關係
- 加權圖:邊有權重,表示距離、成本等
2.3 度(Degree)
- 度:與頂點相連的邊的數量
- 出度:有向圖中從頂點出發的邊的數量
- 入度:有向圖中指向頂點的邊的數量
3. 圖的連通性
3.1 連通圖 vs 非連通圖
- 連通圖:任意兩個頂點之間都存在路徑
- 非連通圖:存在兩個頂點之間沒有路徑
3.2 強連通性(有向圖)
- 強連通圖:任意兩個頂點之間都存在雙向路徑
- 強連通分量:極大強連通子圖
示例:
有向圖:A → B → C → A
↓
D
強連通分量1:{A, B, C} - 這三個節點互相可達
強連通分量2:{D} - 單獨一個節點
圖解:
A ──→ B ──→ C
│ │
│ │
└───────────┘
│
↓
D
3.3 連通分量(無向圖)
- 連通分量:無向圖中的極大連通子圖
- 連通分量數量:圖中連通分量的個數
示例:
無向圖:A-B-C, D-E, F
連通分量1:{A, B, C} - 這三個節點互相連通
連通分量2:{D, E} - 這兩個節點互相連通
連通分量3:{F} - 單獨一個節點
連通分量數量:3
圖解:
A ── B ── C D ── E F
4. 圖的存儲方式
4.1 鄰接矩陣
// 適合稠密圖,空間複雜度O(V²)
vector<vector<int>> graph(V, vector<int>(V, 0));
// graph[i][j] = 1 表示頂點i和j之間有邊
4.2 鄰接表
// 適合稀疏圖,空間複雜度O(V+E)
vector<vector<int>> graph(V);
// graph[i] 存儲與頂點i相鄰的所有頂點
4.3 邊列表
// 存儲所有邊的信息
vector<pair<int, int>> edges; // 無權邊
vector<tuple<int, int, int>> edges; // 加權邊 (u, v, weight)
5. 圖的遍歷算法
5.1 深度優先搜索(DFS)
算法特點:
- 深度優先:儘可能深地搜索圖的分支
- 遞歸實現:使用遞歸棧,代碼簡潔
- 應用場景:路徑查找、連通性判斷、拓撲排序
- 時間複雜度:O(V+E),每個頂點和邊訪問一次
- 空間複雜度:O(V),遞歸棧的深度
執行過程示例:
圖:A-B-C
| |
D E
DFS訪問順序:A → B → C → E → D
代碼實現:
void dfs(int node, vector<bool>& visited, vector<vector<int>>& graph) {
visited[node] = true; // 標記當前節點已訪問
// 遍歷當前節點的所有鄰居
for (int neighbor : graph[node]) {
if (!visited[neighbor]) { // 如果鄰居未被訪問
dfs(neighbor, visited, graph); // 遞歸訪問鄰居
}
}
}
5.2 廣度優先搜索(BFS)
算法特點:
- 廣度優先:逐層訪問,先訪問距離近的節點
- 隊列實現:使用隊列維護訪問順序
- 應用場景:最短路徑、層次遍歷、連通性判斷
- 時間複雜度:O(V+E),每個頂點和邊訪問一次
- 空間複雜度:O(V),隊列的最大長度
執行過程示例:
圖:A-B-C
| |
D E
BFS訪問順序:A → B → D → C → E
層次:第0層:A
第1層:B, D
第2層:C, E
代碼實現:
void bfs(int start, vector<vector<int>>& graph) {
queue<int> q; // 隊列存儲待訪問的節點
vector<bool> visited(graph.size(), false); // 標記數組
q.push(start); // 將起始節點加入隊列
visited[start] = true; // 標記起始節點已訪問
while (!q.empty()) { // 隊列不為空時繼續
int node = q.front(); // 取出隊首節點
q.pop(); // 從隊列中移除
// 遍歷當前節點的所有鄰居
for (int neighbor : graph[node]) {
if (!visited[neighbor]) { // 如果鄰居未被訪問
visited[neighbor] = true; // 標記鄰居已訪問
q.push(neighbor); // 將鄰居加入隊列
}
}
}
}
6. 圖論算法分類
6.1 圖的存儲與遍歷
- DFS/BFS:基礎遍歷算法
- 應用:島嶼數量、被圍繞的區域
6.2 拓撲排序
- Kahn算法:基於入度的拓撲排序
- DFS法:基於深度優先搜索的拓撲排序
- 應用:課程表、任務調度
6.3 並查集
- Union-Find:集合合併與查詢
- 應用:省份數量、朋友圈
6.4 最短路徑算法
- Dijkstra:單源最短路徑(非負權)
- Bellman-Ford:單源最短路徑(可處理負權)
- Floyd-Warshall:多源最短路徑
6.5 最小生成樹
- Kruskal:基於邊的貪心算法
- Prim:基於頂點的貪心算法
7. 時間複雜度總結
|
算法
|
時間複雜度
|
空間複雜度
|
適用場景
|
|
DFS/BFS
|
O(V+E)
|
O(V)
|
圖遍歷
|
|
拓撲排序
|
O(V+E)
|
O(V)
|
DAG排序
|
|
並查集
|
O(α(n))
|
O(V)
|
連通性判斷
|
|
Dijkstra
|
O((V+E)logV)
|
O(V)
|
單源最短路
|
|
Floyd
|
O(V³)
|
O(V²)
|
多源最短路
|
|
Kruskal
|
O(ElogE)
|
O(V)
|
最小生成樹
|
8. DFS與BFS對比
8.1 算法對比
|
比較項
|
DFS(深度優先搜索)
|
BFS(廣度優先搜索)
|
|
搜索策略 |
一條路走到黑(遞歸/棧)
|
一層一層擴展(隊列)
|
|
實現方式 |
通常用 遞歸 或 顯式棧 |
通常用 隊列(queue) |
|
適用場景 |
適合 全路徑搜索、組合問題、回溯問題(如圖遍歷、排列組合) |
適合 最短路徑問題、層次遍歷(如最短步數、層數搜索) |
|
空間複雜度 |
相對較小(與遞歸深度有關)
|
相對較大(需存整層節點)
|
|
典型應用 |
迷宮求解、圖連通分量、全排列
|
最短路徑、最少步數、層序遍歷
|
8.2 DFS搜索過程
假設你在一個迷宮中:
- 看到一個路口 → 選一條路走;
- 走到底發現死路 → 返回上一個岔口;
- 選另一條沒走過的路 → 再次深入;
- 最終找到所有可能的出口。
這整個過程就叫 深度優先搜索(DFS)。
8.3 DFS代碼框架
vector<vector<int>> result; // 保存符合條件的所有路徑
vector<int> path; // 起點到終點的路徑
void dfs (參數){
if (終止條件) {
存放結果;
return;
}
for (選擇:本節點所連接的其他節點) {
處理節點;
dfs(圖,選擇的節點); // 遞歸
回溯,撤銷處理結果
}
}
9. BFS詳解
9.1 BFS使用場景
廣度優先搜索(Breadth First Search,簡稱 BFS)是一種逐層搜索的算法,常用於在圖或樹結構中尋找最短路徑或最小步數的問題。
常見使用場景:
- 最短路徑問題:
例如在無權圖中,尋找從起點到終點的最短路徑。
- 典型例題:二叉樹的最短深度、迷宮最短路徑、網絡中最短連接。
- 層序遍歷:
BFS 天然按層擴展節點,因此在樹結構中可用於層序遍歷。 - 狀態搜索類問題:
如八數碼問題、單詞接龍、棋盤問題等,BFS 可用於尋找最少操作次數。 - 網絡流與最短增廣路:
在最大流算法(如 Edmonds-Karp)中,BFS 用於尋找最短增廣路徑。 - 多源最短路問題:
同時從多個起點出發,計算所有點到最近起點的最短距離。
9.2 BFS搜索過程
BFS 採用隊列(queue)實現“先進先出”的搜索邏輯,從起點開始,依次訪問相鄰節點,逐層向外擴展。
搜索流程:
- 將起點加入隊列,並標記為已訪問;
- 當隊列非空時:
- 取出隊首節點;
- 遍歷該節點的所有鄰接節點;
- 若鄰接節點未訪問,則加入隊列並標記;
- 直到隊列為空或找到目標。
9.3 BFS代碼框架
int dir[4][2] = {
{0, 1}, {1, 0}, {0, -1}, {-1, 0}
};// 表示四個方向
// grid 是地圖,也就是一個二維數組
// visited標記訪問過的節點,不要重複訪問
// x,y 表示開始搜索節點的下標
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
queue<pair<int, int>> que; // 定義隊列
que.push({x, y}); // 起始節點加入隊列
visited[x][y] = true; // 只要加入隊列,立刻標記為訪問過的節點
//加入隊列就代表 走過,就需要標記,而不是從隊列拿出來的時候再去標記走過。要不然會導致很多重複
while(!que.empty()) { // 開始遍歷隊列裏的元素
pair<int ,int> cur = que.front(); que.pop(); // 從隊列取元素
int curx = cur.first;
int cury = cur.second; // 當前節點座標
for (int i = 0; i < 4; i++) { // 開始想當前節點的四個方向左右上下去遍歷
int nextx = curx + dir[i][0];
int nexty = cury + dir[i][1]; // 獲取周邊四個方向的座標
if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 座標越界了,直接跳過
if (!visited[nextx][nexty]) { // 如果節點沒被訪問過
que.push({nextx, nexty}); // 隊列添加該節點為下一輪要遍歷的節點
visited[nextx][nexty] = true; // 只要加入隊列立刻標記,避免重複訪問
}
}
}
}