现在假设有4个城市,到各个城市都有一定的花费,你是一个旅行商要到这4个城市去售卖物品,这4个城市必须都走一遍,问走哪条路线花费最少?(起点可任意,最终要回到起点)
经过分析有以下这几种路线(假设从北京开始):
我们可以看到,如果有4个城市的话,路线一共有:3x2 = 6 条, 5个城市有:4x3x2 = 24条,假设有n个城市,总路线就有:(n - 1)! 条, 时间复杂度很高,如果只有4、5个城市时,可以用动态规划来求解,这种时间消耗可以接收。
但如果更多的城市就只能考虑求近似解了。
我们可以用一张二维表来表示花费,行和列都代表城市编号,二维表格中的元素代表从某个城市到另一个城市的花费。如下图(graph):
d(出发城市,剩余城市集合) ==> 从出发城市开始,走完剩余城市,花费的最少代价
我们可以得到以下递推公式:
我们可以把出发城市用变量 i 代替,剩余城市集合用 j 代替, 遍历 j 时的变量为 k (剩余的某一个城市)
旅行商问题要分多条路线,我们需要在这写众多路线中求解花费最小的,这其实就是一个把大问题拆解成很多子问题的过程。
动态规划思想就是找出递推公式,将当前问题分解成子问题,分阶段进行求解,在求解过程中缓存子问题的解,避免重复计算。
这次我们需要创建一张二维表格,表格的行代表每个城市,列代表剩余城市的集合,比如说从0号城市出发,还需要去1、2、3号城市,集合里装的就是这些城市。这里数组的列元素是一个集合,没怎么见过 ,像这样:
我们可以用二进制数来表示集合中是否存在这个城市,比如说 000 代表没有城市,001 代表有1号城市:
这样我们就可以用 十进制数来表示集合中有几个城市。
如果我们求解 d(0,1 | 2 | 3),我们需要知道之前的子问题的解,从3种情况中选择花费最小的填上去,推导是从右往左进行的,但是填二维表要从左往右填充。
我们第0列的元素是可以根据graph表格直接填充的。先初始化二维表:
static int tsp(int[][] g){
int m = g.length; //城市个数
int n = 1 << (m - 1); //剩余城市的组合数
int[][] dp = new int[m][n];
//填充第0列
for (int k = 0; k < m; k++) {
dp[k][0] = g[k][0];
}
return 0;
}
然后我们再填充后续列:
填充时,我们需要排除不合理的情况,比如出发点是0号城市,剩余城市里包含出发城市这种情况不做处理。我们可以先给一个比较大的值,如果后面有更小距离可以替换。
核心代码(根据这张图理解一下):
static int tsp(int[][] g){
int m = g.length; //城市个数
int n = 1 << (m - 1); //剩余城市的组合数
int[][] dp = new int[m][n];
//填充第0列
for (int k = 0; k < m; k++) {
dp[k][0] = g[k][0];
}
//填充后续列
for (int j = 1; j < n; j++) {
for (int i = 0; i < m; i++) {
dp[i][j] = Integer.MAX_VALUE / 2; //不要给太大,后面做运算可能会产生整数溢出
if(contains(j, i)) continue; //剩余城市包含出发城市直接跳过(不合理)
for (int k = 0; k < m; k++) { //k为1时 k为2时 k表示先要去的城市
if(contains(j, k)){ //剩余城市包含出发城市
dp[i][j] = Integer.min(dp[i][j], g[i][k] + dp[k][exclude(j, k)]);
}
}
}
}
print(dp);
return dp[0][n - 1];
}
这样的数据设计和遍历方式非常nice,表格右边的数据都可以在左边得到。
下面是exclude和contains方法的实现:
/*
110 是否包含 0 = 0 & 1 = 0
110 是否包含 1 = 110 & 1 = 0
110 是否包含 2 = 11 & 1 = 1
110 是否包含 3 = 1 & 1 = 1
110 是否包含 4 = 0 & 1 = 0
*/
static boolean contains(int set, int city) {
return (set >> (city - 1) & 1) == 1;
}
/*
1|2|3 1 => 2|3 1|2|3 2 => 1|3
111 111 异或运算
001 ^ 010 ^
---- ----
110 101
*/
static int exclude(int set, int city) {
return set ^ (1 << (city - 1));
}
package dynamicprogramming;
import java.util.Arrays;
import java.util.stream.Collectors;
/**
* 旅行商问题
*/
public class TravellingSalesmanProblem {
public static void main(String[] args) {
int[][] graph = {
{0, 1, 2, 3},
{1, 0, 6, 4},
{2, 6, 0, 5},
{3, 4, 5, 0},
};
System.out.println(tsp(graph));
//System.out.println(6 >> (0-1));
}
static int tsp(int[][] g){
int m = g.length; //城市个数
int n = 1 << (m - 1); //剩余城市的组合数
int[][] dp = new int[m][n];
//填充第0列
for (int k = 0; k < m; k++) {
dp[k][0] = g[k][0];
}
//填充后续列
for (int j = 1; j < n; j++) {
for (int i = 0; i < m; i++) {
dp[i][j] = Integer.MAX_VALUE / 2; //不要给太大,后面做运算可能会产生整数溢出
if(contains(j, i)) continue; //剩余城市包含出发城市直接跳过(不合理)
for (int k = 0; k < m; k++) { //k为1时 k为2时 k表示先要去的城市
if(contains(j, k)){ //剩余城市包含出发城市
dp[i][j] = Integer.min(dp[i][j], g[i][k] + dp[k][exclude(j, k)]);
}
}
}
}
print(dp);
return dp[0][n - 1];
}
/*
110 是否包含 0 = 0 & 1 = 0
110 是否包含 1 = 110 & 1 = 0
110 是否包含 2 = 11 & 1 = 1
110 是否包含 3 = 1 & 1 = 1
110 是否包含 4 = 0 & 1 = 0
*/
static boolean contains(int set, int city) {
return (set >> (city - 1) & 1) == 1;
}
/*
1|2|3 1 => 2|3 1|2|3 2 => 1|3
111 111 异或运算
001 ^ 010 ^
---- ----
110 101
*/
static int exclude(int set, int city) {
return set ^ (1 << (city - 1));
}
static void print(int[][] dist) {
System.out.println("-------------------------");
for (int[] row : dist) {
System.out.println(Arrays.stream(row).boxed()
.map(x -> x >= Integer.MAX_VALUE / 2 ? "∞" : String.valueOf(x))
.map(s -> String.format("%2s", s))
.collect(Collectors.joining(",", "[", "]")));
}
}
}