跳马问题(骑士周游问题)初探
2013-05-08 12:20
936 查看
跳马问题(骑士周游问题)初探
2007-09-10 16:006253人阅读 评论(11)收藏
举报
算法datefile出版优化
跳马问题也称为骑士周游问题,是算法设计中的经典问题。其一般的问题描述是:
考虑国际象棋棋盘上某个位置的一只马,它是否可能只走63步,正好走过除起点外的其他63个位置各一次?如果有一种这样的走法,则称所走的这条路线为一条马的周游路线。试设计一个算法找出这样一条马的周游路线。
此题实际上是一个汉密尔顿通路问题,可以描述为:
在一个8×8的方格棋盘中,按照国际象棋中马的行走规则从棋盘上的某一方格出发,开始在棋盘上周游,如果能不重复地走遍棋盘上的每一个方格,
这样的一条周游路线在数学上被称为国际象棋盘上马的哈密尔顿链。请你设计一个程序,从键盘输入一个起始方格的坐标,由计算机自动寻找并打印
出国际象棋盘上马的哈密尔顿链。
能够想到的思路是用回溯,马在每一个点最多有8种跳法,遍历所有这8种可能的跳法即可得到结果。这是回溯算法中的子集树的类型,与典型的子集树问题类型不同的是,这里每一枝有8种可能的选择,而典型的子集树问题只有0,1两种选择。
下面是该算法的实现:
/**//*
* File: KnightTravel1.cpp
* Author: eshow
* Date: 2007-09-10
* Question:
考虑国际象棋棋盘上某个位置的一只马,它是否可能只走63步,正好走过除起点外的其他63个位置各一次?如果有一种这样的走法,则称所走的这条路线为一条马的周游路线。试设计一个算法找出这样一条马的周游路线。
* Solution:
使用回溯法,马每一步至多有8种跳法,遍历这8种跳法,得到结果。这是一个子集树的回溯问题,每一个step[i]都在[0, 7]之间。设棋盘大小为N * N,则时间复杂度为O(8^(N * N)),当N = 8时,算法很慢。
*/
#include<stdio.h>
#include<stdlib.h>
#include<memory.h>
constint N
= 8;
int
step[N * N]=
...{-1};
int
chess
=...{0};
int
Jump[8][2]=
...{...{-2,-1},...{-1,-2},...{1,-2},...{2,-1},...{2,1},...{1,2},...{-1,2},...{-2,1}};
int p=
0;
int canJump(int
x, int y)
...{
if (x
>= 0&& x
< N
&& y>=
0&& y
< N
&& chess[x][y]==
0)
return
1;
return
0;
}
void BackTrace(int
t, int x,
int y)
...{
if (t
>= N
* N)
...{
p++;
for (int i=
1; i<= N
* N
-1;
++i)
...{
printf("%d",
step[i]);
}
printf("");
for (int i=
0; i< N;
++i)
...{
for (int j=
0; j< N;
++j)
printf("%2d",
chess[i][j]);
printf("");
}
printf("");
exit(1);
//return;
}
else
...{
for (int i=
0; i<
8;++i)
...{
if (canJump(x+
Jump[i][0], y+ Jump[i][1]))
...{
x+= Jump[i][0];
y+= Jump[i][1];
chess[x][y]= t
+ 1;
step[t]= i;
BackTrace(t+
1, x, y);
chess[x][y]=
0;
x-= Jump[i][0];
y-= Jump[i][1];
}
}
}
}
int main()
...{
int x
= 0;
int y
= 0;
chess[x][y]=
1;
BackTrace(1, x, y);
printf("All Results Number = %d",
p);
}
上述简单回溯算法的时间复杂度是O(8^(N * N)),因为每次都按照Jump定义的顺序遍历,因此在算某些点的时候会很慢。
可以考虑采用启发式的遍历规则:即向前看两步,当每准备跳一步时,设准备跳到(x, y)点,计算(x, y)这一点可能往几个方向跳(即向前看两步),将这个数目设为(x, y)点的权值,将所 有可能的(x, y)按权值排序,从最小的开始,循环遍历所有可能的(x, y),回溯求出结果。算法可以求出所有可能的马跳棋盘路径,算出一个可行 的结果很快,但在要算出所有可能结果时,仍然很慢,因为时间复杂度本质上并没有改变,仍为O(8^(N * N))。下面是实现这一思想的代码:
/**//*
* File: KnightTravel2.cpp
* Author: eshow
* Date: 2007-09-10
* Question:
考虑国际象棋棋盘上某个位置的一只马,它是否可能只走63步,正好走过除起点外的其他63个位置各一次?如果有一种这样的走法,则称所走的这条路线为一条马的周游路线。试设计一个算法找出这样一条马的周游路线。
* Solution:
使用回溯法,马每一步至多有8种跳法,遍历这8种跳法,得到结果。这是一个子集树的回溯问题,每一个step[i]都在[0, 7]之间。设棋盘大小为N * N,则时间复杂度为O(8^(N * N)),当N = 8时,算法很慢。
优化:当每准备跳一步时,设准备跳到(x, y)点,计算(x, y)这一点可能往几个方向跳(即向前看两步),将这个数目设为(x, y)点的权值,将所 有可能的(x, y)按权值排序,从最小的开始,循环遍历所有可能的(x, y),回溯求出结果。算法可以求出所有可能的马跳棋盘路径,算出一个可行 的结果很快,但当N
= 8时,要计算所有可能的结果仍然很慢,原因是结果太多了。BackTrace()函数实现了这种思想。
*/
#include<stdio.h>
#include<stdlib.h>
#include<memory.h>
constint N
= 8;
int
step[N * N]=
...{-1};
int
chess
=...{0};
int
Jump[8][2]=
...{...{-2,-1},...{-1,-2},...{1,-2},...{2,-1},...{2,1},...{1,2},...{-1,2},...{-2,1}};
int p=
0;
int canJump(int
x, int y)
...{
if (x
>= 0&& x
< N
&& y>=
0&& y
< N
&& chess[x][y]==
0)
return
1;
return
0;
}
int weightStep(int
x, int y)
...{
int count
= 0;
for (int i=
0; i<
8;++i)
...{
if (canJump(x+
Jump[i][0], y+ Jump[i][1]))
count++;
}
return count;
}
void inssort(int
a[], int b[],int n)
...{
if (n
<= 0)return;
for (int i=
0; i< n;
++i)
...{
for (int j=
i; j >
0;--j)
...{
if (a[j]
< a[j
-1])
...{
int temp
= a[j
-1];
a[j-
1]= a[j];
a[j]= temp;
temp= b[j
- 1];
b[j-
1]= b[j];
b[j]= temp;
}
}
}
}
void BackTrace(int
t, int x,
int y)
...{
if (t
>= N
* N)
...{
p++;
for (int i=
1; i<= N
* N
-1;
++i)
...{
printf("%d",
step[i]);
}
printf("");
for (int i=
0; i< N;
++i)
...{
for (int j=
0; j< N;
++j)
printf("%2d",
chess[i][j]);
printf("");
}
printf("");
exit(1);
//return;
}
else
...{
int count[8],
possibleSteps[8];
int k
= 0;
for (int i=
0; i<
8;++i)
...{
if (canJump(x+
Jump[i][0], y+ Jump[i][1]))
...{
count[k]= weightStep(x+
Jump[i][0], y+ Jump[i][1]);
possibleSteps[k++]=
i;
}
}
inssort(count, possibleSteps, k);
for (int i=
0; i< k;
++i)
...{
int d
= possibleSteps[i];
x+= Jump[d][0];
y+= Jump[d][1];
chess[x][y]= t
+ 1;
step[t]= d;
BackTrace(t+
1, x, y);
chess[x][y]=
0;
x-= Jump[d][0];
y-= Jump[d][1];
}
}
}
int main()
...{
int x
= 0;
int y
= 0;
chess[x][y]=
1;
BackTrace(1, x, y);
printf("All Results Number = %d",
p);
}
另外,在查阅和搜索骑士问题的资料时,看到很多朋友说可以使用贪心算法,现在做一个验证看贪心法到底对不对:在只需要一个可行结果时,用贪心算法来替代回溯算法,对KnightTravel2稍做一下修改,在每次选择下一步时都贪心的选择权值最小的那一步,这样就省去了回溯的递归,算法复杂度为O(N * N)的线性时间。代码如下:
/**//*
* File: KnightTravel3.cpp
* Author: eshow
* Date: 2007-09-10
* Question:
考虑国际象棋棋盘上某个位置的一只马,它是否可能只走63步,正好走过除起点外的其他63个位置各一次?如果有一种这样的走法,则称所走的这条路线为一条马的周游路线。试设计一个算法找出这样一条马的周游路线。
* Solution:
如果不要求找出所有结果,可以使用贪心算法,在(x, y)的选择时,永远只选择权值最小的那一个跳。就可以很快找到一个结果。travel()函数实现了这种思想。但为何贪心选择可以算出结果有待证明:是一定可以算出,还是可能性很大?验证N = 8的棋盘遍历所有可能的起始点,用贪心法在 x = 5, y = 3时解不出结果,而用回溯遍历所有可能则可以得出结果。因此贪心法解该问题是不正确的。
*/
#include<stdio.h>
#include<stdlib.h>
#include<memory.h>
constint N
= 8;
int
step[N * N]=
...{-1};
int
chess
=...{0};
int
Jump[8][2]=
...{...{-2,-1},...{-1,-2},...{1,-2},...{2,-1},...{2,1},...{1,2},...{-1,2},...{-2,1}};
int canJump(int
x, int y)
...{
if (x
>= 0&& x
< N
&& y>=
0&& y
< N
&& chess[x][y]==
0)
return
1;
return
0;
}
int weightStep(int
x, int y)
...{
int count
= 0;
for (int i=
0; i<
8;++i)
...{
if (canJump(x+
Jump[i][0], y+ Jump[i][1]))
count++;
}
return count;
}
//a是要排序的数组,b是a中的步子的索引,用于贪心选择
int getMin(int a[],int
b[], int n)
...{
if (n
<= 0)-1;
int min
= a[0];
int stepIndex=
b[0];
for (int i=
1; i< n;
++i)
...{
if (min
> a[i])
...{
min= a[i];
stepIndex= b[i];
}
}
return stepIndex;
}
bool travel(int
x, int y)
...{
chess[x][y]=
1;
int x0
= x, y0
= y;
for (int s=
1; s< N
* N;
++s)
...{
int count[8],
possibleSteps[8];
int k
= 0;
for (int i=
0; i<
8;++i)
...{
if (canJump(x+
Jump[i][0], y+ Jump[i][1]))
...{
count[k]= weightStep(x+
Jump[i][0], y+ Jump[i][1]);
possibleSteps[k++]=
i;
}
}
if (k
> 0)
...{
int d
= getMin(count, possibleSteps, k);
x+= Jump[d][0];
y+= Jump[d][1];
chess[x][y]= s
+ 1;
step[s]= d;
}
else
...{
printf("Start at %d, %d can NOT travel the chess.",
x0, y0);
return
false;
}
}
printf("Start at %d, %d can travel the chess:",
x0, y0);
for (int i=
1; i<= N
* N
-1;
++i)
...{
printf("%d",
step[i]);
}
printf("");
for (int i=
0; i< N;
++i)
...{
for (int j=
0; j< N;
++j)
printf("%2d",
chess[i][j]);
printf("");
}
printf("");
return
true;
}
int main()
...{
int x
= 0;
int y
= 0;
chess[x][y]=
1;
travel(x, y);
}
但很遗憾,实验证明贪心法并不是正确的,因为不能证明贪心选择一定会得到问题的解。可以举出反例:当马开始在(5, 3)位置时,使用贪心算法得不到可行路径,但使用改进后的回溯算法KnightTravel2,则可以解出结果。
综上所述,骑士周游问题不能使用贪心法求解。改进后的回溯法是一个可行的方案,但时间复杂度仍然很高。在王晓东的《计算机算法设计与分析》一书上看到该问题可以用分治递归法求解,但一直没有想出答案,网上也很难找到相关方面的资料。
【参考文献】
[1] 《计算机算法设计与分析(第2版)》 王晓东 电子工业出版社/article/9217420.html
相关文章推荐
- 跳马问题(骑士周游问题)初探
- 再探跳马问题(骑士周游问题)
- 再探跳马问题(骑士周游问题)
- 马踏棋盘算法(骑士周游问题)- 数据结构和算法60
- POJ 1915 Knight Moves 骑士遍历问题(跳马问题)
- 【DS】骑士周游问题
- 小甲鱼数据结构和算法--马踏棋盘(骑士周游问题)
- 马踏棋盘算法(骑士周游问题)
- 马踏棋盘算法(骑士周游问题)- 数据结构和算法60
- POJ 2488 A Knight's Journey(DFS——骑士周游问题)
- 数据结构学习之启发式搜索求解骑士周游问题
- 图论 --- 骑士周游问题,DFS
- 骑士周游问题
- 马踏棋盘算法(骑士周游问题)
- HDU 1372(骑士周游问题)
- 跳马问题(骑士问题)
- scau实验题 8600 骑士周游问题(有障碍物)
- 骑士周游问题(暴力解决:回溯法)
- C语言-数据结构-骑士周游-马踏棋盘问题-源代码
- poj_2488_A Knight's Journey_骑士周游问题