图论题型:解题思路与方法

图论题在面试中出现频率中等,但一旦出现就是硬考。和 DP 不同,图论题的套路更固定,模板更明确,一旦掌握骨架就很难出错。

图的表示

邻接表(最常用)

每个节点存一个列表,记录它直接连接的节点。

links = [[0,1],[1,2]]
adj = {0: [1], 1: [0,2], 2: [1]}

建表代码:

const adj: Record<number, number[]> = {}
for (let i = 0; i < n; i++) adj[i] = []
for (const [a, b] of links) {
    adj[a].push(b)  // 无向图要加两次
    adj[b].push(a)
}

适用场景:稀疏图(边数远小于 n²),大部分面试题。

邻接矩阵

用 n×n 的二维数组,matrix[i][j] = 1 表示 i 和 j 之间有边。

适用场景:稠密图,或需要快速判断”两个节点是否直接相连”。

边列表

就是题目给的 links = [[0,1],[1,2]] 本身。通常需要转成邻接表再用。

两种遍历方式

DFS(深度优先搜索)

一路走到底,走不通了回溯。

const visited = new Set<number>()
 
function dfs(node: number) {
    visited.add(node)
    for (const neighbor of adj[node]) {
        if (!visited.has(neighbor)) {
            dfs(neighbor)
        }
    }
}

特点:递归实现,代码简洁。适合连通分量、环检测、拓扑排序。

BFS(广度优先搜索)

一层一层扩展,用队列。

const visited = new Set<number>()
const queue: number[] = [start]
 
while (queue.length > 0) {
    const node = queue.shift()!
    visited.add(node)
    for (const neighbor of adj[node]) {
        if (!visited.has(neighbor)) {
            visited.add(neighbor)
            queue.push(neighbor)
        }
    }
}

特点:迭代实现,没有递归栈溢出风险。适合最短路径、层序遍历。

DFS vs BFS 选择

场景选哪个
连通分量都行,DFS 更简洁
最短路径(无权图)BFS
最短路径(带权图)Dijkstra
环检测DFS
拓扑排序DFS 或 BFS(Kahn 算法)
层序遍历BFS

常见图论题型

题型特征解法典型题
连通分量”有几组/几个连通块”DFS/BFS + 计数第 21 题
最短路径(无权)“最少几步到达”BFS迷宫最短路
最短路径(带权)“最短/最少成本”Dijkstra网络延迟
环检测”是否有环”DFS + 三色标记课程表
拓扑排序”依赖关系/执行顺序”DFS 或 Kahn课程表 II
并查集”合并集合/判断连通”Union-Find朋友圈、冗余连接
网格图二维矩阵当成图DFS/BFS + 四方向遍历岛屿数量

识别信号

题目出现这些词,往图论想:

  • “连通/连通分量/连通块”
  • “路径/最短路径/最少步数”
  • “网络/节点/边/链路”
  • “邻居/相邻/周围”
  • “网格/矩阵/岛屿”
  • “依赖/前置条件/执行顺序”(拓扑排序)
  • “合并集合/是否同一组”(并查集)

解题流程速查卡

1. 读题 → 识别"节点"和"边"是什么
2. 建邻接表(无向图加两次,有向图加一次)
3. 选 DFS 还是 BFS(看问题类型)
4. 写 visited 集合防重复访问
5. 遍历 + 计数/记录路径
6. 检查边界:空图、单节点、全连通、全孤立

并查集(Union-Find)

并查集是图论的另一种解法,专门处理”动态合并集合”的问题。

class UnionFind {
    parent: number[]
    
    constructor(n: number) {
        this.parent = Array(n).fill(0).map((_, i) => i)
    }
    
    find(x: number): number {
        if (this.parent[x] !== x) {
            this.parent[x] = this.find(this.parent[x])  // 路径压缩
        }
        return this.parent[x]
    }
    
    union(x: number, y: number): void {
        const px = this.find(x)
        const py = this.find(y)
        if (px !== py) this.parent[px] = py
    }
    
    count(): number {
        return new Set(this.parent.map((_, i) => this.find(i))).size
    }
}

适用场景:不需要遍历整个图,只需要判断”两个节点是否属于同一组”或”合并两个组”。比 DFS 更适合动态增减边的场景。

常见坑

  1. 无向图建表加两次adj[a].push(b)adj[b].push(a),只加一次会变成有向图。
  2. 忘记 visited:图可能有环,不标记 visited 会死循环。
  3. visited 时机错误:BFS 里要在入队时标记 visited,不是出队时。出队时标记可能导致重复入队。
  4. 网格图忘记边界检查:遍历四个方向时要检查是否越界(row >= 0 && row < rows && col >= 0 && col < cols)。
  5. 递归栈溢出:节点数很大时 DFS 可能栈溢出,改用 BFS(迭代)。

已做过的图论题

参考来源