动态规划求解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 状态转移方程

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

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

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

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

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

在上图中,最右遍那一列,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<Integer> 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<<j)][j] 表示的是当j点即为出发点时的情况
                        // dp[(1<<j)][j]中,(1<<j)表示的状态是计算不到这一步的,因此此种状态表示以j为出发点,不符合上面要求
                        if (dp[(1 << j) | i][j] > 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
TSP问题,即旅行商问题,是指一个旅行商要拜访指定的n个城市,他必须恰好访问每个城市一次,并且最后要回到起点城市。该问题是一个NP问题,没有多项式时间的解法,但可以使用动态规划方法来求解动态规划的思路是将大问题分解为小问题来解决。TSP问题可以分解为子问题:在访问完一部分城市后,从最后一个城市出发到下一个未访问的城市的最短路径。这个子问题可以用一个二维数组来表示。在动态规划过程中,我们需要记录当前已经访问的城市集合和当前所在的城市,根据这些信息来计算当前子问题的解。 以下是一个简单的动态规划求解TSP问题的C语言代码实现(假设所有城市之间的距离存储在一个二维数组dist中): ``` #define N 10 // 假设有10个城市 int tsp(int mask, int pos, int dp[][N]) { if (mask == (1 << N) - 1) { // 所有城市都已访问 return dist[pos]; // 返回回到起点的距离 } if (dp[mask][pos] != -1) { // 如果已经计算过了,直接返回 return dp[mask][pos]; } int ans = INT_MAX; for (int i = 0; i < N; i++) { if (!(mask & (1 << i))) { // 如果城市i未被访问 ans = min(ans, dist[pos][i] + tsp(mask | (1 << i), i, dp)); // 计算最短路径 } } return dp[mask][pos] = ans; } int main() { int dp[1 << N][N]; memset(dp, -1, sizeof(dp)); // 初始化dp数组为-1 printf("最短路径长度为:%d\n", tsp(1, 0, dp)); return 0; } ``` 相关问题: 1. 动态规划还可以用来解决哪些问题? 2. TSP问题有哪些常见的求解方法? 3. 如果有n个城市,TSP问题的时间复杂度是多少?
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值