浅谈可重复访问城市的TSP问题(最短距离 + 具体走法)

小伙伴们,你们好。今天我来浅显的讲一下这个可重复访问城市的TSP问题,所谓的可重复就是城市和路线都随便走,只要最后它的路径总和是最小的就行。

要用到的知识点是 状态压缩dpFloyd算法

一、Floyd算法

Floyd算法:floyd算法学习视频
这个小姐姐会用手算的方式带你了解floyd算法的整个过程,相信看完你就有一种恍然大悟的感觉了

我下面floyd算法的主要作用是让我们得到一个距离二维数组,路径二维数组

1.distance[][]

例子:distance[i][j] 表示点i点j的最短距离
有了这个数组,我们第一步就完成了

2.path[][]

例子:path[i][j]表示点i到点j中间经过了什么点

	public void floyd(Double[][] distance,int[][] path,Double[][] edge) {
        int n = edge.length;//edge数组是一个邻接矩阵
        //初始化一下传进来距离数组
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                path[i][j] = -1;
                if (i!=j) {
                    distance[i][j]  = edge[i][j];
                }else {
                    distance[i][j] = 0.0;
                }
            }
        }
        //核心算法
        for (int k=0;k<n;k++) {
            for (int i=0;i<n;i++) {
                for (int j=0;j<n;j++) {
                    if (distance[i][j]>distance[i][k]+distance[k][j]) {
                        distance[i][j] = distance[i][k]+distance[k][j];
                        path[i][j] = k;//记录路径的数组
                    }
                }
            }
        }
    }

通过调用函数jointPath(…)我们就可以找到v0=>v的完整路径了,这对我们求TSP的路径也是至关重要的

//int[][] path:由floyd得到
//v0:起点,v:终点
//idx[0]:因为我要记住p数组的有效长度,所以用了idx[0]来保证它可以当指针用
//ps:我有点菜,写得不是很好
    public void jointPath(int[][] path,int v0,int v,int[] p,int[] idx) {
        getPath(path,v0,v,p,idx);
        p[idx[0]++] = v;
    }
    //好好用纸来模拟一下这个递归函数,你就悟了
	public void getPath(int[][] path,int v0,int v,int[] p,int[] idx) {
        if (path[v0][v]==-1) {
            p[idx[0]++] = v0;
            return;
        }
        getPath(path,v0,path[v0][v],p,idx);//左边
        getPath(path,path[v0][v],v,p,idx);//右边
    }

写完博客,我会录制视频在b站,你们可以关注一下我,看一下我的视频讲解 : 随风的叶子(我的账号名称)

二、状态压缩dp

视频学习连接,看前33分钟就行
这个视频里面,他会教你dp的概念还有怎么使用与、或、非和异或
这老师讲得还是十分详细的,里面讲解的题目:最短Hamilton路径
这道题和我们要解决的问题有着异曲同工之妙,能够解决上面的这道题,相信下面你们快速的掌握。

欢迎回来,相信你对dp有一定了解了.

下面,我们要有这样一种思维:就是我们只注重点i到点j的最短距离,就是我们把心放在distance[][]数组上面,我们无需关系i到j之间通过了什么点,所以我们dp集合加入了i和j,那么i和j之间包含1了什么点我们不管,也不会加入到dp集合中,我们关心的只有最短,因为点和边是可以重复的

你应该get到我的点了吧

接下来,我就和你讲讲这道题的具体思路:

首先我们开一个dp[1<<n][n]的数组

因为我们要表示每个集合的状态,而状态有0~111…111(2^n-1)

当我们成功的将我们的状态搞到111…111(2^n-1)就是我们胜利的时刻了

下面,我来上代码来说话(要认真看我的注释噢)

dp[s][i] = Math.min(dp[s][i],dp[s^(1<<i)][j]+distance[j][i]);//只是至关重要的,这玩意的作用就是假如还没有将顶点i加入集合中(实际我们已经加入了),通过存在集合当中的顶点j为踏板来到i的距离会不会比原来的距离要短

然后,我还是想和大家说一下
ps: 以下从第0位开始

  • s&(1<<i) 例子:假如s:1101(2进制),i=2(10进制),1<<i=100(2进制),这时1101&0100 = 0100,然后我们就可以通过判断s&(1<<i) 等不等于0来确定s(2进制)第i位是0还是1了
  • s^(1<<i) 例子,假如s:1101(2进制),i=2(10进制),1<<i=100(2进制),这时1101 ^ 0100 = 1001(我们就可以将s(2进制)的第i位变成0了)
 //==================状态压缩DP
        Double[][] dp = new Double[1<<n][n];//开数组
        //初始化dp数组
        for (int i = 0; i < 1<<n; i++) {
            for (int j = 0; j < n; j++) {
                dp[i][j] = Double.MAX_VALUE;
            }
        }
        dp[1][0] = 0.0;//将0号点放进集合里面,0:表示当前在点0处停留
        for (int s=1;s<(1<<n);s++) {//1~11..11(2^n-1[1<<n])
            for (int i=0;i<n;i++) {
                if ((s&(1<<i))!=0) {//顶点i加入了集合当中
                    for (int j = 0; j < n; j++) {
                        if ((s&(1<<j))!=0&&i!=j) {
                            dp[s][i] = Math.min(dp[s][i],dp[s^(1<<i)][j]+distance[j][i]);
                        }
                    }
                }
            }
        }
        Double ans = Double.MAX_VALUE;//初始化一下
        //最后,我们将所有的点都加入了集合当中,那么我们从最后加入的那个点回到原点就大公告成了,从一个点到另一个点的最短距离我们还是从distance[][]里面获取
  //也是这里启示我自己想出来了求路径的方法
        for (int i=1;i<n;i++) {//0是起始点,就不要加入里面了
            if (ans > dp[(1<<n)-1][i]+distance[i][0]) {
                ans = dp[(1<<n)-1][i]+distance[i][0];
            }
        }
        return n==1?0:ans;//健壮性

下面有必要声明一下:
@Autowired
private FloydUtil floydUtil;(这玩意是因为我写springboot项目用了,你们看到函数名知道我在放什么屁就行了,抱拳了)

  • public void TSP_can_repeat_path(Double[][] dp,Double[][] dis,int[] path,int end,int n)
  • public void completePath(int[][] path,int[] p,int[] initP,int[] idx)

ps:参数具体什么含义,看我注解就知道了

  • 我先说一下我第一个算法的作用,现有状态s就是不断的回退状态t,然后找到状态t到状态s的最短路径来确定(再重复一遍:我这里i->j,可能里面还包含了别的点,只是我们通过两点的最短路路径看成它们相通罢了)

在这里插入图片描述
在这里插入图片描述

看这张图:箭头右边的数组表示第i位变位0了,当最后集合变成0就是我们胜利的时刻了(重复一次:从第0位开始

if (index==-1) break;//结束条件,你也可以自己优化一下,反正我就爱这么写[哈哈],当下标没有变化的时候就结束了

这个时候,我们只是成功了一半,

0   1   2   3   13   14   15   12   4   11   16   11   5   6   5   10   17   18   9   19   8   7 0

再再再重复一遍:0=>1或者 10=>7,表示的只是最短路径,我们还要将他们中间存在的点找出来,这个时候的找我们就回到floyd算法里面涉及的找路径,我们通过一个for就能全部找出来了,

floydUtil.getPath(…):我这里这个函数不会将它最后一个元素加入数组中

认真看下面的代码就理解整个过程了,期待我在b站把视频肝出来吧

//求出路径
	/**
     *
     * @param p  (完整路线数组)
     * @param iii (上面数组的有效下标)
     * @param n   (顶点个数)
     * @return   (返回路径最小值)
     */
    public void TSP_can_repeat_path(Double[][] dp,Double[][] dis,int[] path,int end,int n) {//end从0开始
        int s = (1<<n)-1;//path数组的长度等于顶点的个数(11...111)
        int idx = 0;//初始化为0
        path[idx++] = end;//最后一个 去吧你
        //System.out.println(Integer.toBinaryString(s));//二进制,我要看看出现了什么幺蛾子
        while (true) {
            s = s^(1<<end);//新状态
            //System.out.println(Integer.toBinaryString(s)+"-->"+end);
            Double min = Double.MAX_VALUE;
            int index = -1;
            for (int i = 0; i < n; i++) {
                if ( (s&(1<<i))!=0 && min > dp[s][i]+dis[i][end]) {
                    min = dp[s][i]+dis[i][end];
                    index = i;
                }
            }
            if (index==-1) break;
            path[idx] = index;
            end = index;
            idx ++;
        }
        for (int i = 0; i < (n / 2) - 1; i++) {
            int tmp = path[i];
            path[i] = path[n-1-i];
            path[n-1-i] = tmp;
        }
    }

    /**
     *
     * @param path (floyd之后的路径)
     * @param p    (要填补的数组)
     * @param initP (上一层搞的路径)
     * @param idx    (记录下标)
     */
    public void completePath(int[][] path,int[] p,int[] initP,int[] idx) {
        int n = initP.length;
        for (int i = 0; i < n-1; i++) {
            int[] res =  new int[n];
            int[] index = new int[1];
            floydUtil.getPath(path,initP[i],initP[i+1],res,index);
            for (int j = 0; j < index[0]; j++) {
                p[idx[0]++] = res[j];
            }
        }
        p[idx[0]++] = initP[n-1];//最后一个点放进来

    }

下面是我写的完整代码

package com.lin.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class FloydDPUtil {

    @Autowired
    private FloydUtil floydUtil;


    /**
     *
     * @param p  (完整路线数组)
     * @param iii (上面数组的有效下标)
     * @param n   (顶点个数)
     * @return   (返回路径最小值)
     */
    public Double TSP_can_repeat(int[] p,int[] iii,int n) {
        //==================floyd
        Double[][] distance = new Double[n][n];
        int[][] path = new int[n][n];
        floydUtil.floyd(distance,path);
        //==================状态压缩DP
        Double[][] dp = new Double[1<<n][n];
        //初始化dp数组
        for (int i = 0; i < 1<<n; i++) {
            for (int j = 0; j < n; j++) {
                dp[i][j] = Double.MAX_VALUE;
            }
        }
        dp[1][0] = 0.0;//将0号点放进集合里面
        for (int s=1;s<(1<<n);s++) {
            for (int i=0;i<n;i++) {
                if ((s&(1<<i))!=0) {//就是顶点i加入了集合当中
                    for (int j = 0; j < n; j++) {
                        if ((s&(1<<j))!=0&&i!=j) {
                            dp[s][i] = Math.min(dp[s][i],dp[s^(1<<i)][j]+distance[j][i]);
                        }
                    }
                }
            }
        }
        Double ans = Double.MAX_VALUE;
        int idx = -1;
        for (int i=1;i<n;i++) {//0是起始点,就不要加入里面了
            if (ans > dp[(1<<n)-1][i]+distance[i][0]) {
                ans = dp[(1<<n)-1][i]+distance[i][0];
                idx = i;
            }
        }
        //搞路径到数组initP里面
        int[] initP = new int[n+1];
        TSP_can_repeat_path(dp,distance,initP,idx,n);
        initP[n] = 0;
        //开始解析里面的东西,放到数组p中
        completePath(path,p,initP,iii);
        return ans;
    }

    //求出路径
    public void TSP_can_repeat_path(Double[][] dp,Double[][] dis,int[] path,int end,int n) {//end从0开始
        int s = (1<<n)-1;//path数组的长度等于顶点的个数(11...111)
        int idx = 0;//初始化为0
        path[idx++] = end;//最后一个 去吧你
        //System.out.println(Integer.toBinaryString(s));//二进制,我要看看出现了什么幺蛾子
        while (true) {
            s = s^(1<<end);//新状态
            //System.out.println(Integer.toBinaryString(s)+"-->"+end);
            Double min = Double.MAX_VALUE;
            int index = -1;
            for (int i = 0; i < n; i++) {
                if ( (s&(1<<i))!=0 && min > dp[s][i]+dis[i][end]) {
                    min = dp[s][i]+dis[i][end];
                    index = i;
                }
            }
            if (index==-1) break;
            path[idx] = index;
            end = index;
            idx ++;
        }
        for (int i = 0; i < (n / 2) - 1; i++) {
            int tmp = path[i];
            path[i] = path[n-1-i];
            path[n-1-i] = tmp;
        }
    }

    /**
     *
     * @param path (floyd之后的路径)
     * @param p    (要填补的数组)
     * @param initP (上一层搞的路径)
     * @param idx    (记录下标)
     */
    public void completePath(int[][] path,int[] p,int[] initP,int[] idx) {
        int n = initP.length;
        for (int i = 0; i < n-1; i++) {
            int[] res =  new int[n];
            int[] index = new int[1];
            floydUtil.getPath(path,initP[i],initP[i+1],res,index);
            for (int j = 0; j < index[0]; j++) {
                p[idx[0]++] = res[j];
            }
        }
        p[idx[0]++] = initP[n-1];//最后一个点放进来

    }

}

作者:随风

有什么问题可以私聊我qq:2338244917

你们可以关注我的b站账号:随风的叶子 跳转=>随风的叶子

我写完已经凌晨两点半了,还请给我点个赞
在这里插入图片描述
csdn粉丝数快突破300了,谢谢你们的支持

  • 34
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

with the wind(随风)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值