每周一题(8)

国际象棋马跳棋盘问题

问题描述

      设计一个国际象棋的马跳棋盘的演示程序。

基本要求

      将马随即放在国际象棋的88棋盘Board[8][8]的某个方格中,马按照走棋规则进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格。编制非递归程序,求出马的行走路线,并按求出的行走路线将数字1,2,…,64依次填入一个88的方阵,输出之。

测试数据

      马的初始位置(i,j)自定。

实现提示

      下图显示了马位于方格(2,3)时,8个可能的移动位置。一般来说,当马位于位置(i,j)时,可以走到下列8个位置之一:
(i-2,j+1)(i-1,j+2)(i+1,j+2)(i+2,j+1)
(i+2,j-1)(i+1,j-2)(i-1,j-2)(i-2,j-1)
      如果(i,j)靠近棋盘的边缘,上述有些位置可能超出棋盘范围,成为不允许的位置。8个可能位置可以用两个一维数组或链表或二维数组表示。
      位于(i,j)的马可走到的新位置是棋盘范围内的(i+HTry1[h],j+ HTry2[h]),其中h=0,1,…,7。
      每次在多个可走位置中选择其中一个进行试探,其余未曾试探过的可走位置必须用适当结构妥善管理,以备试探失败时的“回溯”(悔棋)使用。
在这里插入图片描述在这里插入图片描述

选作内容

      探讨每次选择位置的“最佳策略”,以减少回溯的次数。

题解

      根据题意,对于棋盘中的每个格子,需要知道它的横纵坐标值、以及序号(第几次走到这个格的),所以可以这样定义格子结构体:

typedef struct{
    int x,y; //坐标
    int number; //序号
} Point;

      格子的起点序号为1。有了每个格子,就可以定义整个棋盘了。棋盘大小为n阶。

int step = 1;
Point board[10005][10005];
int n; //棋盘大小 

      根据情况,每个马棋可以向周围8个方向走日字格。所以可以通过在原来坐标的基础上进行加减法来实现移动,可以这样定义八种走法:

int fx[]= { 1, 2, 2, 1,-1,-2,-2,-1};
int fy[]= { 2, 1,-1,-2,-2,-1, 1, 2};

      定义完需要的所有东西后,就可以开始编写过程了。首先看main()函数,在这里写出了整道题的执行流程。

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;
}

      在main()中写到了,首先输入棋盘的大小n,然后定义第一个落下的棋子,输入第一个落下棋子的位置。随后就可以让他去走棋盘了,可以使用一个走棋盘的函数horseRun(),走完以后使用函数outputResult(),将棋盘走完后的结果输出。接下来就可以先设计这两个函数了。
      首先是horseRun()函数,题目要求是非递归算法,所以这里不能用递归深度优先(尽管棋盘的规模很小)。那么函数类型为空,不需要返回值,入参是第一个棋子,所以函数名为:void horseRun(Point point)。这个函数也是本题的核心函数。
      那么在进行走棋之前,我们先要思考怎样来减少回溯(回溯:当棋子的当前的位置已经不能再继续向下走时,要退回到上一个位置进行第二种走法)次数,设计出能够减少回溯次数的策略,再将这种“剪枝”运用到void horseRun(Point point)函数当中。
      在判断是否应该进行回溯时,要考虑到两点:一是要走的位置不能超出棋盘的边界,二是已经走过的地方不能再走,所以用这样一个函数进行判断:

bool check(int x,int y){
    if(x<1 || y<1 || x>n || y>n || board[x][y].number != 0)
        return false;
    return true;
}

      那么减少回溯的策略(每个位置有8种方向可选择,要减少回溯,本质上就是要尽可能选择可走方向最少的位置,因为能走的方向越少,可能再走回来的方向也就越少),这里我想到了两种办法。第一种:选择所有可能的下一步中走法最少的作为下一步。要实现这种策略,就要想办法判断出当前位置可以有多少种走法。

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;
}

      第二种策略:尽可能优先走靠边的位置,少走中间,这种办法可以用两个极端情况来考虑:在棋盘最中间和在棋盘的顶角处。由最中间出发,可走8个方向,由最顶角出发,只可走2个方向。如下图:
在这里插入图片描述
      随后就是如何判断当前位置更靠近边界,方法就是计算当前位置到达棋盘中点的距离,使用两点间的距离公式:距离2=(x1-x2)2+(y1-y2)2

int nextPosHasDist(int x, int y){
	int cx=n/2,cy=n/2;//中心坐标
	if(n%2==1){
		cx+=1;
		cy+=1;
	}
	int dist=abs(x-cx)^2+abs(y-cy)^2;//距离 
	return dist; 
} 

      有了两种优化方法,接下来就可以编写核心函数了。这里需要用到一个队列,队列里存入的是格子,开始先将起点格子入队,后续每次循环,队首元素就代表当前的格子,将周围8个方向中最合适的格子入队。
步骤一:定义队列,并将起点入队。

queue<Point> pointQueue;
pointQueue.push(point);
Point temp;

步骤二:进入搜索,每次遍历取出队首元素作为当前的格子,并使格子的序号加1。

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 maxDist = 1,不断更新这两个变量来找到最优的位置(有两种情况需要更新,一是可走步数小于当前最小步数时,二是可走步数等于当前最小步数,到中心的距离大等于当前到中心距离的最大距离)。还要设一个标记变量int flag = 0,用来将最优的位置入队,每当找到更优的位置入队了,就要把flag置1,将上次入队的元素出队。
整个函数如下:

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 maxDist = 1;//距离中心最长距离 
        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 || (nextPosHasSteps(x,y)==minStep&&nextPosHasDist(x,y)>=maxDist)){
                    minStep = nextPosHasSteps(x,y);
                    Point t;
                    t.x = x;
                    t.y = y;
                    if(flag) 
						pointQueue.pop();
                    pointQueue.push(t);
                    flag = 1;//如果下一个位置走法更合适,那上一个就要出队了 
                }
            }
        }
    }
}

      到此,核心函数已经编写完毕。接下来是输出函数。为了保证输出格式不被打乱,这里还使用了iomanip库中的setw()函数来控制输出占位3个字符。

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;
}

      运行一下试试:
在这里插入图片描述
      全部代码示例如下:

#include<iostream>
#include<iomanip>
#include<queue>
#include<algorithm>
using namespace std;
/*
使用了两种减少回溯次数的策略:
一、选择所有可能的下一步中走法最少的作为下一步。 
二、在上述前提下,若有多个下一步走法相同,选择距离棋盘中间最远的作为下一步。 
*/
//在某一格子的八种走法 
int fx[]= { 1, 2, 2, 1,-1,-2,-2,-1};
int fy[]= { 2, 1,-1,-2,-2,-1, 1, 2};
 //棋盘中的格子
typedef struct{
    int x,y; //坐标
    int number; //序号
} Point;
//棋盘,使用的时候是从下标为1开始
Point board[10005][10005]; 
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;
}
//判断位置到中心的距离 
int nextPosHasDist(int x, int y){
	int cx=n/2,cy=n/2;//中心坐标
	if(n%2==1){
		cx+=1;
		cy+=1;
	}
	int dist=abs(x-cx)^2+abs(y-cy)^2;//距离 
	return dist; 
} 
//非递归的走法
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 maxDist = 1;//距离中心最长距离 
        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 || (nextPosHasSteps(x,y)==minStep&&nextPosHasDist(x,y)>=maxDist)){
                    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;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值