一、实验目的
1.掌握基于回溯的算法求解旅行商问题的原理。
2.掌握编写回溯法求解旅行商问题函数的具体步骤并理解回溯法的核心思想以及其求解过程。
3.掌握子集树以及其他几种解空间树的回溯方法并具备运用回溯算法的思想设计算法并用于求解其他实际应用问题的能力。
4.深刻体会回溯算法求解问题的便利以及感受使用回溯算法所编写程序的明确结构和良好的可读性。
5.从算法设计分析角度,体验回溯法求解问题的方法和思路,从而对旅行商问题基于回溯法求解有更进一步的理解。
二、实验环境
操作系统:Windows10
文本编辑器:VisualStudio Code
所用语言和编译器:C++ g++
实验终端:WindowsPowerShell
三、实验内容
对于以售货员,其需要到若干个城市取推销自己的商品,现已知各个城市之间的路程(或旅行所需的费用,即路的权重),该售货员需要选择一条路线,该路线使得每个城市经过一遍并最后能返回出发的城市,要求总的路程(或旅行所需总的费用总旅费最少)。
城市即城市之间的权重使用邻接矩阵a表示,矩阵a中对应的数值为边的权重(即城市之间路线的消费a[i][j]表示)。
例如若当前城市和该城市路线之间的费用使用如下邻接矩阵表示
-1 |
30 |
6 |
4 |
30 |
-1 |
5 |
10 |
6 |
5 |
-1 |
20 |
4 |
10 |
20 |
-1 |
通过分析可知,旅行消费的最优解为25,路线为(1,3,2,4,1)。
程序输入为图的顶点个数和各个顶点之间的权重,要求通过算法求解得到旅行消费的最优解和最优路线并输出。
四、算法描述
分析可知,旅行售货员问题的解空间为一棵排列树,对于整个排列数的回溯搜索类似于生成全排列的过程,开始时x=[1,2,……,n],相应的排列树有x[1:n]全排列构成。
在递归函数Backtrack中,当i = n时,当前扩展结点是排列树的叶结点的父结点。此时,回溯算法检测图G是否存在一条从顶点x[n- 1]到顶点x[n]的边和一条从顶点x[n]到顶点1的边。如果这两条边都存在,则找一条旅行商回路。此时,算法还需判断这条回路的费用是否优于已找到的当前最优回路的费用bestc。
如果是,则必须更新当前最优值bestc和当前最优解bestx. 当i < n时,当前扩展结点位于排列树的第i-1层。图G中存在从顶点x[i-1]到顶点x[i]的边时,x[1: i]构成图G的一条路径,且当x[1:i]的费用小于当前最优值时,算法进入排列树的第i层。否则将剪去相应的子树。算法中用变量cc记录当前路径x[1: i]的费用。
如果不考虑更新bestx所需的计算时间,则算法backtrack需要O((n-1)!)计算时间。由于算法backtrack在最坏情况下可能需要更新当前最优解0((n-1)!)次,每次更新bestx需O(n)计算时间,从而整个算法计算复杂性为O(n! )。
求解旅行商问题的回溯函数(backtrack)可以提取为如下几个步骤:
Backtrack函数在搜索状态空间树时,使用二维数组a来表示图,一维数组x表示当前的解数组,bestc定义了当前的值,使用bestx数组定义当前最优解,cc变量定义了当前的路径长度。
:如果i==n,表示搜索到了排列树的底部,首先判断当前是否形成回路并根据当前值和最优值大小关系来更新最优值和最优解。
:若形成了回路(x[n-1]与x[n]连通,x[n]与x[1]连通),则判断当前值是否优于最优值,更新最优值和最优解,若 bestc=-1则说明还没有搜索到一条回路,则先试着求出一个可行解并返回。
:若i不等于n,说明当前在第i层,需要继续搜索。
:判断是否可以进入x[j]子树,x[i-1]与x[j]连通使得1-i层连成一条路径且累计花费优于目前最优值,若可以进入x[j]子树,则交换x[i]与x[j] 并更新路径的长度,进入i+1层。
:返回后,还原路径的长度,比较x[j+1]子树,然后还原之前的解。
代码逻辑如下:
void backtrack(int i)
{
if(i==n){
if(a[x[n-1]][x[n]]!= -1 &&a[x[n]][1]!= -1 ){//说明形成了回路
if(cc+a[x[n-1]][x[n]]+a[x[n]][1]<bestc||bestc==-1){
for(int k=2;k<=n;k++)
bestx[k]=x[k];
bestc=cc+a[x[n-1]][x[n]]+a[x[n]][1];//更新最优值
}
}
return ;
}
else{
for(intj=i;j<=n;j++){
if(a[x[i-1]][x[j]]!=-1&&cc+a[x[i-1]][x[j]]<bestc||bestc==-1){
swap(x[i],x[j]);
cc=cc+a[x[i-1]][x[i]];
backtrack(i+1);
cc=cc-a[x[i-1]][x[i]];
swap(x[i],x[j]);
}
}
}
return ;
}
五、实验结果
第一组输入,设图中有4个顶点,边的个数为6条,城市1和城市2路线之间的权重为30,城市1和城市3路线之间的权重为6,城市1和城市4路线之间的权重为4,城市2和城市3路线之间的权重为5,城市2和城市4路线之间的权重为10,城市3和城市4路线之间的权重为20,通过基于回溯法得算法求解得最优路线为(城市1,城市3,城市2,城市4,城市1),最优值为25。
第二组输入,设图中有4个顶点,边的个数为6条,城市1和城市2路线之间的权重为6,城市1和城市3路线之间的权重为30,城市1和城市4路线之间的权重为5,城市2和城市3路线之间的权重为4,城市2和城市4路线之间的权重为20,城市3和城市4路线之间的权重为10,通过基于回溯法得算法求解得最优路线为(城市1,城市2,城市3,城市4,城市1),最优值为25。
第三组输入,设图中有3个顶点,边的个数为3条,城市1和城市2路线之间的权重为30,城市1和城市3路线之间的权重为20,城市2和城市3路线之间的权重为1,通过基于回溯法得算法求解得最优路线为(城市1,城市2,城市3,城市1),最优值为51。
六、实验总结
本次实验从旅行商问题基于回溯算法求解出发,生动形象的展示了回溯算法在生活中的实用性。关于旅行商问题,该问题是组合优化领域里一个易于描述但却难以处理的NP完全难题,其可能的路径数目与城市的数目是呈指数型增长的。有多种算法可以求解,比如回溯算法和动态规划算法等。通过两种方法的对比学习和这次的回溯算法实现求解,加深了我对旅行商问题和回溯算法进一步的理解。
在本次得代码实现中,对于城市图邻接矩阵得初始化有多种方法,第一种是使用双重for循环遍历二维数组,对每个数组元素赋值。第二种方法是使用memset函数对二维数组表示得邻接矩阵初始化,第三种方法是使用fill函数对二维数组表示得邻接矩阵初始化。其中使用后两种方法初始化矩阵较为方便易懂。
对于fill和memset得使用,由于memset只能按照字节填充字符,对于int类型数组填充只能为0或-1,而使用fill函数初始化可以对数组元素每一个赋任何值,fill函数按照单元赋值,将一个区间的元素都赋同一个值。本次代码使用fill函数对图的邻接矩阵进行初始化,既保证了实现的方便性又保证了赋值的安全性。
进一步了解可知,旅行商问题在很多领域都有所应用,很多问题也都是从旅行商问题延伸和发展的,通过这次旅行商问题的求解,我们可以运用已学过的算法对其他延伸问题进行解决。
使用回溯法求解旅行商问题思路比较清晰,在编程实现时容易调试和修改,并且通过限界函数和约束条件剪枝减少了很多不必要的计算,使得回溯算法求解问题便利高效。学完算法后最有感触的一点就是,算法的精髓并不在于其方式方法,而在于其思想思路。有了算法的思想,那么潜移默化中问题就可以得到解决。
回溯算法的递归通过系统递归栈实现,增加了空间复杂度,但使用递归可以明显减少代码的编写复杂程度,同时使用递归编程时,应注意递归结束条件的编写,防止程序无限运行,造成计算资源的浪费。文章来源:https://www.toymoban.com/news/detail-490102.html
通过本次实验,增加了我对限界函数和约束条件剪枝对于减少计算量的认知(求解时间大幅减少),以及在编程中,应根据实际问题出发,通过对比各种实现方式,选取高效简洁同时安全的实现。这些优化空间启发着我不断学习,尝试各种实现和优化,并且对于求解算法问题的道路上遇到的各种问题,应不断求索以实现进步。文章来源地址https://www.toymoban.com/news/detail-490102.html
#include <iostream>
#include<cstring>
#include<math.h>
const int maxn = 105;
using namespace std;
int n;//定义图的顶点个数
int a[maxn][maxn];//定义图的邻接矩阵
int x[maxn];//定义当前解
int bestx[maxn];//定义当前最优解
int bestc = -1;//定义当前当前值
int cc = 0;//定义当前路径长度,形成环的时候与bestc比较看能不能更新bestc
void backtrack(int i)
{
if(i==n){
if(a[x[n-1]][x[n]]!= -1 &&a[x[n]][1]!= -1 ){//说明形成了回路
if(cc+a[x[n-1]][x[n]]+a[x[n]][1]<bestc||bestc==-1){
for(int k=2;k<=n;k++)
bestx[k]=x[k];
bestc=cc+a[x[n-1]][x[n]]+a[x[n]][1];//更新最优值
}
}
return ;
}
else{
for(int j=i;j<=n;j++){
if(a[x[i-1]][x[j]]!=-1&&cc+a[x[i-1]][x[j]]<bestc||bestc==-1){
swap(x[i],x[j]);
cc=cc+a[x[i-1]][x[i]];
backtrack(i+1);
cc=cc-a[x[i-1]][x[i]];
swap(x[i],x[j]);
}
}
}
return ;
}
int main()
{
int k;
cout << "输入图顶点的个数和边的个数:" << endl;
cin >> n >> k;
fill(a[0], a[0] + maxn * maxn, -1);
int x1, y1, weight;
cout << "输入图中各个边的权重:" << endl;
//初始化邻接矩阵
for(int i=1;i<=k;i++){
cin >> x1 >> y1 >> weight;
a[x1][y1]=a[y1][x1]=weight;
}
//初始化最优解
for(int i=1;i<=n;i++)
bestx[i]=x[i]=i;
backtrack(2);//出发点已知
cout<<"bestc = " << bestc <<endl;
for(int i=1;i<=n;i++)
cout<<bestx[i]<<"->";
cout<<1;
return 0;
}
到了这里,关于基于回溯法求解旅行售货员问题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!