问题原型
给定一系列城市和每对城市之间的距离,求推销员从某个城市出发后经过所有城市,然后回到出发城市的最短路径。
为了方便讲解,我们以2019年字节跳动校招笔试题“毕业旅行问题为例”。牛客原链接:https://www.nowcoder.com/profile/4097742/codeBookDetail?submissionId=58509076
题目描述
小明目前在做一份毕业旅行的规划。打算从北京出发,分别去若干个城市,然后再回到北京,每个城市之间均乘坐高铁,且每个城市只去一次。由于经费有限,希望能够通过合理的路线安排尽可能的省一些路上的花销。给定一组城市和每对城市之间的火车票的价钱,找到每个城市只访问一次并返回起点的最小车费花销。
输入描述
城市个数n(1<n≤20,包括北京)
城市间的车票价钱 n行n列的矩阵 m[n][n]
输出描述
最小车费花销 s
示例1
输入
4
0 2 6 5
2 0 4 4
6 4 0 2
5 4 2 0
输出
13
说明
共 4 个城市,城市 1 和城市 1 的车费为0,城市 1 和城市 2 之间的车费为 2,城市 1 和城市 3 之间的车费为 6,城市 1 和城市 4 之间的车费为 5,依次类推。假设任意两个城市之间均有单程票可购买,且票价在1000元以内,无需考虑极端情况。
这个问题首先需要构造一个图,图的一个对应一座城市,边的权值对应城市到城市之间火车票价格,根据题目描述,这是一个完全图(各个顶点都有一条边两两互相连接),并且各个边没有方向。

这道题一般有两种解法:
- 回溯法
- 动态规划
回溯法
把所有的解通过一棵树表达出来,然后通过深度优先遍历,找到一个解的时候就将其记录下来,最后输出最小的解即可。

如图所示,从根节点到叶子节点的所经过的节点就是其路径,每个边的长度之和就是总车票价格。
当然还有一种优化的方法,就是在遍历过程中,如果发现此时路径长度已经超出了之前找到的最小路径长度,就可以进行剪支操作,即不再遍历。
这种方法的时间复杂度为 O ( n ! ) O(n!) O(n!),当n大于12之后,其计算时间就已经非常大了。这种方法会因为超时无法通过所有的测试数据。
下面贴出代码:
import java.util.Arrays;
import java.util.Scanner;
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[][] arr = new int[n][n];
for(int i = 0; i < arr.length; i++) {
for(int j = 0; j < arr.length; j++) {
arr[i][j] = sc.nextInt();
}
}
boolean[] vis = new boolean[n]; //记录各个城市访问情况
vis[0] = true;
AtomicInteger ans = new AtomicInteger(Integer.MAX_VALUE);
dfs(arr, vis, n, 0, 1, 0, ans);
System.out.println(ans.get());
}
//vn为已经访问的城市数量,local为当前城市编号,price为当前累计票价
private static void dfs(int[][] arr, boolean[] vis, final int n, int local,
int vn, int price, AtomicInteger ans) {
if(price > ans.get()) {
//如果此时价格已经超出了之前找到的最小价格,那么进行剪支操作
return;
}
if(vn == n) {
//如果已经遍历完成
int val = price + arr[local][0]; //因为走完所有城市后还要回到起点所以加上arr[local][0]
if(val < ans.get()) {
ans.set(val);
}
return;
}
for(int i = 1; i < n; i++) {
//因为起点为0所以无需考虑起点
if(vis[i])
continue;
vis[i] = true;
dfs(arr, vis, n, i, vn + 1, price + arr[local][i], ans); //继续遍历
vis[i] = false;
}
}
}
动态规划
我们可以使用动态规划,把一个大问题划分为多个小问题来求解。
例如大问题是从顶点0开始,经过顶点1,2,3然后回到顶点1的最短路程,那么我们可以分割为三个小问题找最优解:
- 从顶点0出发到顶点1,再从顶点1出发,途径2,3城市(不保证访问顺序),然后回到0的最短路径
- 从顶点0出发到顶点2,再从顶点2出发,途径1,3城市,然后回到0的最短路径
- 从顶点0出发到顶点3,再从顶点3出发,途径1,2城市,然后回到0的最短路径
这三个小问题对应的最小值就是问题的最优解。
知道怎么划分子问题后,接下来就是找状态转移方程。
我们定义 d p [ n ] { p 1 , p 2 , . . . , p m } dp[n]\{p_1,p_2,...,p_m\} dp[n]{ p1,p2,...,pm} 为从城市 n n n 出发,途径城市 p 1 , p 2 , . . . , p m p_1,p_2,...,p_m p1,p2,...,pm(不保证访问城市的顺序)然后回到城市0的最短路径。按照上述划分问题的方法,我们可以推导出:
d p [ n ] { p 1 , p 2 , . . . , p m } = m i n ( d p [ p 1 ] { p 2 , . . . , p m } + D p 1 n , d p [ p 2 ] { p 1 , p 3 , . . . , p m } + D p 2 n , . . . . . . , d p [ p m ] { p 1 , p 2 , . . . p m − 1 } + D p m n ) dp[n]\{p_1,p_2,...,p_m\}= min(dp[p_1]\{p_2,...,p_m\}+D_{p_1}^n, dp[p_2]\{p_1,p_3,...,p_m\}+D_{p_2}^n,......,dp[p_m]\{p_1,p_2,...p_{m-1}\}+D_{p_m}^n) dp[n]{ p1,p2,...,pm}=min(dp[p1]{ p2,...,pm}+D