图论专题:Mermaid 图解图搜索、拓扑排序、并查集与 4 道 LeetCode 核心题
用 Mermaid 图解图论中的 DFS、BFS、拓扑排序、并查集和 4 道 LeetCode 经典题,系统掌握图论专题。
图论专题:Mermaid 图解图搜索、拓扑排序、并查集与 4 道 LeetCode 核心题
图论题经常给人一种“类型很多、套路很多”的感觉。
但真正做题时,图论高频题大多还是围绕这几件事:
- 图怎么表示
- 节点之间怎么连
- 如何遍历整张图
- 是否有环
- 是否存在依赖顺序
- 连通性如何维护
这篇文章继续用 Mermaid 图解的方式,把图论里最常见的 DFS、BFS、拓扑排序和并查集串起来,再用 4 道 LeetCode 题把核心模型落地。
学习目标:
- 理解图的基本表示方式与搜索方式。
- 掌握 DFS / BFS 在图中的作用。
- 理解拓扑排序和并查集的典型应用。
- 用 4 道 LeetCode 题覆盖图论高频模型。
- 用一张知识卡片建立图论题的判断框架。
一、图的本质:节点 + 边
图由节点和边组成。
flowchart LR
A((A)) --> B((B))
A --> C((C))
B --> D((D))
C --> D
图题和树题最大的不同是:
- 树通常默认无环
- 图可能有环、可能不连通
所以图题几乎总会多出两个意识:
visited- 连通关系
二、图怎么表示
高频写法通常是邻接表。
flowchart TD
A["0: [1,2]"] --> B["1: [2,3]"]
B --> C["2: [3]"]
C --> D["3: []"]
你可以把它理解成:
- 每个节点都维护一个“它能走到哪些节点”的列表
这样 DFS / BFS 就自然变成:
- 取出当前节点
- 遍历相邻节点
- 对还没访问过的节点继续处理
三、图搜索:DFS 与 BFS
DFS
flowchart TD
A[起点] --> B[深入一个邻居]
B --> C[继续深入]
C --> D[走不通则回退]
适合:
- 连通块搜索
- 是否可达
- 路径枚举
BFS
flowchart TD
A[起点] --> B[第 1 层邻居]
B --> C[第 2 层邻居]
C --> D[第 3 层邻居]
适合:
- 无权图最短路
- 最少步数
- 分层扩展
四、拓扑排序:处理依赖关系
如果图表示“先做 A,才能做 B”这种依赖关系,那么它通常是有向图问题。
拓扑排序适用于:
有向无环图(DAG)中的依赖顺序。
flowchart LR
A[课程 0] --> B[课程 1]
A --> C[课程 2]
B --> D[课程 3]
C --> D
拓扑排序的关键是入度:
- 入度为 0 的点,说明当前没有前置依赖,可以先处理
flowchart TD
A[统计所有节点入度] --> B[把入度为 0 的节点入队]
B --> C[弹出一个节点并加入答案]
C --> D[删除它指向的边]
D --> E[若新节点入度变 0 则入队]
五、并查集:维护连通性
并查集适合处理:
- 两个节点是否连通
- 多次合并连通块
- 动态维护集合归属
flowchart TD
A[节点 1] --> P1[根节点]
B[节点 2] --> P1
C[节点 3] --> P2[另一个根]
D[union] --> E[把两个根合并]
并查集核心只有两件事:
find(x):找根union(a, b):合并两个集合
六、4 道 LeetCode 题目打通图论专题
1)LeetCode 200. 岛屿数量
题型定位:网格图 DFS / BFS。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
int m = static_cast<int>(grid.size()), n = static_cast<int>(grid[0].size());
int count = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == '1') {
++count;
dfs(grid, i, j);
}
}
}
return count;
}
private:
void dfs(vector<vector<char>>& grid, int i, int j) {
if (i < 0 || i >= static_cast<int>(grid.size()) ||
j < 0 || j >= static_cast<int>(grid[0].size()) ||
grid[i][j] != '1') {
return;
}
grid[i][j] = '0';
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
}
};
flowchart TD
A[发现一个陆地] --> B[计数加 1]
B --> C[DFS 扩展整块连通区域]
C --> D[标记已访问]
这题练的是:
- 把网格看成图
- 连通块计数
2)LeetCode 994. 腐烂的橘子
题型定位:多源 BFS。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution {
public:
int orangesRotting(vector<vector<int>>& grid) {
int m = static_cast<int>(grid.size()), n = static_cast<int>(grid[0].size());
queue<pair<int, int>> q;
int fresh = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == 2) q.push({i, j});
if (grid[i][j] == 1) ++fresh;
}
}
int minutes = 0;
vector<pair<int, int>> dirs = {
make_pair(1, 0),
make_pair(-1, 0),
make_pair(0, 1),
make_pair(0, -1)
};
while (!q.empty() && fresh > 0) {
int size = static_cast<int>(q.size());
for (int i = 0; i < size; ++i) {
auto [x, y] = q.front(); q.pop();
for (auto [dx, dy] : dirs) {
int nx = x + dx, ny = y + dy;
if (nx >= 0 && nx < m && ny >= 0 && ny < n && grid[nx][ny] == 1) {
grid[nx][ny] = 2;
--fresh;
q.push({nx, ny});
}
}
}
++minutes;
}
return fresh == 0 ? minutes : -1;
}
};
flowchart TD
A[所有初始腐烂橘子同时入队] --> B[第 1 分钟扩散]
B --> C[第 2 分钟扩散]
C --> D[直到没有新鲜橘子或无法继续]
这题训练的是:
- 多源 BFS
- “按层推进 = 时间推进”
3)LeetCode 207. 课程表
题型定位:拓扑排序 / 判环。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> graph(numCourses);
vector<int> indegree(numCourses, 0);
for (const auto& p : prerequisites) {
graph[p[1]].push_back(p[0]);
++indegree[p[0]];
}
queue<int> q;
for (int i = 0; i < numCourses; ++i) {
if (indegree[i] == 0) q.push(i);
}
int count = 0;
while (!q.empty()) {
int cur = q.front(); q.pop();
++count;
for (int next : graph[cur]) {
if (--indegree[next] == 0) q.push(next);
}
}
return count == numCourses;
}
};
flowchart TD
A[统计入度] --> B[入度为 0 的课程入队]
B --> C[弹出并学习]
C --> D[删掉它指向的依赖边]
D --> E[新的入度为 0 课程入队]
E --> F{所有课程都被处理了吗}
这题训练的是:
- 有向图依赖关系
- 拓扑排序判环
4)LeetCode 547. 省份数量
题型定位:并查集 / 连通块。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class UnionFind {
public:
int count;
vector<int> parent;
explicit UnionFind(int n) : count(n), parent(n) {
iota(parent.begin(), parent.end(), 0);
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
void unite(int a, int b) {
int pa = find(a), pb = find(b);
if (pa == pb) return;
parent[pa] = pb;
--count;
}
};
class Solution {
public:
int findCircleNum(vector<vector<int>>& isConnected) {
int n = static_cast<int>(isConnected.size());
UnionFind uf(n);
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (isConnected[i][j] == 1) uf.unite(i, j);
}
}
return uf.count;
}
};
flowchart TD
A[城市 0 与 1 连通] --> B[合并集合]
C[城市 1 与 2 连通] --> B
B --> D[0 1 2 属于同一省份]
这题练的是:
- 连通块概念
- 并查集维护集合数量
七、图论题怎么快速判断模型
flowchart TD
A[看到图论题] --> B{要求最短步数 / 最少转换}
B -->|是| C[BFS]
B -->|否| D{要求连通块个数 / 是否连通}
D -->|是| E[DFS / BFS / 并查集]
D -->|否| F{是否存在先后依赖}
F -->|是| G[拓扑排序]
F -->|否| H{是否动态合并集合}
H -->|是| I[并查集]
H -->|否| J[继续分析带权图或其他图模型]
八、图论常见错误
1)忘记 visited
图可能有环,不标记访问很容易无限绕圈。
2)把树题习惯带到图题
树天然无环,图不一定。
3)拓扑排序忘了入度更新
如果出队后不更新邻居入度,整套流程就断了。
4)并查集只会 union 不会维护根
路径压缩和按秩合并虽然不是必须,但会显著提升效率。
flowchart TD
A[图搜索] --> B[判断是否已访问]
B -->|否| C[标记并继续]
B -->|是| D[跳过]
九、图论知识卡片
mindmap
root((图论))
搜索
DFS
BFS
结构
邻接表
visited
依赖关系
拓扑排序
入度
连通性
并查集
连通块
高频题型
最短步数
岛屿
课程表
省份数量
复习版要点:
- 图题本质是节点和边的关系处理
- 图和树的关键区别是:图可能有环
- 最短步数优先想到 BFS
- 依赖顺序优先想到拓扑排序
- 动态连通性优先想到并查集
十、最后总结
如果只记一句话,请记这个:
图论高频题看起来多,实质上大多还是在做“搜索、判环、排依赖、维护连通性”。
做题时先判断:
- 这是搜索问题、最短路问题,还是依赖问题
- 图里有没有环
- 是否需要维护连通块
把这篇里的 4 道题做透,图论的高频主干就基本搭起来了。
本文由作者按照 CC BY 4.0 进行授权