国际象棋马跳棋盘问题
问题描述
设计一个国际象棋的马跳棋盘的演示程序。
基本要求
将马随即放在国际象棋的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;
}