AI贪吃蛇前瞻——基于Dijkstra算法的最短路径问题
在贪吃蛇流程结构优化之后,我又不满足于亲自操刀控制这条蠢蠢的蛇,干脆就让它升级成AI,我来看程序自己玩,哈哈。
一、Dijkstra算法原理
作为一种广为人知的单源最短路径算法,Dijkstra用于求解带权有向图的单源最短路径的问题。所谓单源,就是一个源头,也即一个起点。该算法的本质就是一个广度优先搜索,由中心向外层层层拓展,直到遇到终点或者遍历结束。该算法在搜索的过程中需要两个表S及Q,S用来存储已扫描过的节点,Q存储剩下的节点。起点s距离dist[s] = 0;其余点的值为无穷大(具体实现时,表示为某一不可能达到的数字即可)。开始时,从Q中选择一点u,放入S,以u为当前点,修改u周围点的距离。重复上述步骤,直到Q为空。
二、Dijkstra算法在AI贪吃蛇问题中的变化
2.1 地图的表示方法
与平时见到的各种连通图问题不同,贪吃蛇游戏中的地图可以看成是标准的矩形,也即,一个二维数组,图中各个相邻节点的权值为1。因此,我们可以用一个边长*边长的二维数组作为算法的主体数据结构,讲地图有关的数据都集成在数组里。既然选择了二维数组,就要考虑数组元素类型的问题,即我们的数组应该存储哪些信息。作为主要的数据结构,我们希望我们的数组能存储自身的坐标,起点到自身的最短路径,因此我们可以定义这样的一个结构体:
- typedef struct loca{
- int x;
- int y;
- }Local;
- typedef struct unit{
- int value;
- Local local;
- }Unit;
又因为我们需要得到最短路径以求得贪吃蛇下一步的方向,所以在结构体里加一个指针,指向前一个节点的位置。
- typedef struct loca{
- int x;
- int y;
- }Local;
- typedef struct unit{
- int value;
- Local local;
- struct unit *pre;
- }Unit;
假设地图为一个正方形,因此创建一个边长*边长大小的二维数组:
- #define N 5
- Unit mapUnit[N][N];
2.2 队列——待处理节点的集合
有了mapUnit之后,我们还需要一个数据结构来存储接下来需要处理的节点的信息。在此我选择了一个队列,由于C语言不提供标准的接口,就自己草草的写了一个。
- typedef struct queue{
- int head,tail;
- Local queue[N*N];
- }Queue;
- Queue que;
使用了一个定长的数组来作为队列结构,所以为了应对所有的结果,将其长度设为N*N。也正因为是定长数组,队列的进队与出队只需操作表示下标值的head与tail即可。这样虽然不节约空间,但胜在实现方便。
- void push(int x,int y)
- {
- que.tail++;
- que.queue[que.tail].x = x;
- que.queue[que.tail].y = y;
- }
- void pop()
- {
- que.head++;
- }
由于push操作有一个自增操作,所以在初始化时需要将tail设为-1,这样在push第一个节点时可保证head与tail指向同一个位置。
2.3 console坐标——地图的初始化
在我的贪吃蛇链表实现中,前端展示时通过后台的计算逻辑+Pos函数来实现的,也就是现在后台计算结果,再推动前台的变化。因此Pos(),也就是使光标跳转到控制台某位置的函数就尤为重要,这也直接影响了整个项目各元素的坐标表示方法。
简单来说就是console的坐标表示类似于坐标轴中第四象限的表示方法,当然元素都为正值。
所以对于一个N*N的数组,我们可以这样初始化:
- void InitializeMapUnit()
- {
- que.head = 0;
- que.tail = -1;
- for(int i = 0;i<N;i++)
- for(int j = 0;j<N;j++)
- {
- mapUnit[i][j].local.x = i;
- mapUnit[i][j].local.y = j;
- mapUnit[i][j].pre = NULL;
- mapUnit[i][j].value = N*N;
- }
- }
将队列的初始化放在这个函数里实属无奈,这两行语句,又不能在初始化时赋值,又不能在函数体外赋值,放main函数嫌它乱,单独一个函数嫌它慢….就放在地图初始化里了…
三、计算,BFS!
3.1 设置起点
基础的结构与初始化完成后,就需要开始计算了。在此之前,我们需要一个坐标,来作为路径问题的出发点。
- void setOrigin(int x,int y)
- {
- mapUnit[x][y].value = 0;
- push(x,y);
- }
将地图上该点位置的值设为0后,将其压入队列中。在第一轮的BFS中,它四周的点,将成为第二轮计算的原点。
3.2 BFS框架
在该地图的BFS中,我们将依托队列各个元素,来处理它们的邻接节点。两个循环,可以揭示大体的框架:
- void bfs(int end_x,int end_y)
- {
- //当前需要处理的节点
- for(int i = head;i<=tail;i++)
- {
- // 四个方向
- for(int j = 0;j<4;j++)
- {
- // 新节点
- if(mapUnit[new_x][new_y].value == N*N)
- {
- //设置属性
- }
- // 处理过的节点,取小值
- else
- {
- //属性更改Or不变
- }
- }
- }
- //下一轮
- bfs();
- }
3.3 变化的队列
BFS的主体循环依赖于队列的head与tail,但是对新节点的push操作改变了tail的值,所以我们需要在循环开始前将此时(上一轮BFS的结果)的队列状态保存下来,避免队列变化对BFS的影响。
- int head = que.head;
- int tail = que.tail;
- //当前队列
- for(int i = head;i<=tail;i++)
- {
- // TODO...
- }
3.4 节点的坐标
在原来写的BFS中,要获取一个节点的下标需要将一个结构体层层剥开,数组的下标是一个结构体某元素的某元素,绕来绕去,可读性早已被献祭了。
所以这次我吸取了教训,在内循环,也就是处理周围节点时,将其坐标先存储在变量中,用来确保程序的可读性。
- for(int i = head;i<=tail;i++)
- {
- int base_x = que.queue[i].x;
- int base_y = que.queue[i].y;
- // 四个方向
- for(int j = 0;j<4;j++)
- {
- int new_x = base_x + direct[j][0];
- int new_y = base_y + direct[j][1];
- // TODO...
- }
- }
所以我们可以构建出这样一个移动的二维数组:
- // 方向, 上 下 左 右
- int direct[4][2] = {{0,-1},{0,1},{-1,0},{1,0}};
3.4.1 数组越界的处理
在得到了待处理节点的坐标后,需要对其进行判断,确保它在数组内部。
- if(stepRight(new_x,new_y) == false)
- continue;
函数细节如下:
- bool stepRight(int x,int y)
- {
- if(x >= N || y >= N ||
- x < 0 || y < 0)
- return false;
- return true;
- }
3.5 新节点的处理
终于到了访问邻接坐标的时候。一个节点四周的节点,有可能没有被访问过,也可能以及被访问过。我们在初始化时就将所有节点的值设为了一个MAX,通过对值得判断,可以推断出其是否为新节点。
- if(mapUnit[new_x][new_y].value == N*N)
- {
- // ...
- }
- else //取小值
- {
- // ...
- }
3.5.1 未处理节点的处理
对于未处理的节点,对其的操作有两部。一是初始化,值的初始化与指针的初始化。由于两点间的距离为1,所以该节点的值为前一个节点的值+1,当然,他的pre指针也指向前一个节点。
- mapUnit[new_x][new_y].value = mapUnit[base_x][base_y].value +1;
- mapUnit[new_x][new_y].pre = &mapUnit[base_x][base_y];
- push(new_x,new_y);
3.5.2 已处理节点的处理
对于已处理过的节点,需要先将其做一个判断,即寻找最短路径,将其自身的value与前一节点value+1比较,再处理。
- mapUnit[new_x][new_y].value = MIN(mapUnit[new_x][new_y].value,mapUnit[base_x][base_y].value +1);
- if(mapUnit[new_x][new_y].value != mapUnit[new_x][new_y].value)
- mapUnit[new_x][new_y].pre = &mapUnit[base_x][base_y];
3.6 队列的刷新
在处理完一层节点后,新的节点导致了队列中tail的增加,但是head并没有减少,所以在新一轮BFS前,需要将队列的head移动到真正的头部去。
- for(int i = head;i<=tail;i++)
- pop();
在这儿也需要当前BFS轮数前的队列数据。
3.7 最短路径
在地图的遍历完成之后,我们就可以任取一点,得到起点到该点的最短路径。
- void getStep(int x,int y)
- {
- Unit *scan = &mapUnit[x][y];
- if(scan->pre!= NULL)
- {
- int x = scan->local.x;
- int y = scan->local.y;
- scan = scan->pre;
- getStep(scan->local.x,scan->local.y);
- printf(" -> ");
- }
- printf("(%d,%d)",x,y);
- }
四、此路不通——障碍的引入
在贪吃蛇中,由于蛇身长度的存在,以及蛇头咬到自身就结束的特例,我们需要在算法中加入障碍的元素。
对于这个新加入的元素,我们设置一个坐标结构体的数组,来存储所有的障碍。
- #define WALL_CNT 3
- Local Wall[WALL_CNT];
用一个函数来设置障碍:
- void setWall(void)
- {
- Wall[0].x = 1;
- Wall[0].y = 1;
- Wall[1].x = 1;
- Wall[1].y = 2;
- Wall[2].x = 2;
- Wall[2].y = 1;
- }
由于这个项目里数据用于模块测试的随机性,所以手动设置每一个坐标。在之后的贪吃蛇AI中,将接受一个数组——蛇身,来自动完成赋值。
如果将障碍与地图边界等同来看,就能将障碍的判断整合进stepRight()函数。
- bool stepRight(int x,int y)
- {
- // out of map
- if(x >= N || y >= N ||
- x < 0 || y < 0)
- return false;
- // wall
- for(int i = 0;i<WALL_CNT;i++)
- if(Wall[i].x == x && Wall[i].y == y)
- return false;
- return true;
- }
五、简单版本的测试
完成了上诉的模块后,项目就可以无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数据类型。"
有了这随时能走的"闪现"功能,跳出复杂嵌套函数还是事儿嘛?
- #include <setjmp.h>
- jmp_buf jump_buffer;
- int main (void)
- {
- //...
- if(setjmp(jump_buffer) == 0)
- bfs(finishing_x,finishing_y);
- //...
- }
由于跳转需要判断当前节点是否为终点,而终点又是一个局部变量,所以需要改变bfs函数,使其携带终点参数。
再在处理完一个节点后,判断其是否为终点,是则退出。
- for(int i = head;i<=tail;i++)
- {
- //...
- // 四个方向
- for(int j = 0;j<4;j++)
- {
- //...
- if(mapUnit[new_x][new_y].value == N*N)
- {
- //...
- }
- else //取小值
- {
- //...
- }
- if(new_x == end_x && new_y == end_y)
- {
- longjmp(jump_buffer, 1);
- }
- }
- }
6.2 无最短路径时的处理
在判断某一点的路径时,可先判断其是否存在最短路径,存在则输出,否则给出提示信息。
- void getStepNext(int x,int y)
- {
- Unit *scan = &mapUnit[x][y];
- if(scan->pre!= NULL)
- {
- int x = scan->local.x;
- int y = scan->local.y;
- scan = scan->pre;
- getStepNext(scan->local.x,scan->local.y);
- printf(" -> ");
- }
- printf("(%d,%d)",x,y);
- }
- void getStep(int x,int y,int orgin_x,int orgin_y)
- {
- Unit *scan = &mapUnit[x][y];
- Pos(0,10);
- if(scan->pre == NULL)
- {
- printf("NO Path To Point (%d,%d) From Point (%d,%d)!\n",x,y,orgin_x,orgin_y);
- }
- else
- {
- getStepNext(x,y);
- }
- }
七、优化后效果
八、写在后面
算法大体上完成了,将其改为贪吃蛇AI也只需做少量修改。尽量抽个时间把AI写完,不过可能需要一段时间。
在最短路径的解法上,Dijkstra算法并不是最理想的解法。盲目搜索的效率很低。考虑到地图上存在着两点间距离等信息,可以使用一种启发式搜索算法,如BFS(Best-First Search),以及大名鼎鼎的A*算法。在中文互联网我能找到的有关于A*算法的资料不多,将来得花些时间好好研究下。
源码地址:https://github.com/MagicXyxxx/Algorithms
AI贪吃蛇前瞻——基于Dijkstra算法的最短路径问题的更多相关文章
- AI贪吃蛇(二)
前言 之前写过一篇关于贪吃蛇AI的博客,当时虽然取得了一些成果,但是也存在许多问题,所以最近又花了三天时间重新思考了一下.以下是之前博客存在的一些问题: 策略不对,只要存在找不到尾巴的情况就可能失败, ...
- Python制作AI贪吃蛇
前提:本文实现AI贪吃蛇自行对战,加上人机对战,文章末尾附上源代码以及各位大佬的链接,还有一些实现步骤,读者可再次基础上自行添加电脑VS电脑和玩家VS玩家(其实把人机对战写完,这2个都没什么了,思路都 ...
- Python制作AI贪吃蛇,很多很多细节、思路都写下来了!
前提:本文实现AI贪吃蛇自行对战,加上人机对战,读者可再次基础上自行添加电脑VS电脑和玩家VS玩家(其实把人机对战写完,这2个都没什么了,思路都一样) 实现效果: 很多人学习python,不知道从何学 ...
- 4003.基于Dijsktra算法的最短路径求解
基于Dijsktra算法的最短路径求解 发布时间: 2018年11月26日 10:14 时间限制: 1000ms 内存限制: 128M 有趣的最短路...火候欠佳,目前还很难快速盲打出来,需继 ...
- 基于Dijsktra算法的最短路径求解
基于Dijsktra算法的最短路径求解 描述 一张地图包括n个城市,假设城市间有m条路径(有向图),每条路径的长度已知.给定地图的一个起点城市和终点城市,利用Dijsktra算法求出起点到终点之间 ...
- dijkstra算法计算最短路径和并输出最短路径
void dijisitela(int d, int m1) { ], book[], path[], u, v, min; l = ; ; i < n1; i++) { dis[i] = w[ ...
- Dijkstra算法求最短路径(java)(转)
原文链接:Dijkstra算法求最短路径(java) 任务描述:在一个无向图中,获取起始节点到所有其他节点的最短路径描述 Dijkstra(迪杰斯特拉)算法是典型的最短路径路由算法,用于计算一个节点到 ...
- 基于dijkstra算法求地铁站最短路径以及打印出所有的路径
拓展dijkstra算法,实现利用vector存储多条路径: #include <iostream> #include <vector> #include <stack& ...
- 贪吃蛇—C—基于easyx图形库(下):从画图程序到贪吃蛇【自带穿墙术】
上节我们用方向控制函数写了个小画图程序,它虽然简单好玩,但我们不应该止步于此.革命尚未成功,同志还需努力. 开始撸代码之前,我们先理清一下思路.和前面画图程序不同,贪吃蛇可以有很多节,可以用一个足够大 ...
随机推荐
- 有关于mfc webbrowser插件的使用
最近写的东西中常常需要嵌入一些浏览器,微软提供了一个比较好的接口,可以在MFC写的程序中嵌入一个简易的浏览器,是以ActiveX插件的形式提供的接口,使用起来也比较的方便,这里我就简单记录下这个插件的 ...
- 安全运维 -- Linux服务器使用公私钥密匙证书登录
环境:Ubuntu 16 前言 黑客遍地都是,ssh/pop3/ftp等爆破工具的流行让站长的日常运维工作量大大加重.Metasplot,Bruter等工具更是针对以上协议有专门 的破解方法,有字典破 ...
- 绘制3D的js库
有哪些值得推荐的绘制3D的js库? 4 个回答 默认排序 草皮子 HTML5/GAME 4 人赞同了该回答 只用过three.js,所以推荐这个.不清楚你打算用来做什么,总的来说,得看你的运 ...
- BZOJ1106[POI2007]立方体大作战tet - 树状数组
描述 一个叫做立方体大作战的游戏风靡整个Byteotia.这个游戏的规则是相当复杂的,所以我们只介绍他的简单规则:给定玩家一个有2n个元素的栈,元素一个叠一个地放置.这些元素拥有n个不同的编号,每个编 ...
- Laravel 5.5 Api
Laravel api token验证使用方法 从 Laravel 5.2 开始, Laravel 的将路由的配置进行了分拆, 在 routes 目录下有 web.php 和 api.php 两个路由 ...
- AJAX 请求中多出了一次 OPTIONS 请求 导致 Laravel 中间件无法对 Header 传入的 Token 无法获取
背景知识: 我们会发现,在很多post,put,delete等请求之前,会有一次options请求.本文主要是来讨论一下这是什么原因引起的. 根本原因就是,W3C规范这样要求了!在跨域请求中,分为简单 ...
- [SoapUI] Command-Line Arguments
https://support.smartbear.com/readyapi/docs/soapui/running/automating/cli.html Use the test runner t ...
- @Id 和 @column 注解 使用注意
当@Id写字啊 field 上时 ,如过 把 @column 写在 getter 方法上 ,会出现错误 或者不起作用 Unknown column 'gecompanys0_.sourcec' in ...
- PTA第一次作业和第二次作业
PTA的第一次作业第一题: #include <stdio.h> int main (void) { int grade,i,N ,a=0,b=0,c=0,d=0,e=0; printf( ...
- C和C++之间库的互相调用
http://www.cppblog.com/wolf/articles/77828.html 昨晚有个朋友问我关于在C中调用C++库的问题,今天午饭后,由于脖子痛的厉害而没有加入到我们组的“每天一战 ...