第9类题型:图论
问题背景:为什么图论题你总觉得自己会 BFS / DFS,一到面试还是容易把题型判断错
很多实习面试里的图论题,表面根本不写"图"这个字。
它可能长成二维矩阵,像 200. 岛屿数量、994. 腐烂的橘子;也可能长成依赖关系,像 207. 课程表。真正让人失分的地方,往往不是代码敲不出来,而是你没有在前 30 秒里判断清楚:
- 这题的点和边分别是什么。
- 这是在求连通块、最短扩散层数,还是在判断有没有环。
- 该用 DFS、BFS,还是拓扑排序。
如果你现在的状态是"看过图论模板,但一遇到矩阵题和依赖题还是会犹豫",那这一类题必须单独练透。读完这篇,你至少要把 4 件事固定下来:先把题目抽象成图、先决定遍历方式、访问标记要在什么时候打、依赖题要怎么建邻接表和入度表。
文章目录
- 第9类题型:图论
-
- [问题背景:为什么图论题你总觉得自己会 BFS / DFS,一到面试还是容易把题型判断错](#问题背景:为什么图论题你总觉得自己会 BFS / DFS,一到面试还是容易把题型判断错)
- [1. 核心知识点](#1. 核心知识点)
- [2. 图论题型选择表:DFS / BFS / 拓扑排序什么时候用](#2. 图论题型选择表:DFS / BFS / 拓扑排序什么时候用)
- [3. 动画辅助理解:图论题先建模,再决定怎么遍历](#3. 动画辅助理解:图论题先建模,再决定怎么遍历)
- [4. 用样例手推 DFS / 拓扑排序](#4. 用样例手推 DFS / 拓扑排序)
-
- [先手推 DFS:`200. 岛屿数量`](#先手推 DFS:
200. 岛屿数量) - [再手推拓扑排序:`207. 课程表`](#再手推拓扑排序:
207. 课程表)
- [先手推 DFS:`200. 岛屿数量`](#先手推 DFS:
- [5. 这类题在面试里考什么](#5. 这类题在面试里考什么)
- [6. 高频题清单](#6. 高频题清单)
- [7. 这类题最容易犯的 4 个错误](#7. 这类题最容易犯的 4 个错误)
- [8. 代表题精讲 1](#8. 代表题精讲 1)
-
- 题目
- 思路
- [Java 代码](#Java 代码)
- 复杂度
- 边界提醒
- 如果这是面试现场,你可以这样说
- [9. 代表题精讲 2](#9. 代表题精讲 2)
-
- 题目
- 思路
- [Java 代码](#Java 代码)
- 复杂度
- 边界提醒
- 如果这是面试现场,你可以这样说
- [10. 其余题模板与关键片段](#10. 其余题模板与关键片段)
-
- [\`994. 腐烂的橘子\`(https://leetcode.cn/problems/rotting-oranges/)](#
994. 腐烂的橘子) - [\`733. 图像渲染\`(https://leetcode.cn/problems/flood-fill/)](#
733. 图像渲染)
- [\`994. 腐烂的橘子\`(https://leetcode.cn/problems/rotting-oranges/)](#
- [11. 图论题先判断什么](#11. 图论题先判断什么)
- [12. 错题本记录方式](#12. 错题本记录方式)
- [13. 面试前 3 分钟速记](#13. 面试前 3 分钟速记)
- [14. 快速验证:同一套判断能不能落到 200 / 994 / 207](#14. 快速验证:同一套判断能不能落到 200 / 994 / 207)
- [15. 本地样例验证结果](#15. 本地样例验证结果)
- [16. 最后总结:图论题真正难的不是模板,而是建模速度](#16. 最后总结:图论题真正难的不是模板,而是建模速度)
1. 核心知识点
实习面试里的图论,最常见的不是 Dijkstra 或 Floyd,而是下面 3 类基础模型:
- 网格 DFS:常见于"数连通块""把一整片区域染色"。
- 多源 BFS:常见于"按层扩散""求最少几轮传播到位"。
- 拓扑排序:常见于"课程依赖""任务先后关系""判断是否有环"。
你可以先把它们理解成三个固定问法:
- 如果题目在问"这一整片算一个块吗",优先想 DFS / BFS。
- 如果题目在问"最少经过几步扩散完成",优先想 BFS。
- 如果题目在问"这些依赖能不能都做完",优先想拓扑排序。
2. 图论题型选择表:DFS / BFS / 拓扑排序什么时候用
| 题目特征 | 常见问题 | 优先解法 | 你在面试里要先说清楚什么 |
|---|---|---|---|
| 二维矩阵,上下左右连通 | 有几个连通块、整片区域是否连通 | DFS / BFS | 一个格子是不是一个节点,连边规则是什么 |
| 从多个起点同时扩散 | 最少几轮传播完成、最短层数 | BFS | 为什么要按层处理,初始队列里有哪些起点 |
| 课程 / 任务存在先后依赖 | 是否有环、能否完成全部任务 | 拓扑排序 | 邻接表怎么建,入度表示什么 |
真正高频的失误不是"不会写",而是"题型选错":
200. 岛屿数量本质是数连通块,不是在求最短路径。994. 腐烂的橘子本质是多源扩散,不适合拿 DFS 去硬做分钟数。207. 课程表本质是判断依赖图里是否有环,不是在普通遍历里随便走一遍。
3. 动画辅助理解:图论题先建模,再决定怎么遍历
你可以先打开这页分步动画:
这个动画最值得盯住的不是按钮,而是 4 个固定动作:
- 先说明节点和边怎么定义。
- 再决定起点怎么入栈 / 入队。
- 访问邻居时立刻处理
visited,避免重复访问。 - 最后根据题目要求统计答案,是连通块数量、扩散轮数,还是可完成课程数。

4. 用样例手推 DFS / 拓扑排序
这一节非常重要。图论题如果只看模板,不手推状态,你在面试里还是容易卡。
先手推 DFS:200. 岛屿数量
样例:
text
grid = [
['1','1','0','0'],
['1','0','0','1'],
['0','0','1','1']
]
你可以把每个值为 '1' 的格子看成一个陆地节点,上下左右相邻就有边。
手推过程:
| 扫描位置 | 看到什么 | 动作 | 当前岛屿数 |
|---|---|---|---|
(0,0) |
'1' 且未访问 |
岛屿数 +1,从 (0,0) 开始 DFS,把 (0,0)、(0,1)、(1,0) 全部淹没 / 标记 |
1 |
| 继续扫描第一、二行 | 已访问陆地或海水 | 跳过 | 1 |
(1,3) |
'1' 且未访问 |
岛屿数 +1,DFS 扩到 (2,3)、(2,2) |
2 |
| 扫描结束 | 没有新的未访问陆地 | 返回答案 | 2 |
你在面试里要说清楚的关键点是:
- 这题不是"从某个起点找到一条路径",而是"扫描全图,遇到新陆地就扩掉整片连通块"。
- 每次 DFS 的目的不是求最短路,而是"把这一整片陆地都处理掉,避免重复计数"。
再手推拓扑排序:207. 课程表
样例:
text
numCourses = 4
prerequisites = [[1,0],[2,0],[3,1],[3,2]]
这表示:
- 学
1之前先学0 - 学
2之前先学0 - 学
3之前先学1和2
所以邻接表和入度表会变成:
text
0 -> [1, 2]
1 -> [3]
2 -> [3]
3 -> []
indegree = [0, 1, 1, 2]
手推过程:
| 队列 | 出队课程 | 更新后入度变化 | 已处理课程数 |
|---|---|---|---|
[0] |
0 |
1: 1->0,2: 1->0,把 1、2 入队 |
1 |
[1,2] |
1 |
3: 2->1 |
2 |
[2] |
2 |
3: 1->0,把 3 入队 |
3 |
[3] |
3 |
无后续课程 | 4 |
最后处理课程数等于 numCourses,说明图里没有环,可以学完。
你在面试里要强调:
- 入度为
0的课,说明它当前没有前置依赖,可以先学。 - 如果图里有环,那么环上的课程入度永远不可能全部降到
0。 - 所以最后只要比较"处理掉的课程数"和"总课程数"是否相等,就能判断能不能学完。
5. 这类题在面试里考什么
图论题考的不是名词量,而是你能不能把题目稳定抽象成"点 + 边 + 遍历规则"。
面试官通常会看:
- 你能不能先判断它是网格图、普通图还是依赖图。
- 你会不会解释为什么该用 DFS、BFS 或拓扑排序。
- 你能不能把
visited、邻接表、入度表维护清楚。 - 你是否知道答案统计发生在什么时候,是每次发现新连通块时、每一层 BFS 结束时,还是拓扑出队时。
真正拉开差距的地方在于:你不只是把代码敲出来,还能解释"为什么这个统计口径正好对应题目要的答案"。
6. 高频题清单
| 题目 | 来源 | 难度 | 高频属性 |
|---|---|---|---|
200. 岛屿数量 |
LeetCode 热题 100 | Medium | 高频 |
994. 腐烂的橘子 |
LeetCode 热题 100 | Medium | 高频 |
207. 课程表 |
LeetCode 热题 100 | Medium | 高频 |
733. 图像渲染 |
面试经典 150 | Easy | 基础高频 |
7. 这类题最容易犯的 4 个错误
- 看到矩阵题,没有意识到本质上是网格图。
visited标记时机错了,导致重复入队或重复 DFS。- 把"求最少几轮扩散"的题写成 DFS,结果分钟数不好统计。
课程表不会把依赖关系转成邻接表和入度表。
8. 代表题精讲 1
题目
思路
这题本质上是"数二维网格里的连通块数量"。
扫描整个矩阵时,只要看到一个还没处理过的 '1',就说明发现了一座新岛屿,此时答案加一,然后从这个格子出发 DFS,把整片和它连通的陆地全部标记掉。这样后面再扫到这片区域时,就不会重复计数。
这题为什么适合 DFS:
- 每次进入 DFS,目标都是"扩完这一整片连通块"。
- 不需要维护层数,也不需要比较最短路径。
- 递归写法足够直接,面试里表达也最顺。
Java 代码
java
class Solution {
private static final int[][] DIRS = {
{1, 0}, {-1, 0}, {0, 1}, {0, -1}
};
public int numIslands(char[][] grid) {
int rows = grid.length;
int cols = grid[0].length;
int islands = 0;
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
if (grid[row][col] == '1') {
islands++;
dfs(grid, row, col);
}
}
}
return islands;
}
private void dfs(char[][] grid, int row, int col) {
if (row < 0 || row >= grid.length || col < 0 || col >= grid[0].length) {
return;
}
if (grid[row][col] != '1') {
return;
}
grid[row][col] = '0';
for (int[] dir : DIRS) {
dfs(grid, row + dir[0], col + dir[1]);
}
}
}
复杂度
- 时间复杂度:
O(m * n) - 空间复杂度:递归栈最坏
O(m * n)
边界提醒
这题最容易错的地方有三个:
- 忘了在 DFS 里把当前格子标记掉,导致重复访问。
- 只从一个起点出发,却忘了外层还要扫描整张图。
- 把对角线也当成连通方向,结果答案算多。
这题默认只看上下左右,不看斜对角。这个细节如果面试里不先说,容易被追问。
如果这是面试现场,你可以这样说
这题我会把矩阵看成一个网格图,每个陆地格子是节点,上下左右相邻就有边。扫描矩阵时,只要遇到一个还没处理过的 '1',就说明发现了一整片新连通块,答案加一,然后用 DFS 把这片陆地全部标记掉,避免后面重复计数。
9. 代表题精讲 2
题目
思路
这题问的是:所有课程依赖关系里是否存在环。只要没有环,就能完成所有课程。
最稳的做法是拓扑排序:
- 建邻接表,记录一门课学完后能解锁哪些课。
- 统计每门课当前的入度。
- 把所有入度为
0的课程入队。 - 每次弹出一门课,就把它指向的后续课程入度减一。
- 如果某门课入度减到
0,说明它现在没有前置依赖了,可以入队。 - 最后比较处理课程数和总课程数。
Java 代码
java
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
graph.add(new ArrayList<>());
}
int[] indegree = new int[numCourses];
for (int[] edge : prerequisites) {
int course = edge[0];
int pre = edge[1];
graph.get(pre).add(course);
indegree[course]++;
}
Queue<Integer> queue = new ArrayDeque<>();
for (int i = 0; i < numCourses; i++) {
if (indegree[i] == 0) {
queue.offer(i);
}
}
int finished = 0;
while (!queue.isEmpty()) {
int current = queue.poll();
finished++;
for (int next : graph.get(current)) {
indegree[next]--;
if (indegree[next] == 0) {
queue.offer(next);
}
}
}
return finished == numCourses;
}
}
复杂度
- 时间复杂度:
O(V + E) - 空间复杂度:
O(V + E)
边界提醒
这题最容易误判的地方是:
- 以为"遍历过所有点"就够了,但实际上关键是能不能按依赖顺序不断找到入度为
0的点。 - 邻接表方向建反了。这里必须是
pre -> course,表示学完前置课程后,才能解锁后续课程。 - 忘了最后比较
finished == numCourses,导致即使有环也返回了true。
如果这是面试现场,你可以这样说
这题本质是在判断依赖图里有没有环。我会用拓扑排序:先统计每门课的入度,把所有入度为 0 的课入队,再不断出队并更新后续课程的入度。如果最后能处理掉全部课程,说明图里没有环;否则就说明有课程被环卡住了。
10. 其余题模板与关键片段
994. 腐烂的橘子
这题的关键不是"会不会遍历",而是"为什么必须按层扩散"。
- 所有初始腐烂橘子都要先入队,因为它们是同一时刻的多个起点。
- 每处理完一层队列,才代表过去了 1 分钟。
- 因此这题优先想多源 BFS,而不是 DFS。
关键片段:
java
while (!queue.isEmpty() && fresh > 0) {
int size = queue.size();
minutes++;
for (int i = 0; i < size; i++) {
int[] current = queue.poll();
for (int[] dir : DIRS) {
int nextRow = current[0] + dir[0];
int nextCol = current[1] + dir[1];
if (isFresh(grid, nextRow, nextCol)) {
grid[nextRow][nextCol] = 2;
fresh--;
queue.offer(new int[] {nextRow, nextCol});
}
}
}
}
733. 图像渲染
这题是最基础的网格 DFS / BFS 题。面试里如果你第一次接触图论题,往往会先从它开始热身。
11. 图论题先判断什么
真正动手写代码前,你可以先问自己 4 个问题:
- 这题里的节点是什么,是格子、课程,还是普通图节点。
- 边怎么连,是上下左右、依赖关系,还是邻接关系。
- 题目要的是连通块数量、最短扩散层数,还是是否存在环。
visited应该在什么时候标记,是入队时、出队时,还是进入 DFS 时。
这 4 个问题一旦先说清楚,图论题的大部分思路就已经定下来了。
12. 错题本记录方式
图论题建议重点记:
- 这题里的点和边分别是什么。
- 我为什么判断该用 DFS / BFS / 拓扑排序。
visited或入度表是怎么维护的。- 这题的答案到底是在什么时候统计出来的。
13. 面试前 3 分钟速记
- 矩阵上下左右扩散,优先想网格图。
- 数连通块,常见 DFS / BFS。
- 求最少几轮扩散,优先想多源 BFS。
- 课程依赖、先修关系,优先想拓扑排序。
- 图题一上来先说节点、边、访问标记。
14. 快速验证:同一套判断能不能落到 200 / 994 / 207
你在面试前可以用下面这张表做最后自检,看看自己是不是真的把这类题吃透了:
| 题目 | 先识别什么 | 正确做法 | 为什么不是别的做法 |
|---|---|---|---|
200. 岛屿数量 |
网格图、数连通块 | DFS / BFS | 它不是求最短层数,不需要按层统计分钟 |
994. 腐烂的橘子 |
多起点同时扩散 | 多源 BFS | 题目要最少几分钟,天然需要按层推进 |
207. 课程表 |
依赖关系、有无环 | 拓扑排序 | 核心不是遍历所有点,而是看能否持续找到入度为 0 的点 |
如果你能在 30 秒内把这三题都归到正确模型,并说明"为什么不用另两种",那这类图论高频题基本就已经掌握到位了。
15. 本地样例验证结果
为了避免"思路看起来对,但代码细节有误",我按文中的 200 和 207 代码思路做了本地样例验证,输出如下:
text
numIslands=2
canFinish=true
这两行结果分别对应:
200. 岛屿数量的手推样例最终得到2座岛。207. 课程表的依赖样例最终可以完成全部课程,因此返回true。
如果你自己复盘时写出的结果和这里不一致,优先检查 3 个地方:DFS 是否真的把整片陆地都标记掉了、拓扑排序的边方向有没有建反、入度减到 0 时有没有及时入队。
16. 最后总结:图论题真正难的不是模板,而是建模速度
图论题最容易让人慌的地方,是题面不一定直接告诉你"这是图"。但只要你养成固定顺序,很多题都会变简单:
- 先找节点和边。
- 再判断这是连通块、扩散层数,还是依赖有环。
- 然后决定用 DFS、BFS,还是拓扑排序。
- 最后明确答案统计发生在什么时候。
面试里你只要能把这条链路说顺,图论题就不会再只是"背过几个模板",而是真正掌握了这一类题的公共骨架。
上期速递:【Java实习面试算法冲刺】回溯
感谢阅读,记得点赞、关注、收藏,欢迎各位评论区交流!!!