• 回溯法 之 马周游(马跳日)问题


    回溯法的应用很多,下面讲述一个有趣的马周游问题。

    马周游(马跳日)问题:在一个 8*8 的棋盘上(如下图)一匹马从任意位置开始,恰好走过棋盘中的每一格(每个格子有且只能走一次),并且最后还可以回到起点位置。

                                                    


    这个问题其实可以进行推广:即棋盘大小不一定是 8*8 ,只要棋盘大小 M * N 满足:

    ① M >=6 ;N>= 6 ② M N都是偶数  ③ | M-N | <=2   

    当然这个问题还可以缩小:即马周游最后不一定要回到原点,只要遍历走完棋盘中的所有格子即可。


    显然常规的解法就是采用回溯法,并且要在回溯过程中进行剪枝


    一、下面从马周游最简单的问题描述开始做起:即马周游只需要遍历走完棋盘中的所有格子即可,不要求最后要回到起点位置

    解法说明:其实很容易理解到,马周游棋盘,也就是要遍历棋盘中的所有格子有且只能一次,那么很显然就是一个图的遍历问题了。怎么理解呢?

    在前面的文章 八皇后 中,每个皇后都有 8 个位置的选择,那么对应来说就是一个满八叉树(也可以看做是图)的遍历。下面给出一个四皇后问题的图解。在这个图中我们就可以清楚的看到图的遍历过程,而且是深度优先遍历。

     

    那么同样的,关于马周游问题,也是同样要进行图的深度优先遍历过程。那么深度优先遍历过程可以递归实现,可以非递归实现。下面分别给出其实现代码。

    (1)递归实现

    #include<iostream>
    #include <stdlib.h>
    #include <iomanip>
    
    using namespace std;
    
    //马周游的棋盘,注意使用的时候是从下表为1开始
    int board[100][100];
    
    int fx[]= {2,1,-1,-2,-2,-1,1,2};
    int fy[]= {-1,-2,-2,-1,1,2,2,1};
    
    int n; //棋盘大小
    
    //参数x,y 表示棋盘的位置
    //检测(x,y) 对应位置在棋盘中是否合法
    bool check(int x,int y)
    {
        if(x<1 || y<1 || x>n || y>n || board[x][y] != 0)
            return false;
        return true;
    }
    
    //输出结果
    void outputResult(int n)
    {
        for(int i=1; i<=n; i++)
        {
            cout<<endl<<endl;
            for(int j=1; j<=n; j++)
            {
                cout<<setw(3)<<board[i][j]<<" ";
            }
        }
        cout<<endl<<endl;
    }
    
    void runTable(int a,int b,int number)
    {
        if(number == n*n) //已经走完棋盘中所有的点
        {
            outputResult(n); //输出
            exit(1);
        }
    
        for(int i=0; i<8; i++) //表示每一个格都有八种走法
        {
            if(check(a + fx[i],b + fy[i]))
            {
                int x = a + fx[i];
                int y = b + fy[i];
    
                board[x][y] = number+1; //走到下一个位置,设置其序号为 number+1
    
                runTable(x, y,number+1);
                board[x][y] = 0;//回溯
            }
        }
    }
    
    //递归走法
    void horseRun(int x,int y)
    {
        int number = 1;
        board[x][y] = number; //首先确定起始位置这个格是序号为1
        runTable(x, y,number);
    }
    
    int main()
    {
        cout<<"输入棋盘大小n:";
        cin>>n;
    
        int x,y;
        cout<<"输入马周游起始位置x(1~n),y(1~n):";
        cin>>x>>y;
    
        horseRun(x,y);
        return 0;
    }


    运行效果:

               


    说明:以上程序在运行棋盘大小为 6*6 的时候,可以很快跑出结果,但是在棋盘大小为 8*8 的时候,就要花费几秒,说明这个运行的效果不是很好。

    (2)非递归实现

    #include<iostream>
    #include <iomanip>
    #include <queue>
    using namespace std;
    
    //在某一格子的八种走法
    int fx[]= {2,1,-1,-2,-2,-1,1,2};
    int fy[]= {-1,-2,-2,-1,1,2,2,1};
    
    typedef struct
    {
        int x,y; //坐标
        int number; //序号
    } Point; //棋盘中的格子
    
    //马周游的棋盘,注意使用的时候是从下表为1开始
    Point board[10000][10000];
    
    int n; //棋盘大小
    int step =1; //序号
    
    //输出结果
    void outputResult(int n)
    {
        for(int i=1; i<=n; i++)
        {
            cout<<endl<<endl;
            for(int j=1; j<=n; j++)
            {
                cout<<setw(3)<<board[i][j].number<<" ";
            }
        }
        cout<<endl<<endl;
    }
    
    bool check(int x,int y)
    {
        if(x<1 || y<1 || x>n || y>n || board[x][y].number != 0)
            return false;
        return true;
    }
    
    //下一位置有多少种走法
    int nextPosHasSteps(int x, int y)
    {
        int steps = 0;
        for (int i = 0; i < 8; ++i)
        {
            if (check(x + fx[i], y + fy[i]))
                steps++;
        }
        return steps;
    }
    
    //非递归的走法
    void horseRun(Point point)
    {
        queue<Point> pointQueue;
        pointQueue.push(point);
    
        Point temp;
    
        while(!pointQueue.empty())
        {
            temp = pointQueue.front();
            pointQueue.pop();
    
            board[temp.x][temp.y].number = step++;
    
            int minStep = 8;
    
            int flag = 0;
    
            for(int i=0; i<8; i++) //出下一位置走法最少的进入对列
            {
                int x=temp.x + fx[i];
                int y=temp.y + fy[i];
    
                if(check(x,y))
                {
                    if(nextPosHasSteps(x,y) <= minStep)
                    {
                        minStep = nextPosHasSteps(x,y);
    
                        Point t;
                        t.x = x;
                        t.y = y;
    
                        if(flag) pointQueue.pop();
    
                        pointQueue.push(t);
                        flag = 1;
                    }
                }
            }
        }
    }
    
    int main()
    {
        cout<<"输入棋盘大小n:";
        cin>>n;
    
        Point startPoint;
        cout<<"输入马周游起始位置x(1~n),y(1~n):";
        cin>>startPoint.x>>startPoint.y;
    
        horseRun(startPoint);
        //输出结果
        outputResult(n);
        return 0;
    }


    说明:在程序中已经用到了一个剪枝:即每一次都优先走下一位置走法最少的。 关于剪枝的内容下面会具体讲述

    运行效果:



    说明:此种解法可以很快的跑出结果,甚至在 几千乘以几千的棋盘中,都是几乎瞬间跑出结果,效果十分的好,因为这里面用了一个很关键的剪枝


    二、完整解决马周游问题:既要遍历走完棋盘中所有的格子,最后还要回到起点

    有了上面第一个简化版马周游的解决经验,那么完整解决马周游问题,无法就是再添加一个限制条件:最后要回到起点。

    那么首先还是要先介绍一下在这个马周游回溯过程中要用到的剪枝(如果不用剪枝,那么算法执行效率会很低)。

    使用剪枝有3处:

    1、使用Warnsdorff's rule。在当前位置(Now)考虑下一个位置(Next)的时候,优先选择下一个的位置(Next)走法最少的那个。作为当前位置(Now)的下一位置(Next)。

    譬如说:下图所示,当前位置现在要确定下一位置,那么就要所有的下一个位置进行考察,看看假如走到下一个位置,它的下一个位置又有多少种走法,选择下一个位置可能走法最少的作为当前位置(Now)的下一个位置(Next)。


    2、在进行了第一点的剪枝后,如果可以优先选择的下一个位置不止一个,则优先选择离中心位置较远的位置作为下一步(即靠近边边的位置)。

    通俗点理解,第一点的剪枝就是走那些位置可能走到机会比较小的,反正走到的机会小,那么先走完总是好的,要不然你兜一圈回来,还是要走这一个位置的。

    第二点的剪枝就是走法尽量从边边走,然后是往中间靠。


    3、第三点的剪枝,每次都从棋盘的中间点开始出发,然后求出一条合法路径后再平移映射回待求路径。

    怎么理解呢?所谓马周游棋盘,最后还要回到起点。也就是在棋盘中找到一条哈密顿回路。那么不管你是从哪里开始的,最后都是会在这个哈密顿回路中的,那么选取的中点的位置也肯定是在这个回路上的。

    最后,找到这个这个以中点为起点的哈密顿回路后,根据设定起点在这个回路中的序号,映射回以这个位置为起点的马周游路线即可。

    至于为什么要从棋盘中间位置开始呢? 我就不太能解释了。

                                            

    知道了上面的三种剪枝方式,那么具体是要如何实现的呢?

    (1)关于第一点和第二点的剪枝,二者关联很大。那么我们可以将二者结合起来。放到一个结构体中,这个结构体表征是的是下一位置。

    typedef struct NextPos
    {
    
        int nextPosSteps; //表示下一位置有多少种走法;走法少的优先考虑
        int nextPosDirection; //下一位置相对于当前位置的方位
        int nextPosToMidLength; //表示当前位置距中间点距离;距离中间点远的优先考虑
    
        //
        bool operator < (const NextPos &a) const
        {
            return nextPosSteps > a.nextPosSteps && nextPosToMidLength < a.nextPosToMidLength;
        }
    
    };


    注意其中, 下一位置走法少的优先,下一位置距离中点远的优先

    这样我们在挑选下一个位置的时候,可以将符合要求的放到一个优先队列中,这样选取下一位置的时候直接从优先队列拿出了就好了(省去排序的工作)。

    (2)关于第三点的剪枝,其实就是涉及到最后输出结果,这个比较简单。


    下面完整给出代码实现:

    #include <iostream>
    #include <stdlib.h>
    #include <iomanip>
    #include <queue>
    using namespace std;
    
    typedef struct
    {
        int x;
        int y;
    } Step;
    
    Step step[8] = { {-2, -1}, {-1, -2}, { 1, -2}, { 2, -1}, { 2, 1}, { 1, 2}, {-1, 2}, {-2,1} };
    
    typedef struct NextPos
    {
        int nextPosSteps; //表示下一位置有多少种走法;走法少的优先考虑
        int nextPosDirection; //下一位置相对于当前位置的方位
        int nextPosToMidLength; //表示当前位置距中间点距离;距离中间点远的优先考虑
    
        //
        bool operator < (const NextPos &a) const
        {
            return nextPosSteps > a.nextPosSteps && nextPosToMidLength < a.nextPosToMidLength;
        }
    
    };
    
    int board[100][100];
    int M,N; //棋盘大小
    
    //检测这个位置是否可以走
    bool check(int x, int y)
    {
        if (x >= 0 && x < M && y >= 0 && y < N && board[x][y] == 0)
            return true;
        return false;
    }
    //下一位置有多少种走法
    int nextPosHasSteps(int x, int y)
    {
        int steps = 0;
        for (int i = 0; i < 8; ++i)
        {
            if (check(x + step[i].x, y + step[i].y))
                steps++;
        }
        return steps;
    }
    //判断是否回到起点
    bool returnStart(int x, int y)
    {
        //校验最后是否可以回到起点,也就是棋盘的中间位置
        int midx,midy;
        midx = M / 2 - 1;
        midy = N / 2 - 1;
        for (int i = 0; i < 8; ++i)
            if (x + step[i].x == midx && y + step[i].y == midy)
                return true;
        return false;
    }
    
    //输出结果
    void outputResult(int xstart,int ystart)
    {
        int num = M * N;
        int k = num - board[xstart][ystart];
        for (int i = 0; i < M; ++i)
        {
            cout<<endl<<endl;
            for (int j = 0; j < N; ++j)
            {
                board[i][j] = (board[i][j] + k) % num + 1;
                cout<<setw(5)<<board[i][j];
            }
        }
        cout<<endl<<endl;
    }
    
    //某一位置距离棋盘中心的距离
    int posToMidLength(int x,int y)
    {
        int midx = M / 2 - 1;
        int midy = N / 2 - 1;
        return (abs(x - midx) + abs(y - midy));
    }
    
    void BackTrace(int t, int x, int y,int xstart,int ystart)
    {
        //找到结果
        if (t == M * N && returnStart(x,y)) //遍历了棋盘的所以位置,并且最后可以回到起点,形成回路
        {
            outputResult(xstart,ystart);
            exit(1);
        }
        else
        {
            priority_queue<NextPos> nextPosQueue;
            for (int i = 0; i < 8; ++i)
            {
                if (check(x + step[i].x, y + step[i].y))
                {
                    NextPos aNextPos;
                    aNextPos.nextPosSteps = nextPosHasSteps(x + step[i].x, y + step[i].y);
                    aNextPos.nextPosDirection = i;
                    aNextPos.nextPosToMidLength = posToMidLength(x + step[i].x,y + step[i].y);
                    nextPosQueue.push(aNextPos);
                }
            }
    
            while(nextPosQueue.size())
            {
                int d = nextPosQueue.top().nextPosDirection;
                nextPosQueue.pop();
    
                x += step[d].x;
                y += step[d].y;
                board[x][y] = t + 1;
                BackTrace(t + 1, x, y,xstart,ystart);
                //回溯
                board[x][y] = 0;
                x -= step[d].x;
                y -= step[d].y;
            }
        }
    }
    
    
    void horseRun(int xstart,int ystart)
    {
        //初始化棋盘
        for (int i = 0; i < M; i++)
            for (int j = 0; j < N; j++)
                board[i][j] = 0;
        int midx = M / 2 -1;
        int midy = N / 2 -1;
        board[midx][midy] = 1; //从棋盘的中间的位置开始马周游
        BackTrace(1, midx, midy,xstart,ystart);
    }
    
    int main(void)
    {
        //马周游起始位置
        int x, y;
    
        cout<<"请输入棋盘大小m*n�|m-n|<=2 且 m和n都为偶数 且 m,n < 20 :";
        cin>>M>>N;
    
        cout<<"请输入马周游起始位置--横纵坐标0 <= x < "<<M<<"和0 <= y < "<<N<<" :";
        cin>>x>>y;
    
        horseRun(x,y); //执行马周游
        return 0;
    }


    运行效果:



    说明:这个程序的极限是 20 ,当棋盘大小达到 20 * 20 的时候就很难跑出结果,但是小于20的棋盘都可以很快的跑出结果。


    好了,关于马周游问题就讲述到这里,欢迎交流讨论。微笑



  • 相关阅读:
    TCGA下载神器--TCGAbiolinks
    perl模块终极解决方案--转载
    Web前端优化
    关于CDN
    手写RateLimiter
    互联网高并发之限流
    互联网高并发之Hystrix实现服务隔离和降级
    关于Hystrix
    Apollo原理
    SpringBoot2.0之整合Apollo
  • 原文地址:https://www.cnblogs.com/riasky/p/3478384.html
Copyright © 2020-2023  润新知