分治法解骑士巡游问题(Knight's Tour)

问题描述

国际象棋的棋盘为 m × n m\times n 的方格棋盘,现将“马”放在任意指定的方格中,按照“马”走棋的规则(与中国象棋规则一样,马走“日”字)将“马”进行移动。要求每个方格只能进入一次,最终使得“马”走遍棋盘 m × n m\times n 个方格,回到起点。
编写代码,实现马周游操作,用数字给出“马”移动的路径并格式化输出。

参考资料:
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.

解题思路

分治法的核心思想,就是“分而治之”。因此分治求解该问题的基本方法就是

将大棋盘分成小棋盘;
小棋盘直接给出答案;
小棋盘拼接成大棋盘。

m n 2 \lvert m-n\rvert\le2 条件下的解

首先易证明对于 m × n m×n 尺寸的棋盘,若 m × n m×n 为奇,则该棋盘上不存在马的周游回路。例如,若给棋盘每格涂上两种颜色中的一种,所有相邻两格颜色不同,则马的每一步都将从一种颜色的格子跳入另一种颜色的格子。奇数总格数的棋盘需要奇数步才能够遍历完,那么出发时的格子颜色与最后一步到达的格子颜色必定不同,由此证明该棋盘上马的周游无法形成回路。
首先定义结构化概念:如果马的周游路线包括下图所示的几步,则称其为结构化的
structured circuit
直接给出几个基本棋盘的结构化回路:
6x6——10x10
5x6——10x11
从左到右、从上到下分别是 6 × 6 , 6 × 8 , 8 × 8 , 8 × 10 , 10 × 10 , 10 × 12 , 6 × 7 , 7 × 8 , 8 × 9 , 9 × 10 , 10 × 11 , 11 × 12 6\times6, 6\times8, 8\times8, 8\times10, 10\times10, 10\times12, 6\times7, 7\times8, 8\times9, 9\times10, 10\times11, 11\times12 的棋盘的回路。

当棋盘大小 m , n 12 m,n≥12 m n 2 |m-n|≤2 时,棋盘可拆分成4k个结构化的基本棋盘,每相邻的四个棋盘上的回路都可按下图步骤所示合并:断开A、B、C、D,连接E、F、G、H。
combining method
设该算法时间复杂度为 T ( n ) T(n) ,其递归方程为 T ( n ) = { O ( 1 ) n<12 4 T ( n / 2 ) + O ( 1 ) n 12 T(n)=\begin{cases}O(1)&\text{n<12}\\4T(n/2)+O(1) &\text{n}\ge\text{12}\end{cases} 。解得 T ( n ) = O ( n 2 ) T(n)=O(n^2)

编程细节要点为

如何存储棋盘
如何把棋盘正确分割为基本棋盘
如何将基本棋盘的路线正确填入原棋盘

部分关键代码如下

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)
	......
}

m n m、n 任意情况下的解

具体请参考第二篇文献"Knight’s Tour Revisited"。大致思路是,直接给出一系列基本棋盘的曼哈顿回路和曼哈顿链,其中曼哈顿链都满足从棋盘某个角出发在另一角结束。如此就可将大棋盘拆分成若干小棋盘(不一定均分),然后将各小棋盘中的曼哈顿链和曼哈顿回路联接成一个大回路,即得解。除此外,该文章作者还证明了骑士巡游问题有解的必要条件是

m × n m\times n 为偶且 m i n ( m , n ) 5 min(m,n)\ge5

代码实现较复杂暂未完成,有兴趣的读者可以自行尝试。

发布了5 篇原创文章 · 获赞 0 · 访问量 268

猜你喜欢

转载自blog.csdn.net/P01yH3dr0n/article/details/103707597