中国邮路问题编程求解
中国邮路问题(Chinese Postman Problem)是一个非常经典的图论问题:一个邮递员送信,要走完他负责投递的全部街道(所有街道都是双向通行的且每条街道可以经过不止一次),完成任务后回到邮局,应按怎样的路线走,他所走的路程才会最短呢?如果将这个问题抽象成图论的语言,就是给定一个连通图,每条边的权值就是街道的长度,本问题转化为在图中求一条回路,使得回路的总权值最小。
如果街道的连通图为欧拉图,则只要求出图中的一条欧拉回路即可。否则,邮递员要完成任务就必须在某些街道上重复走若干次。如果重复走一次,就加一条平行边,于是原来对应的图形就变成了多重图。只是要求加进的平行边的总权值最小就行了。于是,我们的问题就转化为,在一个有奇度数结点的赋权连通图中,增加一些平行边,使得新图不含奇度数结点,并且增加的边的总权值最小。
求所增加边总权值最小的方案,需要我们找出所有奇度顶点(偶数个)来两两分组,对每小组中的两个点求其最短路径,进而求出每分组的总权值。对所有分组情况,找出最小权值即是最佳方案。
下面直接上源代码。
#include <limits.h> #include <iostream> #include <cstdlib> #include <set> #include <vector> using namespace std; #define MAX_NODE 26 // 最大结点数 #define COST_NO_LINK INT_MAX // 定义结点之间没有连接的花销为INT_MAX吗 int Graph[MAX_NODE][MAX_NODE]; int Cost[MAX_NODE][MAX_NODE]; int V_dingdianshu, E_bianshu, Start_Point; // 顶点数和边数,以及开始的起点(以0开始) int Odd_Grouping[MAX_NODE]; // 为0表示不为奇,为1表示为奇,从2开始表示配对分组情况,如同为2的两个为一组,同为3的两个为一组,…… int Bak_Odd_Grouping[MAX_NODE]; // 最好情况下分组策略的备份,因为可能还有其他情况更好,如果有,就更新此备份。 int SHORTEST_PATH_WEIGHT(COST_NO_LINK); // 如果存在奇度数点,这里是记录的添加最短路径的最小值,是所有点两两分组的最短路径之和的最小值。此值对应Bak_Odd_Grouping所描述的分组情况。 int Dist[MAX_NODE]; // Dijstra算法中,求从v0到v1最短路径结果,里面包含v0到最短路径上各点的最短权值 int ShortCache[MAX_NODE][MAX_NODE]; // Dijstra算法中,求点v0到v1的最短路径记录值,当第一次求时,把结果存到本数组中,下次如果还在相同调用,则直接返回本数组中相应值。 // 数据输入,会用到Graph和Cost void Input() { int i, j; int m, n; char cs, cm, cn; int w; cout << "输入图的顶点数:"; cin >> V_dingdianshu; cout << "输入图边的数目:"; cin >> E_bianshu; cout << "输入起点:"; cin >> cs; Start_Point = cs - 'a'; for(i = 0; i < V_dingdianshu; i++) { for(j = 0; j < V_dingdianshu; j++) { Graph[i][j] = 0; ShortCache[i][j] = 0; Cost[i][j] = COST_NO_LINK; } Cost[i][i] = 0; // 置自己到自己为0 } cout << "输入" << E_bianshu << "条边对应的顶点和权值(顶点从a开始编号):" << endl; for(i = 0; i < E_bianshu; i++) { cin >> cm >> cn >> w; m = cm - 'a'; n = cn - 'a'; Graph[m][n] += 1; Graph[n][m] += 1; Cost[m][n] = w; Cost[n][m] = w; } } // 对图G从v0点开始到v1点算到必要各点的最短距离,结果保存在dist上面。因此不保证dist上的所有数据都是正确的(保证dist[v]是从v0到v的最短距离) // 第三个参数指定是否使用Cache值,如果为真,则一般Dist的值不与当前调用相对应,反之则保证dist[v]是从v0到v的最短距离 // 返回dist[v1],即v0到v1的最短距离值 int Dijstra(int v0, int v1, bool useCache) { if(useCache && ShortCache[v0][v1] != 0) // 之前计算过了,直接返回值 { return ShortCache[v0][v1]; } int i, s, w, min, minIndex; bool Final[MAX_NODE]; // 初始化最短路径长度数据,所有数据都不是最终数据 for (s = 0; s < V_dingdianshu; s++) { Final[s] = false; Dist[s] = COST_NO_LINK; // 初始最大距离 } // 首先选v0到v0的距离一定最短,最终数据 Final[v0] = true; Dist[v0] = 0; s = v0; // 0 预先选中v0点 for (i = 0; i < V_dingdianshu; i++) { // 1 更新该点到其他未选中点的最短路径 for(w = 0; w < V_dingdianshu; w++) { if(!Final[w] // w点未选中 && Cost[s][w] < COST_NO_LINK // 更新点应该与选中点s相连 && Dist[w] > Dist[s] + Cost[s][w]) // 通过点s会有更短的路径 { if(Dist[s] + Cost[s][w] <= 0) { cout << "求最短路径数据溢出。" << endl; exit(-1); } Dist[w] = Dist[s] + Cost[s][w]; } } // 1.5 如果在中间过程找到了目标点v1,则不再继续计算了 if(s == v1) { ShortCache[v0][v1] = Dist[s]; ShortCache[v1][v0] = Dist[s]; return Dist[s]; } // 2 选中相应点 min = COST_NO_LINK; for(w = 0; w < V_dingdianshu; w++) { if(!Final[w] // 未选中 && Dist[w] < min) // 值更小 { minIndex = w; min = Dist[w]; } } s = minIndex; Final[s] = true; } cerr << "程序异常。。。应该早找到了最短路径的" << endl; exit(-1); } // 图的连通性测试 // 参数start用于指定从哪个点开始找(索引从0开始),这样在一定程序上可以提高程序效率 // 空图返回真 // 这里对start功能的定义还应该加上:start点一定要在连通图上 bool ConnectivityTest(int start, bool& bNoPoints) { set<int> nodeSet; // 连通顶点集 vector<int> for_test_nodes; // 与新加入连通点连通的未加入点集 int i, j; set<int> singlePoints; // 图中的单点集 // 先找出单点 bool hasEdge = false; for(i = 0; i < V_dingdianshu; i++) { hasEdge = false; for(j = 0; j < V_dingdianshu; j++) // 这里起始应该是0,不然最后一个点如果是单点则无法判断 { if (Graph[i][j] > 0) { hasEdge = true; break; } } if (!hasEdge) { singlePoints.insert(i); } } bNoPoints = (singlePoints.size() == V_dingdianshu); // 设置bNoPoints标志 if(singlePoints.find(start) != singlePoints.end()) // start点必须在连通图中 { return false; } for_test_nodes.push_back(start); // while(for_test_nodes.size() > 0) { int testNode = for_test_nodes.back(); for_test_nodes.pop_back(); for(i = 0; i < V_dingdianshu; i++) { if(Graph[testNode][i] > 0) { if(nodeSet.insert(i).second) { for_test_nodes.push_back(i); } } } } for(i = 0; i < V_dingdianshu; i++) { if (singlePoints.find(i) == singlePoints.end() && nodeSet.find(i) == nodeSet.end()) // 存在点既不是单点,也不在当前连通顶点集中,则这个点一定在其他连通子图中,返回假 { return false; } } return true; } // 测试图中是否有度为奇的顶点,结果保存在中,返回奇度顶点数 int OddTest() { int i, j, rSum, count; // 初始化 for(i = 0; i < V_dingdianshu; i++) { Odd_Grouping[i] = 0; // 0表示不为奇 Bak_Odd_Grouping[i] = 0; } count = 0; for(i = 0; i < V_dingdianshu; i++) { rSum = 0; for(j = 0; j < V_dingdianshu; j++) { rSum += Graph[i][j]; // 求i行和 } if(rSum % 2 == 1) { Odd_Grouping[i] = 1; count++; } } return count; } void Bak_Grouping() { int i; for(i = 0; i < V_dingdianshu; i++) { Bak_Odd_Grouping[i] = Odd_Grouping[i]; } } // 对奇度顶点进行分组,level值从2开始取值。 // 返回值表示当前这种分组是否是当前所找到中的最好分组。本程序中没有采用其返回值。 bool Grouping(int level) { if(level < 2) { cerr << "小于2的level值是不允许的。" << endl; exit(-1); } int i, j, findI = -1; for(i = 0; i < V_dingdianshu; i++) { if(Odd_Grouping[i] == 1) { Odd_Grouping[i] = level; // 找到第一个组合点。 findI = i; break; } } bool re = true; if(findI == -1) // 这里是形成一对新的组合后的地方,此时应该计算各组合最小路径之和。 { int weightSum = 0; for(i = 2; i < level; i++) // 根据level的值可以知道分组的取值是从2到level-1的,所以i如是计数 { int index[2]; int *pIndex = index; for(j = 0; j < V_dingdianshu; j++) { if(Odd_Grouping[j] == i) { *pIndex = j; if(pIndex == index + 1) // 设置了第二个index值 { break; } pIndex++; } } weightSum += Dijstra(index[0], index[1], true); // 这里暂时只计算最短路权值和,不实际上添加边,最后才添加。这样加边计算只会调用一次。 } if(weightSum < SHORTEST_PATH_WEIGHT) // 当前组合比以往要优,将当前的排列组合情况更新到全局 { Bak_Grouping(); // 如果当前分组比以往都好,备份一下 SHORTEST_PATH_WEIGHT = weightSum; return true; // 找到了更优组合,返回递归调用为真 } else { return false; // 没找到了更优组合,返回递归调用为假 } } else if(findI > -1) { // 上面找到了第一个点了,现在从上面继续找第二个点。 for(/* 继续上面的for */; i < V_dingdianshu; i++) { if(Odd_Grouping[i] == 1) // 找到第二个点 { Odd_Grouping[i] = level; re = Grouping(level + 1); Odd_Grouping[i] = 1; // 无论当前分组是不是当前最好分组,我们都还要继续查找剩余分组情况 } } } else { cerr << "findCount值异常" << endl; exit(-1); } if(findI > -1) { Odd_Grouping[findI] = 1; // 无论当前分组是不是最好分组,我们都还要继续查找剩余分组情况 } return re; } void AddShortPath(int from, int to) { int i, back; Dijstra(from, to, false); // 求最短路径,结果在dist数组中 back = to; while(back != from) // from ... back ... to { for(i = 0; i < V_dingdianshu; i++) { if(i != back && Dist[i] < COST_NO_LINK // from有边到i && Dist[back] < COST_NO_LINK // from有边到back && Dist[i] + Cost[i][back] == Dist[back]) // from通过中继点i再到back的长度恰好等于from到back的长度,即证明点i在最短路径上(注,这里如果(i,back)没有边连接,那么Dist[i] + Cost[i][back]一定为负数) { Graph[i][back]++; // 添加一条边 Graph[back][i]++; back = i; break; } } if(i == V_dingdianshu) // 编程常识:这里break后不会再执行++ { cerr << "程序异常,最短路径出问题了。。。" << endl; exit(-1); } } } // 根据odd数组的分组情况添加最短路径 void AddShortPaths() { int i, j; for(i = 0; i < V_dingdianshu; i++) { if(Bak_Odd_Grouping[i] > 1) { for(j = i + 1; j < V_dingdianshu; j++) { if(Bak_Odd_Grouping[j] == Bak_Odd_Grouping[i]) { AddShortPath(i, j); break; } } } } } // 处理图中可能存在度为奇的情况 void OddDeal() { // 判断是否存在为奇的点,有的话要处理 int oddCount = OddTest(); if(oddCount > 0) { if(oddCount % 2 == 1) { cerr << "这是一个奇怪的图,存在奇数个奇度顶点的连通图吗?" << endl; exit(-1); } // 对为奇的点进行排列组合。。。 Grouping(2); // 这里得到的odd2是最优的 AddShortPaths(); // 根据odd数组添加最短路径 } } /* 用Fleury算法求最短欧拉回游 假设迹wi=v0e1v1…eivi已经选定,那么按下述方法从E-{e1,e2,…,ei}中选取边ei+1: 1)、 ei+1与vi+1相关联; 2)、除非没有别的边可选择,否则 ei+1不能是Gi=G-{e1,e2,…,ei}的割边。 3)、 当(2)不能执行时,算法停止。 */ void Fleury(int start) { int i; int vi = start; // v0e1v1…eivi已经选定 bool bNoPoints, bCnecTest; cout << "你要的结果:"; while(true) { // 找一条不是割边的边ei+1 for(i = 0; i < V_dingdianshu; i++) { if (Graph[vi][i] > 0) { // 假设选定(vi,i)这条边 Graph[vi][i]--; // 这里会破坏全局Graph的值,但暂时没影响了,都不用了。 Graph[i][vi]--; bCnecTest = ConnectivityTest(i, bNoPoints); if(!bNoPoints && !bCnecTest) // 这里一定要传i,这是欲选择边的末端,它应该在连通图中 { Graph[vi][i]++; Graph[i][vi]++; continue; } // 选定(vi,i)这条边 cout << (char)('a' + vi) << "-" << (char)('a' + i) << " "; vi = i; break; } } if (i == V_dingdianshu) { cout << endl; break; // 这里应该是说边找完了 } } } int main() { Input(); bool b; if(!ConnectivityTest(0, b)) // b是无用变量,这里不看。 { cout << "该图不是连通图!\n"; exit(0); } OddDeal(); // 处理可能的奇度点情况 Fleury(Start_Point); // 这里应该用这个算法求欧拉回游 return 0; }