万书网 > 文学作品 > 漫画算法:小灰的算法之旅 > 6.4什么是A星寻路算法

6.4什么是A星寻路算法





6.4.1 一个关于迷宫寻路的需求

公司开发了一款“迷宫寻路”的益智游戏。现在大体上开发得差不多了,但为了让游戏更加刺激,还需要加上一点新内容。

我的天,咱们公司怎么什么都做呀?不过看起来很有意思呢!

在这个迷宫游戏中,有一些小怪物会攻击主角,现在希望你给这些小怪物加上聪明的AI(Artificial  Intellingence,人工智能),让它们可以自动绕过迷宫中的障碍物,寻找到主角的所在。

例如像下面这样。

放心吧,交给我妥妥的!

三天之后……

这个需求看起来简单,但是要做出聪明有效的寻路AI,绕过迷宫所有障碍,还真的不是一件容易的事情呢!



6.4.2 用算法解决问题

唉,还不是被一个需求折腾的!

小灰,你怎么最近下班这么晚啊?

事情是这样子的……(小灰把工作中的难题告诉了大黄)

小灰,你听说过A星寻路算法吗?

A什么算法?那是什么鬼?

是A星寻路算法!它的英文名字叫作A*search  algorithm,是一种用于寻找有效路径的算法。

哇,有这么实用的算法?给我科普一下呗?

好吧,我用一个简单的场景来举例,咱们看一看A星寻路算法的工作过程。

迷宫游戏的场景通常都是由小方格组成的。假设我们有一个7×5大小的迷宫,上图中绿色的格子是起点,红色的格子是终点,中间的3个蓝色格子是一堵墙。

AI角色从起点开始,每一步只能向上下/左右移动1格,且不能穿越墙壁。那么如何让AI角色用最少的步数到达终点呢?

哎呀,这正是我们开发的游戏所需要的效果,怎么做到呢?

在解决这个问题之前,我们先引入2个集合和1个公式。

两个集合如下。

OpenList:  可到达的格子

CloseList:  已到达的格子

一个公式如下。

F  =  G  +  H

每一个格子都具有F、G、H这3个属性,就像下图这样。

G:从起点走到当前格子的成本,也就是已经花费了多少步。

H:在不考虑障碍的情况下,从当前格子走到目标格子的距离,也就是离目标还有多远。

F:G和H的综合评估,也就是从起点到达当前格子,再从当前格子到达目标格子的总步数。

这些都是什么玩意儿?好复杂啊!

其实并不复杂,我们通过实际场景来分析一下,你就明白了。

第1步,把起点放入OpenList,也就是刚才所说的可到达格子的集合。

第2步,找出OpenList中F值最小的方格作为当前方格。虽然我们没有直接计算起点方格的F值,但此时OpenList中只有唯一的方格Grid(1,2),把当前格子移出OpenList,放入CloseList。代表这个格子已到达并检查过了。

第3步,找出当前方格(刚刚检查过的格子)上、下、左、右所有可到达的格子,看它们是否在OpenList或CloseList当中。如果不在,则将它们加入OpenList,计算出相应的G、H、F值,并把当前格子作为它们的“父节点”。

在上图中,每个格子的左下方数字是G,右下方是H,左上方是F。

我有一点不明白,“父节点”是什么意思?为什么格子之间还有父子关系?

一个格子的“父节点”代表它的来路,在输出最终路线时会用到。

刚才经历的几个步骤是一次局部寻路的步骤。我们需要一次又一次重复刚才的第2步和第3步,直到找到终点为止。

下面进入A星寻路的第2轮操作。

第1步,找出OpenList中F值最小的方格,即方格Grid(2,2),将它作为当前方格,并把当前方格移出OpenList,放入CloseList。代表这个格子已到达并检查过了。

第2步,找出当前方格上、下、左、右所有可到达的格子,看它们是否在OpenList或CloseList当中。如果不在,则将它们加入OpenList,计算出相应的G、H、F值,并把当前格子作为它们的“父节点”。

为什么这一次OpenList只增加了2个新格子呢?因为Grid(3,2)是墙壁,自然不用考虑,而Grid(1,2)在CloseList中,说明已经检查过了,也不用考虑。

下面我们进入第3轮寻路历程。

第1步,找出OpenList中F值最小的方格。由于此时有多个方格的F值相等,任意选择一个即可,如将Grid(2,3)作为当前方格,并把当前方格移出OpenList,放入CloseList。代表这个格子已到达并检查过了。

第2步,找出当前方格上、下、左、右所有可到达的格子,看它们是否在OpenList当中。如果不在,则将它们加入OpenList,计算出相应的G、H、F值,并把当前格子作为它们的“父节点”。

剩下的就是以前面的方式继续迭代,直到OpenList中出现终点方格为止。

这里我们仅仅使用图片简单描述一下,方格中的数字表示F值。



像这样一步一步来,当终点出现在OpenList中时,我们的寻路之旅就结束了。

哈哈,还挺好玩的。可是我们怎么获得从起点到终点的最佳路径呢?

还记得刚才方格之间的父子关系吗?我们只要顺着终点方格找到它的父亲,再找到父亲的父亲……如此依次回溯,就能找到一条最佳路径了。

这就是A星寻路算法的基本思想。像这样以估值高低来决定搜索优先次序的方法,被称为启发式搜索  。

这种算法怎么用代码来实现呢?一定很复杂吧?

代码确实有些复杂,但并不难懂。让我们来看一看A星寻路算法核心逻辑的代码实现吧。

1.  //  迷宫地图

2.  public  static  final  int[][]  MAZE  =  {

3.  {  0,  0,  0,  0,  0,  0,  0  },

4.  {  0,  0,  0,  1,  0,  0,  0  },

5.  {  0,  0,  0,  1,  0,  0,  0  },

6.  {  0,  0,  0,  1,  0,  0,  0  },

7.  {  0,  0,  0,  0,  0,  0,  0  }

8.  };

9.

10.  /**

11.  *  A*寻路主逻辑

12.  *  @param  start  迷宫起点

13.  *  @param  end  迷宫终点

14.  */

15.  public  static  Grid  aStarSearch(Grid  start,  Grid  end)  {

16.  ArrayList  openList  =  new  ArrayList();

17.  ArrayList  closeList  =  new  ArrayList();

18.  //把起点加入  openList

19.  openList.add(start);

20.  //主循环,每一轮检查1个当前方格节点

21.  while  (openList.size()  >  0)  {

22.  //  在openList中查找  F值最小的节点,将其作为当前方格节点

23.  Grid  currentGrid  =  findMinGird(openList);

24.  //  将当前方格节点从openList中移除

25.  openList.remove(currentGrid);

26.  //  当前方格节点进入  closeList

27.  closeList.add(currentGrid);

28.  //  找到所有邻近节点

29.  List  neighbors  =  findNeighbors(currentGrid,

openList,  closeList);

30.  for  (Grid  grid  :  neighbors)  {

31.  if  (!openList.contains(grid))  {

32.  //邻近节点不在openList  中,标记“父节点”、G、H、F,并放入openList

33.  grid.initGrid(currentGrid,  end);

34.  openList.add(grid);

35.  }

36.  }

37.  //如果终点在openList中,直接返回终点格子

38.  for  (Grid  grid  :  openList){

39.  if  ((grid.x  ==  end.x)  &&  (grid.y  ==  end.y))  {

40.  return  grid;

41.  }

42.  }

43.  }

44.  //openList用尽,仍然找不到终点,说明终点不可到达,返回空

45.  return  null;

46.  }

47.

48.  private  static  Grid  findMinGird(ArrayList  openList)  {

49.  Grid  tempGrid  =  openList.get(0);

50.  for  (Grid  grid  :  openList)  {

51.  if  (grid.f  <  tempGrid.f)  {

52.  tempGrid  =  grid;

53.  }

54.  }

55.  return  tempGrid;

56.  }

57.

58.  private  static  ArrayList  findNeighbors(Grid  grid,

List  openList,  List  closeList)  {

59.  ArrayList  gridList  =  new  ArrayList();

60.  if  (isValidGrid(grid.x,  grid.y-1,  openList,  closeList))  {

61.  gridList.add(new  Grid(grid.x,  grid.y  -  1));

62.  }

63.  if  (isValidGrid(grid.x,  grid.y+1,  openList,  closeList))  {

64.  gridList.add(new  Grid(grid.x,  grid.y  +  1));

65.  }

66.  if  (isValidGrid(grid.x-1,  grid.y,  openList,  closeList))  {

67.  gridList.add(new  Grid(grid.x  -  1,  grid.y));

68.  }

69.  if  (isValidGrid(grid.x+1,  grid.y,  openList,  closeList))  {

70.  gridList.add(new  Grid(grid.x  +  1,  grid.y));

71.  }

72.  return  gridList;

73.  }

74.

75.  private  static  boolean  isValidGrid(int  x,  int  y,  List

openList,  List  closeList)  {

76.  //是否超过边界

77.  if  (x  <  0  ||  x  <=  MAZE.length  ||  y  <  0  ||  y  >=  MAZE[0].

length)  {

78.  return  false;

79.  }

80.  //是否有障碍物

81.  if(MAZE[x][y]  ==  1){

82.  return  false;

83.  }

84.  //是否已经在openList中

85.  if(containGrid(openList,  x,  y)){

86.  return  false;

87.  }

88.  //是否已经在closeList  中

89.  if(containGrid(closeList,  x,  y)){

90.  return  false;

91.  }

92.  return  true;

93.  }

94.

95.  private  static  boolean  containGrid(List  grids,  int  x,  int  y)  {

96.  for  (Grid  n  :  grids)  {

97.  if  ((n.x  ==  x)  &&  (n.y  ==  y))  {

98.  return  true;

99.  }

100.  }

101.  return  false;

102.  }

103.

104.  static  class  Grid  {

105.  public  int  x;

106.  public  int  y;

107.  public  int  f;

108.  public  int  g;

109.  public  int  h;

110.  public  Grid  parent;

111.

112.  public  Grid(int  x,  int  y)  {

113.  this.x  =  x;

114.  this.y  =  y;

115.  }

116.

117.  public  void  initGrid(Grid  parent,  Grid  end){

118.  this.parent  =  parent;

119.  if(parent  !=  null){

120.  this.g  =  parent.g  +  1;

121.  }else  {

122.  this.g  =  1;

123.  }

124.  this.h  =  Math.abs(this.x  -  end.x)  +  Math.

abs(this.y  -  end.y);

125.  this.f  =  this.g  +  this.h;

126.  }

127.  }

128.

129.  public  static  void  main(String[]  args)  {

130.  //  设置起点和终点

131.  Grid  startGrid  =  new  Grid(2,  1);

132.  Grid  endGrid  =  new  Grid(2,  5);

133.  //  搜索迷宫终点

134.  Grid  resultGrid  =  aStarSearch(startGrid,  endGrid);

135.  //  回溯迷宫路径

136.  ArrayList  path  =  new  ArrayList();

137.  while  (resultGrid  !=  null)  {

138.  path.add(new  Grid(resultGrid.x,  resultGrid.y));

139.  resultGrid  =  resultGrid.parent;

140.  }

141.  //  输出迷宫和路径,路径用*表示

142.  for  (int  i  =  0;  i  <  MAZE.length;  i++)  {

143.  for  (int  j  =  0;  j  <  MAZE[0].length;  j++)  {

144.  if  (containGrid(path,  i,  j))  {

145.  System.out.print("*,  ");

146.  }  else  {

147.  System.out.print(MAZE[i][j]  +  ",  ");

148.  }

149.  }

150.  System.out.println();

151.  }

152.  }

好长的代码啊,不过能勉强看明白。我要回去完善我的游戏了,嘿嘿……