问题描述
国际象棋的棋盘为
的方格棋盘,现将“马”放在任意指定的方格中,按照“马”走棋的规则(与中国象棋规则一样,马走“日”字)将“马”进行移动。要求每个方格只能进入一次,最终使得“马”走遍棋盘
个方格,回到起点。
编写代码,实现马周游操作,用数字给出“马”移动的路径并格式化输出。
参考资料:
Parberry I . An Efficient Algorithm for the Knight’s Tour Problem[J]. Discrete Applied Mathematics, 1997, 73(3):251-260.
Cull, P.; De Curtins, J. (1978). “Knight’s Tour Revisited” . Fibonacci Quarterly. 16: 276–285.
解题思路
分治法的核心思想,就是“分而治之”。因此分治求解该问题的基本方法就是
将大棋盘分成小棋盘;
小棋盘直接给出答案;
小棋盘拼接成大棋盘。
条件下的解
首先易证明对于
尺寸的棋盘,若
为奇,则该棋盘上不存在马的周游回路。例如,若给棋盘每格涂上两种颜色中的一种,所有相邻两格颜色不同,则马的每一步都将从一种颜色的格子跳入另一种颜色的格子。奇数总格数的棋盘需要奇数步才能够遍历完,那么出发时的格子颜色与最后一步到达的格子颜色必定不同,由此证明该棋盘上马的周游无法形成回路。
首先定义结构化概念:如果马的周游路线包括下图所示的几步,则称其为结构化的。
直接给出几个基本棋盘的结构化回路:
从左到右、从上到下分别是
的棋盘的回路。
当棋盘大小
且
时,棋盘可拆分成4k个结构化的基本棋盘,每相邻的四个棋盘上的回路都可按下图步骤所示合并:断开A、B、C、D,连接E、F、G、H。
设该算法时间复杂度为
,其递归方程为
。解得
。
编程细节要点为
如何存储棋盘
如何把棋盘正确分割为基本棋盘
如何将基本棋盘的路线正确填入原棋盘
部分关键代码如下
int main()
{
int m,n;
printf("请输入棋盘大小m,n(|m-n|<=2):\n");
scanf("%d%d",&m,&n);
if (m*n%2||(m<6||n<6)) printf("该棋盘不存在骑士巡游回路\n");
else if (n-m>2||m-n>2) printf("棋盘大小有误\n");
else
{
int circuit[500][500],step=1,i,j,x,y; //circuit记录每格的步序数
printf("请输入马的起点坐标:");
scanf("%d%d",&x,&y);
if (x<=0||x>=m||y<=0||y>=n) printf("非法的坐标\n");
else
{
DivideConquer(0,0,m-1,n-1);
memset(circuit,0,sizeof(circuit));
i=--x;
j=--y;
do
{
circuit[i][j]=step++; //模拟马走棋盘的过程
int a=i,b=j;
i+=direct[ChessBoard[a][b][0]][0];
j+=direct[ChessBoard[a][b][0]][1];
if (circuit[i][j]>1){
i=a+direct[ChessBoard[a][b][1]][0];
j=b+direct[ChessBoard[a][b][1]][1];
}
}while(i!=x||j!=y);
printf("骑士巡游路线如下\n");
for (int p=0;p<m;p++)
{
for (int q=0;q<n;q++)
printf("%d\t",circuit[p][q]);
printf("\n");
}
}
}
return 0;
}
void DivideConquer(int x1,int y1,int x2,int y2)
{
//分治法主要函数
int h=x2-x1+1,w=y2-y1+1,dw=w/4*2+w%2,dh=h/4*2+h%2; //dw,dh分别是棋盘宽和高的一半,且保证了至少有一个是偶数,使得分治时不会出现无解的棋盘
bool flag=false;
if (w<h)
{
flag=true;
int t=w;w=h;h=t; //将棋盘翻转并标记,使得基本棋盘的内容能正确填入
}
if (h==6&&w==6) FillBoard((int*)B6_6,x1,y1,x2,y2,flag); //填充基本棋盘
if (h==6&&w==7) FillBoard((int*)B6_7,x1,y1,x2,y2,flag);
if (h==6&&w==8) FillBoard((int*)B6_8,x1,y1,x2,y2,flag);
if (h==7&&w==8) FillBoard((int*)B7_8,x1,y1,x2,y2,flag);
if (h==8&&w==8) FillBoard((int*)B8_8,x1,y1,x2,y2,flag);
if (h==8&&w==9) FillBoard((int*)B8_9,x1,y1,x2,y2,flag);
if (h==8&&w==10) FillBoard((int*)B8_10,x1,y1,x2,y2,flag);
if (h==9&&w==10) FillBoard((int*)B9_10,x1,y1,x2,y2,flag);
if (h==10&&w==10) FillBoard((int*)B10_10,x1,y1,x2,y2,flag);
if (h==10&&w==11) FillBoard((int*)B10_11,x1,y1,x2,y2,flag);
if (h==10&&w==12) FillBoard((int*)B10_12,x1,y1,x2,y2,flag);
if (h==11&&w==12) FillBoard((int*)B11_12,x1,y1,x2,y2,flag);
if (h>11&&w>11)
{
DivideConquer(x1,y1,x1+dh-1,y1+dw-1); //递归实现分治
DivideConquer(x1+dh,y1,x2,y1+dw-1);
DivideConquer(x1,y1+dw,x1+dh-1,y2);
DivideConquer(x1+dh,y1+dw,x2,y2);
CombineBoard(x1,y1,x2,y2); //合并棋盘
}
}
void FillBoard(int *board,int x1,int y1,int x2,int y2,bool flag)
{
//实现基本棋盘的填充。flag为棋盘翻转标志
......
}
void CombineBoard(int x1,int y1,int x2,int y2)
{
//合并棋盘,时间复杂度为O(1)
......
}
任意情况下的解
具体请参考第二篇文献"Knight’s Tour Revisited"。大致思路是,直接给出一系列基本棋盘的曼哈顿回路和曼哈顿链,其中曼哈顿链都满足从棋盘某个角出发在另一角结束。如此就可将大棋盘拆分成若干小棋盘(不一定均分),然后将各小棋盘中的曼哈顿链和曼哈顿回路联接成一个大回路,即得解。除此外,该文章作者还证明了骑士巡游问题有解的必要条件是
为偶且
代码实现较复杂暂未完成,有兴趣的读者可以自行尝试。