- 实验目的和要求
- 实验目的:
- 理解什么是欧拉图,熟悉欧拉路和欧拉回路的概念。
- 掌握Dijkstra算法,求解最短路径
- 掌握Fleury算法,求解欧拉回路。
- 了解Edmonds-Johnson算法解决中国邮递员问题的基本思路。
- 通过程序实现中国邮递员问题,强化其基本思想和实际应用。
-
- 实验要求:
- 针对下图所示加权图G,给出中国邮递员问题的解决方案。
- 用流程图简述解决中国邮递员问题的流程。
- 对核心算法(如Dijkstra算法、Fleury算法)进行编程实现。
- 分析实验结果,验证其正确性。
- 总结实验,撰写实验心得。
图G
- 实验环境和工具
- 编程语言:
C++
-
- 编程环境(编译器):
Visual Studio 2019
- 实验结果
- 算法流程图
解题思路:
- 求出图G中奇数度顶点集合
- 若奇数度顶点个数为0<=>图G为欧拉图,直接使用Fleury算法求出欧拉回路,即最优邮路。
- 若奇数度顶点个数为2n(n=1,2,3......)<=>图G不是欧拉图,需添加一些重边。
- 用Dijkstra算法求出奇数度顶点两两之间的最短距离。
- 将奇数度顶点两两分组,遍历所有组合情况,根据已经求出的最短距离,找出最短的组合方式,即最优分组。
- 根据最优分组情况为图G添加重复边。
- 对添加重复边之后得到的图G’利用Fleury算法,求出欧拉回路,即此时的最优邮路。
流程图:
整体算法流程图
Fleury算法流程图
- 程序核心代码
判断是否为连通图:bool ConnectivityTest(int start, bool& bNoPoints){set<int> nodeSet; // 连通顶点集vector<int> test_nodes; // 与新加入连通点连通的未加入点集set<int> singlePoints; // 图中的单点集int i, j;// 先找出单点bool hasEdge = false;for (i = 0; i < V; i++){hasEdge = false;// 这里起始应该是0,不然最后一个点如果是单点则无法判断for (j = 0; j < V; j++) {if (Graph[i][j] > 0){hasEdge = true;break;}}if (!hasEdge){singlePoints.insert(i);}}// 设置bNoPoints标志bNoPoints = (singlePoints.size() == V);// start点必须在连通图中if (singlePoints.find(start) != singlePoints.end()) {return false;}test_nodes.push_back(start);while (test_nodes.size() > 0){int testNode = test_nodes.back();test_nodes.pop_back();for (i = 0; i < V; i++){if (Graph[testNode][i] > 0){if (nodeSet.insert(i).second){test_nodes.push_back(i);}}}}for (i = 0; i < V; i++){// 存在点既不是单点,也不在当前连通顶点集中,则这个点一定在其他连通子图中,返回假if (singlePoints.find(i) == singlePoints.end()&& nodeSet.find(i) == nodeSet.end()){return false;}}return true;}Dijkstra算法求最短距离int Dijstra(int v0, int v1, bool useCache){// 之前计算过了,直接返回值if (useCache && Cache[v0][v1] != 0) {return Cache[v0][v1];}int i, s, w, min, minIndex;bool Visited[MAX_NODE];//判断顶点是否被访问过// 初始化最短路径长度数据,所有数据都不是最终数据for (s = 0; s < V; s++){Visited[s] = false;Dist[s] = COST_NO_LINK; // 初始最大距离}// 首先选v0到v0的距离一定最短,最终数据Visited[v0] = true;Dist[v0] = 0;s = v0; // 0 预先选中v0点for (i = 0; i < V; i++){//更新该点到其他未选中点的最短路径for (w = 0; w < V; w++){if (!Visited[w] && Cost[s][w] < COST_NO_LINK&& Dist[w] > Dist[s] + Cost[s][w]){Dist[w] = Dist[s] + Cost[s][w];}}//如果在中间过程找到了目标点v1,则不再继续计算了if (s == v1){Cache[v0][v1] = Dist[s];Cache[v1][v0] = Dist[s];return Dist[s];}//选中相应点min = COST_NO_LINK;for (w = 0; w < V; w++){if (!Visited[w] && Dist[w] < min){minIndex = w;min = Dist[w];}}s = minIndex;Visited[s] = true;}}找出最优分组bool Grouping(int level){int i, j, findI = -1;for (i = 0; i < V; i++){if (Odd_Group[i] == 1){Odd_Group[i] = level; // 找到第一个组合点。findI = i;break;}}bool re = true;// 这里是形成一对新的组合后的地方,此时应该计算各组合最小路径之和。if (findI == -1) {int weightSum = 0;// 根据level的值可以知道分组的取值是从2到level-1的,所以i如是计数for (i = 2; i < level; i++) {int index[2];int* pIndex = index;for (j = 0; j < V; j++){if (Odd_Group[j] == i){*pIndex = j;// 设置了第二个index值if (pIndex == index + 1) {break;}pIndex++;}}weightSum += Dijstra(index[0], index[1], true); // 这里暂时只计算最短路权值和,不实际上添加边,最后才添加。这样加边计算只会调用一次。}// 当前组合比以往要优,将当前的排列组合情况更新到全局if (weightSum < Shortest_Path_Weight) {Best_Grouping(); // 如果当前分组比以往都好,备份一下Shortest_Path_Weight = weightSum;return true; // 找到了更优组合,返回递归调用为真}else{return false; // 没找到了更优组合,返回递归调用为假}}else if (findI > -1){// 上面找到了第一个点了,现在从上面继续找第二个点。for (/* 继续上面的for */; i < V; i++){// 找到第二个点if (Odd_Group[i] == 1) {Odd_Group[i] = level;re = Grouping(level + 1);Odd_Group[i] = 1; // 无论当前分组是不是当前最好分组,我们都还要继续查找剩余分组情况}}}else{cerr << "findCount值异常" << endl;exit(-1);}if (findI > -1){Odd_Group[findI] = 1; // 无论当前分组是不是最好分组,我们都还要继续查找剩余分组情况}return re;}Fleury算法求欧拉回路void Fleury(int start) {int i;int vi = start; // v0e1v1…eivi已经选定bool bNoPoints, bCnecTest;cout << "你要的结果:";while (true) {// 找一条不是割边的边ei+1for (i = 0; i < V; i++) {if (Graph[vi][i] > 0) {// 假设选定(vi,i)这条边Graph[vi][i]--; // 这里会破坏全局Graph的值,但暂时没影响了,都不用了。Graph[i][vi]--;bCnecTest = ConnectivityTest(i, bNoPoints);if (!bNoPoints && !bCnecTest) {Graph[vi][i]++;Graph[i][vi]++;continue;}// 选定(vi,i)这条边cout << (char)('a' + vi) << "->" << (char)('a' + i) << " ";sumWeight += Cost[vi][i];vi = i;break;}}if (i == V) {cout << endl;break; // 到这里边找完了}}}
-
- 运行结果
程序运行结构图
-
- 运行结果分析
假设权重为4的边长度为4个单位长度,其余以此类推。
由上述运行结果可得出图G的最短邮路长度为104个单位长度。
添加的重复边有5条:
- -b 2 a--e 5 c--d 5
d--h 4 j--k 3
添加重复边的总权重为19。
为了验证结果的正确性(也就是所添加重复边的正确性),我采用书本第239页上给出的定义来推导。
- 首先添加重边去除奇数度顶点。
图1
- 删除偶数条重边。
图2
- 调整回路中的权重问题,调整单边和重边。
- 重复边集E的长度之和不超过这个圈的长度的一半
- 图中没有二重以上的边
依据以上两点要求对图进行调整:
图3
图4
图5
可以发现图5已经满足了最优的两点条件,因此图5就是我们需要构造的欧拉图。
从图中可以得到最优情况添加的重复边为:
- -b 2 a--e 5 c--d 5
d--h 4 j--k 3
添加重复边的总权重是19。
图5的最优邮路的长度为104个单位长度。
此结果与我实验得到的结果是完全一致的,一次这次实验的答案是准确无误的。
- 实验心得
本次实验,我结合老师上课所教的内容与自己课后在网上学习到的中国邮递员问题的相关内容以及算法,完成了此次实验,通过程序实现了中国邮递员问题,同时也强化了我对于中国邮递员问题基本思想的理解与实际应用的能力。
通过此次实验,让我加深了对于欧拉图的理解,熟悉了欧拉路和欧拉回路的基本概念,掌握了使用Dijkstra算法求解最短路径的方法,学习了使用Fleury算法求解欧拉回路的方法,同时我也学习并且了解了Edmonds-Johnson算法解决中国邮递员问题的基本思路。
此次实验,我解决中国邮递员问题的主要思路是Edmonds-Johnson算法的思路,但是我对其中的一些步骤进行了调整,Edmonds-Johnson算法在求解完最短路径之后是利用求解完全图,然后再在完全图中找出权值最小的完备匹配。我在这次实验中采用的是将奇数度顶点两两分组,遍历所有组合情况,根据已经求出的最短距离,找出最短的组合方式,即最优分组。根据最优分组情况为图G添加重复边。最后对添加重复边之后得到的图G’利用Fleury算法,求出欧拉回路,即此时的最优邮路。
经过本次实验我深刻的体会到中国邮递员算法是一个十分有意义的算法,它可以解决我们实际生活中的问题,通过这次实验对中国邮递员问题的解决,让我学习到了什么是中国邮递员算法,也让我学习了如何编程解决这个问题,求解出最后的最优邮路。还让我在课本上学习到的欧拉图等内容得到了巩固,并运用到了实际的算法问题之中。
在实验中我也遇到了许多的问题,就比如有的算法思路虽然简单,但是实现起来还是需要琢磨许久,并且还需要在网上或者课本上找寻相关的内容才能最后将其完成,虽然解决问题的过程是艰辛的,是需要花费时间的,但是我认为这是值得的,最后解决了问题之后也会让我觉得无比的喜悦。
最后我想说,这次实验带给我的收获是巨大的,不仅使我编写代码的能力得到了提升,同时也让我学习到了许多有用的算法,使我的知识得到了丰富。因此,在我看来这次实验对于我而言是十分有意义的。
- 源代码
#include <iostream>#include <cstdlib>//包含多种宏和常数值#include <set>#include <vector>#define MAX_NODE 100//最大结点数#define COST_NO_LINK INT_MAX//顶点之间没有连接的权值using namespace std;int Graph[MAX_NODE][MAX_NODE];//图int Cost[MAX_NODE][MAX_NODE];//权重int V, E, SP;//顶点数,边数,起始点int Odd_Group[MAX_NODE];//图的奇偶顶点情况int Best_Group[MAX_NODE];//保存当前最优分组策略int Shortest_Path_Weight(COST_NO_LINK);//添加边的最小权值int Dist[MAX_NODE];//求从v0到v1最短路径结果,里面包含v0到最短路径上各点的最短权值int Cache[MAX_NODE][MAX_NODE];//记录已经求过的最短路径值int sumWeight = 0;//记录最短路径长度//输入图的信息void Input() {int i, j;int m, n;char cs, cm, cn;int w;cout << "输入图的顶点数:";cin >> V;cout << "输入图边的数目:";cin >> E;cout << "输入起点:";cin >> cs;SP = cs - 'a';for (i = 0; i < V; i++){for (j = 0; j < V; j++){Graph[i][j] = 0;Cache[i][j] = 0;Cost[i][j] = COST_NO_LINK;}Cost[i][i] = 0; // 置自己到自己为0}cout << "输入" << E << "条边对应的顶点和权值(顶点从a开始编号):" << endl;for (i = 0; i < E; 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;}}//Dijstra算法求最短距离int Dijstra(int v0, int v1, bool useCache){// 之前计算过了,直接返回值if (useCache && Cache[v0][v1] != 0) {return Cache[v0][v1];}int i, s, w, min, minIndex;bool Visited[MAX_NODE];//判断顶点是否被访问过// 初始化最短路径长度数据,所有数据都不是最终数据for (s = 0; s < V; s++){Visited[s] = false;Dist[s] = COST_NO_LINK; // 初始最大距离}// 首先选v0到v0的距离一定最短,最终数据Visited[v0] = true;Dist[v0] = 0;s = v0; // 0 预先选中v0点for (i = 0; i < V; i++){//更新该点到其他未选中点的最短路径for (w = 0; w < V; w++){if (!Visited[w] && Cost[s][w] < COST_NO_LINK&& Dist[w] > Dist[s] + Cost[s][w]){Dist[w] = Dist[s] + Cost[s][w];}}//如果在中间过程找到了目标点v1,则不再继续计算了if (s == v1){Cache[v0][v1] = Dist[s];Cache[v1][v0] = Dist[s];return Dist[s];}//选中相应点min = COST_NO_LINK;for (w = 0; w < V; w++){if (!Visited[w] && Dist[w] < min){minIndex = w;min = Dist[w];}}s = minIndex;Visited[s] = true;}}// 图的连通性测试bool ConnectivityTest(int start, bool& bNoPoints){set<int> nodeSet; // 连通顶点集vector<int> test_nodes; // 与新加入连通点连通的未加入点集set<int> singlePoints; // 图中的单点集int i, j;// 先找出单点bool hasEdge = false;for (i = 0; i < V; i++){hasEdge = false;// 这里起始应该是0,不然最后一个点如果是单点则无法判断for (j = 0; j < V; j++) {if (Graph[i][j] > 0){hasEdge = true;break;}}if (!hasEdge){singlePoints.insert(i);}}// 设置bNoPoints标志bNoPoints = (singlePoints.size() == V);// start点必须在连通图中if (singlePoints.find(start) != singlePoints.end()) {return false;}test_nodes.push_back(start);while (test_nodes.size() > 0){int testNode = test_nodes.back();test_nodes.pop_back();for (i = 0; i < V; i++){if (Graph[testNode][i] > 0){if (nodeSet.insert(i).second){test_nodes.push_back(i);}}}}for (i = 0; i < V; 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; i++){Odd_Group[i] = 0; // 0表示不为奇Best_Group[i] = 0;}count = 0;for (i = 0; i < V; i++){rSum = 0;for (j = 0; j < V; j++){rSum += Graph[i][j]; // 求i行和}if (rSum % 2 == 1){Odd_Group[i] = 1;count++;}}return count;}//当前最优分组void Best_Grouping(){int i;for (i = 0; i < V; i++){Best_Group[i] = Odd_Group[i];}}// 对奇度顶点进行分组,level值从2开始取值。// 返回值表示当前这种分组是否是当前所找到中的最好分组。bool Grouping(int level){int i, j, findI = -1;for (i = 0; i < V; i++){if (Odd_Group[i] == 1){Odd_Group[i] = level; // 找到第一个组合点。findI = i;break;}}bool re = true;// 这里是形成一对新的组合后的地方,此时应该计算各组合最小路径之和。if (findI == -1) {int weightSum = 0;// 根据level的值可以知道分组的取值是从2到level-1的,所以i如是计数for (i = 2; i < level; i++) {int index[2];int* pIndex = index;for (j = 0; j < V; j++){if (Odd_Group[j] == i){*pIndex = j;// 设置了第二个index值if (pIndex == index + 1) {break;}pIndex++;}}weightSum += Dijstra(index[0], index[1], true); // 这里暂时只计算最短路权值和,不实际上添加边,最后才添加。这样加边计算只会调用一次。}// 当前组合比以往要优,将当前的排列组合情况更新到全局if (weightSum < Shortest_Path_Weight) {Best_Grouping(); // 如果当前分组比以往都好,备份一下Shortest_Path_Weight = weightSum;return true; // 找到了更优组合,返回递归调用为真}else{return false; // 没找到了更优组合,返回递归调用为假}}else if (findI > -1){// 上面找到了第一个点了,现在从上面继续找第二个点。for (/* 继续上面的for */; i < V; i++){// 找到第二个点if (Odd_Group[i] == 1) {Odd_Group[i] = level;re = Grouping(level + 1);Odd_Group[i] = 1; // 无论当前分组是不是当前最好分组,我们都还要继续查找剩余分组情况}}}else{cerr << "findCount值异常" << endl;exit(-1);}if (findI > -1){Odd_Group[findI] = 1; // 无论当前分组是不是最好分组,我们都还要继续查找剩余分组情况}return re;}//加边void AddShortPath(int from, int to){int i, back;Dijstra(from, to, false); // 求最短路径,结果在dist数组中back = to;// from ... back ... towhile (back != from) {for (i = 0; i < V; i++){if (i != back&& Dist[i] < COST_NO_LINK&& Dist[back] < COST_NO_LINK && Dist[i] + Cost[i][back] == Dist[back]){Graph[i][back]++; // 添加一条边Graph[back][i]++;back = i;break;}}}}// 根据odd数组的分组情况添加最短路径void AddShortPaths(){int i, j;for (i = 0; i < V; i++){if (Best_Group[i] > 1){for (j = i + 1; j < V; j++){if (Best_Group[j] == Best_Group[i]){AddShortPath(i, j);break;}}}}}// 处理图中可能存在度为奇的情况void OddDeal(){// 判断是否存在为奇的点,有的话要处理int oddCount = OddTest();if (oddCount > 0){// 对为奇的点进行排列组合。。。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+1for (i = 0; i < V; i++) {if (Graph[vi][i] > 0) {// 假设选定(vi,i)这条边Graph[vi][i]--; // 这里会破坏全局Graph的值,但暂时没影响了,都不用了。Graph[i][vi]--;bCnecTest = ConnectivityTest(i, bNoPoints);if (!bNoPoints && !bCnecTest) {Graph[vi][i]++;Graph[i][vi]++;continue;}// 选定(vi,i)这条边cout << (char)('a' + vi) << "->" << (char)('a' + i) << " ";sumWeight += Cost[vi][i];vi = i;break;}}if (i == V) {cout << endl;break; // 到这里边找完了}}}//主函数int main() {//输入图的数据Input();bool b;if (!ConnectivityTest(0, b)){cout << "该图不是连通图!\n";exit(0);}OddDeal(); // 处理可能的奇度点情况Fleury(SP); // 用Fleury算法求欧拉回游cout << "最优邮路的长度为:" << sumWeight << endl;return 0;}