回溯法有点类似于暴力枚举的搜索过程,回溯法的基本思想是按照深度优先搜索的策略,从根节点出发深度搜索解空间树,当搜索到某一节点时,如果该节点可能包含问题的解,则继续向下搜索;反之回溯到其祖先节点,尝试其他路径搜索。
第一类问题:只要求求得一个可行解,那么搜索到问题的一个解即可结束;
第二类问题:求最优解,那么需要搜索整个解空间树,得到所有解之后择最优作为问题的解。
回溯法与暴力搜索的区别:在搜索到叶子节点之前已经能确定该路径不为最优解时就可以进行剪枝,节省搜索时间。
回溯法有两种模板:子集树和排列树。旅行售货员问题时典型的排列树。
子集树: 所给的问题是从n个元素的集合中找出满足某种性质的子集时, 相应的解空间称为子集树.
子集树通常有个叶结点, 遍历子集树的任何算法均需Ω()的计算时间.
例如: 0-1背包问题的解空间为一棵子集树.
排列树: 当所给的问题是确定n个元素满足某种性质的排列时, 相应的解空间称为排列树.
排列树通常有(n-1)!个叶结点, 遍历排列树需要Ω(n!)的计算时间.
例如: 旅行售货员问题的解空间为一棵排列树.
问题描述
某售货员要到若干城市去推销商品,已知各城市之间的路程,他要选定一条从驻地出发,经过每个城市一遍,最后回到住地的路线,使总的路程最短。
由于只有4个城市,如果规定售货员总是从城市1出发,那么依据排列组合可以得到6种不同的旅行方案,比如12341、13241等等。在这些排列组合基础上可以很容易绘制出一棵排列树,也是该问题的解空间树,排列树如下:
我们可以得到如下信息:
根据解空间树可以得到一些有用的信息:
- 该树的深度为5
- 两个节点之间路径上的标识数组代表所走城市
- 树上没有体现出回到城市1的路径,但实际上计算要考虑这段路程
问题分析
假设起点为 1。
算法开始时 x = [1, 2, 3, …, n]
x[1 : n]有两重含义 x[1 : i]代表前 i 步按顺序走过的城市, x[i + 1 : n]代表还未经过的城市。利用Swap函数进行交换位置。
i = n 时,处在排列树的叶节点的父节点上,此时算法检查图G是否存在一条从顶点x[n-1] 到顶点x[n] 有一条边,和从顶点x[n] 到顶点x[1] 也有一条边。若这两条边都存在,则发现了一个旅行售货员的回路(即:新旅行路线),算法判断这条回路的费用是否优于已经找到的当前最优回路的费用bestcost,若是,则更新当前最优值bestcost和当前最优解bestx。
i < n 时,检查x[i - 1]至x[i]之间是否存在一条边, 若存在,则x [1 : i ] 构成了图G的一条路径,若路径x[1: i] 的耗费小于当前最优解的耗费,则算法进入排列树下一层,否则剪掉相应的子树。
伪代码
定义变量:图的基本信息(邻接矩阵、顶点数、边数),当前解,最优解,标记有边无边(方便初始化和判断)
回溯主函数
代码
//旅行商问题--排列树
#include<iostream>
#include<cstring>
using namespace std;
#define maxn 0x3f3f3f
const int NoEdge = -1; //两个点之间没有边
int citynum; //城市数
int edgenum; //边数
int currentcost; //记录当前的路程
int bestcost; //记录最小的路程(最优)
int Graph[100][100]; //图的边距记录
int x[100]; //记录行走顺序
int bestx[100]; //记录最优行走顺序
void InPut(){
int pos1, pos2, len; //点1 点2 距离
cout<<"请输入城市数和边数(c e):";
cin>>citynum>>edgenum;
memset(Graph, NoEdge, sizeof(Graph));
cout<<"请输入两座城市之间的距离(p1 p2 l):"<<endl;
for(int i = 1; i <= edgenum; ++i)
{
cin>>pos1>>pos2>>len;
Graph[pos1][pos2] = Graph[pos2][pos1] = len;
}
}
//初始化
void Initilize(){
currentcost = 0;
bestcost = maxn;
//初始化行走顺序,第i步走的城市
for(int i = 1; i <= citynum; i++) x[i] = i;
}
void Swap(int &a, int &b){
int temp;
temp = a;
a = b;
b = temp;
}
void BackTrack(int i) //这里的i代表第i步去的城市而不是代号为i的城市
{
if(i == citynum)
{
//进行一系列判断,注意的是进入此步骤的层数应是叶子节点的父节点,而不是叶子节点
if(Graph[x[i - 1]][x[i]] != NoEdge && Graph[x[i]][x[1]] != NoEdge && (currentcost + Graph[x[i - 1]][x[i]] + Graph[x[i]][x[1]] < bestcost || bestcost == maxn))
{
//最小(优)距离=当前的距离+当前城市到叶子城市的距离+叶子城市到初始城市的距离
bestcost = currentcost + Graph[x[i - 1]][x[i]] + Graph[x[i]][x[1]];
for(int j = 1; j <= citynum; ++j)
bestx[j] = x[j];
}
}
else
{
for(int j = i; j <= citynum; ++j)
{
if(Graph[x[i - 1]][x[j]] != NoEdge && (currentcost + Graph[x[i - 1]][x[j]] < bestcost || bestcost == maxn))
{
Swap(x[i], x[j]); //这里i 和 j的位置交换了, 所以下面的是currentcost += Graph[x[i - 1]][x[i]];
currentcost += Graph[x[i - 1]][x[i]];
BackTrack(i + 1); //递归进入下一个城市
currentcost -= Graph[x[i - 1]][x[i]];
Swap(x[i], x[j]);
}
}
}
}
void OutPut()
{
cout<<"最短路程为:"<<bestcost<<endl;
cout << "路线为:" << endl;
for(int i = 1; i <= citynum; ++i)
cout << bestx[i] << " ";
cout << "1" << endl;
}
int main()
{
InPut();
Initilize();
BackTrack(2);
OutPut();
}
/*假设起点为 1。
算法开始时 x = [1, 2, 3, …, n]
x[1 : n]有两重含义 x[1 : i]代表前 i 步按顺序走过的城市,
x[i + 1 : n]代表还未经过的城市。利用Swap函数进行交换位置。
i = n 时,处在排列树的叶节点的父节点上,
此时算法检查图G是否存在一条从顶点x[n-1] 到顶点x[n] 有一条边,
和从顶点x[n] 到顶点x[1] 也有一条边。若这两条边都存在,
则发现了一个旅行售货员的回路(即:新旅行路线),
算法判断这条回路的费用是否优于已经找到的当前最优回路的费用bestcost,
若是,则更新当前最优值bestcost和当前最优解bestx。
i < n 时,检查x[i - 1]至x[i]之间是否存在一条边,
若存在,则x [1 : i ] 构成了图G的一条路径
若路径x[1: i] 的耗费小于当前最优解的耗费,
则算法进入排列树下一层,否则剪掉相应的子树。*/
以前面的样例示范
输入
请输入城市数和边数(c e):4 6
请输入两座城市之间的距离(p1 p2 l):
1 2 30
1 3 6
1 4 4
2 4 10
2 3 5
3 4 20文章来源:https://www.toymoban.com/news/detail-754974.html
输出
最短路程为:25
路线为:
1 3 2 4 1文章来源地址https://www.toymoban.com/news/detail-754974.html
到了这里,关于回溯法--旅行售货员问题--排列树的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!