遗传算法在走迷宫游戏中的应用

我的数据挖掘算法库:https://github.com/linyiqun/DataMiningAlgorithm
我的算法库:https://github.com/linyiqun/lyq-algorithms-lib

前言

遗传(GA)算法是一个非常有意思的算法,因为他利用了生物进化理论的知识进行问题的求解。算法的核心就是把拥有更好环境适应度的基因遗传给下一代,这就是其中的关键的选择操作,遗传算法整体的阶段分为选择,交叉和变异操作,选择操作和变异操作在其中又是比较重要的步骤。本篇文章不会讲述GA算法的具体细节,之前我曾经写过一篇专门的文章介绍过此算法,链接:http://blog.****.net/androidlushangderen/article/details/44041499,里面介绍了一些基本的概念和算法的原理过程,如果你对GA算法掌握的还不错的话,那么对于理解后面遗传算法在走迷宫的应用来说应该不是难事。

算法在迷宫游戏中的应用

先说说走迷宫游戏要解决的问题是什么, 走迷宫游戏说白了就是给定起点,终点,中间设置一堆的障碍,然后要求可达的路径,注意这里指的是可达路径,并没有说一定是最优路径,因为最优路径一定是用步数最少的,这一点还是很不同的。而另一方面,遗传算法也是用来搜索问题最优解的,所以刚刚好可以转移到这个问题上。用一个遗传算法去解决生活中的实际问题最关键的就是如何用遗传算法中的概念表示出来,比如遗传算法中核心的几个概念,基因编码,基因长度的设置,适应度函数的定义,3个概念每个都很重要。好的,目的要求已经慢慢的明确了,下面一个个问题的解决。

为了能让大家更好的理解,下面举出一个例子,如图所示:

遗传算法在走迷宫游戏中的应用

图是自己做的,比较简略,以左边点的形式表示,从图中可以看出,起点位置(4, 4),出口左边为绿色区域位置(1,0),X符号表示的障碍区域,不允许经过,问题就转为搜索出从起点到终点位置的最短路径,因为本身例子构造的不是很复杂,我们按照对角线的方式出发,总共的步数=4-1 + 4-0=7步,只要中间不拐弯,每一步都是靠近目标点方向的移动就是最佳的方式。下面看看如何转化成遗传算法中的概念表示。

个体基因长度

首先是基于长度,因为最后筛选出的是一个个体,就是满足条件的个体,他的基因编码就是问题的最优解,所以就能联想把角色的每一步移动操作看出是一个基因编码,总共7步就需要7个基因值表示,所以基因的长度在本例子中就是7。

基因表示

已经将角色的每一次的移动步骤转化为基因的表示,每次的移动总共有4种可能,上下左右,基因编码是标准的二进制形式,所以可以取值为00代表向上,01向下,10向左,11向右,也就是说,每个基因组用2个编码表示,所以总共的编码数字就是2*7=14个,两两一对。

适应度函数

适应度函数的设置应该是在遗传算法中最重要了吧,以为他的设置好坏直接决定着遗传质量的好坏,基因组表示的移动的操作步骤,给定起点位置,通过基因组的编码组数据,我们可以计算出最终的抵达坐标,这里可以很容易的得出结论,如果最后的抵达坐标越接近出口坐标,就越是我们想要的结果,也就是适应值越高,所以我们可以用下面的公式作为适应度函数:

遗传算法在走迷宫游戏中的应用


(x, y)为计算出的适应值的函数值在0到1之间波动,1为最大值,就是抵达的坐标恰好是出口位置的时候,当然适应度函数的表示不是唯一的。

算法的代码实现

算法地图数据的输入mapData.txt:

  1. 00000
  2. 200-10
  3. 00000
  4. 0-100-1
  5. 00001

就是上面图示的那个例子.

算法的主要实现类GATool.java:

  1. packageGA_Maze;
  2. importjava.io.BufferedReader;
  3. importjava.io.File;
  4. importjava.io.FileReader;
  5. importjava.io.IOException;
  6. importjava.text.MessageFormat;
  7. importjava.util.ArrayList;
  8. importjava.util.Random;
  9. /**
  10. *遗传算法在走迷宫游戏的应用-遗传算法工具类
  11. *
  12. *@authorlyq
  13. *
  14. */
  15. publicclassGATool{
  16. //迷宫出入口标记
  17. publicstaticfinalintMAZE_ENTRANCE_POS=1;
  18. publicstaticfinalintMAZE_EXIT_POS=2;
  19. //方向对应的编码数组
  20. publicstaticfinalint[][]MAZE_DIRECTION_CODE=newint[][]{{0,0},
  21. {0,1},{1,0},{1,1},};
  22. //坐标点方向改变
  23. publicstaticfinalint[][]MAZE_DIRECTION_CHANGE=newint[][]{
  24. {-1,0},{1,0},{0,-1},{0,1},};
  25. //方向的文字描述
  26. publicstaticfinalString[]MAZE_DIRECTION_LABEL=newString[]{"上",
  27. "下","左","右"};
  28. //地图数据文件地址
  29. privateStringfilePath;
  30. //走迷宫的最短步数
  31. privateintstepNum;
  32. //初始个体的数量
  33. privateintinitSetsNum;
  34. //迷宫入口位置
  35. privateint[]startPos;
  36. //迷宫出口位置
  37. privateint[]endPos;
  38. //迷宫地图数据
  39. privateint[][]mazeData;
  40. //初始个体集
  41. privateArrayList<int[]>initSets;
  42. //随机数产生器
  43. privateRandomrandom;
  44. publicGATool(StringfilePath,intinitSetsNum){
  45. this.filePath=filePath;
  46. this.initSetsNum=initSetsNum;
  47. readDataFile();
  48. }
  49. /**
  50. *从文件中读取数据
  51. */
  52. publicvoidreadDataFile(){
  53. Filefile=newFile(filePath);
  54. ArrayList<String[]>dataArray=newArrayList<String[]>();
  55. try{
  56. BufferedReaderin=newBufferedReader(newFileReader(file));
  57. Stringstr;
  58. String[]tempArray;
  59. while((str=in.readLine())!=null){
  60. tempArray=str.split("");
  61. dataArray.add(tempArray);
  62. }
  63. in.close();
  64. }catch(IOExceptione){
  65. e.getStackTrace();
  66. }
  67. introwNum=dataArray.size();
  68. mazeData=newint[rowNum][rowNum];
  69. for(inti=0;i<rowNum;i++){
  70. String[]data=dataArray.get(i);
  71. for(intj=0;j<data.length;j++){
  72. mazeData[i][j]=Integer.parseInt(data[j]);
  73. //赋值入口和出口位置
  74. if(mazeData[i][j]==MAZE_ENTRANCE_POS){
  75. startPos=newint[2];
  76. startPos[0]=i;
  77. startPos[1]=j;
  78. }elseif(mazeData[i][j]==MAZE_EXIT_POS){
  79. endPos=newint[2];
  80. endPos[0]=i;
  81. endPos[1]=j;
  82. }
  83. }
  84. }
  85. //计算走出迷宫的最短步数
  86. stepNum=Math.abs(startPos[0]-endPos[0])
  87. +Math.abs(startPos[1]-endPos[1]);
  88. }
  89. /**
  90. *产生初始数据集
  91. */
  92. privatevoidproduceInitSet(){
  93. //方向编码
  94. intdirectionCode=0;
  95. random=newRandom();
  96. initSets=newArrayList<>();
  97. //每个步骤的操作需要用2位数字表示
  98. int[]codeNum;
  99. for(inti=0;i<initSetsNum;i++){
  100. codeNum=newint[stepNum*2];
  101. for(intj=0;j<stepNum;j++){
  102. directionCode=random.nextInt(4);
  103. codeNum[2*j]=MAZE_DIRECTION_CODE[directionCode][0];
  104. codeNum[2*j+1]=MAZE_DIRECTION_CODE[directionCode][1];
  105. }
  106. initSets.add(codeNum);
  107. }
  108. }
  109. /**
  110. *选择操作,把适值较高的个体优先遗传到下一代
  111. *
  112. *@paraminitCodes
  113. *初始个体编码
  114. *@return
  115. */
  116. privateArrayList<int[]>selectOperate(ArrayList<int[]>initCodes){
  117. doublerandomNum=0;
  118. doublesumFitness=0;
  119. ArrayList<int[]>resultCodes=newArrayList<>();
  120. double[]adaptiveValue=newdouble[initSetsNum];
  121. for(inti=0;i<initSetsNum;i++){
  122. adaptiveValue[i]=calFitness(initCodes.get(i));
  123. sumFitness+=adaptiveValue[i];
  124. }
  125. //转成概率的形式,做归一化操作
  126. for(inti=0;i<initSetsNum;i++){
  127. adaptiveValue[i]=adaptiveValue[i]/sumFitness;
  128. }
  129. for(inti=0;i<initSetsNum;i++){
  130. randomNum=random.nextInt(100)+1;
  131. randomNum=randomNum/100;
  132. //因为1.0是无法判断到的,,总和会无限接近1.0取为0.99做判断
  133. if(randomNum==1){
  134. randomNum=randomNum-0.01;
  135. }
  136. sumFitness=0;
  137. //确定区间
  138. for(intj=0;j<initSetsNum;j++){
  139. if(randomNum>sumFitness
  140. &&randomNum<=sumFitness+adaptiveValue[j]){
  141. //采用拷贝的方式避免引用重复
  142. resultCodes.add(initCodes.get(j).clone());
  143. break;
  144. }else{
  145. sumFitness+=adaptiveValue[j];
  146. }
  147. }
  148. }
  149. returnresultCodes;
  150. }
  151. /**
  152. *交叉运算
  153. *
  154. *@paramselectedCodes
  155. *上步骤的选择后的编码
  156. *@return
  157. */
  158. privateArrayList<int[]>crossOperate(ArrayList<int[]>selectedCodes){
  159. intrandomNum=0;
  160. //交叉点
  161. intcrossPoint=0;
  162. ArrayList<int[]>resultCodes=newArrayList<>();
  163. //随机编码队列,进行随机交叉配对
  164. ArrayList<int[]>randomCodeSeqs=newArrayList<>();
  165. //进行随机排序
  166. while(selectedCodes.size()>0){
  167. randomNum=random.nextInt(selectedCodes.size());
  168. randomCodeSeqs.add(selectedCodes.get(randomNum));
  169. selectedCodes.remove(randomNum);
  170. }
  171. inttemp=0;
  172. int[]array1;
  173. int[]array2;
  174. //进行两两交叉运算
  175. for(inti=1;i<randomCodeSeqs.size();i++){
  176. if(i%2==1){
  177. array1=randomCodeSeqs.get(i-1);
  178. array2=randomCodeSeqs.get(i);
  179. crossPoint=random.nextInt(stepNum-1)+1;
  180. //进行交叉点位置后的编码调换
  181. for(intj=0;j<2*stepNum;j++){
  182. if(j>=2*crossPoint){
  183. temp=array1[j];
  184. array1[j]=array2[j];
  185. array2[j]=temp;
  186. }
  187. }
  188. //加入到交叉运算结果中
  189. resultCodes.add(array1);
  190. resultCodes.add(array2);
  191. }
  192. }
  193. returnresultCodes;
  194. }
  195. /**
  196. *变异操作
  197. *
  198. *@paramcrossCodes
  199. *交叉运算后的结果
  200. *@return
  201. */
  202. privateArrayList<int[]>variationOperate(ArrayList<int[]>crossCodes){
  203. //变异点
  204. intvariationPoint=0;
  205. ArrayList<int[]>resultCodes=newArrayList<>();
  206. for(int[]array:crossCodes){
  207. variationPoint=random.nextInt(stepNum);
  208. for(inti=0;i<array.length;i+=2){
  209. //变异点进行变异
  210. if(i%2==0&&i/2==variationPoint){
  211. array[i]=(array[i]==0?1:0);
  212. array[i+1]=(array[i+1]==0?1:0);
  213. break;
  214. }
  215. }
  216. resultCodes.add(array);
  217. }
  218. returnresultCodes;
  219. }
  220. /**
  221. *根据编码计算适值
  222. *
  223. *@paramcode
  224. *当前的编码
  225. *@return
  226. */
  227. publicdoublecalFitness(int[]code){
  228. doublefintness=0;
  229. //由编码计算所得的终点横坐标
  230. intendX=0;
  231. //由编码计算所得的终点纵坐标
  232. intendY=0;
  233. //基于片段所代表的行走方向
  234. intdirection=0;
  235. //临时坐标点横坐标
  236. inttempX=0;
  237. //临时坐标点纵坐标
  238. inttempY=0;
  239. endX=startPos[0];
  240. endY=startPos[1];
  241. for(inti=0;i<stepNum;i++){
  242. direction=binaryArrayToNum(newint[]{code[2*i],
  243. code[2*i+1]});
  244. //根据方向改变数组做坐标点的改变
  245. tempX=endX+MAZE_DIRECTION_CHANGE[direction][0];
  246. tempY=endY+MAZE_DIRECTION_CHANGE[direction][1];
  247. //判断坐标点是否越界
  248. if(tempX>=0&&tempX<mazeData.length&&tempY>=0
  249. &&tempY<mazeData[0].length){
  250. //判断坐标点是否走到阻碍块
  251. if(mazeData[tempX][tempY]!=-1){
  252. endX=tempX;
  253. endY=tempY;
  254. }
  255. }
  256. }
  257. //根据适值函数进行适值的计算
  258. fintness=1.0/(Math.abs(endX-endPos[0])
  259. +Math.abs(endY-endPos[1])+1);
  260. returnfintness;
  261. }
  262. /**
  263. *根据当前编码判断是否已经找到出口位置
  264. *
  265. *@paramcode
  266. *经过若干次遗传的编码
  267. *@return
  268. */
  269. privatebooleanifArriveEndPos(int[]code){
  270. booleanisArrived=false;
  271. //由编码计算所得的终点横坐标
  272. intendX=0;
  273. //由编码计算所得的终点纵坐标
  274. intendY=0;
  275. //基于片段所代表的行走方向
  276. intdirection=0;
  277. //临时坐标点横坐标
  278. inttempX=0;
  279. //临时坐标点纵坐标
  280. inttempY=0;
  281. endX=startPos[0];
  282. endY=startPos[1];
  283. for(inti=0;i<stepNum;i++){
  284. direction=binaryArrayToNum(newint[]{code[2*i],
  285. code[2*i+1]});
  286. //根据方向改变数组做坐标点的改变
  287. tempX=endX+MAZE_DIRECTION_CHANGE[direction][0];
  288. tempY=endY+MAZE_DIRECTION_CHANGE[direction][1];
  289. //判断坐标点是否越界
  290. if(tempX>=0&&tempX<mazeData.length&&tempY>=0
  291. &&tempY<mazeData[0].length){
  292. //判断坐标点是否走到阻碍块
  293. if(mazeData[tempX][tempY]!=-1){
  294. endX=tempX;
  295. endY=tempY;
  296. }
  297. }
  298. }
  299. if(endX==endPos[0]&&endY==endPos[1]){
  300. isArrived=true;
  301. }
  302. returnisArrived;
  303. }
  304. /**
  305. *二进制数组转化为数字
  306. *
  307. *@parambinaryArray
  308. *待转化二进制数组
  309. */
  310. privateintbinaryArrayToNum(int[]binaryArray){
  311. intresult=0;
  312. for(inti=binaryArray.length-1,k=0;i>=0;i--,k++){
  313. if(binaryArray[i]==1){
  314. result+=Math.pow(2,k);
  315. }
  316. }
  317. returnresult;
  318. }
  319. /**
  320. *进行遗传算法走出迷宫
  321. */
  322. publicvoidgoOutMaze(){
  323. //迭代遗传次数
  324. intloopCount=0;
  325. booleancanExit=false;
  326. //结果路径
  327. int[]resultCode=null;
  328. ArrayList<int[]>initCodes;
  329. ArrayList<int[]>selectedCodes;
  330. ArrayList<int[]>crossedCodes;
  331. ArrayList<int[]>variationCodes;
  332. //产生初始数据集
  333. produceInitSet();
  334. initCodes=initSets;
  335. while(true){
  336. for(int[]array:initCodes){
  337. //遗传迭代的终止条件为是否找到出口位置
  338. if(ifArriveEndPos(array)){
  339. resultCode=array;
  340. canExit=true;
  341. break;
  342. }
  343. }
  344. if(canExit){
  345. break;
  346. }
  347. selectedCodes=selectOperate(initCodes);
  348. crossedCodes=crossOperate(selectedCodes);
  349. variationCodes=variationOperate(crossedCodes);
  350. initCodes=variationCodes;
  351. loopCount++;
  352. //如果遗传次数超过100次,则退出
  353. if(loopCount>=100){
  354. break;
  355. }
  356. }
  357. System.out.println("总共遗传进化了"+loopCount+"次");
  358. printFindedRoute(resultCode);
  359. }
  360. /**
  361. *输出找到的路径
  362. *
  363. *@paramcode
  364. */
  365. privatevoidprintFindedRoute(int[]code){
  366. if(code==null){
  367. System.out.println("在有限的遗传进化次数内,没有找到最优路径");
  368. return;
  369. }
  370. inttempX=startPos[0];
  371. inttempY=startPos[1];
  372. intdirection=0;
  373. System.out.println(MessageFormat.format(
  374. "起始点位置({0},{1}),出口点位置({2},{3})",tempX,tempY,endPos[0],
  375. endPos[1]));
  376. System.out.print("搜索到的结果编码:");
  377. for(intvalue:code){
  378. System.out.print(""+value);
  379. }
  380. System.out.println();
  381. for(inti=0,k=1;i<code.length;i+=2,k++){
  382. direction=binaryArrayToNum(newint[]{code[i],code[i+1]});
  383. tempX+=MAZE_DIRECTION_CHANGE[direction][0];
  384. tempY+=MAZE_DIRECTION_CHANGE[direction][1];
  385. System.out.println(MessageFormat.format(
  386. "第{0}步,编码为{1}{2},向{3}移动,移动后到达({4},{5})",k,code[i],code[i+1],
  387. MAZE_DIRECTION_LABEL[direction],tempX,tempY));
  388. }
  389. }
  390. }
算法的调用类Client.java:

  1. packageGA_Maze;
  2. /**
  3. *遗传算法在走迷宫游戏的应用
  4. *@authorlyq
  5. *
  6. */
  7. publicclassClient{
  8. publicstaticvoidmain(String[]args){
  9. //迷宫地图文件数据地址
  10. StringfilePath="C:\\Users\\lyq\\Desktop\\icon\\mapData.txt";
  11. //初始个体数量
  12. intinitSetsNum=4;
  13. GATooltool=newGATool(filePath,initSetsNum);
  14. tool.goOutMaze();
  15. }
  16. }

算法的输出:

我测了很多次的数据,因为有可能会一时半会搜索不出来,我设置了最大遗传次数100次。

  1. 总共遗传进化了2
  2. 起始点位置(4,4),出口点位置(1,0)
  3. 搜索到的结果编码:10100000100010
  4. 1步,编码为10,向左移动,移动后到达(4,3)
  5. 2步,编码为10,向左移动,移动后到达(4,2)
  6. 3步,编码为00,向上移动,移动后到达(3,2)
  7. 4步,编码为00,向上移动,移动后到达(2,2)
  8. 5步,编码为10,向左移动,移动后到达(2,1)
  9. 6步,编码为00,向上移动,移动后到达(1,1)
  10. 7步,编码为10,向左移动,移动后到达(1,0)
  11. 总共遗传进化了8
  12. 起始点位置(4,4),出口点位置(1,0)
  13. 搜索到的结果编码:10001000101000
  14. 1步,编码为10,向左移动,移动后到达(4,3)
  15. 2步,编码为00,向上移动,移动后到达(3,3)
  16. 3步,编码为10,向左移动,移动后到达(3,2)
  17. 4步,编码为00,向上移动,移动后到达(2,2)
  18. 5步,编码为10,向左移动,移动后到达(2,1)
  19. 6步,编码为10,向左移动,移动后到达(2,0)
  20. 7步,编码为00,向上移动,移动后到达(1,0)
  21. 总共遗传进化了100
  22. 在有限的遗传进化次数内,没有找到最优路径

算法小结

遗传算法在走迷宫中的应用总体而言还是非常有意思的如果你去认真的体会的话,至少让我更加深入的理解了GA算法,如果博友向要亲自实现这算法,我给几点建议,第一是迷宫难度的和初始个体数量的设置,为什么要注意这2点呢,一个是这关系到遗传迭代的次数,在一段时间内有的时候遗传算法是找不出来的,如果找不出来,PC机的CPU会持续高速的计算,所以不要让遗传进行无限制的进行,最好做点次数限制,也可能是我的本本配置太烂了。。在算法的调试中修复了一个之前没发现的bug,就是选择阶段的时候对于随机数的判断少考虑了一种情形,当随机数取到1.0的时候,其实是不能判断到的,因为概念和只会无限接近1,就不知道被划分到哪个区域中了