1. 简介

本文将介绍如何在三种图遍历算法中追踪路径:深度优先搜索(DFS)、广度优先搜索(BFS)和迪杰斯特拉算法(Dijkstra’s Algorithm)。重点在于展示这些算法在找到目标节点的同时,如何返回完整的路径,而不仅仅是路径长度。

虽然 DFS 本身不能保证找到最短路径,但在某些实现中仍可追踪路径;BFS 则在无权重图中能保证找到最短路径;而 Dijkstra 算法则适用于带正权重图的最短路径查找。

2. 递归深度优先搜索中的路径追踪

DFS 有两种实现方式:递归和迭代。在递归版本中追踪路径相对简单,只需在找到目标节点后,将路径逐层返回即可。

以下是一个递归 DFS 的伪代码示例:

algorithm DepthFirstSearch(s, target):
    // INPUT
    //    s = 起始节点
    //    target = 判断是否为目标节点的函数
    // OUTPUT
    //    从 s 到目标节点的路径

    if target(s):
        return [s]

    for v in neighbors(s):
        path <- DepthFirstSearch(v, target)
        
        if path != empty set:
            path <- prepend v to path
            return path
    
    return empty set

2.1 示例

考虑如下图结构:

example1

我们希望从节点 ST 找到一条路径。DFS 会按顺序展开子节点,最终找到路径 [S, A, B, C, T]

example1path

虽然在这个例子中找到了最短路径,但需要注意的是,DFS 并不总是能找到最短路径,路径的准确性取决于子节点的访问顺序。

3. 迭代深度优先搜索中的路径追踪

递归 DFS 在某些场景下可能效率不高,因此我们常使用迭代实现。但迭代方式中路径追踪稍复杂,通常有两种方式:

  • 记录父节点:通过记录每个节点的父节点,在找到目标节点后回溯路径。
  • 保存完整路径:在搜索过程中直接保存每条路径,一旦找到目标节点即可直接返回。

3.1 记录父节点关系

algorithm IterativeDepthFirstSearchWithPathTracing(s, target, neighbors):
    // INPUT
    //    s = 起始节点
    //    target = 目标判断函数
    // OUTPUT
    //    从 s 到目标节点的路径

    if target(s):
        return s

    frontier <- 一个 LIFO 队列,初始只包含 s
    explored <- 一个空集合
    memory <- 用于记录父节点关系的结构
    Set s.parent <- null

    while frontier is not empty:
        u <- pop 最近加入的节点
        add u to explored

        for v in neighbors(u):
            v.parent <- u
            if u not in explored or frontier:
                if target(v):
                    path <- reconstruct path from s to v using memory
                    return path
                add v to frontier

    return empty set

3.2 路径重建

通过父节点回溯路径:

algorithm BackwardPathReconstruction(s, t, memory):
    // INPUT
    //    s = 起始节点
    //    t = 目标节点
    //    memory = 父节点记录结构
    // OUTPUT
    //    从 t 到 s 的路径

    path <- []
    u <- t

    while u != null:
        path.prepend(u)
        u <- memory[u]

    return path

若需要从 s 到 t 的顺序,可将路径反转:

algorithm ForwardPathReconstruction(s, t, memory):
    // INPUT
    //    s = 起始节点
    //    t = 目标节点
    //    memory = 父节点记录结构
    // OUTPUT
    //    从 s 到 t 的路径

    stack <- []
    u <- t

    while u != null:
        push u to stack
        u <- memory[u]

    path <- []
    while stack not empty:
        u <- pop from stack
        path.append(u)

    return path

3.3 父节点记录结构的实现

可以使用指针或哈希表来记录父节点关系:

  • 每个节点保存一个 parent 指针
  • 使用数组或哈希表,以节点 ID 为键,存储其父节点 ID

例如:

int[] parent = new int[totalNodes];
parent[0] = -1; // root node

3.4 示例

以下是一个迭代 DFS 的路径追踪过程示意图:

example2

3.5 直接保存路径

另一种方式是在迭代过程中直接保存路径:

algorithm IterativeDFSWithPathMemorization(s, target, neighbors):
    // INPUT
    //    s = 起始节点
    //    target = 目标判断函数
    // OUTPUT
    //    从 s 到目标节点的路径

    if target(s):
        return [s]

    frontier <- 一个 LIFO 队列,初始包含 (s, [s])
    explored <- 一个空集合

    while frontier not empty:
        (u, path) <- pop from frontier
        add u to explored

        for v in neighbors(u):
            new_path <- path + [v]
            if v not in explored or frontier:
                if target(v):
                    return new_path
                add (v, new_path) to frontier

    return empty set

这种方式虽然直观,但内存开销较大,因为每个节点都保存了完整的路径。

3.6 示例

路径追踪过程示意如下:

example3-1

4. 广度优先搜索中的路径追踪

BFS 和 DFS 的路径追踪方式类似,但 BFS 使用 FIFO 队列,因此更适合寻找最短路径。

4.1 示例

BFS 从 ST 的路径查找过程如下:

example bfs

路径重建与 DFS 相同,使用父节点回溯即可:

algorithm BreadthFirstSearch(s, target, neighbors):
    // INPUT
    //    s = 起始节点
    //    target = 目标判断函数
    // OUTPUT
    //    从 s 到目标节点的路径

    if target(s):
        return [s]

    frontier <- FIFO queue with s
    explored <- empty set
    s.parent <- null
    t <- null

    while frontier not empty:
        u <- pop oldest node
        add u to explored

        for v in neighbors(u):
            v.parent <- u
            if v not in explored or frontier:
                if target(v):
                    t <- v
                    break
                add v to frontier
        if target(v):
            break

    if t is null:
        return empty set
    else:
        return reconstruct path from s to t

5. 迪杰斯特拉算法中的路径追踪

Dijkstra 算法用于带权图中的最短路径查找,其路径追踪方式与 BFS 类似,但需要维护一个 prev 数组来记录每个节点的前驱节点。

algorithm Dijkstra(s, G):
    // INPUT
    //    s = 起始节点
    //    G = 图结构
    // OUTPUT
    //    dist = 每个节点的最短距离
    //    prev = 前驱节点数组

    queue <- 一个最小优先队列
    dist <- 初始化为无穷大
    prev <- 初始化为 null

    dist[s] <- 0
    for each node v in G:
        add v to queue with priority dist[v]

    while queue not empty:
        u <- extract min from queue
        for v in neighbors(u):
            if dist[u] + weight(u, v) < dist[v]:
                dist[v] <- dist[u] + weight(u, v)
                prev[v] <- u
                update v's priority in queue

    return (dist, prev)

路径重建方式如下:

algorithm BackwardPathReconstruction(s, t, prev):
    // INPUT
    //    s = 起始节点
    //    t = 目标节点
    //    prev = 前驱数组
    // OUTPUT
    //    从 t 到 s 的路径

    path <- []
    u <- t

    while u != null:
        path.prepend(u)
        u <- prev[u]

    return path

6. 总结

算法 路径追踪方式 是否保证最短路径
DFS(递归) 递归返回路径
DFS(迭代) 父节点记录或路径保存
BFS 父节点记录 是(无权图)
Dijkstra 前驱数组记录 是(带权图)

建议

  • 若图无权重,且需最短路径,优先使用 BFS
  • 若图有权重,使用 Dijkstra 并记录前驱节点
  • DFS 适用于路径存在性判断或非最短路径问题

⚠️ 注意:DFS 和迭代 DFS 的路径准确性依赖于节点访问顺序,实际应用中应根据需求选择合适的方式。


原始标题:Tracing the Path in DFS, BFS, and Dijkstra’s Algorithm