一、马踏棋盘算法
1、国际象棋的棋盘为8*8的方格棋盘,将“马”放在任意指定的方格中,按照“马”走棋的规则将“马”进行移动。要求每个方格只能进入一次,最终使得“马”走遍棋盘64个方格。
2、关于国际象棋“马”的走法:以“日”字为单位往斜对角上的那个方格走。如下图所示马在当前位置就有8种走法,如果不考虑边缘的特殊情况,那么需要8^64的时间复杂度才可以把整个棋盘的一个正确路径找出来。
3、对于在n*n的棋盘上,当n>=5且为偶数的时候,以任意点作点都有解。
4、实现代码如下:
/************************************************************/
/** 马踏棋盘算法(骑士周游问题) **/
/************************************************************/
#include <stdio.h>
#include <time.h>
#define X 8
#define Y 8
int chess[X][Y];
/**
* 打印棋盘,棋盘中每个格子的数值就是遍历的次序
*/
void Print()
{
int i, j;
for(i = 0; i < X; i++)
{
for(j = 0; j < Y; j++)
{
printf("%2d\t", chess[i][j]);
}
printf("\n");
}
printf("\n");
}
/**
* 找到基于马当前在棋盘中的位置的(x,y)坐标的下一个可走的位置的坐标,如果成功找到则返回1,且直接修改原来位置的坐标;
* 否则返回0
* 成功找到的条件是该位置存在且还没有被马走过
* @param x:当前马所在棋盘位置的x坐标
* @param y:当前马所在棋盘位置的y坐标
* @param count:不考虑边缘的情况,马在任意一位置的下一个位置都可能有八种情况,count就是对这八种情况进行判断
只要找到其中一种就返回1
*/
int NextXY(int *x, int *y, int count)
{
switch(count)
{
case 1:
if(*x + 2 <= X - 1 && *y - 1 >= 0 && chess[*x + 2][*y - 1] == 0)
{
*x += 2;
*y -= 1;
return 1;
}
break;
case 2:
if(*x + 2 <= X - 1 && *y + 1 <= Y - 1 && chess[*x + 2][*y + 1] == 0)
{
*x += 2;
*y += 1;
return 1;
}
break;
case 3:
if(*x + 1 <= X - 1 && *y - 2 >= 0 && chess[*x + 1][*y - 2] == 0)
{
*x += 1;
*y -= 2;
return 1;
}
break;
case 4:
if(*x + 1 <= X - 1 && *y + 2 <= Y - 1 && chess[*x + 1][*y + 2] == 0)
{
*x += 1;
*y += 2;
return 1;
}
break;
case 5:
if(*x - 2 >= 0 && *y - 1 >= 0 && chess[*x - 2][*y - 1] == 0)
{
*x -= 2;
*y -= 1;
return 1;
}
break;
case 6:
if(*x - 2 >= 0 && *y + 1 <= Y - 1 && chess[*x - 2][*y + 1] == 0)
{
*x -= 2;
*y += 1;
return 1;
}
break;
case 7:
if(*x - 1 >= 0 && *y - 2 >= 0 && chess[*x - 1][*y - 2] == 0)
{
*x -= 1;
*y -= 2;
return 1;
}
break;
case 8:
if(*x - 1 >= 0 && *y + 2 <= Y - 1 && chess[*x - 1][*y + 2] == 0)
{
*x -= 1;
*y += 2;
return 1;
}
break;
default:
break;
}
return 0;
}
/**
* 深度优先遍历棋盘
* @param x:当前马即将要走的位置的x坐标
* @param y:当前马即将要走的位置的y坐标
* @param step:当前马即将要走的这个位置是第step步,也就是说马已经走过了step-1个棋盘
*/
int TraversalChessBoard(int x, int y, int step)
{
// 定义x1和y1变量保存马即将要走的这个位置的坐标
int x1 = x, y1 = y;
// flag保存是否有下一个可走的位置;count用于循环判断八种可走的位置
int flag = 0, count = 1;
// 标记该位置已经走过了
chess[x][y] = step;
// 如果此时step等于棋盘格子的个数,则说明已经把棋盘的每一个格子都走过一次了
// 则打印棋盘输出走过的顺序;这也是下面的递归的返回条件吧
if(step == X * Y)
{
Print();
return 1;
}
// 如果还没有走完棋盘,则选择下一个位置,依次判断那八种可能的情况
flag = NextXY(&x1, &y1, count);
while(flag == 0 && count < 8)
{
count++;
flag = NextXY(&x1, &y1, count);
}
// 在当前位置找到下一个可走的位置
while(flag)
{
// 递归调用走向下一个位置,因为在NextXY函数中直接修改了当前位置的坐标,所以此时的x1和y1就表示下一个可走位置的坐标
// 如果此时返回的是1,说明棋盘已经走完了,继续向上返回1
if(TraversalChessBoard(x1, y1, step + 1))
{
return 1;
}
// 返回的不是1,则返回上一个位置,在上一个位置重新选择一个可走位置继续
x1 = x;
y1 = y;
// 前count种可走位置的情况都判断过了,从count+1种情况继续判断
count++;
flag = NextXY(&x1, &y1, count);
while(flag == 0 && count < 8)
{
count++;
flag = NextXY(&x1, &y1, count);
}
}
// 八种可能的情况都判断过了,还是没有下一个可走的位置了
// 那么说明该次深度遍历失败,此种走法不行,则应该从当前位置回退到上一个位置
// 所以重新标记当前位置还没有被走过
if(0 == flag)
{
chess[x][y] = 0;
}
// 如果都没有可走的位置了,则返回0,代表继续向后回退
return 0;
}
int main()
{
int i, j;
clock_t start, finish;
start = clock();
for(i = 0; i < X; i++)
{
for(j = 0; j < Y; j++)
{
chess[i][j] = 0; // 初始化棋盘
}
}
// 从坐标(2,0)开始走
if(!TraversalChessBoard(2, 0, 1))
{
printf("遍历棋盘失败......\n");
}
finish = clock();
printf("本次遍历共耗时:%f秒\n", (double)(finish - start)/CLOCKS_PER_SEC);
return 0;
}
5、从实现代码上来看,该算法运用到了递归法、回溯法以及图的深度遍历思想,不断的递归判断,失败再回退,直到成功遍历或者失败为止。不过实现方式还有很大的可优化空间。运行结果如下,需要化很长一段时间来运行,不过起始点的选择很重要,有些时候选择了其他的起始点,则有可能很久很久都计算不出来。
6、其他相关概念
哈密尔顿路径:图G中的哈密尔顿路径指的是经过图G中每个顶点,且只经过一次的一条轨迹。如果这条轨迹是一条闭合的路径(从起点出发不重复地遍历所有点后仍能回到起始点),那么这条路径称为哈密尔顿回路。没有闭合的叫通路或路径。