1. 概述

在本教程中,我们将讨论如何在图中找到一条遍历所有节点的最短路径。

我们将首先明确问题定义并给出一个示例。随后,介绍两种不同的解决方案:暴力法基于 Dijkstra 的状态压缩法


2. 问题定义

给定一个图 $ G $,其中包含 $ V $ 个节点(编号从 $ 0 $ 到 $ V - 1 $)和 $ E $ 条带权边。我们的任务是找出一条路径,遍历所有节点,并使该路径的总权重最小。

需要注意的是:

  • 我们可以重复访问节点
  • 路径的起点和终点可以是任意节点
  • 所求路径是所有满足“遍历所有节点”条件的路径中总权重最小的那一个

来看一个示例:

WeightedGraph

上图中,遍历所有节点的最短路径为:
0 → 1 → 4 → 1 → 2 → 3,总权重为 6


3. 暴力法

3.1. 核心思想

  • 枚举所有节点的排列顺序
  • 每个排列表示一个访问顺序
  • 使用 Floyd-Warshall 算法预处理任意两点间的最短路径
  • 对每个排列,计算路径总权重,取最小值作为答案

3.2. 实现代码

algorithm BruteForceApproach(V, G):
    // INPUT
    //     V = the number of nodes in the graph
    //     G = the graph stored as an adjacency list
    // OUTPUT
    //     Returns the shortest path visiting all nodes in G

    answer <- infinity
    distance <- Calculate_Floyd_Warshall(G) // a matrix
    permutations <- Calculate_Permutations()

    for permutation in permutations:
        cost <- 0
        previous <- permutation[0]
        for node in permutation:
            cost <- cost + distance[previous, node]
            previous <- node
        if cost < answer:
            answer <- cost

    return answer

3.3. 时间复杂度分析

  • Floyd-Warshall:$ O(V^3) $
  • 所有排列数量为 $ V! $,每个排列遍历需要 $ O(V) $
  • 总时间复杂度:**$ O(V^3 + V \times V!) $**

⚠️ 该方法仅适用于 $ V \leq 10 $ 的小规模图,否则计算量爆炸


4. Dijkstra 状态压缩法

4.1. 核心思想

使用 Dijkstra 算法的变种,结合状态压缩(bitmask)来表示已访问的节点集合:

  • 每个状态用 (当前节点, 已访问节点集合) 表示
  • 初始时,每个节点作为起点,其 bitmask 只有对应位为 1
  • 使用优先队列进行松弛操作,逐步更新状态
  • 最终答案为所有节点中,bitmask 全为 1 时的最小代价

✅ 优点:相比暴力法,大大减少了无效路径的计算
❌ 缺点:空间复杂度较高,适用于 $ V \leq 16 $ 的情况

4.2. 实现代码

algorithm DijkstraApproach(V, G):
    // INPUT
    //     V = the number of nodes in the graph
    //     G = the graph stored as an adjacency list
    // OUTPUT
    //     Returns the shortest path visiting all nodes

    cost <- a matrix whose all entries are infinity
    priority_queue <- make an empty priority queue
    for node from 0 to V-1:
        priority_queue.add(node, 2^node)
        cost[node, 2^node] <- 0

    while priority_queue is not empty:
        current <- priority_queue.front().node
        mask <- priority_queue.front().bitmask
        priority_queue.pop()
        for child in G[current]:
            add <- weight(current, child)
            if cost[child, mask or 2^child] > cost[current, mask] + add:
                priority_queue.add(child, mask or 2^child)
                cost[child][mask or 2^child] <- cost[current][mask] + add

    answer <- infinity
    for node from 0 to V-1:
        answer <- min(answer, cost[node, 2^V - 1])

    return answer

4.3. 时间复杂度分析

  • 状态总数为 $ V \times 2^V $
  • 每次状态入队出队操作为 $ O(\log(V \times 2^V)) $
  • 总时间复杂度:**$ O(V \times 2^V \times \log(V \times 2^V)) $**

5. 总结

我们讨论了图中遍历所有节点的最短路径问题,并提供了两种解决方案:

方法 时间复杂度 适用场景
暴力法 $ O(V^3 + V \times V!) $ $ V \leq 10 $
Dijkstra + Bitmask $ O(V \times 2^V \times \log(V \times 2^V)) $ $ V \leq 16 $

✅ 推荐使用第二种方法,尤其在节点数稍多时优势明显
⚠️ 注意空间开销,合理设置状态数组大小



原始标题:Finding the Shortest Path in a Graph Visiting All Nodes