这两天看到了一个用动态规划方法求解TSP问题的案例,原文代码是用C++写的,本人照着写成了java的代码,可以运行出相同的最后结果,但是不知道该如何得到最终的访问城市序列。
但是其中的每个步骤已经弄得很详细了,算是把明白的记录下来,不懂得留下来有机会再研究。
参考文章:https://mp.weixin.qq.com/s/gLO9UffCMEqqMVxkOfohFA
感谢原作者,感谢感谢。
这个就百度就好了
具体解TSP问题时,没有必要将所有的DP步骤全部写出来。将状态转移方程写明白就可以理解代码了。下面仔细讲解状态转移方程。
i 表示当前路径中最后一个访问的城市节点(将要去的下一个城市,决策变量);V' 表示已经访问过的城市集合(状态)。
d ( i , V' ) 表示从初始点出发,经过V'集合中的所有城市一次,最终到达城市 i,的最小花费。
假设s为起点,则d ( t , { s, q, t } ) = 0 表示:s -> q -> t 这条路径的最小花费
d(i, V' + { i }) 表示的是:s -> V' -> k -> i ,这条路径的最小花费。
要想找到下一个访问城市i,以达到最小花费,需要在V'(已访问城市集合)中找到路径末端的城市,用路径末端城市与下一个可能的访问城市进行匹配,找到最小花费的匹配。
所谓状态压缩,就是利用二进制以及位运算来实现对于本来应该很大的数组的操作。而求解动态规划问题,很重要的一环就是状态的表示,一般来说,一个数组即可保存状态。但是有这样的一些题目,它们具有DP问题的特性,但是状态中所包含的信息过多,如果要用数组来保存状态的话需要四维以上的数组。于是,我们就需要通过状态压缩来保存状态,而使用状态压缩来保存状态的DP就叫做状态压缩DP。(以上是废话)
假设问题中一共有四个访问点,编号为:0,1,2,3
在上图中,最右遍那一列,0 ~ 15即为V'的值,用十进制数字来保存当前状态。
例如:当V' = 3时,3的二进制为左边的四列 0 0 1 1 ,表示已经访问过的城市集合中有城市0和城市1。
而且,通过左移运算(<<),计算出n个城市节点一共有多少种可能的状态。例如:四个城市节点时,(1 << 4) = 16,一共有16种状态。
代码部分是我照着那个朋友的C++代码直接写的java代码,可能会有还未发现的错误。希望朋友们指正。就其中三点做主要说明。
这句代码在sovle()函数中, j 为下一步要访问的城市编号,i(状态)为已经访问过的城市集合,当状态 i 中已经包含了城市 j ,则过滤掉这个状态,跳出本次循环。
if ((i & (1 << j)) != 0) continue;
例如:i = 12 , j = 4 。将 i 和 j 都转化为二进制:i = 1 1 0 0 , j = 0 1 0 0 。通过位运算&,可以判断出 i & j = 0 1 0 0 ,此时在状态 i 中已经访问过了城市 j 。
此种情况下,就可以跳过i = 12 的状态,遍历下一个状态。
这句代码在sovle()函数中,i(状态)为已经访问过的城市集合,用来过滤掉不是从城市0出发的状态。
if ((i & 1) == 0) continue;
例如:i = 12 ,将 i 转化为二进制:i = 1 1 0 0 。将1转化为二进制:1 = 0 0 0 1,这表示路径中只有出发的城市0。通过位运算&,可以判断出 i & j = 0 0 0 0 ,此时在状态 i 不是从城市0出发 。
这句代码在sovle()函数,据说有两个功能:确保 k 节点已经在集合 i 中、确保 k 是上一步转过来的。
“ 确保 k 节点已经在集合 i 中” ,这个功能的原理在4.1中已经有讲解。
但是 “确保 k 是上一步转过来的 ” 这个功能还没发现是如何实现的。
只有实现了这个功能,才能保证将城市 j 连接到了路径的末端,而不是连接到了路径中的某个城市。
if (((i & (1 << k)) > 0))
这个还有待解决
package TSP01;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.*;
public class Main {
final static double INF = 999999;
static int N; // 城市数量
static double distance[][]; // 距离矩阵
static Vertex vertexs[]; // 储存城市结点的数组
static double dp[][]; // 存储状态
// dp[i][j]:j表示状态,i表示当前城市
static int p[]; // p[i]的值表示i节点的前驱节点
static double ans = INF; // 总距离
static int pre[]; // 索引为状态,值为此状态中上一次加入到状态数组中的车辆
public static void main(String[] args) throws FileNotFoundException {
// 1.读取数据
input("src\\TSP01\\a.txt", 1);
// 2.初始化状态
init_dp();
// 3.解问题
solve();
// 4.输出答案
// 4.1 按顺序将逆序访问序列添加到list链表中
List list = new LinkedList<>();
int t = 0;
list.add(0);
while(p[t]!=0){
list.add(p[t]);
t = p[t];
}
// 4.2 反转列表
Collections.reverse(list);
// 4.3 输出正序的城市访问序列
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " -> ");
}
System.out.println();
System.out.println("总路程为:" + ans);
for (int i = 0; i < N; i++) {
System.out.println(i + "的前置节点为:" + p[i]);
}
}
/**
* 读取数据(数据类型有两种,1是矩阵,0是二位数组)
*
* @param filename 文件名
* @param type 数据类型
*/
public static void input(String filename, int type) throws FileNotFoundException {
File file = new File(filename);
Scanner sc = new Scanner(file);
if (type == 1) {
N = sc.nextInt(); // 城市数量
p = new int[N];
distance = new double[N][N];
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
distance[i][j] = sc.nextDouble();
}
}
} else if (type == 0) {
N = sc.nextInt(); // 城市数量
p = new int[N];
vertexs = new Vertex[N];
distance = new double[N][N];
while (sc.hasNext()) {
int i = sc.nextInt() - 1; // 城市编号(从0开始)
vertexs[i] = new Vertex();
vertexs[i].id = i + 1;
vertexs[i].x = sc.nextDouble();
vertexs[i].y = sc.nextDouble();
}
for (int i = 0; i < N; i++) {
distance[i][i] = 0;
for (int j = 0; j < i; j++) {
distance[i][j] = edc_2d(vertexs[i], vertexs[j]);
distance[j][i] = distance[i][j];
}
}
} else {
System.out.println("类型输入错误");
}
sc.close();
}
// 计算两点之间的距离
public static double edc_2d(Vertex a, Vertex b) {
return Math.sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
// 初始化状态
public static void init_dp() {
dp = new double[(1 << N) + 5][]; // 富余出5个
for (int i = 0; i < (1 << N); i++) { // 遍历所有状态
dp[i] = new double[N + 5];
for (int j = 0; j < N; j++) { // 每个状态下出发点到达每个点的距离
dp[i][j] = INF;
}
}
}
// 解决问题
public static void solve() {
int M = (1 << N); // 状态的个数
pre = new int[M];
pre[0] = 0; // 初始化为0
dp[1][0] = 0; // 确定出发点
// 下面的每个十进制的i转化成N位的二进制,就可以代表当前的状态。0代表没有经过这个点,1代表已经过这个点。
for (int i = 1; i < M; i++) { // 遍历每种状态
for (int j = 1; j < N; j++) { // 每种状态下遍历每个可访问的下一个城市
// 过滤1(按位与运算):过滤掉状态中已经包含了城市节点j的状态
if ((i & (1 << j)) != 0) continue;
// 过滤2(按位与运算):默认必须从0号城市开始出发,过滤掉不是从0号城市出发的状态
if ((i & 1) == 0) continue;
// 尝试使用V'集合中的节点连接下一个节点。...->k->j
for (int k = 0; k < N; k++) {
// 1. 确保k节点已经在集合中
// 2. 确保k是上一步转过来的(如何确保源代码中也没有做到)
// 只有当k是上一步转过来的,才可以得到一个TSP环,而不是其中有多个子环
// 这个问题在源代码中也没有得到解决
if (((i & (1 << k)) > 0)) {
// dp[(1< dp[i][k] + distance[k][j]) {
dp[(1 << j) | i][j] = dp[i][k] + distance[k][j]; // 状态转移方程
pre[i] = j;
String q = Integer.toBinaryString(i);
System.out.println("i= " + i + " : " + q + " p[" + j + "] = " + k);
}
if(dp[i][j] > dp[i][k] + distance[k][j]){
p[j] = k;
}
}
}
}
// System.out.println("状态"+i+"的前一个添加进来的节点为:"+pre[i]);
}
for (int i = 0; i < N; i++) {
if (ans > dp[M - 1][i] + distance[i][0]) {
ans = dp[M - 1][i] + distance[i][0];
p[0] = i;
}
}
}
}
// 访问城市节点的类
class Vertex {
double x, y; // 坐标
int id; // 节点编号
public Vertex() {
}
@Override
public String toString() {
return "Vertex{" +
"x=" + x +
", y=" + y +
", id=" + id +
'}';
}
public Vertex(int id) {
this.id = id;
}
}
4 0 3 6 7 5 0 2 3 6 4 0 2 3 7 5 0
16 1 38.24 20.42 2 39.57 26.15 3 40.56 25.32 4 36.26 23.12 5 33.48 10.54 6 37.56 12.19 7 38.42 13.11 8 37.52 20.44 9 41.23 9.10 10 41.17 13.05 11 36.08 -5.21 12 38.47 15.13 13 38.15 15.35 14 37.51 15.17 15 35.49 14.32 16 39.36 19.56