当前位置:   article > 正文

人工智能经典问题,八数码问题求解,启发式搜索法(Astar算法),C语言版,保证看懂,分析到位,注释详细,没有bug_启发式搜索算法求解8数码问题

启发式搜索算法求解8数码问题

一、问题描述

  1. 3*3的方格棋盘上,摆放着1到8这八个数码,有1个方格是空。
  2. 要求对空格执行空格左移、空格右移、空格上移和空格下移这四个操作使得棋盘从初始状态(图左)到目标状态(图右)。

二、迟来的代码

   

  1. // 备注:曼哈顿距离等于当前状态与目标状态的水平距离和垂直距离之和
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <time.h>
  5. #include <math.h>
  6. #define N 3 // 阶数,可以改为更高阶
  7. // 定义一个结构体来表示棋盘状态
  8. typedef struct node
  9. {
  10. int cost; // 从初始状态到本状态的代价
  11. int data[N][N]; // 存放棋盘状态
  12. struct node *prev; // 链表中的前指针
  13. struct node *next; // 链表中的后指针
  14. struct node *father;// 搜索树的父节点
  15. }node;
  16. node *open; // open表,存放未拓展的节点
  17. node *close; // close表,存放已经拓展的节点
  18. static int n = 0; // 用来记录总共搜索的次数
  19. int src[N][N] = {2, 8, 3, 1, 0, 4, 7, 6, 5}; // 初始状态
  20. int cur[N][N]; // 当前状态
  21. int dest[N][N] = {1, 2, 3, 8, 0, 4, 7, 6, 5}; // 目标状态
  22. // 生成一个节点
  23. node *initList()
  24. {
  25. int i, j;
  26. node *p = malloc(sizeof(node));
  27. if(!p)
  28. {
  29. printf("malloc fail\n");
  30. return NULL;
  31. }
  32. p->prev = p;
  33. p->next = p;
  34. p->father = NULL;
  35. p->cost = 0;
  36. // 初始化棋盘状态
  37. for(i = 0; i < N; i++)
  38. {
  39. for(j = 0; j < N; j++)
  40. {
  41. p->data[i][j] = -1;
  42. }
  43. }
  44. return p;
  45. }
  46. // 头插法,把节点插入到链表头部
  47. void head_insert(node *head, node *p)
  48. {
  49. p->next = head->next;
  50. p->prev = head;
  51. head->next->prev = p;
  52. head->next = p;
  53. }
  54. // 尾插法,把节点插入到链表尾部
  55. void tail_insert(node *head, node *p)
  56. {
  57. p->prev = head->prev;
  58. head->prev->next = p;
  59. p->next = head;
  60. head->prev = p;
  61. }
  62. // 弹出节点
  63. void Remove(node *p)
  64. {
  65. p->prev->next = p->next;
  66. p->next->prev = p->prev;
  67. p->next = p;
  68. p->prev = p;
  69. }
  70. // 清空链表
  71. void clearList(node *head)
  72. {
  73. // 空表不需要再清空
  74. if(!head || head->next == head)
  75. {
  76. printf("list is null\n");
  77. return;
  78. }
  79. // 头删法,跟头插法相似,循环删除首元节点
  80. for(node *p = head->next; p != head; p = head->next)
  81. {
  82. Remove(p); // 弹出节点p
  83. free(p); // 释放节点p的空间
  84. }
  85. }
  86. // 初始化棋盘
  87. void init()
  88. {
  89. int i, j, k;
  90. int a[N*N] = {1, 2, 3, 8, 0, 4, 7, 6, 5};
  91. int flag[N*N] = {0}; // 用来标记0~8中哪个数字已经出现过
  92. srand(time(0));
  93. for(i = 0; i < N; i++)
  94. {
  95. for(j = 0; j < N; j++)
  96. {
  97. // 尝试生成一个0~8间的数字
  98. k = rand()%(N*N);
  99. while(1)
  100. {
  101. // 如果该数字未出现
  102. if(flag[k] == 0)
  103. {
  104. flag[k] = 1; // 修改标志位
  105. src[i][j] = a[k]; // 初始化对应src的位置
  106. break;
  107. }
  108. // 重新生成数字
  109. k = rand()%(N*N);
  110. }
  111. }
  112. }
  113. }
  114. // 复制棋盘状态
  115. void copy(int source[N][N], int dest1[N][N])
  116. {
  117. int i, j;
  118. for(i = 0; i < N; i++)
  119. {
  120. for(j = 0; j < N; j++)
  121. {
  122. dest1[i][j] = source[i][j];
  123. }
  124. }
  125. }
  126. // 打印棋盘状态
  127. void display(int s[N][N])
  128. {
  129. int i, j;
  130. for(i = 0; i < N; i++)
  131. {
  132. for(j = 0; j < N; j++)
  133. {
  134. // 把0当做空格输出
  135. if(s[i][j] > 0)
  136. {
  137. printf("%d ", s[i][j]);
  138. }
  139. else if(s[i][j] == 0)
  140. {
  141. printf(" ");
  142. }
  143. }
  144. printf("\n");
  145. }
  146. }
  147. // 寻找出棋盘中的空格键
  148. void findSpace(int s[N][N], int *x, int *y)
  149. {
  150. int i, j;
  151. for(i = 0; i < N; i++)
  152. {
  153. for(j = 0; j < N; j++)
  154. {
  155. if(s[i][j] == 0) // 找到空格
  156. {
  157. *x = i;
  158. *y = j;
  159. }
  160. }
  161. }
  162. }
  163. // 空格上移
  164. void up(int s[N][N], int x, int y)
  165. {
  166. // 空格必须是处于第2行或者第3行,才能上移
  167. if(x >= 1)
  168. {
  169. s[x][y] = s[x-1][y];
  170. s[x-1][y] = 0;
  171. }
  172. }
  173. // 空格下移
  174. void down(int s[N][N], int x, int y)
  175. {
  176. // 空格必须是处于第1行或者第2行,才能下移
  177. if(x <= 1)
  178. {
  179. s[x][y] = s[x+1][y];
  180. s[x+1][y] = 0;
  181. }
  182. }
  183. // 空格左移
  184. void left(int s[N][N], int x, int y)
  185. {
  186. // 空格必须是处于第2列或者第3列,才能左移
  187. if(y >= 1)
  188. {
  189. s[x][y] = s[x][y-1];
  190. s[x][y-1] = 0;
  191. }
  192. }
  193. // 空格右移
  194. void right(int s[N][N], int x, int y)
  195. {
  196. // 空格必须是处于第1列或者第2列,才能右移
  197. if(y <= 1)
  198. {
  199. s[x][y] = s[x][y+1];
  200. s[x][y+1] = 0;
  201. }
  202. }
  203. // 采用曼哈顿距离,计算从某个状态cmp到当前状态的代价
  204. // 曼哈顿距离指,与目标位置的水平距离和垂直距离之和
  205. // 书上给出的代价是从起始节点S到任一节点i的路径代价g(i)
  206. // 修改代价时,采用g(j) = g(i) + cost(i, j);
  207. // cost(i, j)指从当前i节点到它的后继节点j的代价
  208. // 但是这里因为每次空格操作,只会改变2个位置
  209. // 所以cost(i, j)都相同,因此可以省略,直接计算g(j)
  210. void compute_cost(node *p, int cmp[N][N])
  211. {
  212. int i, j, m, n;
  213. for(i = 0; i < N; i++)
  214. {
  215. for(j = 0; j < N; j++)
  216. {
  217. // 如果有对应位置不匹配的
  218. // 则重头搜索出与cmp[i][j]匹配的p->data[m][n]
  219. if(p->data[i][j] != cmp[i][j])
  220. {
  221. for(m = 0; m < N; m++)
  222. {
  223. for(n = 0; n < N; n++)
  224. {
  225. // 搜索到与cmp[i][j]匹配的p->data[m][n]
  226. // 计算曼哈顿距离后立马结束搜索
  227. if(p->data[m][n] == cmp[i][j])
  228. {
  229. // 累加每个位置的曼哈顿距离
  230. p->cost += abs(m-i) + abs(n-j);
  231. m = N; // 修改m值,结束搜索
  232. break;
  233. }
  234. }
  235. }
  236. }
  237. }
  238. }
  239. }
  240. // 检查close表中是否有重复的状态
  241. int checkSame(int cmp[N][N])
  242. {
  243. int i, j;
  244. node *p;
  245. for(p = close->next; p != close; p = p->next)
  246. {
  247. for(i = 0; i < N; i++)
  248. {
  249. for(j = 0; j < N; j++)
  250. {
  251. if(p->data[i][j] != cmp[i][j])
  252. {
  253. i = N + 1;
  254. break;
  255. }
  256. }
  257. }
  258. if(i == N && j == N)
  259. {
  260. return 1;
  261. }
  262. }
  263. return 0;
  264. }
  265. // 检查当前棋盘状态是否为目标状态
  266. int checkWin()
  267. {
  268. int i, j;
  269. for(i = 0; i < N; i++)
  270. {
  271. for(j = 0; j < N; j++)
  272. {
  273. // 检测到有不同的就立马返回,节省时间
  274. if(cur[i][j] != dest[i][j])
  275. {
  276. return 0;
  277. }
  278. }
  279. }
  280. return 1;
  281. }
  282. // 打印从初始状态到目标状态的路径
  283. void showWin()
  284. {
  285. int i = 1;
  286. node *p, *win;
  287. // 创建一个win表,用来存放路径
  288. win = initList();
  289. if(!win)
  290. {
  291. printf("win malloc fail\n");
  292. return;
  293. }
  294. // 由于节点中只有father(父节点),而打印路径是从父节点开始的
  295. // 所以必须先从close表选择出正确的路径,因为close中可能存放不是正确路径的节点
  296. // 可以唯一确定的是 close->prev 一定是目的状态,由此循环往上寻找其父节点
  297. for(p = close->prev; p && p != close; p = p->father)
  298. {
  299. Remove(p); // 从close表中弹出p节点
  300. head_insert(win, p); // 把p节点用头插法插入到win表中
  301. }
  302. printf("the solution as follow:\n");
  303. // 经过头插法创建的win表,一定是按从初始状态到目标状态的排序的
  304. for(p = win->next; p != win; p = p->next, i++)
  305. {
  306. printf("step %d:\n", i);
  307. display(p->data);
  308. printf("\n");
  309. }
  310. clearList(win); // 清空win表
  311. free(win); // 内存回收
  312. }
  313. // 把当前节点(tmp) 的后继节点插入到open表中
  314. void add(node *tmp, int cmp[N][N])
  315. {
  316. node *p = initList();
  317. if(!p)
  318. {
  319. printf("malloc fail\n");
  320. return;
  321. }
  322. copy(cmp, p->data); // 复制棋盘状态
  323. p->father = tmp; // 修改父节点指针
  324. compute_cost(p, src); // 修改从初始状态到当前状态的代价
  325. compute_cost(p, dest); // 修改从当前状态到目标状态的代价
  326. head_insert(open, p); // BFS用尾插法,DFS用头插法
  327. }
  328. // 从表中选择一个节点,使其代价p->cost最小
  329. // 通常是从open表中选择,如果有多个点,则随便选一个
  330. node *min_cost(node *head)
  331. {
  332. // 空表直接退出
  333. if(!head || head->next == head)
  334. {
  335. return NULL;
  336. }
  337. node *p, *min;
  338. min = head->next;
  339. for(p = min->next; p != head; p = p->next)
  340. {
  341. if(p->cost < min->cost)
  342. {
  343. min = p;
  344. }
  345. }
  346. return min;
  347. }
  348. // 等代价搜索法
  349. void Astar()
  350. {
  351. printf("start Astar\n");
  352. node *tmp;
  353. int x, y, k, cmp[N][N];
  354. add(NULL, src); // 把初始状态加入到open表中
  355. while(1)
  356. {
  357. // 如果open表为空,就可以退出了
  358. if(open->next == open)
  359. {
  360. printf("fail\n");
  361. return;
  362. }
  363. // 从open表中选择一个节点,使其代价p->cost最小
  364. tmp = min_cost(open);
  365. // 如果tmp为空,则说明open表为空
  366. // 但是一般不会出现,因为前面已经对open表进行判空处理
  367. // 这里写的原因就是特意为了增强算法的健壮性
  368. if(!tmp)
  369. {
  370. printf("fail\n");
  371. return;
  372. }
  373. Remove(tmp);
  374. tail_insert(close, tmp);
  375. printf("now go :%d\n", ++n);
  376. // 打印当前搜索出的棋盘状态
  377. copy(tmp->data, cur);
  378. printf("current:\n");
  379. display(cur);
  380. // 检测当前棋盘状态是否为目标状态
  381. if(checkWin())
  382. {
  383. printf("success\n\n");
  384. printf("solution as follow:\n");
  385. showWin();
  386. return;
  387. }
  388. k = 0; // 记录当前节点的后继节点个数
  389. findSpace(cur, &x, &y); // 寻找当前棋盘状态的空格坐标
  390. printf("try up\n");
  391. // 空格必须是处于第2行或者第3行,才能上移
  392. if(x >= 1)
  393. {
  394. copy(cur, cmp); // 先复制当前状态cur到cmp中
  395. up(cmp, x, y); // cmp尝试向上移
  396. // 判断cmp上移后close表中是否有重复状态
  397. // 没有重复才可以把cmp状态加入到close表中
  398. if(checkSame(cmp) == 0)
  399. {
  400. k++; // 当前节点的后继节点数加一
  401. add(tmp, cmp); // 把后继节点加入到open表中
  402. printf("can up\n");
  403. }
  404. }
  405. printf("try down\n");
  406. // 空格必须是处于第1行或者第2行,才能下移
  407. if(x <= 1)
  408. {
  409. copy(cur, cmp); // 先复制当前状态cur到cmp中
  410. down(cmp, x, y); // cmp尝试向下移
  411. // 判断cmp下移后close表中是否有重复状态
  412. // 没有重复才可以把cmp状态加入到close表中
  413. if(checkSame(cmp) == 0)
  414. {
  415. k++; // 当前节点的后继节点数加一
  416. add(tmp, cmp); // 把后继节点加入到open表中
  417. printf("can down\n");
  418. }
  419. }
  420. printf("try left\n");
  421. // 空格必须是处于第2列或者第3列,才能左移
  422. if(y >= 1)
  423. {
  424. copy(cur, cmp); // 先复制当前状态cur到cmp中
  425. left(cmp, x, y); // cmp尝试向左移
  426. // 判断cmp左移后close表中是否有重复状态
  427. // 没有重复才可以把cmp状态加入到close表中
  428. if(checkSame(cmp) == 0)
  429. {
  430. k++; // 当前节点的后继节点数加一
  431. add(tmp, cmp); // 把后继节点加入到open表中
  432. printf("can left\n");
  433. }
  434. }
  435. printf("try right\n");
  436. // 空格必须是处于第1列或者第2列,才能右移
  437. if(y <= 1)
  438. {
  439. copy(cur, cmp); // 先复制当前状态cur到cmp中
  440. right(cmp, x, y); // cmp尝试向右移
  441. // 判断cmp右移后close表中是否有重复状态
  442. // 没有重复才可以把cmp状态加入到close表中
  443. if(checkSame(cmp) == 0)
  444. {
  445. k++; // 当前节点的后继节点数加一
  446. add(tmp, cmp); // 把后继节点加入到open表中
  447. printf("can right\n");
  448. }
  449. }
  450. // 如果当前状态没有后继节点,应该从close表中删除该状态
  451. if(k == 0)
  452. {
  453. Remove(tmp); // 弹出当前状态
  454. free(tmp); // 释放当前状态
  455. printf("del done\n\n");
  456. }
  457. }
  458. }
  459. int main()
  460. {
  461. open = initList(); // 初始化open表
  462. close = initList(); // 初始化close表
  463. if(!open || !close)
  464. {
  465. printf("初始化状态失败\n");
  466. return -1;
  467. }
  468. // 测试阶段建议直接定义src, init()后的src很大程度上无解
  469. // init();
  470. printf("src:\n");
  471. display(src);
  472. Astar();
  473. // 清空open表和close表并回收内存
  474. clearList(open);
  475. clearList(close);
  476. free(open);
  477. free(close);
  478. printf("DFS finish\n");
  479. return 0;
  480. }

 运行截图

 

 

三、简单分析

        如果认真且仔细看完代码的程序猿同学,根据注释的讲解,可以说基本上可以看懂了,下面再来讲解一下算法的流程图和关键易错点。

        流程图:

         关键易错点:

        1、open表用来存放未拓展的节点,close表用来存放已经拓展的节点,所以说,解的路径存放在close表中,但并不一定完全是close表中的节点,因为有些是其他不符合的节点。

        2、注意区分节点中的数据结构,一个版本中father指针的作用是保留父节点的指针,方便打印解的路径,而prev和next的作用是用来方便节点在链表中的操作,所以说,可以采用数组的方式来存储,此时就不需要prev和next指针了,这里不详细展开。

        3、算法结束的条件是:open表为空,即不存在任何后继节点了,此时为无解;或者当前状态就是目标状态,即找到了正确的解。

        4、空格操作的选择顺序:算法中对上移,下移,左移,右移的先后顺序要统一,不同的上下左右移动顺序会影响解的路径的长度,这点需要程序猿同学自己画图理解一下,这里比较难说明。

        5、如果存在解,则解的路径可能有多条,而使用BFS一定可以打印出一条最优解,注意用词是打印,不是搜索的过程是最优,而如果使用DFS则不一定可以打印出一条最优解,如果使用A*算法(启发式搜索)则可以保证搜索过程是最优的,自然就可以打印出一条最优解。

        6、启发式搜索,每次总是选择最有希望的节点加以拓展。这里的代价计算为:从初始状态到当前状态的代价和当前状态到目标状态的代价之和,代价采用曼哈顿距离计算。

        7、曼哈顿距离指,状态i与状态j之间水平距离和垂直距离之和。

四、小小总结

        本期八数码启发式搜索求解做的有点急,但是给的代码中有一些编程小技巧,细心的可以看出来,记得收藏起来下期出八数码A*算法求解, 敬请关注!^_^

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/370826
推荐阅读
相关标签
  

闽ICP备14008679号