07 · 图论

来源: labuladong BFS/DFS 框架 + 代码随想录图论专题
核心价值: 网格是图的特例,图的遍历是 DFS/BFS 的终极应用
题量: 4 题


一、本质理解

图的本质是 节点的集合 + 节点之间的连接关系

labuladong 将图归为”链表的延伸”——树的每个节点有多个子节点(多叉树),而图就是多叉树的进一步泛化(可以有环)。

核心关系:

链表 → 二叉树 → 多叉树 → 图
(线性) (二叉)(多叉) (有环)

二、图的表示

邻接表(最常用)

# 有向图
graph = {
    0: [1, 2],
    1: [3],
    2: [3],
    3: []
}
 
# 无向图(双向记录)
graph = {
    0: [1, 2],
    1: [0, 3],
    2: [0, 3],
    3: [1, 2]
}

网格(隐式图)

网格可以看作一个特殊的图,每个格子有 4 个邻居(上下左右)。


三、核心模板

模板 1:图 DFS

def dfs_graph(graph):
    visited = set()
    
    def dfs(node):
        if node in visited:
            return
        visited.add(node)
        # 处理节点
        for neighbor in graph[node]:
            dfs(neighbor)
    
    # 遍历所有节点(处理非连通图)
    for node in graph:
        if node not in visited:
            dfs(node)

模板 2:图 BFS(求最短路径/最少步数)

from collections import deque
 
def bfs_graph(start, target, graph):
    q = deque([start])
    visited = {start}
    step = 0
    
    while q:
        size = len(q)
        for _ in range(size):
            node = q.popleft()
            if node == target:
                return step
            for neighbor in graph[node]:
                if neighbor not in visited:
                    visited.add(neighbor)
                    q.append(neighbor)
        step += 1
    return -1

模板 3:网格 DFS(岛屿问题)

def num_islands(grid):
    if not grid:
        return 0
    
    m, n = len(grid), len(grid[0])
    count = 0
    
    def dfs(i, j):
        if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] != '1':
            return
        grid[i][j] = '0'  # 标记为已访问(沉岛)
        dfs(i + 1, j)
        dfs(i - 1, j)
        dfs(i, j + 1)
        dfs(i, j - 1)
    
    for i in range(m):
        for j in range(n):
            if grid[i][j] == '1':
                count += 1
                dfs(i, j)
    
    return count

模板 4:多源 BFS(腐烂的橘子)

from collections import deque
 
def oranges_rotting(grid):
    m, n = len(grid), len(grid[0])
    q = deque()
    fresh = 0
    
    for i in range(m):
        for j in range(n):
            if grid[i][j] == 2:
                q.append((i, j))
            elif grid[i][j] == 1:
                fresh += 1
    
    if fresh == 0:
        return 0
    
    minutes = 0
    dirs = [(1, 0), (-1, 0), (0, 1), (0, -1)]
    
    while q and fresh > 0:
        minutes += 1
        for _ in range(len(q)):
            i, j = q.popleft()
            for di, dj in dirs:
                ni, nj = i + di, j + dj
                if 0 <= ni < m and 0 <= nj < n and grid[ni][nj] == 1:
                    fresh -= 1
                    grid[ni][nj] = 2
                    q.append((ni, nj))
    
    return minutes if fresh == 0 else -1

模板 5:拓扑排序(Kahn 算法 / BFS 版)

from collections import deque
 
def can_finish(num_courses, prerequisites):
    # 建图 + 计算入度
    graph = [[] for _ in range(num_courses)]
    indegree = [0] * num_courses
    
    for course, pre in prerequisites:
        graph[pre].append(course)
        indegree[course] += 1
    
    # 入度为 0 的节点入队
    q = deque([i for i in range(num_courses) if indegree[i] == 0])
    count = 0
    
    while q:
        node = q.popleft()
        count += 1
        for neighbor in graph[node]:
            indegree[neighbor] -= 1
            if indegree[neighbor] == 0:
                q.append(neighbor)
    
    return count == num_courses  # 无环则能完成

四、Trie(前缀树)

class Trie:
    def __init__(self):
        self.children = {}
        self.is_end = False
    
    def insert(self, word):
        node = self
        for ch in word:
            if ch not in node.children:
                node.children[ch] = Trie()
            node = node.children[ch]
        node.is_end = True
    
    def search(self, word):
        node = self._find(word)
        return node is not None and node.is_end
    
    def starts_with(self, prefix):
        return self._find(prefix) is not None
    
    def _find(self, word):
        node = self
        for ch in word:
            if ch not in node.children:
                return None
            node = node.children[ch]
        return node

五、Hot 100 图论题目清单

题号题目难度核心技巧建议用时
200岛屿数量Medium网格 DFS/BFS,沉岛法30 min
994腐烂的橘子Medium多源 BFS,层数统计30 min
207课程表Medium拓扑排序(Kahn 算法)35 min
208实现 TrieMedium嵌套字典建树30 min

六、易错点与技巧

  1. DFS vs BFS 选择:求最短路径/最少步数用 BFS,求连通性/路径用 DFS
  2. visited 的两种方式:额外 visited 集合(通用)vs 原地修改(网格题”沉岛”)
  3. 多源 BFS:腐烂橘子是典型,多个起点同时入队,逐层扩散
  4. 拓扑排序的两种实现:BFS(Kahn)和 DFS(检测后序遍历的逆序),Kahn 更直观
  5. Trie 的内存:每个字符一个节点,字符串量大时内存消耗大

七、复杂度总结

操作时间复杂度空间复杂度
图 DFS/BFSO(V + E)O(V)
网格 DFSO(m × n)O(m × n)
拓扑排序O(V + E)O(V + E)
Trie 操作O(len(word))O(总字符数)

八、参考与延伸