旅行商问题(笔记)

介绍:

现在假设有4个城市,到各个城市都有一定的花费,你是一个旅行商要到这4个城市去售卖物品,这4个城市必须都走一遍,问走哪条路线花费最少?(起点可任意,最终要回到起点)

旅行商问题(笔记)_第1张图片


分析:

经过分析有以下这几种路线(假设从北京开始):

旅行商问题(笔记)_第2张图片

我们可以看到,如果有4个城市的话,路线一共有:3x2 = 6 条, 5个城市有:4x3x2 = 24条,假设有n个城市,总路线就有:(n - 1)! 条, 时间复杂度很高,如果只有4、5个城市时,可以用动态规划来求解,这种时间消耗可以接收。

但如果更多的城市就只能考虑求近似解了。

我们可以用一张二维表来表示花费,行和列都代表城市编号,二维表格中的元素代表从某个城市到另一个城市的花费。如下图(graph):

旅行商问题(笔记)_第3张图片

d(出发城市,剩余城市集合) ==> 从出发城市开始,走完剩余城市,花费的最少代价

我们可以得到以下递推公式:

旅行商问题(笔记)_第4张图片

我们可以把出发城市用变量 i 代替,剩余城市集合用 j 代替, 遍历 j 时的变量为 k (剩余的某一个城市)

旅行商问题(笔记)_第5张图片

旅行商问题要分多条路线,我们需要在这写众多路线中求解花费最小的,这其实就是一个把大问题拆解成很多子问题的过程。

注意:

动态规划思想就是找出递推公式,将当前问题分解成子问题,分阶段进行求解,在求解过程中缓存子问题的解,避免重复计算。

这次我们需要创建一张二维表格,表格的行代表每个城市,列代表剩余城市的集合,比如说从0号城市出发,还需要去1、2、3号城市,集合里装的就是这些城市。这里数组的列元素是一个集合,没怎么见过 ,像这样:

旅行商问题(笔记)_第6张图片

我们可以用二进制数来表示集合中是否存在这个城市,比如说 000 代表没有城市,001 代表有1号城市:

旅行商问题(笔记)_第7张图片       旅行商问题(笔记)_第8张图片

这样我们就可以用 十进制数来表示集合中有几个城市。

代码分析:

如果我们求解 d(0,1 | 2 | 3),我们需要知道之前的子问题的解,从3种情况中选择花费最小的填上去,推导是从右往左进行的,但是填二维表要从左往右填充。

我们第0列的元素是可以根据graph表格直接填充的。先初始化二维表:

static int tsp(int[][] g){
        int m = g.length;  //城市个数
        int n = 1 << (m - 1); //剩余城市的组合数
        int[][] dp = new int[m][n];
        //填充第0列
        for (int k = 0; k < m; k++) {
            dp[k][0] = g[k][0];
        }
        return 0;
    }

然后我们再填充后续列:

旅行商问题(笔记)_第9张图片

填充时,我们需要排除不合理的情况,比如出发点是0号城市,剩余城市里包含出发城市这种情况不做处理。我们可以先给一个比较大的值,如果后面有更小距离可以替换。

核心代码(根据这张图理解一下):

旅行商问题(笔记)_第10张图片

static int tsp(int[][] g){
        int m = g.length;  //城市个数
        int n = 1 << (m - 1); //剩余城市的组合数
        int[][] dp = new int[m][n];
        //填充第0列
        for (int k = 0; k < m; k++) {
            dp[k][0] = g[k][0];
        }
        //填充后续列
        for (int j = 1; j < n; j++) {
            for (int i = 0; i < m; i++) {
                dp[i][j] = Integer.MAX_VALUE / 2;  //不要给太大,后面做运算可能会产生整数溢出
                if(contains(j, i)) continue; //剩余城市包含出发城市直接跳过(不合理)
                for (int k = 0; k < m; k++) {  //k为1时  k为2时   k表示先要去的城市
                    if(contains(j, k)){   //剩余城市包含出发城市
                        dp[i][j] = Integer.min(dp[i][j], g[i][k] + dp[k][exclude(j, k)]);
                    }
                }
            }
        }
        print(dp);
        return dp[0][n - 1];
    }

这样的数据设计和遍历方式非常nice,表格右边的数据都可以在左边得到。

下面是exclude和contains方法的实现:

/*
        110 是否包含 0 = 0 & 1 = 0
        110 是否包含 1 = 110 & 1 = 0
        110 是否包含 2 = 11 & 1 = 1
        110 是否包含 3 = 1 & 1 = 1
        110 是否包含 4 = 0 & 1 = 0
     */
    static boolean contains(int set, int city) {
        return (set >> (city - 1) & 1) == 1;
    }

    /*
            1|2|3   1 => 2|3        1|2|3  2 => 1|3
            111                     111    异或运算
            001 ^                   010 ^
            ----                   ----
            110                     101

     */
    static int exclude(int set, int city) {
        return set ^ (1 << (city - 1));
    }

完整代码:
package dynamicprogramming;

import java.util.Arrays;
import java.util.stream.Collectors;

/**
 * 旅行商问题
 */
public class TravellingSalesmanProblem {

    public static void main(String[] args) {
        int[][] graph = {
                {0, 1, 2, 3},
                {1, 0, 6, 4},
                {2, 6, 0, 5},
                {3, 4, 5, 0},
        };
        System.out.println(tsp(graph));
        //System.out.println(6 >> (0-1));
    }

    static int tsp(int[][] g){
        int m = g.length;  //城市个数
        int n = 1 << (m - 1); //剩余城市的组合数
        int[][] dp = new int[m][n];
        //填充第0列
        for (int k = 0; k < m; k++) {
            dp[k][0] = g[k][0];
        }
        //填充后续列
        for (int j = 1; j < n; j++) {
            for (int i = 0; i < m; i++) {
                dp[i][j] = Integer.MAX_VALUE / 2;  //不要给太大,后面做运算可能会产生整数溢出
                if(contains(j, i)) continue; //剩余城市包含出发城市直接跳过(不合理)
                for (int k = 0; k < m; k++) {  //k为1时  k为2时   k表示先要去的城市
                    if(contains(j, k)){   //剩余城市包含出发城市
                        dp[i][j] = Integer.min(dp[i][j], g[i][k] + dp[k][exclude(j, k)]);
                    }
                }
            }
        }
        print(dp);
        return dp[0][n - 1];
    }

    /*
        110 是否包含 0 = 0 & 1 = 0
        110 是否包含 1 = 110 & 1 = 0
        110 是否包含 2 = 11 & 1 = 1
        110 是否包含 3 = 1 & 1 = 1
        110 是否包含 4 = 0 & 1 = 0
     */
    static boolean contains(int set, int city) {
        return (set >> (city - 1) & 1) == 1;
    }

    /*
            1|2|3   1 => 2|3        1|2|3  2 => 1|3
            111                     111    异或运算
            001 ^                   010 ^
            ----                   ----
            110                     101

     */
    static int exclude(int set, int city) {
        return set ^ (1 << (city - 1));
    }

    static void print(int[][] dist) {
        System.out.println("-------------------------");
        for (int[] row : dist) {
            System.out.println(Arrays.stream(row).boxed()
                    .map(x -> x >= Integer.MAX_VALUE / 2 ? "∞" : String.valueOf(x))
                    .map(s -> String.format("%2s", s))
                    .collect(Collectors.joining(",", "[", "]")));
        }
    }
}

你可能感兴趣的:(笔记,动态规划,算法)