首页 > 试题广场 >

毕业旅行问题

[编程题]毕业旅行问题
  • 热度指数:14411 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 32M,其他语言64M
  • 算法知识视频讲解
小明目前在做一份毕业旅行的规划。打算从北京出发,分别去若干个城市,然后再回到北京,每个城市之间均乘坐高铁,且每个城市只去一次。由于经费有限,希望能够通过合理的路线安排尽可能的省一些路上的花销。给定一组城市和每对城市之间的火车票的价钱,找到每个城市只访问一次并返回起点的最小车费花销。

输入描述:
城市个数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元以内,无需考虑极端情况。
状态压缩加二维动态规划。
其中城市的数量<20, 因此可以使用一个int保存已经遍历过的城市, 去过的城市对应的位数为1,。 
现在使用一个二维数组保存所有的状态。
dp[n][state], 其中n代表当前在城市n, state代表已经访问过的城市数量。
初始从0出发, 状态为0, 然后对单独到所有城市进行初始化(后面发现多余), 访问第一个城市i, 状态变为 1<<i, 对应的状态为 dp[i][1<<i]. 并使用一个set保存当前访问一个城市时的状态。 随后开始访问第二个城市。 状态转移方程为: dp[j][oldState|(1<<j)] = Math.min(原值, dp[i][oldState] + matrix[i][j] ) ).
最终访问完所有城市后, 状态变为 (1<<n)-2 。 
当访问完所有城市后, 对于每个城市, 计算 最小的,dp[i][endState] + matrix[i][0].

import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Scanner;
import java.util.Set;

// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
    static int res;
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        // 注意 hasNext 和 hasNextLine 的区别
        while (in.hasNextInt()) { // 注意 while 处理多个 case
            int n = in.nextInt();
            int[][] matrix = new int[n][n];
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    matrix[i][j] = in.nextInt();
                }
            }
            long res = getShortestWay(n, matrix);
            System.out.println(res);
        }
    }
    static long getShortestWay (int n, int[][] matrix) {
        boolean[] visited = new boolean[n]; Arrays.fill(visited, false);
        int state_num = (1<<n) - 1; // 途径 1 -> n-1 
        long[][] dp = new long[n][state_num+1];
        for (int i = 0; i < n; i++) {
            Arrays.fill(dp[i],  Integer.MAX_VALUE/2);
        }
        Set<Integer> set = new HashSet<>();

        for (int i = 1; i < n; i++) {
            set.add(1<<i);
            dp[i][1<<i] = matrix[0][i];
        }

        int endState = (1<<n)-2;
        while (!set.isEmpty()) {
            Set<Integer> newSet = new HashSet<>();
            for (int ss:set) {
                if (ss == endState) continue;
                // 当前状态 是 ss
                for (int i = 1; i < n; i++) {
                    // if (dp[i][ss] == Integer.MAX_VALUE/2) {
                    //     continue;
                    // }
                    for (int j = 1; j < n; j++) {
                        if (((1<<j) & ss) != 0 ) continue; // 如果已经访问过, 则取消
                        int newState = ss | (1<<j);
                        dp[j][newState] = Math.min(dp[j][newState], 
                                          dp[i][ss] + matrix[i][j]);
                        newSet.add(newState);
                    }
                }
            }
            set = newSet;
        }
        long res = Integer.MAX_VALUE/2;
        for (int i = 1; i < n; i++) {
            res = Math.min(res, dp[i][endState] + matrix[i][0]);
        }
        return res;   
    }
}


编辑于 2023-12-20 20:02:30 回复(0)
/*
参考 旅行商问题 :https://blog.csdn.net/qq_39559641/article/details/101209534?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ETopBlog-1.topblog&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ETopBlog-1.topblog&utm_relevant_index=1
 */
import java.util.Scanner;

public class Main {
    private void sln() {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[][] prices = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                prices[i][j] = sc.nextInt();
            }
        }
        int m = 1 << (n-1);
        int[][] dp = new int[n][m];
        // 对dp数组进行初始化
        for(int i=0; i<n; i++) {
            dp[i][0] = prices[i][0];
        }

        for(int j=1; j<m; j++) {
            for(int i=0; i<n; i++) {
                dp[i][j] = Integer.MAX_VALUE;
                // 集合j中包含i城市,就是看j的二进制表达i位置上是不是1,是的话就是包含
                if(((j >> (i-1)) & 1) == 1) continue;
                // 不包含
                else {
                    for(int k=1; k<n; k++) {
                        if(((j >> (k-1)) & 1) == 0) continue;// 如果集合j中不包含k城市,就不符合要求
                        dp[i][j] = Math.min(dp[i][j], prices[i][k] + dp[k][j^(1 << (k-1))]);// 集合j中去掉k城市
                    }
                }
            }
        }
        System.out.println(dp[0][m-1]);
    }

    public static void main(String[] args) {
        new Main.sln();
    }
}
发表于 2022-05-05 11:11:45 回复(0)
这题最直观的解法就是在暴力法的基础上加记忆化,加状态压缩,用一个bit表示一个站点的访问情况。动态规划就是记忆化递归的迭代写法
附AC代码
import java.util.*;

public class Main{
    
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        int N = sc.nextInt();
        int[][] nums = new int[N][N];
        for(int i=0; i < N; i++){
            for(int j=0; j < N; j++){
                nums[i][j] = sc.nextInt();
            }
        }
        Integer[][] memo = new Integer[(1<<N)][N];
        System.out.println(DFS(nums, 0, 0, memo));
    }
    
    private static int DFS(int[][] nums, int idx, int state, Integer[][] memo){
        int n = nums.length;
        if(state == (1<<n)-2){ //防止出现0-1-2-0-3的情况
            return nums[idx][0];
        }
        if(memo[state][idx] != null) return memo[state][idx];
        int ret = Integer.MAX_VALUE;
        for(int i=1; i < n; i++){
            if((state & (1<<i)) != 0) continue;
            ret = Math.min(ret, nums[i][idx] + DFS(nums, i, (state ^ (1<<i)), memo));
        }
        memo[state][idx] = ret;
        return ret;
    }
    
}


发表于 2021-08-13 13:32:03 回复(1)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
/**
 *动态规划————旅行商问题
 初学编程,整理前辈代码,加上了自己的理解,供大家参考
 * @author WangJie
 *
 */
public class Main {
    
    //读数器
    public static int reader(StreamTokenizer st) throws IOException {
        st.nextToken();
        return (int)st.nval;
    }
    public static void main(String[] args) throws IOException {
        
        //写入数据
        StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
        int n = reader(st);
        int[][] cost = new int[n][n];
        for(int i = 0;i < n;i++) {
            for(int j = 0;j < n;j++) {
                cost[i][j] = reader(st);
            }
        }
        
        /*
         分析:
         假设有0,1,2,3四个城市,起点不会影响结果,因此选城市0为起点,创建一个二维数组dp[][],用二维数组元素的值代表最低花费,用行坐标代表起点城市,列坐标代表接下来要去的城市(注列坐标用二进制表示,如接下来去1,3城市,则使二进制数第一三位为1,即用101表示)。
         因此以0为起点的最低花费可表示为dp[0][111];
         
         以0为起点三种情况:
         以0为起点,再进一步选定3为起点,最低花费可表示为cost[0][3]+dp[3][011];
         以0为起点,再进一步选定2为起点,最低花费可表示为cost[0][2]+dp[2][101];
         以0为起点,再进一步选定1为起点,最低花费可表示为cost[0][1]+dp[1][110];
         
         取上面三个式子最小值,假设第一种最小,即cost[0][3]+dp[3][011]最小
         
         以0为起点,再进一步选定3为起点两种情况:
         以0为起点,再进一步选定3为起点,再进一步选定2为起点,最低花费可表示为cost[0][3]+cost[3][2]+dp[2][001];
         以0为起点,再进一步选定3为起点,再进一步选定1为起点,最低花费可表示为cost[0][3]+cost[3][1]+dp[1][010];
         
         取上面两个式子最小值,假设第一种最小,即cost[0][3]+cost[3][2]+dp[2][001]最小
         最低花费可表示为cost[0][3]+cost[3][2]+cost[2][1]+dp[1][000]
         */
        
        int all = 1 << (n -1); //确定dp列数:000,001,010,100,011,101,110,111。2^(n-1)个
        int[][] dp = new int [n][all]; //建立dp
        
         //由以上分析知,最终结果会出现dp[i][000],即从此点回到起始点的花费,以此初始化数组
        for(int i = 0;i < n;i++) { 
            dp[i][0] = cost[i][0];
        }
        
        for(int j = 1;j < all;j++) { /* j=1(在以上分析的例子中二进制为001)代表最后经过城市1
                                         j=2(在以上分析的例子中二进制为010)代表最后经过城市2
                                         j=3(在以上分析的例子中二进制为011)代表最后经过城市1和2
                                         …………*/
            for(int i = 0;i < n;i++) { //在以上分析的例子中,当j = 1时,此循环代表遍历dp[i][001]
                dp[i][j] = Integer.MAX_VALUE; //给元素设定一个最大值,方便寻找更小的值
                if(((j >> (i - 1)) & 1) == 0) { /*此式子代表当dp[i][j]当j中的二进制数从右面数第i个数为零时,才进行后面运算,因为i代表以该城市为起点,后面不能再经过该城市*/
                    for(int k = 1;k < n;k++) { //查找j代表经过的城市
                        if(((j >> (k - 1)) & 1) == 1) { /*在以上分析的例子中,如j为101,则当k=1和3时,条件为true*/
                            dp[i][j] = Math.min(dp[i][j], cost[i][k] + dp[k][j^(1 << (k - 1))]); /*其中式子:cost[i][k] + dp[k][j^(1 << (k - 1))]代表从城市i出发,然后经过城市k,的最小花费,对应以上分析的例子中“以0为起点三种情况:”*/
                        }
                    }
                }
            }
        }
        System.out.println(dp[0][all - 1]); /*在以上分析的例子中all-1转化成二进制,为111,此代码代表以0为起点,经过1,2,3城市最低花费*/
    }
}

发表于 2020-10-26 21:34:10 回复(1)
用递归+回溯+剪枝,通过50%,剩余超时,欢迎大家讨论优化
import java.util.Scanner;
public class Main {
      public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[][] cost = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                cost[i][j] = sc.nextInt();
            }
        }
        System.out.println(permute(cost));
    }
    public static int permute (int[][] cost){
        if(cost == null) return 0;
        len = cost.length;
        if(len == 0) return 0;
        boolean[] used = new boolean[len];
        used[0] = true;
        dfs(0,used,cost);
        return min;
    }
    private static int num = 1;
    private static int len;
    private static int sum = 0;
    private static int min = Integer.MAX_VALUE;
    private static int dfs(int index,boolean[] used,int[][] cost){
        if (num == len) {
            sum += cost[index][0];
            min = Math.min(sum,min);
            sum -= cost[index][0];
            return min;
        }
        for (int i = 1; i < len; i++) {
            if (used[i] || index == i) continue;//已经去过的城市
            sum +=cost[index][i];
            if(sum > min){ //当前经费大于当前最小全程经费
                sum -= cost[index][i];
                continue;
            }
            used[i] = true;
            num++;
            dfs(i,used,cost);
            sum -=cost[index][i];;
            used[i] = false;
            num--;
        }
        return min;
    }
}
编辑于 2020-08-22 15:11:49 回复(0)
自上而下的dp版本
import java.util.*;
public class Main {
    //备忘录
    public static int[][] memo;
    //自上而下的dp函数
    public static int dp(int city, int j, int[][] dist) {
        //中止条件
        if(j == 0)
            return dist[city][0];
        //如果命中,直接返回
        if(memo[city][j] != -1)
            return memo[city][j];
        int ans = Integer.MAX_VALUE;
        for(int i = 1; i < dist.length; i++) {
            if(((j >> (i-1)) & 1) == 1) {
                j ^= (1 << (i-1));
                //记录这一轮的最小答案
                ans = Math.min(ans, dist[city][i]+dp(i, j, dist));
                j ^= (1 << (i-1));
            }
        }
        //添加至备忘录
        memo[city][j] = ans;
        return ans;
    }
    
    
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int cityNum = in.nextInt();// 城市数目
        int[][] dist = new int[cityNum][cityNum];
        for (int i = 0; i < dist.length; i++) {
            for (int j = 0; j < cityNum; j++) {
                dist[i][j] = in.nextInt();
            }
        }
        //对1进行左移n-1位,值刚好等于2^(n-1)
        int V = 1 << (cityNum - 1);
        memo = new int[cityNum][V];
        //初始化memo备忘录
        for (int i = 0; i < cityNum; i++) {
            for(int j = 0; j < V; j++) {
                memo[i][j] = -1;
            }
        }
        int ans = dp(0 , V-1, dist);
        System.out.println(ans);
    }
}


发表于 2020-07-11 16:57:46 回复(0)