旅行商问题

2021-04-15  本文已影响0人  Plutorres

描述

给定一组城市和每对城市之间的火车票的价钱,找到每个城市只访问一次并返回起点的最小车费花销(城市总数 n <= 20)。

分析

记这 n 个城市为 c_0, c_1, ... , c_{n-1},第一次需要从 c_1, ... , c_{n-1} 中选择一个城市,第二次从剩下的 n-2 个城市中选择一个...,一直到第 n-1 次仅剩一个城市可供选择,一共有 (n-1)! 种可能的情况。考虑动态规划的解法,建立一个数组 dp 记录从城市 c_k 出发,访问一个城市集合 S 中的每个城市一次并最终返回城市 c_0 所需的最小车费。我们最终要求的就是 dp[c_0][{c_1,c_2,..,c_{n-1}}]dp 数组第一维可以直接用下标 k 表示城市 c_k,难点在于第二维对城市集合的表示。我们要表示的是集合 \{c_1,c_2,..,c_{n-1} \}\及其所有子集,包括空集一共有 2^{n-1} 种情况,不难发现,0 ~ 2^{n-1}-1 每一个数的二进制表示都可以表示其中一种集合,0 表示空集,2^{n-1}-1 二进制表示是 n-11,恰好可以表示\{c_1,c_2,..,c_{n-1}\}\。我们从 dp[c_0][c_1,c_2,..,c_{n-1}] 出发,从 c_1 遍历到 c_{n-1} 选择其中一个 c_k 使得 fale(c_0, c_k) + dp[c_k][{c_1,c_2,..,c_{n-1}}- c_k] 最小,迭代这个过程直至 dp 的第二维为空集,其中 dp[c_k][\emptyset] = fale(c_0, c_k),表示从 c_k 返回 c_0。上面描述的是正向推导路径的过程,实际上我们的求值顺序是反向的,以确保后面可以用上前面所求的结果。

代码

#include <bits/stdc++.h>

using namespace std;

const int N = 25;
int a[N][N];

// 旅行商问题
void initDp(vector<vector<int>>& dp, int n, int m) {
    // 先处理第0列,表示直接从当前城市返回起点城市
    for(int i = 0; i < n; i++) {
        dp[i][0] = a[i][0];
    }

    // 从第一列 {1} 到最后一列 {1,2,...,n-1}
    for(int j = 1; j < m; j++) {
        int p = 0; // 用于判断的指针
        for(int i = 0; i < n; i++) {
            dp[i][j] = INT_MAX;
            if(p & j) continue; // 集合中存在当前dp的起点i

            // 遍历 j 所表示的集合
            int p2 = 1; int k = 1;
            while(p2 <= j) {
                if(p2 & j) {  // 表示 k 在集合中
                    dp[i][j] = min(dp[i][j], a[i][k] + dp[k][j-p2]);
                }
                p2 = (p2 << 1); k++;
            }

            p = (p << 1); // 指针右移
        }
    }
}

int main() {
    int n; cin >> n;
    for(int i = 0; i < n; i++) {
        for(int j = 0; j < n; j++) {
            cin >> a[i][j];  // 可以理解为边(i, j)的权重
        }
    }

    int m = (1 << (n-1)); // m = 2^(n-1),表示除起点外(n-1)个城市的组合数
    // 0 ~ m-1 的二进制表示恰好对应集合[1 ~ n-1]的一种选择情况
    vector<vector<int>> dp(n, vector<int>(m, 0));
    initDp(dp, n, m);  // 初始化dp数组

    cout << dp[0][m-1] << '\n';  // 最小费用

    // 在得到dp数组的情况下,正向推导一条最优路径
    int h = m-1;
    int pre = 0; cout << pre;
    while(h) {
        int minV = INT_MAX, minK = 0;
        int p2 = 1; int k = 1;

        // 遍历 h 表示的集合,选取其中达到最小值的节点
        while(p2 <= h) {
            if((p2 & h) && a[pre][k] + dp[k][h-p2] < minV) {  // 表示 k 在集合中
                minV = a[pre][k] + dp[k][h-p2];
                minK = k;
            }
            p2 = (p2 << 1); k++;
        }

        cout << " -> " << minK;
        h -= (1 << (minK-1));
        pre = minK;
    }
    return 0;
}
上一篇 下一篇

猜你喜欢

热点阅读