利用回溯算法求马周游问题

马周游问题描述

对于一个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个位置,那么问题来了,我们选择哪个节点?

  1. 假如我们选择了位置35,那么从35号位置出发可以选择的位置有:18、25、29、41、45、50、52总共7个位置,因为从20跳转到35,因此不会选择20号节点,即使如此,35号节点可以选择的位置也有7个。
  2. 假如我们选择了位置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实现的带界面的马周游的源代码,最最核心的是上面介绍的函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值