[算法设计与分析] 回溯法求解骑士问题/马周游问题 (C++)

实验课程:算法分析与设计

实验名称:回溯法的应用[骑士问题] (综设型实验)

 

第一部分 实验内容

1.实验目标

(1)熟悉使用回溯法求解问题的基本思路。

(2)掌握回溯算法的程序实现方法。

(3)理解回溯算法的特点。

2.实验任务

(1)从所给定的题目中选择一题,使用回溯法求解之。

(2)用文字来描述你的算法思路,包括解空间、限界函数、算法主要步骤等。

(3)使用C/C++语言编程实现算法。

(4)记录运行结果,包括输入数据,问题解答及运行时间。

(5)分析算法最坏情况下时间复杂度和空间复杂度。

(6)谈谈实验后的感想,包括关于该问题或类似问题的求解算法的建议。

3.实验设备及环境

PC;C/C++等编程语言。

4.实验主要步骤

(1)    根据实验目标,明确实验的具体任务;

(2)   设计求解问题的回溯算法,并编写程序实现算法;

(3)   设计实验数据并运行程序、记录运行的结果;

(4)   分析算法时空性能;

(5)   实验后的心得体会。

第二部分 问题及算法

1.     问题描述

[骑士问题]:在n*n的棋盘上,指定马的初始位置,按照马跳日的规则,给出马跳过棋盘每格恰好一次最后回到出发位置的一种跳法。

2.回溯法的一般思路

骑士问题,即马周游棋盘,也就是要遍历棋盘中的所有格子有且只能一次,那么很显然就是一个图的遍历问题了。

3.求解问题的回溯算法描述

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

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

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

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

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

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

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

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

4. 算法实现的关键技巧

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

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

 

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

 

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

 

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

第三部分 实验结果与分析

1.     实验数据及结果

测试数据和运行输出及结果

8*8 从(1,1)开始

《[算法设计与分析] 回溯法求解骑士问题/马周游问题 (C++)》

2.    实验分析及结论

结论:马可以从任何一格出发恰好访问每个方格一次,并回到起始位置上。

第四部分 心得与展望

1.     自我评价及心得体会

回溯法并不简单,参考了许多资料才略知一二。

2.  展望

超过20的棋盘执行非常缓慢,希望未来能够优化它。

第五部分 附录

源程序 

#include <iostream>
#include <cstdlib>
#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} };

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() {
    //马周游起始位置
    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;
}

 

参考文献

 

[1]    算法设计技巧与分析  [沙特] M.H.Alsuwaiyel(阿苏外耶)著 (电子工业出版社)

 

    原文作者:骑士周游问题
    原文地址: https://blog.csdn.net/a120790391/article/details/80713067
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞