A*算法介绍
寻路,即找到一条从某个起点到某个终点的可通过路径。而因为实际情况中,起点和终点之间的直线方向往往有障碍物,便需要一个搜索的算法来解决。
有一定算法基础的同学可能知道从某个起点到某个终点通常使用深度优先搜索(DFS),DFS搜索的搜索方向一般是8个方向(如果不允许搜索斜向,则有4个),但是并无优先之分。
为了让DFS搜索更加高效,结合贪心思想,我们给搜索方向赋予了优先级,直观上离终点最近的方向(直观上的意思是无视障碍物的情况下)为最优先搜索方向,这就是A*算法。
A*算法步骤解析
(如下图,绿色为起点,红色为终点,蓝色为不可通过的墙。)
从起点开始往四周各个方向搜索。
(这里的搜索方向有8个方向)
为了区分搜索方向的优先级,我们给每个要搜索的点赋予2个值。
G值(耗费值):指从起点走到该点要耗费的值。
H值(预测值):指从该点走到终点的预测的值(从该点到终点无视障碍物情况下预测要耗费的值,也可理解成该点到终点的直线距离的值)
在这里,值 = 要走的距离
(实际上,更复杂的游戏,因为地形不同(例如陷阱,难走的沙地之类的),还会有相应不同的权值:值 = 要走的距离 * 地形权值)
我们还定义直着走一格的距离等于10,斜着走一格的距离等于14(因为45°斜方向的长度= sqrt(10^2+10^2) ≈ 14)
为什么A*寻路能找到最优解?
通过使用启发函数f(n)=g(n)+h(n),找到下一个移动最小代价的点
这条公式意思:F是从起点经过该点再到达终点的预测总耗费值。通过计算F值,我们可以优先选择F值最小的方向来进行搜索。
(每个点的左上角为F值,左下角为G值,右下角为H值)
计算出每个方向对应点的F,G,H值后,
还需要给这些点赋予当前节点的指针值(用于回溯路径。因为一直搜下去搜到终点后,如果没有前一个点的指针,我们将无从得知要上次经过的是哪个点,只知道走到终点最终耗费的最小值是多少)
然后我们将这些点放入openList(开启列表:用于存放可以搜索的点)。
然后再将当前点放入closeList(关闭列表:用于存放已经搜索过的点,避免重复搜索同一个点)
然后再从openList取出一个F值最小(最优先方向)的点,进行上述同样的搜索。
在搜索过程中,如果搜索方向上的点是障碍物或者关闭列表里的点,则跳过之。
通过递归式的搜索,多次搜索后,最终搜到了终点。
搜到终点后,然后通过前一个点的指针值,我们便能从终点一步步回溯通过的路径点。
(红色标记了便是回溯到的点)
A*算法优化思路
openList使用优先队列(二叉堆)
可以看到openlist(开启列表),需要实时添加点,还要每次取出最小值的点。
所以我们可以使用优先队列(二叉堆)来作为openList的容器。
优先队列(二叉堆):插入一个点的复杂度为O(logN),取出一个最值点复杂度为O(logN)
障碍物列表,closeList 使用二维表(二维数组)
由于障碍物列表和closeList仅用来检测是否能通过,所以我们可以使用bool二维表来存放。
//假设已经定义Width和Height分别为地图的长和宽
bool barrierList[Width][Height];
bool closetList[Width][Height];
有某个点(Xa,Yb),可以通过
if(barrierList[Xa][Yb]&&closeList[Xa][Yb])来判断。
因为二维表用下标访问,效率很高,但是耗空间比较多。(三维地图使用三维表则更耗内存。不过现在计算机一般都不缺内存空间,所以尽量提升运算时间为主)
这是一个典型的牺牲内存空间换取运算时间的例子。
深度限制
有时要搜的路径非常长,利用A*算法搜一次付出的代价很高,造成游戏的卡顿。
那么为了保证每次搜索不会超过一定代价,可以设置深度限制,每搜一次则深度+1,搜到一定深度限制还没搜到终点,则返还失败值。
A*算法实现(C++代码)
1 #include <iostream>
2 #include <list>
3 #include <vector>
4 #include <queue>
5
6 struct OpenPoint{
7 int x;
8 int y;
9 int cost; // 耗费值
10 int pred; // 预测值
11 OpenPoint* father; // 父节点
12 OpenPoint() = default;
13 OpenPoint(int pX,int pY, int endX, int endY, int c, OpenPoint* fatherp) : x(pX),y(pY),cost(c), father(fatherp) {
14 //相对位移x,y取绝对值
15 int relativeX = std::abs(endX - pX);
16 int relativeY = std::abs(endY - pY);
17 //x,y偏移值n
18 int n = relativeX - relativeY;
19 //预测值pred = (max–n)*14+n*10+c
20 pred = std::max(relativeX, relativeY) * 14 - std::abs(n) * 4 + c;
21 }
22 };
23
24 //比较器,用以优先队列的指针类型比较
25 struct OpenPointPtrCompare {
26 bool operator()(OpenPoint* a, OpenPoint* b) {
27 return a->pred > b->pred;
28 }
29 };
30
31 const int width = 30; //地图长度
32 const int height = 100; //地图高度
33 char mapBuffer[width][height]; //地图数据
34 int depth = 0; //记录深度
35 const int depthLimit = 2000; //深度限制
36 bool closeAndBarrierList[width][height]; //记录障碍物+关闭点的二维表
37 //八方的位置
38 int direction[8][2] = { {1,0},{0,1},{-1,0},{0,-1},{1,1},{ -1,1 },{ -1,-1 },{ 1,-1 } };
39 //使用最大优先队列
40 std::priority_queue<OpenPoint*, std::vector<OpenPoint*>, OpenPointPtrCompare> openlist;
41 //存储OpenPoint的内存空间
42 std::list<OpenPoint> pointList(depthLimit);
43
44 //是否在障碍物或者关闭列表
45 inline bool inBarrierAndCloseList(int pX,int pY) {
46 if (pX < 0 || pY < 0 || pX >= width || pY >= height)
47 return true;
48 return closeAndBarrierList[pX][pY];
49 }
50
51 //创建一个开启点
52 inline OpenPoint* createOpenPoint(int pX,int pY,int endX,int endY, int c, OpenPoint* fatherp) {
53 pointList.emplace_back(pX,pY,endX,endY, c, fatherp);
54 return &pointList.back();
55 }
56
57 // 开启检查,检查父节点
58 void open(OpenPoint& pointToOpen, int endX,int endY) {
59 //将父节点从openlist移除
60 openlist.pop();
61 //深度+1
62 depth++;
63 //检查p点八方的点
64 for (int i = 0; i < 4; ++i)
65 {
66 int toOpenX = pointToOpen.x + direction[i][0];
67 int toOpenY = pointToOpen.y + direction[i][1];
68 if (!inBarrierAndCloseList(toOpenX,toOpenY)) {
69 openlist.push(createOpenPoint(toOpenX, toOpenY, endX,endY, pointToOpen.cost + 10, &pointToOpen));
70 }
71 }
72 for (int i = 4; i < 8; ++i)
73 {
74 int toOpenX = pointToOpen.x + direction[i][0];
75 int toOpenY = pointToOpen.y + direction[i][1];
76 if (!inBarrierAndCloseList(toOpenX, toOpenY)) {
77 openlist.push(createOpenPoint(toOpenX, toOpenY, endX, endY, pointToOpen.cost + 14, &pointToOpen));
78 }
79 }
80 //最后移入closelist
81 closeAndBarrierList[pointToOpen.x][pointToOpen.y] = true;
82 }
83
84 //开始搜索路径
85 std::list<OpenPoint*> findway(int startX,int startY, int endX,int endY) {
86 std::list<OpenPoint*> road;
87 // 创建并开启一个父节点
88 openlist.push(createOpenPoint(startX,startY, endX,endY, 0, nullptr));
89 OpenPoint* toOpen = nullptr;
90 //重复寻找预测和花费之和最小节点开启检查
91 while (!openlist.empty())
92 {
93 toOpen = openlist.top();
94 // 找到终点后,则停止搜索
95 if (toOpen->x == endX && toOpen->y ==endY) {break;}//若超出一定深度(1000深度),则搜索失败
96 if (depth >= depthLimit) {
97 toOpen = nullptr;
98 break;
99 }
100 open(*toOpen, endX,endY);
101 }
102 for (auto rs = toOpen; rs != nullptr; rs = rs->father) {road.push_back(rs);}
103 return road;
104 }
105
106 //创建地图
107 void createMap() {
108 for (int i = 0; i < width; ++i)
109 for (int j = 0; j < height; ++j) {
110 //五分之一概率生成障碍物,不可走
111 if (rand() % 5 == 0) {
112 mapBuffer[i][j] = '*';
113 closeAndBarrierList[i][j] = true;
114 }
115 else {
116 mapBuffer[i][j] = ' ';
117 closeAndBarrierList[i][j] = false;
118 }
119 }
120 }
121
122 //打印地图
123 void printMap() {
124 for (int i = 0; i < width; ++i) {
125 for (int j = 0; j < height; ++j)
126 std::cout << mapBuffer[i][j];
127 std::cout << std::endl;
128 }
129 std::cout << std::endl << std::endl << std::endl;
130 }
131
132 int main() {
133 //起点
134 int beginX = 0;
135 int beginY = 0;
136 //终点
137 int endX = 29;
138 int endY = 99;
139 //创建地图
140 createMap();
141 //保证起点和终点都不是障碍物
142 mapBuffer[beginX][beginY] = mapBuffer[endX][endY] = ' ';
143 closeAndBarrierList[beginX][beginY] = closeAndBarrierList[endX][endY] = false;
144 //A*搜索得到一条路径
145 std::list<OpenPoint*> road = findway(beginX,beginY,endX,endY);
146 //将A*搜索的路径经过的点标记为'O'
147 for (auto& p : road){mapBuffer[p->x][p->y] = 'O';}
148 //打印走过路后的地图
149 printMap();
150 system("pause");
151 return 0;
152 }
示例效果:
为什么A*寻路并不能保证它找到最好的解?
A星算法其实只能得到一种近似最优解,实际上对于寻路问题,往往存在不止一个最优解,如果非要找出所有的解就只能遍历所有可能的路径一一比较,但这样效率太低,所以A星算法并不去遍历整个地图,而是只遍历了最短路径上的点和其周围的点,所以得到的是一种近似最优解。
A*寻路是贪婪算法吗?
贪婪算法( greedy algorithm)自顶向下,寻找每一步的最优解
A* 搜索算法,会判断当前最优解与上一步的最优解(有可能会返回上一步),并不是一直从起点向下都是求最优解所以不是贪婪算法
M*寻路算法的改进与A*的对比
M*里面对每个机器人使用A*算法,从开始点到目的地计算出移动代价最小的路径,当机器人之前将要发生碰撞的时候,将其他机器人视为障碍点退回上一步重新计算移动代价最小的路径