dp-双调欧几里德旅行商问题

双调欧几里德旅行商问题

算法导论3rd - 15.3

问题描述

平面上n个点,确定一条连接各点的最短闭合旅程。这个解的一般形式为NP的(在多项式时间内可以求出)

J.L. Bentley 建议通过只考虑双调旅程(bitonictour)来简化问题,这种旅程即为从最左点开始,严格地从左到右直至最右点,然后严格地从右到左直至出发点。在这种情况下,多项式的算法是可能的。事实上,存在确定的最优双调路线的O(n*n)时间的算法。

下图(b)显示了同样的7个点的最短双调路线。

分析

将节点按x方向排序,从最左端开始出发,分为两条路径,严格从左到右依次经过不重复的点,且不能跳过节点,到达最右侧节点。
用cost[i][j]表示两条路径分别到达i,j点的最小距离代价。即两条路径 <0...i>和<0...j> 的总代价。
如果<0...i...N>和<0...j...N>是两条最短路径,那么<0...i>和<0...j>也是最短路径,由此,问题可以进行分解。

假设共有7个节点,从左往右依次标为0...6。
代价表格cost[i][j]假设i>j。(cost[i][j]与cost[j][i]相等)
如果将问题理解为依次将0...6号节点往两条路径中添加,则计算i节点的代价时,0...i-1号节点已经添加到两条路径中了,不能跳过节点。即计算cost[i][0...i-1]时cost[i-1][0...i-2]已经计算完成。
两条路径到达6号节点的前一步是其中一条路径已经到达了5,可能的路径有:cost[5][0],cost[5][1],cost[5][2],cost[5][3],cost[5][4].

cost[5][0]表示一条路经经过了012345,而另一条路径还没出发;
cost[5][1]表示一条路经经过了02345,另一条路径为01;
cost[5][2]则可能有两种情况:01345|02 和 0345|012,选择距离代价最小者;
cost[5][3]则有4种情况;
cost[5][4]有8种情况,不一一列举;

如法炮制,可以分析cost[5][0...4],然后再推导cost[4][0...3],依次类推,直到cost[0][0]

递推关系

cost[6][6] = min(
    cost[5][0] + dist[5,6] + dist[0,6],
    cost[5][1] + dist[5,6] + dist[1,6],
    cost[5][2] + dist[5,6] + dist[2,6],
    cost[5][3] + dist[5,6] + dist[3,6],
    cost[5][4] + dist[5,6] + dist[4,6],
)

cost[5][4] = min(
    cost[0][4] + dist[0][5],
    cost[1][4] + dist[1][5],
    cost[2][4] + dist[2][5],
    cost[3][4] + dist[3][5],
)
// 因为不允许跳过,所以当 i!=j+1 时,只有一种情况到达当前节点,以下几种情况皆是,后同
cost[5][3] = cost[4][3] + dist[4][5]
cost[5][2] = cost[4][2] + dist[4][5]
cost[5][1] = cost[4][1] + dist[4][5]
cost[5][0] = cost[4][0] + dist[4][5]

cost[4][3] = min(
    cost[0][3] + dist[0][4],
    cost[1][3] + dist[1][4],
    cost[2][3] + dist[2][4],
)
cost[4][2] = cost[3][2] + dist[3][4]
cost[4][1] = cost[3][1] + dist[3][4]
cost[4][0] = cost[3][0] + dist[3][4]

cost[3][2] = min(
    cost[0][2] + dist[0][3],
    cost[1][2] + dist[1][3],
)
cost[3][1] = cost[2][1] + dist[2][3]
cost[3][0] = cost[2][0] + dist[2][3]

cost[2][1] = cost[0][1] + dist[0][2]
cost[2][0] = cost[1][0] + dist[1][2]

cost[1][0] = cost[0][0] + dist[0][1]

cost[0][0] = 0

在计算时,将代价最小的路径记录下来。

程序

// 双调欧几里德旅行商问题

#include <iostream>
#include <new>
#include <time.h>
#include <float.h>
#include <vector>
#include <string.h>
#include <math.h>

using namespace std;

struct Node {
    int x;
    int y;
};

void solve();
template<typename T>
void print_mat(T** mat, int len1, int len2);


int main(int argc, char** argv) {
    clock_t start, end;
    start = clock();
    solve();
    end = clock();
    cout << "time cost: " << (end-start)*1000./CLOCKS_PER_SEC << " ms\n";

    return 0;
}

void solve() {
    vector<Node> nodes{
        {1,1},
        {2,7},
        {3,4},
        {6,3},
        {7,6},
        {8,2},
        {9,5},
    };
    // dist是各点间距离,只使用右上
    double** dist = new double*[nodes.size()];
    // cost左下是距离代价,右上记录当前路径的上一步的路径(节点)
    double** cost = new double*[nodes.size()];
    for (int i = 0; i < nodes.size(); ++i) {
        dist[i] = new double[nodes.size()]{0};
        cost[i] = new double[nodes.size()]{0};
    }
    for (int i = 0; i < nodes.size(); ++i) {
        for (int j = 0; j < i; ++j) {
            dist[j][i] = sqrt(double((nodes[i].x-nodes[j].x)*(nodes[i].x-nodes[j].x)
                        + (nodes[i].y-nodes[j].y)*(nodes[i].y-nodes[j].y)));
        }
    }
    cost[0][0] = 0;
    cost[1][0] = dist[0][1];
    for (int i = 2; i < nodes.size() - 1; ++i) {
        for (int j = 0; j < i; ++j) {
            if (j == i-1) {
                cost[i][j] = DBL_MAX;
                double c;
                for (int k = 0; k < j; ++k) {
                    c = cost[j][k] + dist[k][i];
                    if (c < cost[i][j]) {
                        cost[i][j] = c;
                        cost[j][i] = k;
                    }
                }
            } else {
                cost[i][j] = cost[i-1][j] + dist[i-1][i];
                cost[j][i] = i;
            }
        }
    }
    // 最右侧点路径计算(N为点的个数,下标从0开始)
    // cost[N-1][N-1] = cost[N-2][k] + dist[N-2][N-1] + dist[k][N-1] (N=nodes.size())
    // 与最右侧点相连的点其中一个是倒数第二个点,即nodes[N-2],另一个点设为n,此处确定n的值
    int n = -1;
    cost[nodes.size()-1][nodes.size()-1] = DBL_MAX;
    for (int k = 0; k < nodes.size() - 2; ++k) {
        double c = cost[nodes.size()-2][k] + dist[nodes.size()-2][nodes.size()-1]
            + dist[k][nodes.size()-1];
        if (c < cost[nodes.size()-1][nodes.size()-1]) {
            cost[nodes.size()-1][nodes.size()-1] = c;
            n = k;
        }
    }
    cout << "cost: \n";
    print_mat(cost, nodes.size(), nodes.size());
    cout << "dist: \n";
    print_mat(dist, nodes.size(), nodes.size());
    cout << n << endl;      // 倒数第二步的路径(节点)

    // 输出路径
    int i = nodes.size() - 2, j = n, k;
    cout << i << " -> " << nodes.size() - 1 << endl;
    cout << j << " -> " << nodes.size() - 1 << endl;
    while (i) {
        k = cost[j][i];
        if (k == i) {
            --i;
            cout << i << " -> " << k << endl;
        } else {
            cout << k << " -> " << i << endl;
            i = j;
            j = k;
        }
    }

    for (int i = 0; i < nodes.size(); ++i) {
        delete[] dist[i];
        delete[] cost[i];
    }
    delete[] dist;
    delete[] cost;
}

template<typename T>
void print_mat(T** mat, int len1, int len2) {
    for (int i = 0; i < len1; ++i) {
        for (int j = 0; j < len2; ++j) {
            cout << mat[i][j] << "\t";
        }
        cout << endl;
    }
}

测试:

cost:
0	0	2	3	4	5	0
6.08276	0	0	3	4	5	0
9.24504	9.68831	0	0	4	5	0
12.4073	12.8506	14.6302	0	1	5	0
15.5696	16.0129	17.7925	17.9496	0	3	0
19.6927	20.136	21.9156	22.0727	20.1857	0	0
0	0	0	0	0	0	25.584
dist:
0	6.08276	3.60555	5.38516	7.81025	7.07107	8.94427
0	0	3.16228	5.65685	5.09902	7.81025	7.28011
0	0	0	3.16228	4.47214	5.38516	6.08276
0	0	0	0	3.16228	2.23607	3.60555
0	0	0	0	0	4.12311	2.23607
0	0	0	0	0	0	3.16228
0	0	0	0	0	0	0
4
5 -> 6
4 -> 6
3 -> 5
1 -> 4
2 -> 3
0 -> 2
0 -> 1
time cost: 0.114 ms
posted @ 2023-08-13 17:44  keep-minding  阅读(35)  评论(0编辑  收藏  举报