中国邮递员问题是一个和旅行商问题比较相关但又不太相同的一个问题,而且个人感觉理解的难度更大一点,当然,这就是仁者见仁,智者见智了,旅行商问题是不能回头的,一个节点访问过了不能回来了,并不需要走完所有的路,但是中国邮递员问题可以多次访问一个节点,因为中国邮递员问题要求的是要访问所有的街道,即,每条街道必须访问到。在这样的前提下,如果规划路径使得返回邮局时路程最短。
试想,假设每条路都走一次,最终恰好还能返回出发点,这样的拓扑图画出来应该满足怎样的结构呢?每个节点要进去还要出去,所以,每个点的入度和出度必须相等(起始点也一样),也就是说,每个节点的度应该是偶数个,满足这样的拓扑结构的图就是欧拉回路。如下图所示:
每个节点入度出度一定相同,所以总的度就是偶数。
但是,在实际生活中,或者实际拓扑中,节点的度很可能不是奇数个,因此,这也是解决中国邮递员问题最重要的一步,构造欧拉回路。
一个拓扑结构可以构造出很多个欧拉回路,但是,我们这道题要求的最短的路径,而原有的路径一定都是,所以,构造欧拉回路的边决定了最终的结果。
基于上述理解,判断是否为欧拉回路,如果是,直接相加返回结果,如果不是,那存在奇数度的节点一定有偶数个,如何选择更好的构造方案也成为了解决该问题第二个重要的点。
构造欧拉回路:
如上图所示,V8,V2,V4,V6都是奇数度点,因此,将这些点进行标记,来构造欧拉回路。
如下图所示,各种方案:
这几种方案都是构造欧拉回路的方案,但是,相对比会发现,只有最后一个是最优的结果,那如何得到最优结果呢,其实需要以下步骤来得到最优结果:
1、根据统计每个节点度的数目,标记出奇数度的节点。
2、奇数节点一定是有偶数个,因此,最终的距离应该是:两两点之间的最短距离,而两两点之间的最短距离可以用Floyd或者Dijkstra或者bellman-ford算法来得到。个人建议使用Floyd算法,不容易出错。
3、遍历所有的组合情况,求出最短的组合方式,例如比较 d(2,8)+d(4,6),d(2,6)+d(4,8), d(2,4)+d(6,8), 然后取最小值即为最终的优化方案:d(2,8)+d(4,6)。求解该步骤的时候,可以使用DFS来寻找最短路径方案,也可以利用DFS的思想,改为动态规划来实现,在别的博客上看到状压dp的字眼,应该是说这块的东西。动态规划思路的代码相对没有DFS更符合人们的思路,答题思路是构造一个dp表,行值为1,列值表示该集合中所含的元素,例如:集合7表示所含元素在奇度数组中下标为{1, 2, 3}的集合,即为二进制表示下,哪个位置为1,就含有对应的点,在这种方法下,为了方便操作,一般在邮递员问题中,点的编号是从1开始的,而求解的顺序应该是先求解小集合,再求解大集合,举个例子:假设现在得到的奇数度点的数组为:[2,4,6,8](为了代码方便,实际上该数组在后续代码的实现上是[0,2,4,6,8]),一共有四个有效点,则dp数组应为1×16的规模,dp[0]表示一个点也没有的集合,空集自然距离也为0,而更大集合j的最短距离值应该由更小的集合i及不在i中的两个点x,y求得,举个例子,当求出集合3({2, 4})的最短距离时,记录在dp[3]中,而集合15({2,4,6,8})的最短距离可以用dp[3]+d[6][8]来完成更新,如果dp[3]+d[6][8]<dp[15],则更新dp[15]。同理,当求出集合5({2,6})的最短距离时,记录在dp[5]中,则可以用dp[5]+d[4][8]<dp[15],则更新dp[15],该动态规划算法通过利用小数据集更新大数据集的距离,最终返回dp的最后一个值作为最优路径的结果。
最终的方案结果是:原图中的所有路径+构造时新添加的路径。
方案步骤:
1、生成邻接矩阵,利用Floyd算法求出每两个点之间的最短距离。(Floyd)
2、判断整个过程是否是欧拉回路,如果不是构造欧拉回路。
3、利用动态规划或者DFS计算构造欧拉回路的最优方案。
4、计算原路径与新构造路径的长度总和即为最终结果。
以下是参考代码:
步骤一:Floyd
void Floyd (vector<vector<int>>& graph, int N) {
for (int k=1; k<=N; ++k) {
for (int i=1; i<=N; ++i) {
for (int j=1; j<=N; ++j) {
if (i == j) {
continue;
}
if (graph[i][k]!=-1 && graph[k][j]!=-1) {
graph[i][j] = graph[i][j]==-1 ? graph[i][k]+graph[k][j] : min (graph[i][j], graph[i][k]+graph[k][j]);
}
}
}
}
}
这里要说明,我的程序中,-1表示路不通,所以加了一些判断条件,看上去比较复杂,其实也可以将数值定义成比较大的值,代码就比较简单,但是那样做有时候有越界危险。
步骤二:
int shortestPath (vector<vector<int>>& graph, vector<int>& dev, int N) {
Floyd (graph, N);
vector<int> odds;
odds.push_back(0);
for (int i=1; i<=N; ++i) {
if (dev[i]&1) {
odds.push_back(i);
}
}
int res = 0;
int ods = odds.size()-1;
vector<int> dp((1<<ods), -1);
dp[0] = 0;
for (int i=0; i<(1<<ods); ++i) {
int x = 1;
while ((1<<(x-1)) & i) {
++x;
}
for (int y=x+1; y<=ods; ++y) {
if ((1<<(y-1)) & i) {
continue;
}
dp[i|(1<<(x-1))|(1<<(y-1))] = dp[i] != -1 && graph[odds[x]][odds[y]] != -1 ? dp[i|(1<<(x-1))|(1<<(y-1))] == -1 ? dp[i]+graph[odds[x]][odds[y]] : min(dp[i|(1<<(x-1))|(1<<(y-1))], dp[i]+graph[odds[x]][odds[y]]) : dp[i|(1<<(x-1))|(1<<(y-1))];
}
}
for (int i=0; i<(1<<ods); ++i) {
cout << dp[i] << " ";
}
cout << endl;
cout << dp[(1<<ods)-1] << endl;
return dp[(1<<ods)-1];
}
该程序中,graph是已经求得的最短路径结果,数组dev记录的是所有节点的度,我们在下面的程序中,选择奇数度的节点组成odds数组,而后构建dp数组,通过小的集合i,以及不在i中的两点x和y计算更大的集合j的距离,通过多次迭代求出最优结果,因为我的dp初始化都是-1,也就是说-1在程序中的含义表示无限大,不可行的意思,所以要加一些比较细节的判断。
以下是整个程序的代码:
#include <bits/stdc++.h>
using namespace std;
class Solution {
private:
void Floyd (vector<vector<int>>& graph, int N) {
for (int k=1; k<=N; ++k) {
for (int i=1; i<=N; ++i) {
for (int j=1; j<=N; ++j) {
if (i == j) {
continue;
}
if (graph[i][k]!=-1 && graph[k][j]!=-1) {
graph[i][j] = graph[i][j]==-1 ? graph[i][k]+graph[k][j] : min (graph[i][j], graph[i][k]+graph[k][j]);
}
}
}
}
}
public:
int shortestPath (vector<vector<int>>& graph, vector<int>& dev, int N) {
Floyd (graph, N);
vector<int> odds;
odds.push_back(0);
for (int i=1; i<=N; ++i) {
if (dev[i]&1) {
odds.push_back(i);
}
}
int res = 0;
int ods = odds.size()-1;
vector<int> dp((1<<ods), -1);
dp[0] = 0;
for (int i=0; i<(1<<ods); ++i) {
int x = 1;
while ((1<<(x-1)) & i) {
++x;
}
for (int y=x+1; y<=ods; ++y) {
if ((1<<(y-1)) & i) {
continue;
}
dp[i|(1<<(x-1))|(1<<(y-1))] = dp[i] != -1 && graph[odds[x]][odds[y]] != -1 ? dp[i|(1<<(x-1))|(1<<(y-1))] == -1 ? dp[i]+graph[odds[x]][odds[y]] : min(dp[i|(1<<(x-1))|(1<<(y-1))], dp[i]+graph[odds[x]][odds[y]]) : dp[i|(1<<(x-1))|(1<<(y-1))];
}
}
return dp[(1<<ods)-1];
}
};
int main()
{
int N = 4, R = 5;
//cin >> N >> R;
//vector<vector<int>> routes(R, vector<int> (3, 0));
vector<vector<int>> graph (N+1, vector<int> (N+1, -1));
vector<int> dev (N+1, 0);
int res = 0;
//vector<vector<int>> routes(R, vector<int> (3, 0));
vector<vector<int>> routes = {{1, 2, 3}, {2, 3, 4}, {3, 4, 5}, {1, 4, 10}, {1, 3, 12}};
for (int i=0; i<R; ++i) {
int x = routes[i][0], y = routes[i][1], z = routes[i][2];
//cin >> x >> y >> z;
graph[x][y] = z;
graph[y][x] = z;
++dev[x]; ++dev[y];
res += z;
}
cout << res << endl;
Solution solve;
res += solve.shortestPath (graph, dev, N);
return 0;
}