图论算法题复习笔记
整理邻接表建图、BFS/DFS 遍历、网格图 dx dy、拓扑排序、并查集、BFS 与 Dijkstra 最短路,并附一组精简练习题。
这里整理一组练图论基础模板的 LeetCode 题单。目标不是追求难题,而是把这些常用写法练熟:
邻接表建图、BFS、DFS、网格 BFS/DFS、拓扑排序、并查集、最短路、网络流。
一、入门:先练「图怎么存」
题目
| 题号 | 题名 | 练什么 |
|---|---|---|
| 797 | 所有可能的路径 | 有向图邻接表 + DFS |
| 841 | 钥匙和房间 | 邻接表 + BFS/DFS |
| 1971 | 寻找图中是否存在路径 | 无向图建图 + BFS/DFS |
| 1791 | 找出星型图的中心节点 | 边的理解 |
邻接表建图模板
先把这个板子练熟(有向图):
vector<vector<int>> g(n);
for (int i = 0; i < edges.size(); i++) {
int u = edges[i][0];
int v = edges[i][1];
g[u].push_back(v);
}
无向图就是双向各 push 一次:
g[u].push_back(v);
g[v].push_back(u);
注意:无向图只 push 一边会漏掉连通关系;建图前 vector<vector<int>> g(n) 先开好大小。
二、BFS / DFS 基础遍历
题目
| 题号 | 题名 | 推荐做法 |
|---|---|---|
| 547 | 省份数量 | DFS / BFS |
| 841 | 钥匙和房间 | DFS / BFS |
| 1971 | 寻找图中是否存在路径 | BFS |
| 2316 | 统计无向图中无法互相到达点对数 | DFS 统计连通块大小 |
BFS 核心模板
vector<int> visited(n, 0);
queue<int> q;
q.push(start);
visited[start] = 1;
while (!q.empty()) {
int cur = q.front();
q.pop();
for (int i = 0; i < g[cur].size(); i++) {
int next = g[cur][i];
if (visited[next] == 1) {
continue;
}
visited[next] = 1;
q.push(next);
}
}
注意:入队时立刻标 visited,不然同一节点会重复入队。
三、网格图:dx dy 板子
这是面试最常见的一类。
题目
| 题号 | 题名 | 练什么 |
|---|---|---|
| 200 | 岛屿数量 | 网格 DFS/BFS |
| 695 | 岛屿的最大面积 | DFS 统计面积 |
| 994 | 腐烂的橘子 | 多源 BFS |
| 542 | 01 矩阵 | 多源 BFS |
| 733 | 图像渲染 | 简单 Flood Fill |
| 130 | 被围绕的区域 | 从边界反向 DFS/BFS |
| 1091 | 二进制矩阵中的最短路径 | 八方向 BFS |
| 417 | 太平洋大西洋水流问题 | 反向 DFS/BFS |
四方向板子
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, 1, -1};
for (int k = 0; k < 4; k++) {
int nx = x + dx[k];
int ny = y + dy[k];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
continue;
}
// 处理 grid[nx][ny]
}
其中 n = grid.size(),m = grid[0].size(),越界直接 continue。
八方向板子
int dx[8] = {1, 1, 1, 0, 0, -1, -1, -1};
int dy[8] = {1, 0, -1, 1, -1, 1, 0, -1};
四、拓扑排序:课程表这一类
这是你现在正在练的。
题目
| 题号 | 题名 | 练什么 |
|---|---|---|
| 207 | 课程表 | 拓扑排序判环 |
| 210 | 课程表 II | 输出拓扑序 |
| 802 | 找到最终的安全状态 | 反图 + 拓扑 / DFS 判环 |
| 1462 | 课程表 IV | 传递依赖 |
| 310 | 最小高度树 | 拓扑剥叶子 |
| 1203 | 项目管理 | 分组拓扑排序,偏难 |
最核心先刷:
207 -> 210 -> 802 -> 310
拓扑排序模板
queue<int> q;
vector<vector<int>> g(n);
vector<int> indeg(n, 0);
for (int i = 0; i < edges.size(); i++) {
int u = edges[i][0];
int v = edges[i][1];
g[u].push_back(v);
indeg[v]++;
}
for (int i = 0; i < n; i++) {
if (indeg[i] == 0) {
q.push(i);
}
}
int learned = 0;
while (!q.empty()) {
int learning = q.front();
q.pop();
learned++;
for (int i = 0; i < g[learning].size(); i++) {
int toBeLearn = g[learning][i];
indeg[toBeLearn]--;
if (indeg[toBeLearn] == 0) {
q.push(toBeLearn);
}
}
}
return learned == n;
注意:learned == n 才是无环,小于 n 说明有环。减入度只对邻居操作,别动 indeg[u]。
五、并查集:连通性专用
并查集适合处理:
谁和谁属于同一个集合。
题目
| 题号 | 题名 | 练什么 |
|---|---|---|
| 547 | 省份数量 | 并查集入门 |
| 684 | 冗余连接 | 找环 |
| 1971 | 寻找图中是否存在路径 | 连通性 |
| 1319 | 连通网络的操作次数 | 连通块数量 |
| 990 | 等式方程的可满足性 | 字符并查集 |
| 721 | 账户合并 | 字符串并查集,偏实战 |
UnionFind 板子
class UnionFind {
public:
vector<int> parent;
UnionFind(int n) {
parent.resize(n);
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
void merge(int a, int b) {
int pa = find(a);
int pb = find(b);
if (pa != pb) {
parent[pa] = pb;
}
}
};
注意:merge 前先对 a、b 各做一次 find,直接 parent[a] = b 会丢掉路径压缩后的根。
六、最短路:BFS / Dijkstra
普通无权图最短路,用 BFS。 带权正边最短路,用 Dijkstra。
BFS 最短路题
| 题号 | 题名 | 练什么 |
|---|---|---|
| 1091 | 二进制矩阵中的最短路径 | 八方向 BFS |
| 752 | 打开转盘锁 | 状态图 BFS |
| 127 | 单词接龙 | 状态图 BFS,偏难 |
| 433 | 最小基因变化 | BFS |
Dijkstra 题
| 题号 | 题名 | 练什么 |
|---|---|---|
| 743 | 网络延迟时间 | Dijkstra 模板 |
| 1514 | 概率最大的路径 | Dijkstra 变形 |
| 1631 | 最小体力消耗路径 | Dijkstra on Grid |
| 787 | K 站中转内最便宜的航班 | 最短路变形,偏难 |
Dijkstra 堆优化板子(743 完整例题)
// 743 网络延迟时间:从 k 出发到所有点的最短路,取最大值
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
// 邻接表:g[u] = {v, w}
vector<vector<pair<int, int>>> g(n + 1);
for (auto& e : times) {
g[e[0]].push_back({e[1], e[2]});
}
const int INF = 0x3f3f3f3f;
vector<int> dist(n + 1, INF); // dist 全部初始化 INF
dist[k] = 0;
// 小根堆:{当前距离, 节点}
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;
pq.push({0, k});
while (!pq.empty()) {
auto [d, u] = pq.top();
pq.pop();
if (d > dist[u]) {
continue; // 过期的堆顶,跳过
}
for (auto& [v, w] : g[u]) {
if (d + w < dist[v]) { // 松弛
dist[v] = d + w;
pq.push({dist[v], v});
}
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (dist[i] == INF) {
return -1; // 有点到不了
}
ans = max(ans, dist[i]);
}
return ans;
}
变形点:
- 1514:求概率最大,松弛方向反过来。dist 初始 0、起点为 1,
d * w > dist[v]才更新。 - 1631:网格图直接当最短路跑,格子就是点,边权是相邻格高度差,松弛改成
max(d, w)取最小。 - 787:限制 k 次中转,堆里的状态带上已飞次数
(d, u, cnt),cnt > k + 1就跳过;也可以按层数做 k + 1 轮松弛。
七、网络流:Dinic 板子
什么时候用:
最大流 / 最小割、二分图匹配、有容量限制的分配问题。
建模三板斧:
- 超级源汇:源点连所有「供给」,汇点收所有「需求」。
- 容量 = 限制:这个资源能用多少,边容量就设多少。
- 二分图:源连左边、右边连汇,左右之间容量设 1。
板子(背到会默写):
struct Edge {
int to, cap, rev; // rev: 反向边在 g[to] 里的下标
};
class Dinic {
public:
vector<vector<Edge>> g;
vector<int> level, it;
Dinic(int n) : g(n), level(n), it(n) {}
void addEdge(int u, int v, int cap) {
g[u].push_back({v, cap, (int)g[v].size()});
g[v].push_back({u, 0, (int)g[u].size() - 1}); // 反向边容量 0
}
// bfs 分层
bool bfs(int s, int t) {
fill(level.begin(), level.end(), -1);
queue<int> q;
level[s] = 0;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
for (auto& e : g[u]) {
if (e.cap > 0 && level[e.to] < 0) {
level[e.to] = level[u] + 1;
q.push(e.to);
}
}
}
return level[t] >= 0;
}
// dfs 增广 + 当前弧优化
int dfs(int u, int t, int f) {
if (u == t) {
return f;
}
for (int& i = it[u]; i < (int)g[u].size(); i++) {
Edge& e = g[u][i];
if (e.cap > 0 && level[e.to] == level[u] + 1) {
int d = dfs(e.to, t, min(f, e.cap));
if (d > 0) {
e.cap -= d;
g[e.to][e.rev].cap += d; // 反向边加回流量,留后悔药
return d;
}
}
}
return 0;
}
int maxFlow(int s, int t) {
int flow = 0;
while (bfs(s, t)) {
fill(it.begin(), it.end(), 0);
int f;
while ((f = dfs(s, t, INT_MAX)) > 0) {
flow += f;
}
}
return flow;
}
};
LeetCode 直接考网络流的题很少,但板子要能默写。面试碰到「分配 / 匹配 + 容量」就往这上面想。
八、DFS 判环
拓扑排序可以 BFS 做,DFS 也能判环。你可以后面补。
题目
| 题号 | 题名 | 练什么 |
|---|---|---|
| 207 | 课程表 | DFS 三色标记判环 |
| 802 | 找到最终的安全状态 | DFS 判环 |
| 1059 | 从始点到终点的所有路径 | DFS 判环,偏难 |
三色标记
0 = 没访问
1 = 正在访问
2 = 访问完成
九、推荐刷题顺序
不要一上来全刷,按这个顺序来:
第一组:邻接表 + 普通 BFS/DFS
841 -> 1971 -> 547
第二组:网格图 dx dy
733 -> 200 -> 695 -> 994 -> 542
第三组:拓扑排序
207 -> 210 -> 802 -> 310
第四组:并查集
547 -> 1971 -> 684 -> 1319 -> 990
第五组:状态图 BFS
752 -> 1091 -> 433 -> 127
第六组:Dijkstra
743 -> 1631 -> 1514
十、精简题单
要是你只想快速掌握面试图论板子,刷这 15 道就够有感觉:
841 钥匙和房间
1971 寻找图中是否存在路径
547 省份数量
733 图像渲染
200 岛屿数量
695 岛屿的最大面积
994 腐烂的橘子
542 01 矩阵
207 课程表
210 课程表 II
802 找到最终的安全状态
684 冗余连接
1319 连通网络的操作次数
752 打开转盘锁
743 网络延迟时间
你现在最该练的是:
207 课程表
210 课程表 II
841 钥匙和房间
200 岛屿数量
994 腐烂的橘子
这五道刷完,图论最基础的几个板子就真的立住了。