基于回溯法求解旅行售货员问题

一、实验目的

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变量定义了当前的路径长度。

  1. :如果i==n,表示搜索到了排列树的底部,首先判断当前是否形成回路并根据当前值和最优值大小关系来更新最优值和最优解。

  1. :若形成了回路(x[n-1]与x[n]连通,x[n]与x[1]连通),则判断当前值是否优于最优值,更新最优值和最优解,若 bestc=-1则说明还没有搜索到一条回路,则先试着求出一个可行解并返回。

  1. :若i不等于n,说明当前在第i层,需要继续搜索。

  1. :判断是否可以进入x[j]子树,x[i-1]与x[j]连通使得1-i层连成一条路径且累计花费优于目前最优值,若可以进入x[j]子树,则交换x[i]与x[j] 并更新路径的长度,进入i+1层。

  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函数对图的邻接矩阵进行初始化,既保证了实现的方便性又保证了赋值的安全性。

进一步了解可知,旅行商问题在很多领域都有所应用,很多问题也都是从旅行商问题延伸和发展的,通过这次旅行商问题的求解,我们可以运用已学过的算法对其他延伸问题进行解决。

使用回溯法求解旅行商问题思路比较清晰,在编程实现时容易调试和修改,并且通过限界函数和约束条件剪枝减少了很多不必要的计算,使得回溯算法求解问题便利高效。学完算法后最有感触的一点就是,算法的精髓并不在于其方式方法,而在于其思想思路。有了算法的思想,那么潜移默化中问题就可以得到解决。

回溯算法的递归通过系统递归栈实现,增加了空间复杂度,但使用递归可以明显减少代码的编写复杂程度,同时使用递归编程时,应注意递归结束条件的编写,防止程序无限运行,造成计算资源的浪费。

通过本次实验,增加了我对限界函数和约束条件剪枝对于减少计算量的认知(求解时间大幅减少),以及在编程中,应根据实际问题出发,通过对比各种实现方式,选取高效简洁同时安全的实现。这些优化空间启发着我不断学习,尝试各种实现和优化,并且对于求解算法问题的道路上遇到的各种问题,应不断求索以实现进步。

  • 3
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值