图论算法模板:BFS/DFS、Dijkstra、拓扑排序、并查集与最小生成树

图论高频算法的可直接套用模板库:DFS/BFS 遍历、堆优化 Dijkstra、Kahn 拓扑排序、带路径压缩的并查集、Kruskal/Prim 最小生成树,每个模板配适用场景与复杂度。

图论算法模板

图论原理与复杂度证明见 图论算法详解。本文是背模板速查手册:六张可直接套用的模板,配合适用场景与选型决策,面试现场「识别题型 → 背模板 → 改条件」。

一、图遍历模板:DFS / BFS

邻接表构建(通用第一步)

from collections import defaultdict, deque

def build_graph(edges):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)   # 无向图加反向;有向图去掉这行
    return graph

DFS 模板(递归 + visited)

def dfs_template(graph, start, n):
    visited = [False] * n
    def dfs(node):
        visited[node] = True
        for neighbor in graph[node]:
            if not visited[neighbor]:
                dfs(neighbor)
    dfs(start)

BFS 模板(队列,求最短步数/层数)

def bfs_template(graph, start, n):
    visited = [False] * n
    queue = deque([start])
    visited[start] = True
    dist = 0
    while queue:
        for _ in range(len(queue)):     # 按层处理
            node = queue.popleft()
            for neighbor in graph[node]:
                if not visited[neighbor]:
                    visited[neighbor] = True
                    queue.append(neighbor)
        dist += 1
    return dist

网格图套路(岛屿问题 LC 200)

网格图也是一种图:每个格子有 4 个邻居(上下左右),用方向数组遍历。

void dfs(int[][] grid, int r, int c) {
    if (r < 0 || c < 0 || r >= grid.length || c >= grid[0].length
        || grid[r][c] == '0') return;
    grid[r][c] = '0';                      // 原地标记,省 visited
    int[] dr = {-1, 1, 0, 0}, dc = {0, 0, -1, 1};
    for (int i = 0; i < 4; i++) dfs(grid, r + dr[i], c + dc[i]);
}

选型:求最短路径/层数用 BFS(无权图);求连通分量/路径存在性/回溯用 DFS;网格题默认 DFS + 原地标记。

二、Dijkstra 模板(堆优化)

单源最短路径,无负权边时最优。

import heapq

def dijkstra(graph, start, n):
    dist = [float('inf')] * n
    dist[start] = 0
    heap = [(0, start)]          # (当前距离, 节点)
    while heap:
        d, node = heapq.heappop(heap)
        if d > dist[node]:       # 过期记录,跳过
            continue
        for neighbor, weight in graph[node]:
            nd = d + weight
            if nd < dist[neighbor]:
                dist[neighbor] = nd
                heapq.heappush(heap, (nd, neighbor))
    return dist

复杂度:O((V + E) log V)。

高频变体:

  • 网格 Dijkstra(LC 743 网络延迟、LC 1514 概率路径):把网格格点当作节点,代价换成边权/概率。
  • 多源 Dijkstra:初始时把多个源点一起入堆,求「离所有源点的最近距离」(LC 542 01 矩阵)。
  • 方案数:额外维护 ways[node],当 nd == dist[neighbor] 时累加。

注意事项:Dijkstra 不支持负权边——有负权用 Bellman-Ford/SPFA;若担心堆中大量过期记录,加 d > dist[node] 跳过即可。

三、拓扑排序模板(Kahn + 判环)

拓扑排序针对有向无环图(DAG),按入度从 0 的节点开始逐层剥离。

def topo_sort(num_nodes, edges):
    graph = defaultdict(list)
    in_degree = [0] * num_nodes
    for u, v in edges:
        graph[u].append(v)
        in_degree[v] += 1

    queue = deque([i for i in range(num_nodes) if in_degree[i] == 0])
    order = []
    while queue:
        node = queue.popleft()
        order.append(node)
        for neighbor in graph[node]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)

    # 有环则长度 < num_nodes
    return order if len(order) == num_nodes else []

复杂度:O(V + E)。

适用场景:课程表(LC 207/210)、编译依赖、任务调度、字典序排序(LC 269 外星词典——先把字符关系建成边再做拓扑)。

判环技巧:拓扑完成后检查 len(order) != num_nodes 即有环;DFS 版用三色标记(0 未访问/1 访问中/2 已访问),遇到 1 即有环。

四、并查集模板(路径压缩 + 按秩合并)

并查集解决动态连通性:合并、查是否连通、统计连通分量数。

class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n
        self.count = n               # 连通分量数

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])   # 路径压缩
        return self.parent[x]

    def union(self, x, y):
        px, py = self.find(x), self.find(y)
        if px == py:
            return False
        if self.rank[px] < self.rank[py]:                # 按秩合并
            px, py = py, px
        self.parent[py] = px
        if self.rank[px] == self.rank[py]:
            self.rank[px] += 1
        self.count -= 1
        return True

    def connected(self, x, y):
        return self.find(x) == self.find(y)

均摊复杂度:O(α(N)),几乎常数。

适用场景:

  • 判连通分量(LC 200 岛屿、LC 547 省份数量)。
  • 冗余连接(LC 684):加入的边若已连通则成环。
  • 账户合并(LC 721)、Kruskal 最小生成树。

注意事项:涉及坐标的二维网格可把 (r, c) 映射为 r * cols + c 的一维下标。

五、最小生成树模板

Kruskal(按边排序,配并查集,适合稀疏图)

def kruskal(edges, n):
    uf = UnionFind(n)
    edges.sort(key=lambda x: x[2])          # 按边权升序
    mst, total = [], 0
    for u, v, w in edges:
        if uf.union(u, v):                  # 不成环才选
            mst.append((u, v, w))
            total += w
            if len(mst) == n - 1:
                break
    return mst, total

Prim(从点出发 + 堆,适合稠密图)

def prim(graph, n, start=0):
    visited = [False] * n
    visited[start] = True
    heap = [(w, start, v) for v, w in graph[start]]
    heapq.heapify(heap)
    mst, total = [], 0
    while heap and len(mst) < n - 1:
        w, u, v = heapq.heappop(heap)
        if visited[v]:
            continue
        visited[v] = True
        mst.append((u, v, w))
        total += w
        for nxt, nw in graph[v]:
            if not visited[nxt]:
                heapq.heappush(heap, (nw, v, nxt))
    return mst, total

复杂度:Kruskal O(E log E),Prim O((V + E) log V)。

选型:稀疏图(E ≈ V)用 Kruskal;稠密图(E ≈ V²)用 Prim 邻接矩阵版 O(V²);边已经有序时 Kruskal 天然占优。

六、图算法选型速查

问题类型算法复杂度
遍历 / 连通分量DFS / BFSO(V + E)
无权图最短步数BFSO(V + E)
单源最短(无负权)DijkstraO((V + E) log V)
单源最短(有负权)Bellman-Ford / SPFAO(V·E)
全源最短Floyd-WarshallO(V³)
拓扑排序 / 判环Kahn / DFS 三色O(V + E)
动态连通性并查集O(α(N))
最小生成树Kruskal / PrimO(E log E) / O((V+E) log V)

七、常见问题

Q: 面试时图题的「一上来先做什么」?
先问三点:有向还是无向?加权还是无权?稀疏还是稠密?回答后立刻决定邻接表/矩阵与算法,边说边建图。

Q: DFS 与 BFS 怎么快速取舍?
要求最短路/最少步数 → BFS;递归回溯、路径列举 → DFS;只求连通性 → 两者都行,DFS 写起来短。

Q: 并查集的路径压缩和按秩合并都要吗?
路径压缩必须(否则可能退化成链);按秩合并是额外保险。面试写全加分,忘掉按秩合并不影响正确性。

Q: 模板直接背下来,遇到变体怎么改?
模板是骨架,变体改三处:图的构建(边/网格/隐式图)、状态定义(距离/概率/路径数)、合并条件(如 Kruskal 的边权阈值)。


相关文章:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页