旅行商问题解法(2019年字节跳动校招笔试算法题“毕业旅行问题”)

问题原型

给定一系列城市和每对城市之间的距离,求推销员从某个城市出发后经过所有城市,然后回到出发城市的最短路径。

为了方便讲解,我们以2019年字节跳动校招笔试题“毕业旅行问题为例”。牛客原链接:https://www.nowcoder.com/profile/4097742/codeBookDetail?submissionId=58509076

题目描述

小明目前在做一份毕业旅行的规划。打算从北京出发,分别去若干个城市,然后再回到北京,每个城市之间均乘坐高铁,且每个城市只去一次。由于经费有限,希望能够通过合理的路线安排尽可能的省一些路上的花销。给定一组城市和每对城市之间的火车票的价钱,找到每个城市只访问一次并返回起点的最小车费花销。

输入描述

城市个数n(1<n≤20,包括北京)
城市间的车票价钱 n行n列的矩阵 m[n][n]

输出描述

最小车费花销 s

示例1

输入

4
0 2 6 5
2 0 4 4
6 4 0 2
5 4 2 0

输出

13

说明

共 4 个城市,城市 1 和城市 1 的车费为0,城市 1 和城市 2 之间的车费为 2,城市 1 和城市 3 之间的车费为 6,城市 1 和城市 4 之间的车费为 5,依次类推。假设任意两个城市之间均有单程票可购买,且票价在1000元以内,无需考虑极端情况。

这个问题首先需要构造一个图,图的一个对应一座城市,边的权值对应城市到城市之间火车票价格,根据题目描述,这是一个完全图(各个顶点都有一条边两两互相连接),并且各个边没有方向。

这道题一般有两种解法:

  • 回溯法
  • 动态规划

回溯法

把所有的解通过一棵树表达出来,然后通过深度优先遍历,找到一个解的时候就将其记录下来,最后输出最小的解即可。

如图所示,从根节点到叶子节点的所经过的节点就是其路径,每个边的长度之和就是总车票价格。
当然还有一种优化的方法,就是在遍历过程中,如果发现此时路径长度已经超出了之前找到的最小路径长度,就可以进行剪支操作,即不再遍历。

这种方法的时间复杂度为 O ( n ! ) O(n!) O(n!),当n大于12之后,其计算时间就已经非常大了。这种方法会因为超时无法通过所有的测试数据。

下面贴出代码:

import java.util.Arrays;
import java.util.Scanner;
import java.util.concurrent.atomic.AtomicInteger;

public class Main {
   
 
    public static void main(String[] args) {
   
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[][] arr = new int[n][n];
        for(int i = 0; i < arr.length; i++) {
   
            for(int j = 0; j < arr.length; j++) {
   
                arr[i][j] = sc.nextInt();
            }
        }
 
        boolean[] vis = new boolean[n];  //记录各个城市访问情况
        vis[0] = true;
        AtomicInteger ans = new AtomicInteger(Integer.MAX_VALUE);
        dfs(arr, vis, n, 0, 1, 0, ans);
        System.out.println(ans.get());
    }
 
 	//vn为已经访问的城市数量,local为当前城市编号,price为当前累计票价
    private static void dfs(int[][] arr, boolean[] vis, final int n, int local, 
    		int vn, int price, AtomicInteger ans) {
   
        if(price > ans.get()) {
     //如果此时价格已经超出了之前找到的最小价格,那么进行剪支操作
            return;
        }
 
        if(vn == n) {
     //如果已经遍历完成
            int val = price + arr[local][0];  //因为走完所有城市后还要回到起点所以加上arr[local][0]
            if(val < ans.get()) {
   
                ans.set(val);
            }
            return;
        }
 
        for(int i = 1; i < n; i++) {
     //因为起点为0所以无需考虑起点
            if(vis[i])
                continue;
            vis[i] = true;
            dfs(arr, vis, n, i, vn + 1, price + arr[local][i], ans);  //继续遍历
            vis[i] = false;
        }
    }
 
}

动态规划

我们可以使用动态规划,把一个大问题划分为多个小问题来求解。

例如大问题是从顶点0开始,经过顶点1,2,3然后回到顶点1的最短路程,那么我们可以分割为三个小问题找最优解:

  • 从顶点0出发到顶点1,再从顶点1出发,途径2,3城市(不保证访问顺序),然后回到0的最短路径
  • 从顶点0出发到顶点2,再从顶点2出发,途径1,3城市,然后回到0的最短路径
  • 从顶点0出发到顶点3,再从顶点3出发,途径1,2城市,然后回到0的最短路径

这三个小问题对应的最小值就是问题的最优解。

知道怎么划分子问题后,接下来就是找状态转移方程。

我们定义 d p [ n ] { p 1 , p 2 , . . . , p m } dp[n]\{p_1,p_2,...,p_m\} dp[n]{ p1,p2,...,pm} 为从城市 n n n 出发,途径城市 p 1 , p 2 , . . . , p m p_1,p_2,...,p_m p1,p2,...,pm(不保证访问城市的顺序)然后回到城市0的最短路径。按照上述划分问题的方法,我们可以推导出:

d p [ n ] { p 1 , p 2 , . . . , p m } = m i n ( d p [ p 1 ] { p 2 , . . . , p m } + D p 1 n , d p [ p 2 ] { p 1 , p 3 , . . . , p m } + D p 2 n , . . . . . . , d p [ p m ] { p 1 , p 2 , . . . p m − 1 } + D p m n ) dp[n]\{p_1,p_2,...,p_m\}= min(dp[p_1]\{p_2,...,p_m\}+D_{p_1}^n, dp[p_2]\{p_1,p_3,...,p_m\}+D_{p_2}^n,......,dp[p_m]\{p_1,p_2,...p_{m-1}\}+D_{p_m}^n) dp[n]{ p1,p2,...,pm}=min(dp[p1]{ p2,...,pm}+D

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值