[关闭]
@iStarLee 2019-08-13T05:46:21.000000Z 字数 13304 阅读 434

GlobalPlanner —— A*算法

navigation


Reference

1 Introduction

1.1 常见路径搜索算法

1.2 A*的一个简单描述

在凹型障碍物的例子中,A*找到一条和Dijkstra算法一样好的路径:
image_1dhvnkjvgeb71n5s15o07lqebn9.png-7.2kB

成功的秘决在于,它把Dijkstra算法(靠近初始点的结点)和BFS算法(靠近目标点的结点)的信息块结合起来。在讨论A*的标准术语中

在上图中,

当从初始点向目标点移动时,A*权衡这两者。每次进行主循环时,它检查f(n)最小的结点n,其中f(n) = g(n) + h(n)。

2 启发式算法

启发式函数h(n)告诉A*从任意结点n到目标结点的最小代价评估值。

2.1 A*对启发式函数的使用

启发式函数可以控制A*的行为:

所以我们得到一个很有趣的情况,那就是我们可以决定我们想要从A*中获得什么。理想情况下(注:原文为At exactly the right point),我们想最快地得到最短路径。如果我们的目标太低,我们仍会得到最短路径,不过速度变慢了;如果我们的目标太高,那我们就放弃了最短路径,但A*运行得更快。

A*的这个特性非常有用。例如,你会发现在某些情况下,你希望得到一条好的路径("good" path)而不是一条完美的路径("perfect" path)。为了权衡g(n)和h(n),你可以修改任意一个。

在学术上,如果启发式函数值是对实际代价的低估,A*算法被称为简单的A算法(原文为simply A)。然而,我继续称之为A*,因为在实现上是一样的,并且在游戏编程领域并不区别A和A*。

2.2 速度还是精确度?

速度和精确度之间的选择前不是静态的。你可以基于CPU的速度、用于路径搜索的时间片数、地图上物体(units)的数量、物体的重要性、组(group)的大小、难度或者其他任何因素来进行动态的选择。取得动态的折衷的一个方法是,建立一个启发式函数用于假定通过一个网格空间的最小代价是1,然后建立一个代价函数(cost function)用于测量(scales):

速度和精确度之间的选择并不是全局的。在地图上的某些区域,精确度是重要的,你可以基于此进行动态选择。
在安全区域,可以考虑不寻找精确的最短路径而取近似路径,因此寻路快;但在危险区域,逃跑的安全性和逃跑速度是重要的,即路径的精确度是重要的,因此可以多花点时间用于寻找精确路径。

2.3 衡量单位

A*计算f(n) = g(n) + h(n)。为了对这两个值进行相加,这两个值必须使用相同的衡量单位。如果g(n)用小时来衡量而h(n)用米来衡量,那么A*将会认为g或者h太大或者太小,因而你将不能得到正确的路径,同时你的A*算法将运行得更慢。

2.4 精确的启发式函数

  如果你的启发式函数精确地等于实际最佳路径(optimal path),如下一部分的图中所示,你会看到此时A*扩展的结点将非常少。A*算法内部发生的事情是:在每一结点它都计算f(n) = g(n) + h(n)。当h(n)精确地和g(n)匹配(译者注:原文为match)时,f(n)的值在沿着该路径时将不会改变。不在正确路径(right path)上的所有结点的f值均大于正确路径上的f值(译者注:正确路径在这里应该是指最短路径)。如果已经有较低f值的结点,A*将不考虑f值较高的结点,因此它肯定不会偏离最短路径。

2.4.1 预计算的精确启发式函数

  构造精确启发函数的一种方法是预先计算任意一对结点之间最短路径的长度。在许多游戏的地图中这并不可行。然后,有几种方法可以近似模拟这种启发函数:

然后添加一个启发函数h’用于评估从任意位置到达邻近导航点(waypoints)的代价。(如果愿意,后者也可以通过预计算得到。)最终的启发式函数可以是:

或者如果你希望一个更好但是更昂贵的启发式函数,则分别用靠近结点和目标的所有的w1,w2对对上式进行求值。(译者注:原文为or if you want a better but more expensive heuristic, evaluate the above with all pairs w1, w2 that are close to the node and the goal, respectively.)

2.4.2 线性精确启发式算法

  在特殊情况下,你可以不通过预计算而让启发式函数很精确。如果你有一个不存在障碍物和slow地形,那么从初始点到目标的最短路径应该是一条直线。

  如果你正使用简单的启发式函数(我们不知道地图上的障碍物),则它应该和精确的启发式函数相符合(译者注:原文为match)。如果不是这样,则你会遇到衡量单位的问题,或者你所选择的启发函数类型的问题。

2.5 网格地图中的启发式算法

  在网格地图中,有一些众所周知的启发式函数。

2.5.1 曼哈顿距离

标准的启发式函数是曼哈顿距离(Manhattan distance)。考虑你的代价函数并找到从一个位置移动到邻近位置的最小代价D,因此,我的游戏中的启发式函数应该是曼哈顿距离的D倍

  1. // 曼哈顿距离
  2. H(n) = D * (abs ( n.x goal.x ) + abs ( n.y goal.y ) )

你应该使用符合你的代价函数的衡量单位。

image_1dhvostst1b2613g61mfo1pgftmnm.png-11.7kB

曼哈顿距离又称为出租车距离,曼哈顿距离不是距离不变量,当坐标轴变动时,点间的距离就会不同

2.5.2 对角线距离

如果在你的地图中你允许对角运动那么你需要一个不同的启发函数。(4 east, 4 north)的曼哈顿距离将变成8*D。然而,你可以简单地移动(4 northeast)代替,所以启发函数应该是4*D。这个函数使用对角线,假设直线和对角线的代价都是D:

  1. // 对角线距离
  2. h(n) = D * max(abs(n.x - goal.x), abs(n.y - goal.y))

image_1dhvp6guitnc18bq1np317rl1gn13.png-6kB

如果对角线运动的代价不是D,但类似于D2 = sqrt(2) *D,则上面的启发函数不准确。你需要一些更准确(原文为sophisticated)的东西:

  1. // 计算h_diagonal(n):沿着斜线可以移动的步数
  2. h_diagonal(n) = min(abs(n.x - goal.x), abs(n.y - goal.y))
  3. // h_straight(n):曼哈顿距离
  4. h_straight(n) = (abs(n.x - goal.x) + abs(n.y - goal.y))
  5. // 然后合并这两项,让所有的斜线步都乘以D2,剩下的所有直线步(注意这里是曼哈顿距离的步数减去2倍的斜线步数)都乘以D
  6. h(n) = D2 * h_diagonal(n) + D * (h_straight(n) - 2*h_diagonal(n)))

2.5.3 欧几里得距离

如果你的单位可以沿着任意角度移动(而不是网格方向),那么你也许应该使用直线距离:

  1. h(n) = D * sqrt((n.x-goal.x)^2 + (n.y-goal.y)^2)

然而,如果是这样的话,直接使用A*时将会遇到麻烦,因为代价函数g不会match启发函数h。因为欧几里得距离比曼哈顿距离和对角线距离都短,你仍可以得到最短路径,不过A*将运行得更久一些:
image_1dhvpcufl18pa7tv2nmjg1bp41g.png-7.4kB

2.5.4 平方后的欧几里得距离

我曾经看到一些A*的网页,其中提到让你通过使用距离的平方而避免欧几里得距离中昂贵的平方根运算:

  1. // THIS is wrong!
  2. h(n) = D * ((n.x-goal.x)^2 + (n.y-goal.y)^2)

不要这样做!这明显地导致衡量单位的问题。当A*计算f(n) = g(n) + h(n),距离的平方将比g的代价大很多,并且你会因为启发式函数评估值过高而停止。对于更长的距离,这样做会靠近g(n)的极端情况而不再计算任何东西,A*退化成BFS:

2.5.5 Breaking ties

2.5.6 区域搜索

  如果你想搜索邻近目标的任意不确定结点,而不是某个特定的结点,你应该建立一个启发函数h’(x),使得h’(x)为h1(x), h2(x), h3(x)。。。的最小值,而这些h1, h2, h3是邻近结点的启发函数。然而,一种更快的方法是让A*仅搜索目标区域的中心。一旦你从OPEN集合中取得任意一个邻近目标的结点,你就可以停止搜索并建立一条路径了。

3 Implementation notes

3.1 算法流程

3.1.1 基本概念总结

3.1.2 算法解析

算法流程参看的是geeksforgeeks,这个上面的是英文写的,但是都比较浅显,非常容易理解。

  1. // A* Search Algorithm
  2. 1. Initialize the open list
  3. 2. Initialize the closed list
  4. put the starting node on the open
  5. list (you can leave its f at zero)
  6. 3. while the open list is not empty
  7. a) find the node with the least f on
  8. the open list, call it "q"
  9. b) pop q off the open list
  10. c) generate q's 8 successors and set their
  11. parents to q
  12. d) for each successor
  13. i) if successor is the goal, stop search
  14. successor.g = q.g + distance between
  15. successor and q
  16. successor.h = distance from goal to
  17. successor (This can be done using many
  18. ways, we will discuss three heuristics-
  19. Manhattan, Diagonal and Euclidean
  20. Heuristics)
  21. successor.f = successor.g + successor.h
  22. ii) if a node with the same position as
  23. successor is in the OPEN list which has a
  24. lower f than successor, skip this successor
  25. iii) if a node with the same position as
  26. successor is in the CLOSED list which has
  27. a lower f than successor, skip this successor
  28. otherwise, add the node to the open list
  29. end (for loop)
  30. e) push q on the closed list
  31. end (while loop)

3.2 算法实现

我根据上面的代码理解,修改了一下代码,代码量减少了很多

  1. // A C++ Program to implement A* Search Algorithm
  2. #include<bits/stdc++.h>
  3. using namespace std;
  4. #define ROW 9
  5. #define COL 10
  6. // Creating a shortcut for int, int pair type
  7. typedef pair<int, int> Pair;
  8. // Creating a shortcut for pair<int, pair<int, int>> type
  9. typedef pair<double, pair<int, int>> pPair;
  10. // A structure to hold the neccesary parameters
  11. struct cell {
  12. // Row and Column index of its parent
  13. // Note that 0 <= i <= ROW-1 & 0 <= j <= COL-1
  14. int parent_i, parent_j;
  15. // f = g + h
  16. double f, g, h;
  17. };
  18. // A Utility Function to check whether given cell (row, col)
  19. // is a valid cell or not.
  20. bool isValid(int row, int col)
  21. {
  22. // Returns true if row number and column number
  23. // is in range
  24. return (row >= 0) && (row < ROW) &&
  25. (col >= 0) && (col < COL);
  26. }
  27. // A Utility Function to check whether the given cell is
  28. // blocked or not
  29. bool isUnBlocked(int grid[][COL], int row, int col)
  30. {
  31. // Returns true if the cell is not blocked else false
  32. if (grid[row][col] == 1)
  33. return (true);
  34. else
  35. return (false);
  36. }
  37. // A Utility Function to check whether destination cell has
  38. // been reached or not
  39. bool isDestination(int row, int col, Pair dest)
  40. {
  41. if (row == dest.first && col == dest.second)
  42. return (true);
  43. else
  44. return (false);
  45. }
  46. // A Utility Function to calculate the 'h' heuristics.
  47. double calculateHValue(int row, int col, Pair dest)
  48. {
  49. // Return using the distance formula
  50. return ((double) sqrt((row - dest.first)*(row - dest.first)
  51. + (col - dest.second)*(col - dest.second)));
  52. }
  53. // A Utility Function to trace the path from the source
  54. // to destination
  55. void tracePath(cell cellDetails[][COL], Pair dest)
  56. {
  57. printf("\nThe Path is ");
  58. int row = dest.first;
  59. int col = dest.second;
  60. stack<Pair> Path;
  61. while (!(cellDetails[row][col].parent_i == row
  62. && cellDetails[row][col].parent_j == col))
  63. {
  64. Path.push(make_pair(row, col));
  65. int temp_row = cellDetails[row][col].parent_i;
  66. int temp_col = cellDetails[row][col].parent_j;
  67. row = temp_row;
  68. col = temp_col;
  69. }
  70. Path.push(make_pair(row, col));
  71. while (!Path.empty())
  72. {
  73. pair<int, int> p = Path.top();
  74. Path.pop();
  75. printf("-> (%d,%d) ", p.first, p.second);
  76. }
  77. return;
  78. }
  79. // traversing neighbors
  80. bool TraversingEightNeighbors(cell cellDetails[][COL], Pair dest,
  81. bool closedList[][COL], int grid[][COL],
  82. set<pPair> &openList, bool &foundDest, int i, int j)
  83. {
  84. /*
  85. Generating all the 8 successor of this cell
  86. N.W N N.E
  87. \ | /
  88. \ | /
  89. W----Cell----E
  90. / | \
  91. / | \
  92. S.W S S.E
  93. Cell-->Popped Cell (i, j)
  94. N --> North (i-1, j)
  95. S --> South (i+1, j)
  96. E --> East (i, j+1)
  97. W --> West (i, j-1)
  98. N.E--> North-East (i-1, j+1)
  99. N.W--> North-West (i-1, j-1)
  100. S.E--> South-East (i+1, j+1)
  101. S.W--> South-West (i+1, j-1)*/
  102. vector<pair<int, int>> neighborsIndex = {make_pair(i - 1, j),
  103. make_pair(i + 1, j),
  104. make_pair(i, j + 1),
  105. make_pair(i, j - 1),
  106. make_pair(i - 1, j + 1),
  107. make_pair(i - 1, j - 1),
  108. make_pair(i + 1, j + 1),
  109. make_pair(i + 1, j - 1)};
  110. for (int ind = 0; ind < neighborsIndex.size(); ++ind)
  111. {
  112. int ni = neighborsIndex[ind].first;
  113. int nj = neighborsIndex[ind].second;
  114. // Only process this cell if this is a valid one
  115. if (isValid(ni, nj) == true)
  116. {
  117. // If the destination cell is the same as the
  118. // current successor
  119. if (isDestination(ni, nj, dest) == true)
  120. {
  121. // Set the Parent of the destination cell
  122. cellDetails[ni][nj].parent_i = i;
  123. cellDetails[ni][nj].parent_j = j;
  124. printf("The destination cell is found\n");
  125. tracePath(cellDetails, dest);
  126. foundDest = true;
  127. return true;
  128. }
  129. // If the successor is already on the closed
  130. // list or if it is blocked, then ignore it.
  131. // Else do the following
  132. else if (closedList[ni][nj] == false && isUnBlocked(grid, ni, nj) == true)
  133. {
  134. // To store the 'g', 'h' and 'f' of the 8 successors
  135. double gNew = cellDetails[i][j].g + 1.0;
  136. double hNew = calculateHValue(ni, nj, dest);
  137. double fNew = gNew + hNew;
  138. // If it isn’t on the open list, add it to
  139. // the open list. Make the current square
  140. // the parent of this square. Record the
  141. // f, g, and h costs of the square cell
  142. // OR
  143. // If it is on the open list already, check
  144. // to see if this path to that square is better,
  145. // using 'f' cost as the measure.
  146. if (cellDetails[ni][nj].f == FLT_MAX ||
  147. cellDetails[ni][nj].f > fNew)
  148. {
  149. openList.insert(make_pair(fNew, make_pair(ni, nj)));
  150. // Update the details of this cell
  151. cellDetails[ni][nj].f = fNew;
  152. cellDetails[ni][nj].g = gNew;
  153. cellDetails[ni][nj].h = hNew;
  154. cellDetails[ni][nj].parent_i = i;
  155. cellDetails[ni][nj].parent_j = j;
  156. }
  157. }
  158. }
  159. }
  160. return false;
  161. }
  162. // A Function to find the shortest path between
  163. // a given source cell to a destination cell according
  164. // to A* Search Algorithm
  165. void aStarSearch(int grid[][COL], Pair src, Pair dest)
  166. {
  167. // If the source is out of range
  168. if (isValid(src.first, src.second) == false)
  169. {
  170. printf("Source is invalid\n");
  171. return;
  172. }
  173. // If the destination is out of range
  174. if (isValid(dest.first, dest.second) == false)
  175. {
  176. printf("Destination is invalid\n");
  177. return;
  178. }
  179. // Either the source or the destination is blocked
  180. if (isUnBlocked(grid, src.first, src.second) == false ||
  181. isUnBlocked(grid, dest.first, dest.second) == false)
  182. {
  183. printf("Source or the destination is blocked\n");
  184. return;
  185. }
  186. // If the destination cell is the same as source cell
  187. if (isDestination(src.first, src.second, dest) == true)
  188. {
  189. printf("We are already at the destination\n");
  190. return;
  191. }
  192. // Create a closed list and initialise it to false which means
  193. // that no cell has been included yet
  194. // This closed list is implemented as a boolean 2D array
  195. bool closedList[ROW][COL];
  196. memset(closedList, false, sizeof(closedList));
  197. // Declare a 2D array of structure to hold the details
  198. //of that cell
  199. cell cellDetails[ROW][COL];
  200. int i, j;
  201. for (i = 0; i < ROW; i++)
  202. {
  203. for (j = 0; j < COL; j++)
  204. {
  205. cellDetails[i][j].f = FLT_MAX;
  206. cellDetails[i][j].g = FLT_MAX;
  207. cellDetails[i][j].h = FLT_MAX;
  208. cellDetails[i][j].parent_i = -1;
  209. cellDetails[i][j].parent_j = -1;
  210. }
  211. }
  212. // Initialising the parameters of the starting node
  213. i = src.first, j = src.second;
  214. cellDetails[i][j].f = 0.0;
  215. cellDetails[i][j].g = 0.0;
  216. cellDetails[i][j].h = 0.0;
  217. cellDetails[i][j].parent_i = i;
  218. cellDetails[i][j].parent_j = j;
  219. /*
  220. Create an open list having information as-
  221. <f, <i, j>>
  222. where f = g + h,
  223. and i, j are the row and column index of that cell
  224. Note that 0 <= i <= ROW-1 & 0 <= j <= COL-1
  225. This open list is implenented as a set of pair of pair.*/
  226. set<pPair> openList;
  227. // Put the starting cell on the open list and set its
  228. // 'f' as 0
  229. openList.insert(make_pair(0.0, make_pair(i, j)));
  230. // We set this boolean value as false as initially
  231. // the destination is not reached.
  232. bool foundDest = false;
  233. while (!openList.empty())
  234. {
  235. // Find the node with the least f on the open list
  236. pPair p = *openList.begin();
  237. // Remove this vertex from the open list
  238. openList.erase(openList.begin());
  239. // Add this vertex to the closed list
  240. i = p.second.first;
  241. j = p.second.second;
  242. closedList[i][j] = true;
  243. // if find destination, return
  244. if (TraversingEightNeighbors(cellDetails, dest, closedList, grid,
  245. openList, foundDest, i, j))
  246. return;
  247. }
  248. // When the destination cell is not found and the open
  249. // list is empty, then we conclude that we failed to
  250. // reach the destiantion cell. This may happen when the
  251. // there is no way to destination cell (due to blockages)
  252. if (foundDest == false)
  253. printf("Failed to find the Destination Cell\n");
  254. return;
  255. }
  256. // Driver program to test above function
  257. int main()
  258. {
  259. /* Description of the Grid-
  260. 1--> The cell is not blocked
  261. 0--> The cell is blocked */
  262. int grid[ROW][COL] =
  263. {
  264. {1, 0, 1, 1, 1, 1, 0, 1, 1, 1},
  265. {1, 1, 1, 0, 1, 1, 1, 0, 1, 1},
  266. {1, 1, 1, 0, 1, 1, 0, 1, 0, 1},
  267. {0, 0, 1, 0, 1, 0, 0, 0, 0, 1},
  268. {1, 1, 1, 0, 1, 1, 1, 0, 1, 0},
  269. {1, 0, 1, 1, 1, 1, 0, 1, 0, 0},
  270. {1, 0, 0, 0, 0, 1, 0, 0, 0, 1},
  271. {1, 0, 1, 1, 1, 1, 0, 1, 1, 1},
  272. {1, 1, 1, 0, 0, 0, 1, 0, 0, 1}
  273. };
  274. // Source is the left-most bottom-most corner
  275. Pair src = make_pair(8, 0);
  276. // Destination is the left-most top-most corner
  277. Pair dest = make_pair(0, 0);
  278. aStarSearch(grid, src, dest);
  279. return (0);
  280. }

3.3 算法总结

  1. cellDetails[ni][nj].parent_i = i;
  2. cellDetails[ni][nj].parent_j = j;

所以回溯路径的判断条件是

  1. //最终row和col会达到初始点的位置
  2. while (!(cellDetails[row][col].parent_i == row
  3. && cellDetails[row][col].parent_j == col))
  4. {
  5. Path.push(make_pair(row, col));
  6. int temp_row = cellDetails[row][col].parent_i;
  7. int temp_col = cellDetails[row][col].parent_j;
  8. row = temp_row;
  9. col = temp_col;
  10. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注