动态规划求解TSP问题(java,状态压缩)

使用动态规划方法求解TSP问题

这两天看到了一个用动态规划方法求解TSP问题的案例,原文代码是用C++写的,本人照着写成了java的代码,可以运行出相同的最后结果,但是不知道该如何得到最终的访问城市序列。

但是其中的每个步骤已经弄得很详细了,算是把明白的记录下来,不懂得留下来有机会再研究。

参考文章:https://mp.weixin.qq.com/s/gLO9UffCMEqqMVxkOfohFA

感谢原作者,感谢感谢。

1. 什么是动态规划(DP),什么是TSP问题

这个就百度就好了

2. 将DP应用到TSP问题中,建立动态规划模型。

具体解TSP问题时,没有必要将所有的DP步骤全部写出来。将状态转移方程写明白就可以理解代码了。下面仔细讲解状态转移方程。

2.1 指标函数 d ( i , V' )

i 表示当前路径中最后一个访问的城市节点(将要去的下一个城市,决策变量);V' 表示已经访问过的城市集合(状态)。

d ( i , V' ) 表示从初始点出发,经过V'集合中的所有城市一次,最终到达城市 i,的最小花费。

假设s为起点,则d ( t , { s, q, t } ) = 0 表示:s -> q -> t 这条路径的最小花费

2.2 状态转移方程

动态规划求解TSP问题(java,状态压缩)_第1张图片

d(i, V' + { i }) 表示的是:s -> V' -> k -> i ,这条路径的最小花费。

要想找到下一个访问城市i,以达到最小花费,需要在V'(已访问城市集合)中找到路径末端的城市,用路径末端城市与下一个可能的访问城市进行匹配,找到最小花费的匹配。

3. 状态压缩(这里仅介绍本文用到的状态压缩方式)

 所谓状态压缩,就是利用二进制以及位运算来实现对于本来应该很大的数组的操作。而求解动态规划问题,很重要的一环就是状态的表示,一般来说,一个数组即可保存状态。但是有这样的一些题目,它们具有DP问题的特性,但是状态中所包含的信息过多,如果要用数组来保存状态的话需要四维以上的数组。于是,我们就需要通过状态压缩来保存状态,而使用状态压缩来保存状态的DP就叫做状态压缩DP。(以上是废话)

假设问题中一共有四个访问点,编号为:0,1,2,3

动态规划求解TSP问题(java,状态压缩)_第2张图片

在上图中,最右遍那一列,0 ~ 15即为V'的值,用十进制数字来保存当前状态。

例如:当V' = 3时,3的二进制为左边的四列 0 0 1 1 ,表示已经访问过的城市集合中有城市0城市1

而且,通过左移运算(<<),计算出n个城市节点一共有多少种可能的状态。例如:四个城市节点时,(1 << 4) = 16,一共有16种状态。

4. 代码部分

代码部分是我照着那个朋友的C++代码直接写的java代码,可能会有还未发现的错误。希望朋友们指正。就其中三点做主要说明。

4.1 过滤1

这句代码在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 的状态,遍历下一个状态。

4.2 过滤2

这句代码在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出发

 4.3 在状态i中寻找

这句代码在sovle()函数,据说有两个功能:确保 k 节点已经在集合 i 中、确保 k 是上一步转过来的。

“ 确保 k 节点已经在集合 i 中” ,这个功能的原理在4.1中已经有讲解。

但是 “确保 k 是上一步转过来的 ” 这个功能还没发现是如何实现的。

只有实现了这个功能,才能保证将城市 j 连接到了路径的末端,而不是连接到了路径中的某个城市。

if (((i & (1 << k)) > 0))

4.4 如何得出城市访问序列

这个还有待解决

5 代码部分

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;
    }
}

6. 算例部分

6.1 type = 1

4
0   3   6   7
5   0   2   3
6   4   0   2
3   7   5   0

6.2 type = 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

你可能感兴趣的:(算法,java,数据结构)