Travelling Salesman Problem

30 Dec 2024 | 29 分钟阅读

在本教程中,我们将讨论旅行商问题及其在不同编程语言中使用不同方法的解决方案和实现。

那么,让我们开始吧。

理解旅行商问题

旅行商问题(也称为旅行推销员问题或 TSP)是一个 NP-hard 图计算问题,其中推销员必须访问集合中给出的所有城市(在图中用顶点表示)一次。所有这些城市之间的距离(在图中用边表示)是已知的。我们被要求找到推销员访问所有城市并返回原点城市的最短可能路线。

注意:汉密尔顿回路和 TSP 之间存在差异。汉密尔顿回路问题是找出是否存在一个恰好访问每个城市一次的回路。然而,在这个问题中,我们已经知道汉密尔顿回路存在,因为图是完整的,而且实际上存在许多这样的回路。因此,问题是找出最小权重的汉密尔顿回路。

让我们看一些演示该问题的例子

旅行商问题示例 1

输入

Travelling Salesman Problem

输出

Travelling Salesman Problem

旅行商问题示例 2

输入

Travelling Salesman Problem

输出

Minimum Weight Hamiltonian Cycle: EACBDE = 32

旅行商问题的解决方案

在下一节中,我们将讨论用于解决问题以及在 C、C++、Java 和 Python 等不同编程语言中的实现的各种方法。

以下是我们即将使用的一些方法

  1. 简单或朴素方法
  2. 动态规划方法
  3. 贪心法

现在让我们详细讨论上述方法

使用简单方法解决旅行商问题

在以下方法中,我们将使用下面提到的步骤来解决问题

步骤 1:首先,我们将城市 1 作为起点和终点。由于路线是循环的,任何一点都可以作为起点。

步骤 2:作为第二步,我们将生成所有可能的城市排列,即 (n-1)!

步骤 3:之后,我们将计算每个排列的成本,并记录最小成本排列。

步骤 4:最后,我们将返回成本最低的排列。

用不同编程语言实现简单方法来解决旅行商问题

既然我们已经成功理解了用简单方法解决旅行商问题的步骤,现在是时候看看它在 C++、Java 和 Python 等不同编程语言中的实现了。

C++ 中的代码实现

以下是旅行商问题算法在 C++ 编程语言中的实现

文件:TSP.cpp

输出

Minimum Cost : 90

说明

在上面的代码片段中,我们包含了 'bits/stdc++.h' 头文件,使用了标准命名空间,并定义了一个常量 V = 4,表示图中节点的数量。然后我们定义了一个函数来查找最小权重汉密尔顿回路成本,名为 travelling_salesman_problem(),它接受一个二维数组表示图和一个表示起始节点的整数。在此函数内部,我们创建了一个向量来存储图中的节点。然后我们使用 for 循环遍历定义的顶点数,并使用向量的 push_back() 方法将每个元素推送到向量中(如果它不是源节点)。然后我们将最小路径权重的初始值设置为 INT_MAX(最大整数值)。

然后我们使用 do-while 循环来更新所有可能排列的最小路径权重。在此循环中,我们将当前路径权重的初始值设置为 0,然后遍历向量,根据图的边增加当前路径权重。然后我们通过比较当前路径权重和存储的值来更新最小路径权重。然后我们返回结果路径权重并定义了一个 main 函数。我们在 main 函数中使用邻接矩阵定义了图,并设置了起始节点。然后我们调用 travelling_salesman_problem() 函数来查找最小权重汉密尔顿回路成本,并将返回值存储在一个变量中。最后,我们为用户打印了这个变量。

结果,最小权重汉密尔顿回路成本,即 90,被打印给用户。

Java 代码实现

以下是旅行商问题算法在 Java 编程语言中的实现

文件:TSP.java

输出

Minimum Cost : 90

说明

在上面的代码片段中,我们从 'java.util' 库导入了所有内容,并将主类定义为 TSP 来解决旅行商问题。在此类中,我们初始化了一个常量 V = 4,它表示图中节点的总数。然后我们定义了一个函数来查找最小权重汉密尔顿回路成本,名为 travelling_salesman_problem(),它接受一个二维数组表示图和一个表示起始节点的整数。在此函数内部,我们使用 ArrayList 创建了一个整数数组来存储图中的节点。然后我们使用 for 循环遍历定义的顶点数,并使用 add() 方法将每个元素添加到向量中(如果它不是源节点)。然后我们将最小路径权重的初始值设置为 Integer.MAX_VALUE(最大整数值)。

然后我们使用 do-while 循环来更新所有可能排列的最小路径权重。在此循环中,我们将当前路径权重的初始值设置为 0,然后遍历数组,根据图的边增加当前路径权重。然后我们通过比较当前路径权重和存储的值来更新最小路径权重。然后我们返回结果路径权重。之后,我们定义了几个额外的函数来帮助我们交换数组元素、反转它们以及进行排列。然后我们定义了 main 函数,使用邻接矩阵创建图并设置起始节点。然后,我们调用 travelling_salesman_problem() 函数来查找最小权重汉密尔顿回路成本,并将返回值存储在一个变量中。最后,我们为用户打印了这个变量。

结果,最小权重汉密尔顿回路成本,即 90,被打印给用户。

Python中的代码实现

以下是旅行商问题算法在 Python 编程语言中的实现

文件:TSP.py

输出

Minimum Cost : 90

说明

在上面的代码片段中,我们从 itertools 模块导入了 permutations 类,从 sys 模块导入了 maxsize 变量。然后我们初始化了一个常量 V = 4,表示图中节点的总数。然后我们定义了一个函数来查找最小权重汉密尔顿回路成本,名为 travelling_salesman_problem(),它接受一个列表表示图和一个表示起始节点的整数。在此函数内部,我们创建了一个空列表来存储图中的节点。然后我们使用 for 循环遍历定义的顶点数,并使用 append() 方法将每个元素添加到向量中(如果它不是源节点)。然后我们将最小路径权重的初始值设置为 maxsize(最大整数值)。

然后我们为节点数实例化了 permutations() 类,并使用 for 循环遍历每个排列并相应地更新最小路径权重。在此循环中,我们将当前路径权重的初始值设置为 0,然后遍历列表,根据图的边增加当前路径权重。然后我们通过比较当前路径权重和存储的值来更新最小路径权重。然后我们返回结果路径权重。然后我们定义了 main 函数,创建图并设置起始节点。然后,我们调用 travelling_salesman_problem() 函数来查找最小权重汉密尔顿回路成本,并将返回值存储在一个变量中。最后,我们为用户打印了这个变量。

结果,最小权重汉密尔顿回路成本,即 90,被打印给用户。

旅行商问题使用简单方法的时空复杂度

  • 旅行商问题使用简单或朴素方法的时间复杂度O(N!),其中 N 是城市的数量。
  • 旅行商问题使用简单或朴素方法的空间复杂度O(1)

使用动态规划方法解决旅行商问题

在以下方法中,我们将使用下面提到的步骤来解决问题

步骤 1:在旅行商问题算法中,我们将接受需要访问的城市子集 N、城市之间的距离以及起始城市 S 作为输入。每个城市都由一个唯一的城市 ID 表示(例如 1, 2, 3, 4, 5, ..., n)。

步骤 2:让我们将 1 作为问题的起点和终点。对于每个其他节点 V(除了 1),我们将计算以 1 为起点、V 为终点、所有节点恰好出现一次的最小成本路径。

步骤 3:设该路径的成本为 cost(i),相应回路的成本将是 cost(i) + distance(i, 1),其中 distance(i, 1) 是从 V 到 1 的距离。最后,我们将返回所有 [cost(i) + distance(i, 1)] 值中的最小值。

上述过程看起来很简单。但是,主要问题是如何计算 cost(i)?为了借助动态规划计算 cost(i),我们需要一些关于子问题的递归关系。

我们定义一个术语 C(S, i) 为访问集合 S 中的每个节点一次的最小成本路径的成本,从 1 开始并在 i 结束。我们将从所有大小为 2 的子集开始,并计算所有大小为 S 的子集的 C(S, i),依此类推。

注意:节点 1 必须存在于每个子集中。

用不同编程语言实现动态规划方法来解决旅行商问题

现在我们将看到使用自顶向下递归 + 记忆化方法在 C++、Java 和 Python 等不同编程语言中实现动态规划解决方案。

为了维护子集,我们可以利用位掩码来表示子集中剩余的节点。由于使用位进行操作更快,而且图中只有少量节点,所以位掩码更适合使用。

例如

  • 10100 表示节点 2 和 4 仍在集合中待处理。
  • 010010 表示节点 1 和 4 仍在子集中。

注意:忽略第零位,因为图是基于 1 的。

C++ 中的代码实现

以下是旅行商问题算法在 C++ 编程语言中的实现

文件:TSP.cpp

输出

Minimum Cost : 90

说明

在上面的代码片段中,我们包含了 iostream 头文件并使用了标准命名空间。然后我们定义了一个常量来表示顶点总数,最大整数大小以避免溢出问题,一个图,以及一个用于自顶向下递归方法的记忆化矩阵。然后我们定义了一个名为 travelling_salesman_problem() 的函数,它接受两个参数 - 第一个参数是可迭代对象,第二个参数是位掩码。在此函数中,我们定义了一个递归的基本情况,该情况仅在掩码中设置了当前位和第一位时执行,这意味着所有其他节点都已访问。

之后,我们进行了记忆化,并将最小成本初始化为最大整数值。然后我们使用 for 循环遍历掩码中的所有节点。我们递归地计算遍历掩码中除选定节点外的节点的成本,然后通过采取最短可能路径返回起始节点。对于 main 函数,我们已将结果成本初始化为最大整数值,并使用 for 循环遍历图的节点,通过为该特定节点调用 travelling_salesman_problem() 函数来计算新的最小成本,并将最小成本值设置为结果成本。最后,我们为用户打印了结果。

结果,最小权重汉密尔顿回路成本,即 90,被打印给用户。

Java 代码实现

以下是旅行商问题算法在 Java 编程语言中的实现

文件:TSP.java

输出

Minimum Cost : 90

说明

在上面的代码片段中,我们将一个类定义为 TSP。在此类中,我们定义了一个常量来表示顶点总数,最大整数大小以避免溢出问题,一个图,以及一个用于自顶向下递归方法的记忆化矩阵。然后我们定义了一个名为 travelling_salesman_problem() 的函数,它接受两个参数 - 第一个参数是可迭代对象,第二个参数是位掩码。在此函数中,我们定义了一个递归的基本情况,该情况仅在掩码中设置了当前位和第一位时执行,这意味着所有其他节点都已访问。之后,我们进行了记忆化,并将最小成本初始化为最大整数值。

然后我们使用 for 循环遍历掩码中的所有节点。我们递归地计算遍历掩码中除选定节点外的节点的成本,然后通过采取最短可能路径返回起始节点。对于 main 函数,我们已将结果成本初始化为最大整数值,并使用 for 循环遍历图的节点,通过为该特定节点调用 travelling_salesman_problem() 函数来计算新的最小成本,并将最小成本值设置为结果成本。最后,我们为用户打印了结果。

结果,最小权重汉密尔顿回路成本,即 90,被打印给用户。

Python中的代码实现

以下是旅行商问题算法在 Python 编程语言中的实现

文件:TSP.py

输出

Minimum Cost : 90

说明

在上面的代码片段中,我们定义了一个常量来表示顶点总数,一个图,以及一个用于自顶向下递归方法的记忆化矩阵。然后我们定义了一个名为 travelling_salesman_problem() 的函数,它接受两个参数 - 第一个参数是可迭代对象,第二个参数是位掩码。在此函数中,我们定义了一个递归的基本情况,该情况仅在掩码中设置了当前位和第一位时执行,这意味着所有其他节点都已访问。之后,我们进行了记忆化,并将最小成本初始化为最大整数值。

然后我们使用 for 循环遍历掩码中的所有节点。我们递归地计算遍历掩码中除选定节点外的节点的成本,然后通过采取最短可能路径返回起始节点。对于 main 函数,我们已将结果成本初始化为最大整数值,并使用 for 循环遍历图的节点,通过为该特定节点调用 travelling_salesman_problem() 函数来计算新的最小成本,并将最小成本值设置为结果成本。最后,我们为用户打印了结果。

结果,最小权重汉密尔顿回路成本,即 90,被打印给用户。

旅行商问题使用动态规划方法的时空复杂度

  • 旅行商问题使用动态规划方法的时间复杂度O(N2 * 2N),其中 O(N * 2N) 是递归中唯一子问题的最大数量,而 O(N) 是每个节点中的转换(通过 for 循环)。
  • 旅行商问题使用动态规划方法的空间复杂度O(N * 2N),其中 N 是节点的数量。

使用贪心方法解决旅行商问题

在以下方法中,我们将使用下面提到的步骤来解决问题

步骤 1:首先,我们将创建两个主要的数据持有者 - 第一个数据持有者将是列表,用于存储城市索引(根据城市之间距离的输入矩阵),第二个将是用于存储结果的数组。

步骤 2:在第二步中,我们将遍历给定的邻接矩阵中的所有城市,并通过检查从当前城市到达任何城市的成本是否低于当前成本来更新成本。

步骤 3:最后,我们将通过上述步骤生成最小路径回路并返回其最小成本。

用不同编程语言实现贪心方法来解决旅行商问题

既然我们已经成功理解了使用贪心方法解决旅行商问题的上述过程,现在是时候看看它在 C++、Java 和 Python 等不同编程语言中的实现了。

C++ 中的代码实现

以下是旅行商问题算法在 C++ 编程语言中的实现

文件:TSP.cpp

输出

Minimum Cost : 90

说明

在上面的代码片段中,我们包含了 'bits/stdc++.h' 头文件并使用了标准命名空间。然后我们定义了一个名为 travelling_salesman_problem() 的函数,它接受一个二维数组作为其参数。在此函数中,我们定义了一些变量并用初始值进行了初始化。然后我们使用 while 循环遍历图并计算最小成本。如果计数超过图的大小,我们还包含了一个 break 语句。我们更新了最小成本值以及路线(如果图中的选定值低于当前最小成本)。

我们还通过添加最终最小成本并重置变量来更新总成本。然后我们使用 for 循环通过评估总最小成本来生成最小路径回路,并为用户打印结果。在 main 函数中,我们使用二维向量定义了图,并调用 travelling_salesman_problem() 函数来查找最小权重汉密尔顿回路成本。

结果,最小权重汉密尔顿回路成本,即 90,被打印给用户。

Java 代码实现

以下是旅行商问题算法在 Java 编程语言中的实现

文件:TSP.java

输出

Minimum Cost : 90

说明

在上面的代码片段中,我们从 'java.util' 库导入了所有内容,并将主类定义为 TSP 来解决旅行商问题。在此类中,我们定义了一个名为 travelling_salesman_problem() 的函数,它接受一个二维数组作为其参数。在此函数中,我们定义了一些变量并用初始值进行了初始化。然后我们使用 while 循环遍历图并计算最小成本。如果计数超过图的大小,我们还包含了一个 break 语句。

如果图中的选定值低于当前最小成本,我们更新了最小成本值以及路线。我们还通过添加最终最小成本并重置变量来更新总成本。然后我们使用 for 循环通过评估总最小成本来生成最小路径回路,并为用户打印结果。在 main 函数中,我们定义了图并调用 travelling_salesman_problem() 函数来查找最小权重汉密尔顿回路成本。

结果,最小权重汉密尔顿回路成本,即 90,被打印给用户。

Python中的代码实现

以下是旅行商问题算法在 Python 编程语言中的实现

文件:TSP.py

输出

Minimum Cost : 90

说明

在上面的代码片段中,我们将主类定义为 TSP 来解决旅行商问题。在此类中,我们定义了一个名为 travelling_salesman_problem() 的函数,它接受一个二维列表作为其参数。在此函数中,我们定义了一些变量并用初始值进行了初始化。然后我们使用 while 循环遍历图并计算最小成本。如果计数超过图的大小,我们还包含了一个 break 语句。

如果图中的选定值低于当前最小成本,我们更新了最小成本值以及路线。我们还通过添加最终最小成本并重置变量来更新总成本。然后我们使用 for 循环通过评估总最小成本来生成最小路径回路,并为用户打印结果。在 main 函数中,我们定义了图并调用 travelling_salesman_problem() 函数来查找最小权重汉密尔顿回路成本。

结果,最小权重汉密尔顿回路成本,即 90,被打印给用户。

旅行商问题使用贪心方法的时空复杂度

  • 旅行商问题使用贪心方法的时间复杂度O(N2 * log N),其中 N 是城市的数量。
  • 旅行商问题使用贪心方法的空间复杂度O(N)

下一个主题Kosaraju 算法