马踏棋盘:一个贪心算法实例

Posted 吃代码的猫

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了马踏棋盘:一个贪心算法实例相关的知识,希望对你有一定的参考价值。

问题描述:

在8×8方格(国际象棋)的棋盘上,从任意指定方格出发,为马寻找一条走遍棋盘每一格并且只经过一次的一条路径

问题分析:

首先这是一个搜索问题,运用深度优先搜索进行求解是完全可行的,它输入的是全部解,但是马遍历当8×8时解是非常之多的,用天文数字形容也不为过,这样一来求解的过程就非常慢,并且出一个解也非常慢。

怎么才能快速地得到部分解呢?

其实马踏棋盘的问题很早就有人提出,且早在1823年,J.C.Warnsdorff就提出了一个有名的算法。在每个结点对其子结点进行选取时,优先选择‘出口’最小的进行搜索,如: 马当前的位置在(i,j)只有三个出口,它们的位置是(i+2,j+1)、(i-2,j+1)和(i-1,j-2),如分别走到这三个位置,这三个位置又分别会有不同的出口,假定这三个位置的出口个数分别为4、2、3,则程序就选择让走向出口个数最少的(i-2,j+1)位置。该过程没有回溯,但对于某些开始位置实际上有解,而该算法不能找到解,对于这种情况,只要改变八种可能出口的选择顺序就能找到解

为什么要这样选取?

这是一种局部调整最优的做法,如果优先选择出口多的子结点,那出口少的子结点就会越来越多,很可能出现‘死’结点(顾名思义就是没有出口又没有跳过的结点),这样对下面的搜索纯粹是徒劳,这样会浪费很多无用的时间,反过来如果每次都优先选择出口少的结点跳,那出口少的结点就会越来越少,这样跳成功的机会就更大一些。这种算法称为为贪心算法。

算法思想步骤概括为下面的几点:

 
   
   
 
  1. 1.找到一个位置的各个方向的出口(OUT1)。

  2. 2.对各个方向的出口进行二次出口(OUT2)的搜索。

  3. 3.记录每个方向上出口(OUT1)的二次出口(OUT2),(OUT2)最小的的那个(OUT1)即为接下来要访问的位置。

  4. 4.将每次访问过的位置放入容器。

  5. 5.如果不能够正常找到,则回退一步,改变出口顺序。

代码如下(C/C++):

 
   
   
 
  1. #include <stdio.h>

  2. #include <stdlib.h>

  3. #define N 8 //棋盘的规模

  4. #define M 8 //棋子有8个方向可以走

  5. /*采用贪心算法

  6. *每一次选择的时候选择当前节点可走方向中出口最少的那一个

  7. *这样走的步数越多,剩下的节点中可走的方向越多,能够遍历所有点的可能性越大

  8. *但是这存在一个问题,就是可能这个点本来是可以走过所有点的,但是由于选择

  9. *导致没有走出,这时只要遍历8个方向就一定能够走出来

  10. */

  11. int VISITED[N][N];//记录已经走过的点

  12. /*

  13. *node结构体:path中每个元素的结构

  14. *x:int,记录横坐标

  15. *y:int,记录纵坐标

  16. *dir:int,记录处于该点时行走的方向

  17. */

  18. struct node

  19. {

  20. int x;

  21. int y;

  22. int dir;

  23. };

  24. /*

  25. *stack结构体:记录行走路径的结构体

  26. *point:int,栈顶指针

  27. */

  28. struct Stack

  29. {

  30. node path[N * N];

  31. int point;

  32. };

  33. /*

  34. *Dir:记录行走的的坐标变化单位量

  35. *x:int,x的变化量

  36. *y:int,y的变化量

  37. */

  38. struct Dir

  39. {

  40. int x;

  41. int y;

  42. };

  43. //记录每个点八个方向中哪些可以走完全部的点,默认全部都可以

  44. //主要用于回退

  45. int nodeCanDir[N][N][M];

  46. Dir direction[M] = {{2, 1}, {1, 2}, {-1, 2}, {-2, 1}, {-2, -1}, {-1, -2}, {1, -2}, {2, -1}}; //记录某个方向的坐标变化值

  47. /*

  48. *eBound:检查坐标是否越界

  49. *input:

  50. * x:int,横坐标

  51. * y:int,纵坐标

  52. *return:

  53. *bool,越界返回false,否则返回true

  54. */

  55. bool eBound(int x, int y)

  56. {

  57. if (x < 0 || x >= N)

  58. {

  59. return false;

  60. }

  61. else if (y < 0 || y >= N)

  62. {

  63. return false;

  64. }

  65. return true;

  66. }

  67. /*

  68. *cDir:算出某点可走的方向数

  69. *input:

  70. * x:int,横坐标

  71. * y:int,纵坐标

  72. *return:

  73. *int,该点可走的方向数

  74. */

  75. int cDire(int x, int y)

  76. {

  77. int number = 0;

  78. int temp_x;

  79. int temp_y;

  80. for (int i = 0; i < M; i++)

  81. { //按照i方向更新坐标

  82. temp_x = x + direction[i].x;

  83. temp_y = y + direction[i].y;

  84. //判断这个方向是否可走,可走的话number加1

  85. if (eBound(temp_x, temp_y) && VISITED[temp_x][temp_y] == 0)

  86. {

  87. number++;

  88. }

  89. }

  90. return number;

  91. }

  92. /*

  93. *selDir:选择行走方向(这个方向是a的话,那么走到a后可行走的方向数最少)

  94. *input:

  95. * x:int,横坐标

  96. * y:int,纵坐标

  97. *return:

  98. *dire:行走方向

  99. *int,所选方向拥有的可走方向数

  100. */

  101. int selDir(int x, int y, int *dire)

  102. {

  103. int minDir = 0; //可走方向最少的方向

  104. int minNum = M + 1; //可走方向最少的那个方向有多少可以走的

  105. int temp;

  106. int temp_x;

  107. int temp_y;

  108. for (int i = 0; i < M; i++)

  109. {

  110. temp_x = x + direction[i].x;

  111. temp_y = y + direction[i].y;

  112. //如果这个方向是出口,走到这个出口时下一步有多少个出口

  113. if (eBound(temp_x, temp_y) && nodeCanDir[x][y][i] == 0 && VISITED[temp_x][temp_y] == 0)

  114. {

  115. temp = cDire(x + direction[i].x, y + direction[i].y);

  116. //更新具有最小出口点的坐标

  117. minDir = (minNum >= temp) ? i : minDir;

  118. minNum = (minNum >= temp) ? temp : minNum;

  119. }

  120. }

  121. *dire = minDir;

  122. return minNum;

  123. }

  124. /*

  125. *push:进栈函数

  126. * x:int,横坐标

  127. * y:int,纵坐标

  128. * dire:行走方向

  129. */

  130. void push(Stack *p, int x, int y, int dire)

  131. {

  132. int point = p->point;

  133. p->path[point].x = x;

  134. p->path[point].y = y;

  135. p->path[point].dir = dire;

  136. p->point++;

  137. }

  138. /*

  139. *pop:出栈函数

  140. *p:栈顶指针

  141. *f:存储出栈点信息

  142. *return:

  143. *是否出栈成功

  144. */

  145. bool pop(Stack *p,node *f){

  146. //如果栈空,则无法出栈

  147. if(p->point==0){

  148. return false;

  149. }

  150. else{

  151. f->x=p->path[p->point].x;

  152. f->y=p->path[p->point].y;

  153. f->dir=p->path[p->point].dir;

  154. p->point--;

  155. return true;

  156. }

  157. }

  158. /*

  159. *jump:行走函数

  160. *p:栈顶指针

  161. * x:int,横坐标

  162. * y:int,纵坐标

  163. */

  164. bool jump(Stack *p, int x, int y)

  165. {

  166. if (p->point == N * N)

  167. {

  168. return true;

  169. }

  170. else

  171. {

  172. int minDir;

  173. int minNum;

  174. minNum = selDir(x, y, &minDir);

  175. //如果selDir过程中没有任何可走的方向

  176. if (minNum == M + 1)

  177. {

  178. node *f=(node*)malloc(sizeof(node));

  179. //如果是最后一个点

  180. if (p->point == N * N - 1)

  181. {

  182. push(p, x, y, minDir);

  183. free(f);

  184. return true;

  185. }//出栈后没有空,则回溯

  186. else if(pop(p,f)){

  187. x=f->x;

  188. y=f->y;

  189. VISITED[x][y]=0;

  190. nodeCanDir[x][y][f->dir]=1;//此路不通

  191. free(f);

  192. return jump(p,x,y);

  193. }else{

  194. printf("Sorry! Path don't exist.");

  195. free(f);

  196. return false;

  197. }

  198. }

  199. else

  200. { //如果是正常找到一个方向,将当前节点进栈,继续递归寻找

  201. //将当前点入栈

  202. push(p, x, y, minDir);

  203. VISITED[x][y] = 1;

  204. //更新点坐标

  205. x = x + direction[minDir].x;

  206. y = y + direction[minDir].y;

  207. //继续下一步

  208. return jump(p, x, y);

  209. }

  210. }

  211. }

  212. int main()

  213. {

  214. Stack *p = (Stack *)malloc(sizeof(Stack));

  215. p->point = 0; //堆栈初始化

  216. printf("Please input start position between (0,0) and (8,8):\n");

  217. int startX = 2;

  218. int startY = 5;

  219. printf("x:");

  220. scanf("%d",&startX);

  221. printf("y:");

  222. scanf("%d",&startY);

  223. if (jump(p, startX, startY))

  224. {

  225. for (int i = 0; i < p->point; i++)

  226. {

  227. printf("(%d,%d)--", p->path[i].x, p->path[i].y);

  228. if(i!=0&&i%6==0){

  229. printf("\n");

  230. }

  231. }

  232. }

  233. printf("\n");

  234. free(p);

  235. system("pause");

  236. return 0;

  237. }

结果:

参考文章: 主要参考了这篇文章关于回溯的处理用C语言解决棋盘上马遍历问题和这篇文章对贪心算法用于马踏棋盘的讲解马踏棋盘(马的遍历问题)


以上是关于马踏棋盘:一个贪心算法实例的主要内容,如果未能解决你的问题,请参考以下文章

马踏棋盘算法详解

程序员常用 10 种算法之马踏棋盘算法

Day600&601.马踏棋盘算法 -数据结构和算法Java

小甲鱼数据结构和算法--马踏棋盘(骑士周游问题)

算法马踏棋盘算法 骑士走周游算法

第28章 算法优化体验课 - 骑士周游问题