马周游问题描述
对于一个8*8的棋盘,用下列的方式编号
1 2 3 4 5 6 7 8
9 10 11 12 13 14 15 16
17 18 19 20 21 22 23 24
25 26 27 28 29 30 31 32
33 34 35 36 37 38 39 40
41 42 43 44 45 46 47 48
49 50 51 52 53 54 55 56
57 58 59 60 61 62 63 64如果它走63步正好经过除起点外的其他位置各一次,这样一种走法则称马的周游路线,设计一个算法,从给定的起点出发,找出它的一条周游路线。马的走法是“日”字形路线。
回溯法和分支限界算法
如果盲目的使用回溯法,但是不添加一些限界函数(在回溯过程中用于发现某个节点上肯定不可能存在答案节点,直接跳过这个节点,不在搜索它以及它的子节点的方法),如果不添加分支限界函数,就是最蠢的穷举法,然而穷举法算马周游太慢了,绝对不是最佳选择。
分支限界函数一:
我们先来分析一下问题。假如说我们当前正在20号位置,从20号位置出发,可以跳的位置有:3、5、10、14、26、30、35、37总共8个位置,那么问题来了,我们选择哪个节点?
- 假如我们选择了位置35,那么从35号位置出发可以选择的位置有:18、25、29、41、45、50、52总共7个位置,因为从20跳转到35,因此不会选择20号节点,即使如此,35号节点可以选择的位置也有7个。
- 假如我们选择了位置3,那么从3号位置出发可以选择的位置有:9、13、18、20号总共4个位置
很显然,如果让我们去选择,我们肯定会选择从20号节点跳转到3号节点,因为从3号节点出发的时候可以选择的位置变少了。于是我们得到了结论:咳咳咳。。。
每次选择节点的时候,都会选择下一跳的候选节点中候选节点数目最少的节点**
分支限界函数二:
接着我们考虑下一个问题,假如说现在我们现在有两个候选节点A和B,而且A和B的下一跳候选节点相等,而且都是最少的,这时候我们如何选择?
毫不犹豫选择距离棋盘中心更远的那个点,因为距离棋盘更远,这个点就更加靠近边缘,靠近边缘,靠近边缘可以极大的减少候选节点的个数,能够极大的加快回溯速度。
马周游核心代码
//curVertex表示当前访问的节点编号
//remainVertex表示没有访问过的节点数
//trvlSqn 表示已经走过的序列号码
bool TravelAlgorithm(int curVertex, int remainVertex, std::vector<int>& trvlSqn)
{
//如果当前已经剩余的顶点数为0,将当前点加入trvlSqn中返回,这是函数出口
if(remainVertex == 0)
{
return true;
}
//求出该点的下一跳候选点,FndHrsStp函数求其候选节点,返回一个vector
std::vector<int> cnddtVrtx = FndHrsStp(curVertex,trvlSqn);
//如果候选节点vector为空,直接返回,否则对vector中的每一个调用该算法
if(cnddtVrtx.size()==0)
return false;
//VrteAndCnddctVrtxSt是自定义的数据结构,稍后奉上
std::vector<VrteAndCnddctVrtxSt> cnddtVertexSet; //保存下一跳节点的候选节点集合等信息
VrteAndCnddctVrtxSt vacvs; //数据集合
//对每一个候选节点,再次求他们的下一跳候选节点,并且按照待选节点从少到多排序
for(auto it = cnddtVrtx.cbegin();it!=cnddtVrtx.cend();++it)
{
//将该候选点放入遍历序列中
trvlSqn.push_back(*it);
//求该点的候选节点集合,cnddctVertx是一个vector<int>
vacvs.cnddctVertx = FndHrsStp(*it,trvlSqn);
//保存当前候选节点的编号
vacvs.vertex = *it;
//候选节点集合中加入新的点的相关数据
cnddtVertexSet.push_back(vacvs);
//GexAxis是根据节点的编号(1~64)来确定该在棋盘中的行和列的函数,稍后奉上
pair<int,int> pos2 = GetAxis(*it);
//求出该候选点到中心距离
vacvs.length = (pos2.first-edge/2)*(pos2.first-edge/2)+
(pos2.second-edge/2)*(pos2.second-edge/2);
//删除这个待考察节点,该节点就在末尾
trvlSqn.pop_back();
}
//对获取到的待选节点的下一跳待考察节点集合进行排序,该函数是一个lambda函数
auto f = [](VrteAndCnddctVrtxSt v1,VrteAndCnddctVrtxSt v2) -> bool {
if(v1.cnddctVertx.size()!=v2.cnddctVertx.size())
return v1.cnddctVertx.size()<v2.cnddctVertx.size();
else
return v1.length<v2.length;};
//调用库函数排序
stable_sort(cnddtVertexSet.begin(),cnddtVertexSet.end(),f);
//对这些已经排好序的考察节点出发,逐个开始遍历
for(auto it = cnddtVertexSet.cbegin();it!=cnddtVertexSet.cend();++it)
{
//放入遍历序列中,因为递归时假设该点已经被遍历
trvlSqn.push_back((*it).vertex);
bool rs = TravelAlgorithm((*it).vertex, remainVertex-1, trvlSqn);
//如果myResult的结果为vertexNum,表明遍历成功
if(rs)
return true;
//否则遍历失败,删除路径上的这个点,继续测试下一个节点
trvlSqn.pop_back();
}
return false;
}
//这里奉上待考察节点的下一跳候选节点信息存储的数据结构
class VrteAndCnddctVrtxSt
{
public:
VrteAndCnddctVrtxSt(){}
int vertex; //节点编号
int length; //距离地图中心距离
std::vector<int> cnddctVertx; //该节点的下一跳节点集合
bool operator < (const VrteAndCnddctVrtxSt& v1)const;
std::pair<int,int> GetAxis(const int& pos) const;
};
这里是我用QT实现的带界面的马周游的源代码,最最核心的是上面介绍的函数。