动态规划求解旅行商问题,tsp问题求最优解,附代码实现

旅行商问题,即TSP问题(Traveling Salesman Problem)又译为旅行推销员问题、货郎担问题,是数学领域中著名问题之一。假设有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。

一般分2大类:

  • 完全图:两两城市间都有直达的路线,这条路线不需要经过中间其他节点;

  • 非完全图:偶尔有两个城市间的路线需要经过其他中间节点。

所以该怎么求解呢,我们很容易想到一种类似于穷举的思路:现在假设我们要拜访11个城市,从城市1出发,最后回到城市1。显然,从城市1出来后,我们随即可以选择剩余的10个城市之一进行拜访(这里所有城市都是连通的,总是可达的,而不连通的情况属于个人特殊业务的装饰处理,不是本文考虑范畴),那么很显然这里就有10种选择,以此类推,下一次就有9种选择…总的可选路线数就是:10!。也就是说需要用for循环迭代10!次,才能找出所有的路线,进而筛选出最短的那条来。如果只拜访个10个城市或许还好的话(需要迭代3628800次),那要拜访100个城市(需要迭代9.3326215443944 * 10^157)简直就是计算机的噩梦!

这是个NP完全问题,穷举算法的效率又不高,那我们该如何通过一个多项式时间复杂度的算法快速求出这个先后次序呢?目前比较主流的方法是采用一些随机的、启发式的搜索算法,比如遗传算法、蚁群算法、模拟退货算法、粒子群算法等。但这些算法都有一个缺点,就是不一定能求出最优解,只能收敛于(近似逼近)最优解,得到一个次优解,因为他们本质都是随机算法,大多都会以类似“一定概率接受或舍去”的思路去筛选解。各算法的实现思路都有不同,但也或多或少有互相借鉴的地方,有的与随机因子有关、有的与初始状态有关、有的与随机函数有关、有的与选择策略有关……

本文主要讲述动态规划方法,时间复杂度相对搜索算法来说要高一些,但可以求得最优结果。复杂度近似N*2^(n-1)


案例数据,图和邻接矩阵如下:

不能走的话用-1表示

解决这个问题,最需要表达的就是城市选择集合,为程序实现方便,这里用二进制串表示集合。比如集合{1,3,5,6,7}表示成二进制串用1110101,其中集合里面有的数对应的位数写成1,没有的写成0。

则要判断第3位是不是1,就把 1110101右移(3-1)位,得到11101,然后结果和00001进行 & 运算,如果结果是1说明第3位是1,否则说明第3位是0。

标注一下,

  1. 对于第y个城市,他的二进制表达为,1<<(y-1)
  2. 对于数字x,要看它的第i位是不是1,那么可以通过判断布尔表达式 (((x >> (i - 1) ) & 1) == 1的真值来实现。

要使用动态规划,需要问题本身有最优子结构,我们需要找到要解决的问题的子问题。

  题目要求,从0出发,经过[1,2,3]这几个城市,然后回到0,使得花费最少。要实现这个要求,需要从下面三个实现方案中选择花费最少的方案。

    1、 从0出发,到1,然后再从1出发,经过[2,3]这几个城市,然后回到0,使得花费最少。

    2、 从0出发,到2,然后再从2出发,经过[1,3]这几个城市,然后回到0,使得花费最少。

    3、 从0出发,到3,然后再从3出发,经过[1,2]这几个城市,然后回到0,使得花费最少。

  可以发现,三个小的解决方案的最优解,构成了大的解决方案,所以这个问题具有最优子结构,可以用动态规划来实现。

  设置一个二维的动态规划表dp,定义符号{1,2,3}表示经过[1,2,3]这几个城市,然后回到0。

  那么题目就是求dp[0][{1,2,3}]。将{1,2,3}表示成二进制,就是111,对应10进制的7,所以题目是在求dp[0][7];

  要求三个方案的最小值意味:

    dp[0][{1,2,3}] = min{ C01+dp[1][{2,3}] ,C02+dp[2][{1,3}] ,C03+dp[3][{1,2}]}

    其中C01 表示从0出发到1的距离。

    dp[1][{2,3}] = min{ C12+dp[2][{3}] ,C13+dp[3][{1}]}

    dp[2][{3}] = C23+dp[3][{}]

    dp[3][{}]就是从3出发,不经过任何城市,回到0的花费,所以dp[3][{}] = C30

  先确定一下dp表的大小,有n个城市,从0开始编号,那么dp表的行数就是n,列数就是2^(n-1),即1 << (n – 1),集合{1,2,3}的子集个数。在求解的时候,第一列的值对应这从邻接矩阵可以导出,后面的列可以有前面的列和邻接矩阵导出。所以求出的动态规划表就是:

初始化第一列,即j=0

for(int i =0;i <n;i++){                      
    dp[i][0] = C[i][0];                        
}

计算第二列:

j = 1;       //可以把j带入
for(int i = 0;i < n;i++){
    dp[i][j] = C[i][1]+dp[1][0]
}

后面的规律比较麻烦的一点在于要集合和二进制转换:

    先看我们要求的最终结果:从0出发,经过{1,2,3},最后回到起点,这里的{1,2,3}对应的就是111=7(忘了就查上表)

    不难得出:dp[0][7] = min{C01 + dp[1][6], C02+ dp[2][5], C03 + dp[3][3]}

    实际上就3种选择,算最小值,一个for循环搞定,但注意到列数好像并没有规律,6,5,3必须要明确出一个算法我们才能用循环搞定。

    先看去k=1的路,列6 = (111) ^ (1)得到,(1) = 1城市二进制

    再看去k=2的路,列5 = (111) ^ (10)得到。(10) = 2城市二进制

    最后看去k=3的路,列3 = (111) ^ (100)得到。(100) = 3城市二进制

    公式出来了,列数=j^城市的二进制表达=j^(1<<(k-1)),我们开始开始构建循环。

但有2个要注意:

1、求dp[2][3]的时候。就是求从2出发,经过{1,2},显然不合理,因为{1,2}包含2了,也就是3包含了城市2,这种情况需要忽略掉。也就是判断数字3的二进制位的第2位是不是1,是1就表示不合理。判断条件为之前说到的:(((x >> (i - 1) ) & 1) == 1

2、求dp[2][5]的时候。就是求从2出发,经过{1,3},这时不会有k=2这条路,因为{1,3}并不含有2,需要排除掉。也就是判断数字5的二进制位的第2位是不是1,是1就可以走,不是1就不用算了。

  根据以上的推导,最后求dp表的代码实现就是:

for(int j = 1;j < 1 << (n - 1);j++){        
    for(int  i= 0;i < n;i++){               
        dp[i][j] = 0x7ffff;
        if(((j >> (i - 1)) & 1) == 1){          
            continue;   
        }   
        for(int k = 1;k < n;k++){       
            if(((j >> (k - 1)) & 1) == 0){
                continue;                           
            }
            if(dp[i][j] > C[i][k] + dp[k][j ^ (1 << (k - 1))]){
                dp[i][j] = C[i][k] + dp[k][j ^ (1 << (k - 1))];
            }
        }
    }
}

最终程序的返回值就是dp表左上角的那个数字return dp[0][(1<<(cityCount - 1)) - 1];

一个完整运行的例子是:

public class TravelingSalesman {
    public static void main(String[] args) {
        int cityCount = 4;
        int[][] roadInfo = new int[][]{
             {0, 1, 10},
             {1, 0, 10},
             {1, 3, 25},
             {3, 1, 25},
             {3, 2, 30},
             {2, 3, 30},
             {0, 2, 15},
             {2, 0, 15},
             {1, 2, 35},
             {2, 1, 35}
        };
        
        int roadmap[][] = new int[cityCount][cityCount];        //转成邻接矩阵方便取数
        int dp[][] = new int [cityCount][1 << (cityCount - 1)];
        string path[][] = new string [cityCount][1 << (cityCount - 1)];
        for(int i = 0;i < cityCount;i++){
            for(int j = 0;j < cityCount;j++){    
                roadmap[i][j] = 0x7ffff;                        //用0x7ffff表示无穷大
            }
        }
        for(int i = 0;i < roadInfo.length;i++){                 //邻接矩阵
            roadmap[roadInfo[i][0]][roadInfo[i][1]] = roadInfo[i][2];
        }

        for(int i =0;i <cityCount;i++){                          //先求dp表第一列
            dp[i][0] = roadmap[i][0];                            //求出了每个城市回到起点的距离了。
            path[i][j]=i;                                        //记录初始路径。
        }
                                                
        for(int j = 1;j < 1 << (cityCount - 1);j++){             //再求其他列
            for(int  i= 0;i < cityCount;i++){                    //从i出发,要去包含j = {010101}的    城市
                dp[i][j] = 0x7ffff;
                if(((j >> (i - 1)) & 1) == 1){                   //如果已经到过j了,就continue
                    continue;    
                }    
                for(int k = 1;k < cityCount;k++){                 //看能不能先到k城市
                    if(((j >> (k - 1)) & 1) == 0){
                        continue;                                 //不能先到k城市,continue;
                    }
                    if(dp[i][j] > roadmap[i][k] + dp[k][j ^ (1 << (k - 1))]){
                        dp[i][j] = roadmap[i][k] + dp[k][j ^ (1 << (k - 1))];
                        path[i][j]=i+path[k][j ^ (1 << (k - 1))]; //找到更短路径,覆盖之前结果。
                    }
                }
            }
        }
        System.out.println(dp[0][(1<<(cityCount - 1)) - 1]);
        System.out.println(path[0][(1<<(cityCount - 1)) - 1]);
    }
}

若要显示dp表,可以在求出dp表以后加上一下代码:

System.out.printf("%10d",0);
for(int j = 0;j < 1 << (cityCount - 1) ;j++){
    System.out.printf("%10d",j);
}
System.out.println();
for(int i = 0;i < cityCount;i++){
    System.out.printf("%10d",i);
    for(int j = 0;j < 1 << (cityCount - 1) ;j++){
        if(dp[i][j] == 0x7ffff) dp[i][j] = -1;
        System.out.printf("%10d",dp[i][j]);
    }
    System.out.println();
}

 

  • 30
    点赞
  • 170
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值