文章目录
问题描述
-
某售货员要到4个城市去推销商品,已知各城市之间的路程,如右图所示。
-
请问他应该如何选定一条从城市1出发,经过每个城市一遍,最后回到城市1的路线,使得总的周游路程最小?
思路分析
确定问题的解空间,先表达问题
- 首先,都必须将输入数据进行保存,针对图这里使用二维矩阵!然后遍历都是从邻接矩阵出发,进行遍历的!
- 而我们在使用树型结构进行图的遍历的时候,很容易发现,重复的点,是不需要进行遍历,只有第一个点,是需要重复遍历的!
- 所以,总的来说,就需要两个结构:
- 邻接矩阵来保存对应图结构
- 一维矩阵来记录,每一个点是否被访问。
根据题目要求,确定约束函数和上界函数
-
约束函数:记录所有已经访问过的点,需要根绝当前状态是否已经是访问过的结点的情况!
-
上限函数:判定当前的状态的价值是否已经超过了对应当前完成的价值!
实现代码与分析
使用程序语言表示图
约束函数和上限函数的定义
//保存当前已知的最短路径,这里先定义一个极大值,一旦比这个值小,就换成更小的值
int minPath = 99;
//保存当前的状态对应的路径,这里总共有四个结点,总共只需要走四次
int path[4];
/*
描述:旅行售货问题的约束函数和上限函数,确保路径不重复并且路径的价值最小
参数:m表示当前问题的状态,也是递归地层数,记录m次选择所在的位置结点
pathValue:表示当前的状态对应的路径的值
返回:true:表示当前的结点符合对应的情况
false:表示当前的结点不符合对应的情况
原理:遍历已有的数组,查看是否会重复
比较已经有的保存的最大值,进行判定
*/
bool constrainAndBound(int m,int pathValue){
//约束函数,查看当前的决定是否符合状态
for (int i = 0; i < m; ++i)
{
if (path[m] == path[i])
{
return false;
}
}
//上限函数,判定当前的路径值是否是最短的
//如果是没有到达目标路径,已经是比原来的长,可以直接退出了
if (m < 3)
{
pathValue > minPath;
return false;
}
return true;
}
回溯算法回顾
- 定义问题的解空间,定义问题的所有情况,包括最优解。
- 确定易于搜索的解空间结构:
- 子集树
- 排列树
- 确定约束函数或者是限界函数
- 从根节点进行深度遍历搜索解空间
实现源代码
- 这道题,有点不同,那就是默认是从第一个结点出发,指定了最初的出发点,而且是制定了完整的循环是五个结点,所以遍历的终止条件应该在第五层。
//下述为旅行售货问题的代码
//保存当前已知的最短路径,这里先定义一个极大值,一旦比这个值小,就换成更小的值
int minPath = 99;
//保存当前的状态对应的路径,这里总共有四个结点,总共只需要走四次
int path[5] = { 0 };
//定义一个地点数
int placeNum = 4;
/*
描述:旅行售货问题的约束函数和上限函数,确保路径不重复并且路径的价值最小
参数:m表示当前问题的状态,也是递归地层数,记录m次选择所在的位置结点
pathValue:表示当前的状态对应的路径的值
返回:true:表示当前的结点符合对应的情况
false:表示当前的结点不符合对应的情况
原理:遍历已有的数组,查看是否会重复
比较已经有的保存的最大值,进行判定
*/
bool constrainAndBound(int m, int pathValue) {
//约束函数,查看当前的决定是否符合状态
//限制0号结点的位置
if (m != 4 && path[m] == 0) return false;
for (int i = 1; i < m; ++i)
{
//限制不能重复
if (path[m] == path[i]) return false;
}
//上限函数,判定当前的路径值是否是最短的
//在没有到达重点之前,已经比最短的路径要长,就没有意义了
if (pathValue > minPath) return false;
return true;
}
/*
描述:旅行售货问题的具体实现
参数:m表示对应递归的深度
matrix二维矩阵,表示对应矩阵的形状
pathValue表示当前的路径长度
返回:无返回值,递归调用
原理:在递归的过程中生成树并遍历对应的树
*/
void SellPath(int m, int Matrix[4][4], int pathValue) {
//首先到达递归的尽头,是已经是第四个目标了
if (m == 5)
{
//到达了最底线,直接比较对应长度
if (pathValue < minPath) minPath = pathValue;
//cout << "最短的路径是:" << minPath << endl;
}
else {
//如果是不满足的情况,就进行递归遍历,遍历的是每一个地点的邻接点
for (int i = 0; i < placeNum; ++i)
{
//遍历到当前的状态,就应该遍历到当前状态对应的结点
//定位到二维矩阵中对应的行,进行遍历,找到当前目标的结点。
if (Matrix[path[m - 1]][i] != 0)
{
//对路程进行累加
pathValue += Matrix[path[m - 1]][i];
//状态进行选择,确定当前的状态是否合法
path[m] = i;
int temp = path[m];
//约束条件,进行判定
if (constrainAndBound(m, pathValue))
{
//如果满足递归条件,就进行递归,往下一个方向进行递归
SellPath(m + 1, Matrix, pathValue);
}
//不满足条件,就减去当前的路径
pathValue -= Matrix[path[m-1]][i];
}
}
}
}
int main(){
cout << "旅游售货问题的展示" << endl;
cout << "总共有四个结点,分别是1,2,3,4" << endl;
cout << "总共有6条边,按照长度由低到高分别是4,5,6,10,20,30" << endl;
//初始化邻接矩阵
int nodeNum = 4;
int Matrix[4][4];
int pathValue = 0;
for (int i = 0; i < nodeNum; ++i)
{
for (int j = 0; j < nodeNum; ++j)
{
Matrix[i][j] = 0;
}
}
Matrix[0][1] = 30;
Matrix[0][2] = 6;
Matrix[0][3] = 4;
Matrix[1][0] = 30;
Matrix[1][2] = 5;
Matrix[1][3] = 10;
Matrix[2][0] = 6;
Matrix[2][1] = 5;
Matrix[2][3] = 20;
Matrix[3][0] = 4;
Matrix[3][1] = 10;
Matrix[3][2] = 20;
cout << "输出矩阵如下:" << endl;
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
cout << Matrix[i][j] << " ";
}
cout << endl;
}
SellPath(1, Matrix, pathValue);
cout << "最终的结果是:" << minPath << endl;
cout << endl;
return 0;
}
出现的问题
在遍历的时候,在1号结点和2号结点之间横跳一次,使得最后找不到最终的目标节点
- 一开始默认第一个结点是0号结点,而且默认最后一个结点也是0号结点,所以,就要在约束函数中加上对应的判定条件,限制0号结点的位置和选择!
虽然这里有四个结点,但是你要走五个节点,从1出发,最后还要回到1!
- 所以记录递归状态的数组,就要是长度为5的一维数组,但是遍历是从第二个结点开始遍历的,第一个默认是0结点!
主要用到两个东西,保存递归状态的一维矩阵和保存数组信息的二维矩阵
- 注意:上述两者之间的关系是差了一个状态的,m状态的递归选项是由m-1状态的递归选项决定
分析与总结
- 这道题纠缠了很久,想了很多,也发现了很多不一样的地方!关键是这道题需要对解题过程和回溯过程都要有很深的理解!因为他有很多不一样的地方,很多变式!你要根据情况去设定不同的条件进行判定!
- 这道题确实让我收获很多,但是我的代码不是很好看,至少在我看来是比较乱的!而且质量不高,应该好好修改!但是奈何在思考解答方法这件事上,已经投入了很多的时间,所以没有时间进行修改!
- 有事请加扣扣!651378276!可以一块学习,进步!
参考代码——参考课件上
template<class T>
//结点构成数组
T Traveling<T>::TSP1(int v[]){
//这段就是求取最大的累加值
bestc = 1;
//n个结点
for (int i = 1,MaxCost = 0; i <= n; ++i)
{
for (int j = 1; j <= n; ++j)
{
//遍历所有的结点对应边,来获取某节点最大的边
if (a[i][j] != noEdge && a[i][j] > MaxCost)
{
MaxCost = a[i][j];
}
}
//如果最大的边都是不存在的边,那就是没有边
if (MaxCost == noEdge)
{
return noEdge;
}
//对最大的边进行累加
bestc += MaxCost;
}
//这里记录的是货郎售货的路径
x = new int[n+1];
for(i = 1;i <= n;i ++){
x[i] = i;
}
//bestc保存对应的结点的序号,路径的结点序号
bestx = v;
//用来临时保存路径
cc = 0;
//从第二层开始遍历,因为默认第一个结点就是一号结点
tSP1(2);
delete []x;
return bestc;
}
template<class T>
void Traveling<T>::tSP1(int i){
//递归终止的条件,这里应该是经过n次的选择,又重新回到了1号结点
if (i == n)
{
//这里是和上界进行比较
//上一个结点到下一个结点是有边的
//当前结点到一号结点是有边的
//并且当前已经保存的路径加上上面的两段距离是小于上界的
if(a[x[n-1][x[n]] != noEdge && a[x[n]][1] != noEdge
&& cc+a[x[n-1]][x[n]] + a[x[n]][1] < bestc){
for (int j = 0; j <= n; ++i)
{
bestx[j] = x[j];
}
bestc == cc + a[x[n-1]][x[n]] + a[x[n]][1];
}
}else{
//递归向下进行的条件
for (int j = 0; j <= n; ++j)
{
//递归遍历当前路径下的对应的行列式
//如果当前的递归的结点的上一个结点到j有边的,并且路径是小于最短路径的
//将该点和目标路径进行交换
if(a[x[i-1]][x[j]] != noEdge && (cc + a[x[i-1]][x[j]] < bestc)){
Swap(x[i],x[j]);
cc += a[x[i-1][x[i]]];
//向下进行深一步的递归
tSP1(i+1);
cc -= a[x[i-1]][x[i]];
Swap(x[i],x[j]);
}
}
}
}
- 我觉得上面的思路很棒,但是理解了很久,交换数组来记录遍历的顺序会不会很累?比较难想。