小伙伴们,你们好。今天我来浅显的讲一下这个可重复访问城市的TSP问题,所谓的可重复就是城市和路线都随便走,只要最后它的路径总和是最小的就行。
要用到的知识点是 状态压缩dp 和 Floyd算法
Floyd算法:floyd算法学习视频
这个小姐姐会用手算的方式带你了解floyd算法的整个过程,相信看完你就有一种恍然大悟的感觉了
我下面floyd算法的主要作用是让我们得到一个距离二维数组,路径二维数组
例子:distance[i][j] 表示点i到点j的最短距离
有了这个数组,我们第一步就完成了
例子: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);//右边
}
视频学习连接,看前33分钟就行
这个视频里面,他会教你dp的概念还有怎么使用与、或、非和异或
这老师讲得还是十分详细的,里面讲解的题目:最短Hamilton路径
这道题和我们要解决的问题有着异曲同工之妙,能够解决上面的这道题,相信下面你们快速的掌握。
你应该get到我的点了吧
接下来,我就和你讲讲这道题的具体思路:
首先我们开一个dp[1< 因为我们要表示每个集合的状态,而状态有0~111…111(2^n-1) 当我们成功的将我们的状态搞到111…111(2^n-1)就是我们胜利的时刻了 下面,我来上代码来说话(要认真看我的注释噢) dp[s][i] = Math.min(dp[s][i],dp[s^(1<;//只是至关重要的,这玩意的作用就是假如还没有将顶点i加入集合中(实际我们已经加入了),通过存在集合当中的顶点j为踏板来到i的距离会不会比原来的距离要短 然后,我还是想和大家说一下 下面有必要声明一下: ps:参数具体什么含义,看我注解就知道了 看这张图:箭头右边的数组表示第i位变位0了,当最后集合变成0就是我们胜利的时刻了(重复一次:从第0位开始) if (index==-1) break;//结束条件,你也可以自己优化一下,反正我就爱这么写[哈哈],当下标没有变化的时候就结束了 这个时候,我们只是成功了一半, 再再再重复一遍:0=>1或者 10=>7,表示的只是最短路径,我们还要将他们中间存在的点找出来,这个时候的找我们就回到floyd算法里面涉及的找路径,我们通过一个for就能全部找出来了, 下面是我写的完整代码 作者:随风 有什么问题可以私聊我qq:2338244917 你们可以关注我的b站账号:随风的叶子 跳转=>随风的叶子
ps: 以下从第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<
@Autowired
private FloydUtil floydUtil;(这玩意是因为我写springboot项目用了,你们看到函数名知道我在放什么屁就行了,抱拳了)
0 1 2 3 13 14 15 12 4 11 16 11 5 6 5 10 17 18 9 19 8 7 0
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];//最后一个点放进来
}
}