【动态规划】二进制思维(状压DP)巧妙求解TSP(旅行商问题),输出最短路径及其长度(C++)

思路参考:https://blog.csdn.net/joekwok/article/details/4749713

旅行商问题为关键词搜索,找了一圈没有发现比较优雅的解决方法,大多是固定大小与数据,所以打算自己写一个。

这种算法思想叫做状态压缩动态回归(状压DP),感兴趣的话还可以以此为关键词搜索了解更多有关内容。本文只介绍该算法思想在旅行商问题(Travelling salesman problem,TSP)上的应用。

旅行商问题的讲解可以参考这个链接,我对其代码进行了改进,使得能针对不同的带权图,修改宏表示的城市个数N、全局变量表示的带权图距离矩阵distances[N][N]就能解决不同的TSP(旅行商问题)。本代码中distances[N][N]采用随机数生成,可根据自己需要更改。

由于内存的限制,本代码在城市个数N ≤ \le 15时运行正常;当城市个数N > > > 15时,建议将数据存入文件,通过读取文件实现。

代码的关键是用二进制01来代表城市集合,例如对于城市1,2,3组成的集合 { 1 , 2 , 3 } \{1,2,3\} {1,2,3},用二进制 111 111 111表示,即十进制的 7 7 7,二维数组dp的第7列;对于城市1,3组成的集合 { 1 , 3 } \{1,3\} {1,3},用二进制 101 101 101表示,即十进制的 5 5 5,二维数组dp的第5列。

所以,我们的目标是 d p [ 0 ] [ 11 1 2 ] dp[0][111_2] dp[0][1112],即 d p [ 0 ] [ 7 10 ] dp[0][7_{10}] dp[0][710],代表从 0 0 0号城市出发,将要走过城市群 { 1 , 2 , 3 } \{1,2,3\} {1,2,3},动态规划的过程参考上面提到的链接

下面代码的理解上,重点是看懂里面很多的对二进制数的操作,其他就不难理解了。

填表法的动态规划结果打印都是递归的方法,path[i][j]存储:从i号城市出发,将要经过j的二进制形式表示的城市群,i号城市的下一个城市的编号。

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <iomanip>
using namespace std;

#define N 15
#define MAX 0x3f3f3f3f

// int distances[N][N] = {{MAX, 3, 6, 7}, {5, MAX, 2, 3}, {6, 4, MAX, 2}, {3, 7, 5, MAX}};

int removeCity(int j, int k) { // 从j二进制表示的城市集合中去除k号城市(k位设为0)
    return j - (1 << (k-1));
}

int TSP(int dp[][1 << (N-1)], int path[][1 << (N-1)], int distances[][N]) {
    if (N == 1) return 0; // 只有一个结点

    for (int i = 1; i < N; i++) {
        dp[i][0] = distances[i][0]; // 初始化所有城市到0号城市的距离
    }

    for (int j = 1; j < (1 << (N-1)); j++) {
        for (int i = 1; i < N; i++) {
            if ((j >> (i-1)) % 2 == 0) { // i号城市不在j二进制形式表示的城市集合里
                int min = MAX;
                int next_city = i;
                for (int k = 1; k < N; k++) {
                    if ((j >> (k-1)) % 2 != 0) { // k号城市在j二进制形式表示的城市集合里
                        int temp = distances[i][k] + dp[k][removeCity(j, k)]; // 表示把k号城市从j城市集合中去除
                        if (temp < min) {
                            min = temp;
                            next_city = k;
                        }
                    }
                }
                dp[i][j] = min;
                path[i][j] = next_city;
            }
        }
    }
    
    // 填左上角元素
    int min = MAX;
    int next_city = 0;
    int j = (1 << (N-1)) - 1; // 代表除0号城市外的所有城市组成的集合
    for (int k = 1; k < N; k++) {
        int temp = distances[0][k] + dp[k][removeCity(j, k)];
        if (temp < min) {
            min = temp;
            next_city = k;
        }
    }
    dp[0][j] = min;
    path[0][j] = next_city;

    
    /*for (int i = 0; i < N; i++) { // 打印动态规划表
        for (int j = 0; j < 1 << (N-1); j++) {
            if (dp[i][j] == 0)
                cout << "\\" << " ";
            else
                cout << dp[i][j] << " ";
        }
        cout << endl;
    }*/

    return dp[0][j];
}

void printPath(int path[][1 << (N-1)], int i, int j) { // 打印路线 i为出发城市编号 j为剩下城市组成的集合
    if (j != 0) {
        cout << i << " -> ";
        int next_city = path[i][j];
        printPath(path, next_city, removeCity(j, next_city));
    }
    else {
        cout << i << " -> " << 0;
    }
}

int main() {
    int distances[N][N] = {0};
    int path[N][1 << (N-1)] = {0};
    int dp[N][1 << (N-1)] = {0};
    // 1 << (N-1) == pow(2, N-1)
    // 纵轴为0,1,2...表示城市编号
    // 横轴为000,001,010,...第x位为1,代表该集合里面有x号城市

    // 随机初始化distances[N][N]
    srand((unsigned int) time(NULL));
    for (int i = 0; i < N; i++) {
        for (int j = i; j < N; j++) {
            if (i == j) distances[i][j] = MAX;
            else {
                int temp = rand();
                while (temp == 0) {
                    temp = rand();
                }
                distances[i][j] = distances[j][i] = temp;
            }
        }
    }

    // 打印代价矩阵
    cout << "结点数为:[" << N << "]" << endl;
    cout << "代价矩阵为:" << endl;
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            if (distances[i][j] == MAX) cout << setw(5) << "INF" << " ";
            else cout << setw(5) << distances[i][j] << " ";
        }
        cout << endl;
    }

    // 对动态规划过程计时(毫秒 ms)
    clock_t start = clock();
    int d = TSP(dp, path, distances);
    clock_t spent = (double) (clock() - start) / CLOCKS_PER_SEC * 1000;
    cout << "花费时间:" << spent << "ms" << endl;
    cout << "最短距离为:" << d << endl;
    cout << "旅行路线为:";
    printPath(path, 0, (1 << (N-1))-1);

    return 0;
}

代码运行结果:

结点数为:[15]
代价矩阵为:
  INF  3729 13549 16032  5214 16144 28132 31901 18181 27523 31894 28842 30136 32028 13307
 3729   INF 13870 20822  6156 17167 11654 12498  3912 27011 11758  2954 17805 25778  3749
13549 13870   INF 28116  8097 10456 19276  7130 27613 14974 18319 22185 25320 15417 30216
16032 20822 28116   INF 30596  9669  1172 26208 21886 23824 20555 32440 10568 23940 25786
 5214  6156  8097 30596   INF 25671 25415 30145 13972 22340 10217 18764 26840  1542 26170
16144 17167 10456  9669 25671   INF 23768 14337  7018 15538 14276  9598  3189  8273 16100
28132 11654 19276  1172 25415 23768   INF 30561 26938 31215 11396 24667 13566 11725 18858
31901 12498  7130 26208 30145 14337 30561   INF 25576  2459 16751 19612 19143  2594  5038
18181  3912 27613 21886 13972  7018 26938 25576   INF   162 15277 26820 23305  6925 19514
27523 27011 14974 23824 22340 15538 31215  2459   162   INF  1011 24124 24952  7724 24260
31894 11758 18319 20555 10217 14276 11396 16751 15277  1011   INF  6932  1358 23763 30872
28842  2954 22185 32440 18764  9598 24667 19612 26820 24124  6932   INF 20353 22669 11973
30136 17805 25320 10568 26840  3189 13566 19143 23305 24952  1358 20353   INF 18427  6381
32028 25778 15417 23940  1542  8273 11725  2594  6925  7724 23763 22669 18427   INF 16329
13307  3749 30216 25786 26170 16100 18858  5038 19514 24260 30872 11973  6381 16329   INF
花费时间:13ms
最短距离为:79328
旅行路线为:0 -> 1 -> 11 -> 10 -> 9 -> 8 -> 13 -> 6 -> 3 -> 5 -> 12 -> 14 -> 7 -> 2 -> 4 -> 0
  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值