目录
题目:
问题描述
一个8×8的棋盘上有一个马初始位置为(a,b),他想跳到(c,d),问是否可以?如果可以,最少要跳几步?
输入格式
一行四个数字a,b,c,d。
输出格式
如果跳不到,输出-1;否则输出最少跳到的步数。
样例输入
1 1 2 3
样例输出
1
数据规模和约定
0<a,b,c,d≤8且都是整数。
分析:
1. 题目给了一张8*8的图。
2.玩过象棋的朋友都知道马是可以跳到任意位置的,所以从初始位置到目标位置一定可以到达。如果你不知道,没关系,你可以自己推理一下,如果你不想自己推理,也没关系,也可以让程序帮我们做。
3.那么每个点位会有8种跳的可能,但边角的点位有可能跳出棋盘。
4.棋盘上有8*8=64个点位,我们求某一点按一定要求到达另一点。很明显,这是一道无权图的最短路径问题。
5.无权图的最短路径问题我们可以使用广度优先遍历来实现。
6.想象以初始位置结点为起始节点,跳一次可达到的结点为第一层,跳两次可达到的结点为第二层...,我们广度优先的一层层遍历,先找到目标结点,就输出层数。那么最少步数就出来啦。
注:因为我已知棋子一定是可以跳到目标位置的,所以没有进行是否跳得到的判断(你问我怎么知道,其实我不怎么玩象棋,但我思考后发现做出这种判断用代码实现怪麻烦的,就自己画了个棋盘在上面推了推发现,所有位置都能跳到。)
这里有大佬做的漫画式讲解,完全解析 图的 “最短路径” 问题,链接在这:
https://zhuanlan.zhihu.com/p/65340385
C++代码:
#include <iostream>
#include <queue>
using namespace std;
//因为 0<a,b,c,d≤8且都是整数。
//所以创建棋盘,行列号分别为1~8
//因为网络上关于图的最短路径问题搞这个那个的算法很复杂,
//但实际这道题很简单,这是一个已知起点终点的无权无向图的最短路径问题,是最短路径里最简单的了
//所以我们不要把问题复杂化
//首先,建立该无向图
//简单问题简单做,不涉及增加与删除,做个顺序结构表示就好
struct chess_position
{
//定义结构体——棋子位置,8*8的棋盘有64个棋子位置构成
int x; // 行,范围1~8;
int y; // 列,范围1~8;
int next_num; // 从该位置可以跳到几个位置
int next[8]; // 从该位置可以跳到位置序号
};
struct chess_position checkerboard[64]; //定义棋盘。
//在定义完后,紧接着的那就是初始化了。
//初始化要做什么呢?
//1.每个棋子位置都是有序号的,0~63,那么这些棋子的坐标x,y需要确定。
//2.每个棋子都要找到其下一步能跳到的位置、数量。
//因为棋盘是8*8的,所以不用担心初始化的时间复杂度问题。
void Init_checkerboard()
{
int j,k; j=k=1; // 为棋盘位置赋值坐标
for(int i=0;i<64;i++)
{
checkerboard[i].x=j;
checkerboard[i].y=k;
checkerboard[i].next_num=0;
k++;
if(k>8)
{
k=1;
j++;
}
}
//接着要为每个棋子找到其下下一步可以走的位置
for(int i=0;i<64;i++)
{
//下一步位置有8个
int x= checkerboard[i].x;
int y= checkerboard[i].y;
//可以自己画张纸,看一下位置。
int next_x[8]={x-1,x+1,x-2,x+2,x-2,x+2,x-1,x+1};
int next_y[8]={y-2,y-2,y-1,y-1,y+1,y+1,y+2,y+2};
//然后判断位置是否在棋盘内,
//然后计算其序号,放入结构体next中,记下数量放入next_num中
for(int j=0;j<8;j++)
{
//判断其是否在棋盘内
if(next_x[j]>=1&&next_x[j]<=8)
{
if(next_y[j]>=1&&next_y[j]<=8)
{
//如果我们通过下一步棋子坐标,遍历棋盘在其中找序号,那未免太蠢了
//通过位置算出序号是比较快的。
int next_no;
next_no= (next_x[j]-1)*8+next_y[j]-1;
checkerboard[i].next[checkerboard[i].next_num]=next_no;
checkerboard[i].next_num++;
//初始化完成。
}
}
}
}
}
int main()
{
Init_checkerboard();
/*
//检查一下这个结构
for(int i=0;i<64;i++)
{
printf("%d %d %d ",checkerboard[i].x,checkerboard[i].y,checkerboard[i].next_num);
for(int j=0;j<checkerboard[i].next_num;j++)
{
printf("%d ",checkerboard[i].next[j]);
}
printf("\n");
}
*/
int a,b,c,d;
scanf("%d%d%d%d",&a,&b,&c,&d);
//在实现了图的数据结构、输入数据后
//首先将起始坐标与目的坐标转为序号。
int start_num=(a-1)*8+b-1;
int aim_num=(c-1)*8+d-1;
//然后我们要开始进行广度优先遍历。
queue<int> q; // 广度优先遍历所需的队列
int res=0; // 记录结果
q.push(start_num);
int now_float_num=1; // 当前层结点数
int next_float_num=0; // 下一层层结点数
int time=0; // 记录已遍历结点个数,以判断当前层数。
while (!q.empty())
{
if(q.front()==aim_num)
{
//找到了
break;
}
else
{
time++;
//这个结点不是,得把他的子节点压入队列
for(int i=0;i<checkerboard[q.front()].next_num;i++)
{
//checkerboard[q.front()].next_num是当前结点的子节点个数
q.push(checkerboard[q.front()].next[i]);
}
//然后下一层的结点数更新。
next_float_num+=checkerboard[q.front()].next_num;
//然后吐出当前遍历过的结点
q.pop();
//然后得判断一下当前层的结点是不是都访问完了
if(time==now_float_num)
{
//若是访问完了,那么就得
//1.更新层数
//2.更新当前层结点数
//3.更新下一层结点数
//4.重置当前层遍历个数
res++;
now_float_num=next_float_num;
next_float_num=0;
time=0;
}
}
}
printf("%d",res);
return 0;
}
最后:
代码中的广度优先遍历与普遍性的广度优先遍历有些不同:
1.这里在遍历时通过计数来计算遍历层数,这里遍历层数即结果
2.没有对已经访问过的位置进行标记,因为这里不需要
3.在输出结果时,没有考虑到找不到路径的情况,因为我们已知一定有解的。
这样,我们就很容易的就做出了这道题的普适性解,为什么普适呢?因为无论棋盘多大,都能做出一点修改依然适用。而且最终程序性能也还不错。
细心的小伙伴会发现,其中棋子位置数据结构中的坐标其实是可有可无的,但方便理解嘛。反正也不占多少空间。
代码中其实有很多没必要的地方,就像把棋子位置编号,存储其下一步可能之类的。但这样有助于思考,可以让问题简单化。所以按照这样的思路,代码整体看起来好像很长,但思考难度小,容易编写,且在问题较大时(当然,这个8*8是很小的),能够通过牺牲空间来解放时间,而且代码有很多是注释。
欢迎交流。