在贪吃蛇流程结构优化之后,我又不满足于亲自操刀控制这条蠢蠢的蛇,干脆就让它升级成AI,我来看程序自己玩,哈哈。

一、Dijkstra算法原理

作为一种广为人知的单源最短路径算法,Dijkstra用于求解带权有向图的单源最短路径的问题。所谓单源,就是一个源头,也即一个起点。该算法的本质就是一个广度优先搜索,由中心向外层层层拓展,直到遇到终点或者遍历结束。该算法在搜索的过程中需要两个表S及Q,S用来存储已扫描过的节点,Q存储剩下的节点。起点s距离dist[s] = 0;其余点的值为无穷大(具体实现时,表示为某一不可能达到的数字即可)。开始时,从Q中选择一点u,放入S,以u为当前点,修改u周围点的距离。重复上述步骤,直到Q为空。

二、Dijkstra算法在AI贪吃蛇问题中的变化

2.1 地图的表示方法

与平时见到的各种连通图问题不同,贪吃蛇游戏中的地图可以看成是标准的矩形,也即,一个二维数组,图中各个相邻节点的权值为1。因此,我们可以用一个边长*边长的二维数组作为算法的主体数据结构,讲地图有关的数据都集成在数组里。既然选择了二维数组,就要考虑数组元素类型的问题,即我们的数组应该存储哪些信息。作为主要的数据结构,我们希望我们的数组能存储自身的坐标,起点到自身的最短路径,因此我们可以定义这样的一个结构体:

  1. typedef struct loca{  
  2.     int x;  
  3.     int y;  
  4. }Local;  
  5.     
  6. typedef struct unit{  
  7.     int value;  
  8.     Local local;  
  9. }Unit;  

又因为我们需要得到最短路径以求得贪吃蛇下一步的方向,所以在结构体里加一个指针,指向前一个节点的位置。

  1. typedef struct loca{  
  2.     int x;  
  3.     int y;  
  4. }Local;  
  5.     
  6. typedef struct unit{  
  7.     int value;  
  8.     Local local;  
  9.     struct unit *pre;  
  10. }Unit;  

假设地图为一个正方形,因此创建一个边长*边长大小的二维数组:

  1. #define N 5  
  2.     
  3. Unit mapUnit[N][N];  

2.2 队列——待处理节点的集合

有了mapUnit之后,我们还需要一个数据结构来存储接下来需要处理的节点的信息。在此我选择了一个队列,由于C语言不提供标准的接口,就自己草草的写了一个。

  1. typedef struct queue{  
  2.     int head,tail;  
  3.     Local queue[N*N];  
  4. }Queue;  
  5.     
  6. Queue que;  

使用了一个定长的数组来作为队列结构,所以为了应对所有的结果,将其长度设为N*N。也正因为是定长数组,队列的进队与出队只需操作表示下标值的head与tail即可。这样虽然不节约空间,但胜在实现方便。

  1. void push(int x,int y)  
  2. {  
  3.     que.tail++;  
  4.     que.queue[que.tail].x = x;  
  5.     que.queue[que.tail].y = y;  
  6. }  
  7.     
  8. void pop()  
  9. {  
  10.     que.head++;  
  11. }  

由于push操作有一个自增操作,所以在初始化时需要将tail设为-1,这样在push第一个节点时可保证head与tail指向同一个位置。

2.3 console坐标——地图的初始化

在我的贪吃蛇链表实现中,前端展示时通过后台的计算逻辑+Pos函数来实现的,也就是现在后台计算结果,再推动前台的变化。因此Pos(),也就是使光标跳转到控制台某位置的函数就尤为重要,这也直接影响了整个项目各元素的坐标表示方法。

简单来说就是console的坐标表示类似于坐标轴中第四象限的表示方法,当然元素都为正值。

所以对于一个N*N的数组,我们可以这样初始化:

  1. void InitializeMapUnit()  
  2. {  
  3.     que.head = 0;  
  4.     que.tail = -1;  
  5.     
  6.     for(int i = 0;i<N;i++)  
  7.         for(int j = 0;j<N;j++)  
  8.         {  
  9.             mapUnit[i][j].local.x = i;  
  10.             mapUnit[i][j].local.y = j;  
  11.             mapUnit[i][j].pre = NULL;  
  12.             mapUnit[i][j].value = N*N;   
  13.         }  
  14. }  

将队列的初始化放在这个函数里实属无奈,这两行语句,又不能在初始化时赋值,又不能在函数体外赋值,放main函数嫌它乱,单独一个函数嫌它慢….就放在地图初始化里了…

三、计算,BFS!

3.1 设置起点

基础的结构与初始化完成后,就需要开始计算了。在此之前,我们需要一个坐标,来作为路径问题的出发点。

  1. void setOrigin(int x,int y)  
  2. {  
  3.     mapUnit[x][y].value = 0;  
  4.     push(x,y);  
  5. }  

将地图上该点位置的值设为0后,将其压入队列中。在第一轮的BFS中,它四周的点,将成为第二轮计算的原点。

3.2 BFS框架

在该地图的BFS中,我们将依托队列各个元素,来处理它们的邻接节点。两个循环,可以揭示大体的框架:

  1. void bfs(int end_x,int end_y)  
  2. {  
  3.     //当前需要处理的节点   
  4.     for(int i = head;i<=tail;i++)  
  5.     {  
  6.         //  四个方向   
  7.         for(int j = 0;j<4;j++)  
  8.         {  
  9.             // 新节点   
  10.             if(mapUnit[new_x][new_y].value == N*N)  
  11.             {  
  12.                 //设置属性   
  13.             }  
  14.             //  处理过的节点,取小值  
  15.             else       
  16.             {  
  17.                 //属性更改Or不变   
  18.             }  
  19.         }  
  20.     }  
  21.     //下一轮   
  22.     bfs();  
  23. }  

3.3 变化的队列

BFS的主体循环依赖于队列的head与tail,但是对新节点的push操作改变了tail的值,所以我们需要在循环开始前将此时(上一轮BFS的结果)的队列状态保存下来,避免队列变化对BFS的影响。

  1. int head = que.head;  
  2. int tail = que.tail;  
  3. //当前队列  
  4. for(int i = head;i<=tail;i++)  
  5. {  
  6.     // TODO...   
  7. }   

3.4 节点的坐标

在原来写的BFS中,要获取一个节点的下标需要将一个结构体层层剥开,数组的下标是一个结构体某元素的某元素,绕来绕去,可读性早已被献祭了。

所以这次我吸取了教训,在内循环,也就是处理周围节点时,将其坐标先存储在变量中,用来确保程序的可读性。

  1. for(int i = head;i<=tail;i++)  
  2. {  
  3.         int base_x = que.queue[i].x;  
  4.         int base_y = que.queue[i].y;  
  5.             
  6.         //  四个方向   
  7.         for(int j = 0;j<4;j++)  
  8.         {  
  9.             int new_x = base_x + direct[j][0];  
  10.             int new_y = base_y + direct[j][1];  
  11.                 
  12.             // TODO...   
  13.         }   
  14. }  

所以我们可以构建出这样一个移动的二维数组:

  1. //          方向, 上       下       左   右   
  2. int direct[4][2] = {{0,-1},{0,1},{-1,0},{1,0}};  

3.4.1 数组越界的处理

在得到了待处理节点的坐标后,需要对其进行判断,确保它在数组内部。

  1. if(stepRight(new_x,new_y) == false)  
  2.     continue;  

函数细节如下:

  1. bool stepRight(int x,int y)  
  2. {  
  3.     if(x >= N || y >= N ||  
  4.         x < 0 || y < 0)  
  5.         return false;  
  6.     return true;  
  7. }  

3.5 新节点的处理

终于到了访问邻接坐标的时候。一个节点四周的节点,有可能没有被访问过,也可能以及被访问过。我们在初始化时就将所有节点的值设为了一个MAX,通过对值得判断,可以推断出其是否为新节点。

  1. if(mapUnit[new_x][new_y].value == N*N)  
  2. {  
  3.     // ...  
  4. }  
  5. else    //取小值   
  6. {  
  7.     // ...  
  8. }  

3.5.1 未处理节点的处理

对于未处理的节点,对其的操作有两部。一是初始化,值的初始化与指针的初始化。由于两点间的距离为1,所以该节点的值为前一个节点的值+1,当然,他的pre指针也指向前一个节点。

  1. mapUnit[new_x][new_y].value = mapUnit[base_x][base_y].value +1;  
  2. mapUnit[new_x][new_y].pre = &mapUnit[base_x][base_y];  
  3. push(new_x,new_y);  

3.5.2 已处理节点的处理

对于已处理过的节点,需要先将其做一个判断,即寻找最短路径,将其自身的value与前一节点value+1比较,再处理。

  1. mapUnit[new_x][new_y].value = MIN(mapUnit[new_x][new_y].value,mapUnit[base_x][base_y].value +1);  
  2. if(mapUnit[new_x][new_y].value != mapUnit[new_x][new_y].value)  
  3. mapUnit[new_x][new_y].pre = &mapUnit[base_x][base_y];  

3.6 队列的刷新

在处理完一层节点后,新的节点导致了队列中tail的增加,但是head并没有减少,所以在新一轮BFS前,需要将队列的head移动到真正的头部去。

  1. for(int i = head;i<=tail;i++)  
  2.     pop();  

在这儿也需要当前BFS轮数前的队列数据。

3.7 最短路径

在地图的遍历完成之后,我们就可以任取一点,得到起点到该点的最短路径。

  1. void getStep(int x,int y)  
  2. {  
  3.     Unit *scan = &mapUnit[x][y];  
  4.     if(scan->pre!= NULL)  
  5.     {  
  6.         int x = scan->local.x;  
  7.         int y = scan->local.y;  
  8.         scan = scan->pre;  
  9.         getStep(scan->local.x,scan->local.y);  
  10.         printf(" -> ");  
  11.     }  
  12.     printf("(%d,%d)",x,y);  
  13. }  

四、此路不通——障碍的引入

在贪吃蛇中,由于蛇身长度的存在,以及蛇头咬到自身就结束的特例,我们需要在算法中加入障碍的元素。

对于这个新加入的元素,我们设置一个坐标结构体的数组,来存储所有的障碍。

  1. #define WALL_CNT 3   
  2. Local Wall[WALL_CNT];  

用一个函数来设置障碍:

  1. void setWall(void)  
  2. {  
  3.     Wall[0].x = 1;  
  4.     Wall[0].y = 1;  
  5.         
  6.     Wall[1].x = 1;  
  7.     Wall[1].y = 2;  
  8.         
  9.     Wall[2].x = 2;  
  10.     Wall[2].y = 1;  
  11. }  

由于这个项目里数据用于模块测试的随机性,所以手动设置每一个坐标。在之后的贪吃蛇AI中,将接受一个数组——蛇身,来自动完成赋值。

如果将障碍与地图边界等同来看,就能将障碍的判断整合进stepRight()函数。

  1. bool stepRight(int x,int y)  
  2. {  
  3. //  out of map   
  4.     if(x >= N || y >= N ||  
  5.         x < 0 || y < 0)  
  6.         return false;  
  7. //  wall  
  8.     for(int i = 0;i<WALL_CNT;i++)  
  9.         if(Wall[i].x == x && Wall[i].y == y)  
  10.             return false;  
  11.         
  12.     return true;  
  13. }  

五、简单版本的测试

完成了上诉的模块后,项目就可以无BUG但是低效的跑了。我们来试一试,在一个5*5的地图中,起点在中间,为(2,2),终点在起点的上上方,为(2,0),设置三面围墙,分别是(1,1),(2,1),(3,1)。如下:

看看效果。

图中二维数组打印了各个坐标点的value,即该点到起点的最短路径。25为墙,0为起点。可以看到到终点需要六步,路径是先往左,再往上,左后向右到终点。

任务完成,看似不错。把终点换近一些看看,就(1,4)吧。

喔,问题出来了。我取一个非常近的点,但是整张图都被遍历了,效率太低了,要改进。

还有一个问题,如果将起点周围四个点都设置为墙,结果应该是无法得到其余点的最短路径,但现阶段的结果还不尽如人意:

六、优化

6.1 遍历的半途结束

在BFS中,如果找到了终点,那就可以退出遍历,直接输出结果。不过这样的一个递归树,要随时终止可不容易。我一开始想到了"万恶之源"goto,不过goto不能跨函数跳转,随后又想到了非本地跳转setjmp与longjmp。

"与刺激的abort()和exit()相比,goto语句看起来是处理异常的更可行方案。不幸的是,goto是本地的:它只能跳到所在函数内部的标号上,而不能将控制权转移到所在程序的任意地点(当然,除非你的所有代码都在main体中)。

为了解决这个限制,C函数库提供了setjmp()和longjmp()函数,它们分别承担非局部标号和goto作用。头文件<setjmp.h>申明了这些函数及同时所需的jmp_buf数据类型。"

有了这随时能走的"闪现"功能,跳出复杂嵌套函数还是事儿嘛?

  1. #include <setjmp.h>  
  2. jmp_buf jump_buffer;  
  3.     
  4. int main (void)  
  5. {  
  6.     //...  
  7.     if(setjmp(jump_buffer) == 0)  
  8.         bfs(finishing_x,finishing_y);  
  9.     //...  
  10. }  

    由于跳转需要判断当前节点是否为终点,而终点又是一个局部变量,所以需要改变bfs函数,使其携带终点参数。

    再在处理完一个节点后,判断其是否为终点,是则退出。

  11. for(int i = head;i<=tail;i++)  
  12. {  
  13.     //...  
  14.     //  四个方向   
  15.     for(int j = 0;j<4;j++)  
  16.     {  
  17.         //...  
  18.         if(mapUnit[new_x][new_y].value == N*N)  
  19.         {  
  20.             //...  
  21.         }  
  22.         else    //取小值   
  23.         {  
  24.             //...  
  25.         }  
  26.         if(new_x == end_x && new_y == end_y)  
  27.         {  
  28.             longjmp(jump_buffer, 1);  
  29.         }  
  30.     }  
  31. }  

6.2 无最短路径时的处理

在判断某一点的路径时,可先判断其是否存在最短路径,存在则输出,否则给出提示信息。

  1. void getStepNext(int x,int y)  
  2. {  
  3.     Unit *scan = &mapUnit[x][y];  
  4.     if(scan->pre!= NULL)  
  5.     {  
  6.         int x = scan->local.x;  
  7.         int y = scan->local.y;  
  8.         scan = scan->pre;  
  9.         getStepNext(scan->local.x,scan->local.y);  
  10.         printf(" -> ");  
  11.     }  
  12.     printf("(%d,%d)",x,y);  
  13. }  
  14.     
  15. void getStep(int x,int y,int orgin_x,int orgin_y)  
  16. {  
  17.     Unit *scan = &mapUnit[x][y];  
  18.     Pos(0,10);  
  19.         
  20.     if(scan->pre == NULL)  
  21.     {  
  22.         printf("NO Path To Point (%d,%d) From Point (%d,%d)!\n",x,y,orgin_x,orgin_y);  
  23.     }  
  24.     else  
  25.     {  
  26.         getStepNext(x,y);  
  27.     }  
  28. }  

七、优化后效果

八、写在后面

算法大体上完成了,将其改为贪吃蛇AI也只需做少量修改。尽量抽个时间把AI写完,不过可能需要一段时间。

在最短路径的解法上,Dijkstra算法并不是最理想的解法。盲目搜索的效率很低。考虑到地图上存在着两点间距离等信息,可以使用一种启发式搜索算法,如BFS(Best-First Search),以及大名鼎鼎的A*算法。在中文互联网我能找到的有关于A*算法的资料不多,将来得花些时间好好研究下。

源码地址:https://github.com/MagicXyxxx/Algorithms

AI贪吃蛇前瞻——基于Dijkstra算法的最短路径问题的更多相关文章

  1. AI贪吃蛇(二)

    前言 之前写过一篇关于贪吃蛇AI的博客,当时虽然取得了一些成果,但是也存在许多问题,所以最近又花了三天时间重新思考了一下.以下是之前博客存在的一些问题: 策略不对,只要存在找不到尾巴的情况就可能失败, ...

  2. Python制作AI贪吃蛇

    前提:本文实现AI贪吃蛇自行对战,加上人机对战,文章末尾附上源代码以及各位大佬的链接,还有一些实现步骤,读者可再次基础上自行添加电脑VS电脑和玩家VS玩家(其实把人机对战写完,这2个都没什么了,思路都 ...

  3. Python制作AI贪吃蛇,很多很多细节、思路都写下来了!

    前提:本文实现AI贪吃蛇自行对战,加上人机对战,读者可再次基础上自行添加电脑VS电脑和玩家VS玩家(其实把人机对战写完,这2个都没什么了,思路都一样) 实现效果: 很多人学习python,不知道从何学 ...

  4. 4003.基于Dijsktra算法的最短路径求解

    基于Dijsktra算法的最短路径求解 发布时间: 2018年11月26日 10:14   时间限制: 1000ms   内存限制: 128M 有趣的最短路...火候欠佳,目前还很难快速盲打出来,需继 ...

  5. 基于Dijsktra算法的最短路径求解

    基于Dijsktra算法的最短路径求解   描述 一张地图包括n个城市,假设城市间有m条路径(有向图),每条路径的长度已知.给定地图的一个起点城市和终点城市,利用Dijsktra算法求出起点到终点之间 ...

  6. dijkstra算法计算最短路径和并输出最短路径

    void dijisitela(int d, int m1) { ], book[], path[], u, v, min; l = ; ; i < n1; i++) { dis[i] = w[ ...

  7. Dijkstra算法求最短路径(java)(转)

    原文链接:Dijkstra算法求最短路径(java) 任务描述:在一个无向图中,获取起始节点到所有其他节点的最短路径描述 Dijkstra(迪杰斯特拉)算法是典型的最短路径路由算法,用于计算一个节点到 ...

  8. 基于dijkstra算法求地铁站最短路径以及打印出所有的路径

    拓展dijkstra算法,实现利用vector存储多条路径: #include <iostream> #include <vector> #include <stack& ...

  9. 贪吃蛇—C—基于easyx图形库(下):从画图程序到贪吃蛇【自带穿墙术】

    上节我们用方向控制函数写了个小画图程序,它虽然简单好玩,但我们不应该止步于此.革命尚未成功,同志还需努力. 开始撸代码之前,我们先理清一下思路.和前面画图程序不同,贪吃蛇可以有很多节,可以用一个足够大 ...

随机推荐

  1. dede 复制文章,远程图片无法本地化

    解决方法: 1.找到dede的后台目录,在后台目录下的inc下找到inc_archives_functions.php 2.搜索GetCurContent函数,找到如下这段代码: preg_match ...

  2. MySql删除表、数据

    程度从强到弱 1.drop  table tb        drop将表格直接删除,没有办法找回 2.truncate (table) tb       删除表中的所有数据,不能与where一起使用 ...

  3. Linux统计某文件夹下文件的个数

    ls -l |grep "^-"|wc -l 统计某文件夹下目录的个数 ls -l |grep "^d"|wc -l 统计文件夹下文件的个数,包括子文件夹里的 ...

  4. db2 统计信息 runstats

    1.runstats的语法:runstats on table [模式名].[表名] with distribution and detailed indexes all注意:你可以在所有列上,或者仅 ...

  5. JDK 泛型之 Type

    JDK 泛型之 Type 一.Type 接口 JDK 1.5 引入 Type,主要是为了泛型,没有泛型的之前,只有所谓的原始类型.此时,所有的原始类型都通过字节码文件类 Class 类进行抽象.Cla ...

  6. http mimetype为multipart/x-mixed-replace报文

    http://blog.csdn.net/gmstart/article/details/7064034 服务器推送(Server Push) 推送技术的基础思想是将浏览器主动查询信息改为服务器主动发 ...

  7. KbmMW 服务器架构简介

    kbmmw 由于文档比较少,很多同学开始用时很难理解.一直准备写一个关于kbmmw 架构的东西. 这几天与红鱼儿(blog)研究服务器线程时,整理了一下,大概画了一下kbmmw (版本4.5)服务器的 ...

  8. 2018.08.31 bzoj3566: [SHOI2014]概率充电器(概率dp+容斥原理)

    传送门 概率dp好题啊. 用f[i]" role="presentation" style="position: relative;">f[i] ...

  9. 29. What Makes a True Leader ? 合格的领导者由何物决定 ?

    29. What Makes a True Leader ? 合格的领导者由何物决定 ? ① Reading leadership literature,you'd sometimes think t ...

  10. CentOS里vim基本操作

    1.关于退出 :wq!  ----强制保存退出 :wq  ---- 保存退出 :x   ----- 作用和:wq 一样 ZZ  ---- 作用和:wq一样,(注意Z是大写的,并且不是在命令模式) :q ...