A星算法
A星算法
搜寻算法俗称A星算法。这是一种在图形平面有多个节点的路径上,求出由起点到目标点的最小路径耗费算法,主要搜寻路径的方式为启发推进式。常用于游戏中的NPC(Non-Player-Controlled Character 即“非人控制玩家角色)的移动计算,或线上游戏的BOT的移动计算。
此种寻找最佳路径的算法类似于图论中寻找最优树,通常是通盘考虑选择某一路径的路径耗费,在所有可通过的路径中总有一条路径相对于其他任何可通行的路径来说是耗费最少的。在图论中寻找最佳路径时每一条的路径耗费是已知且固定的,但在用A星算法求解最佳路径时,沿着不同的路径前进,尽管是同一节点但其耗费可能缺是不同的,这便是是启发式寻路的精髓。
运用此方式时,首先将实际问题抽象出来,用矩阵的形式表示问题中的各元素,包括起点位置,目标点位置,以及出现的障碍物。我们会逐渐地发现在寻路方面都是将实际问题抽象地用矩阵表示,之后通过对矩形的操作模拟实现寻路过程。
其基本思想是以起点为中心,其周围紧邻的8个点都通过指针指向它,在其周围点内选择最佳路径点并以它为中心点将其还未建立指针联系的周围点(可行的,这在后文中解释)通过指针指向它,并选择最佳路径点再以此点为中心寻路直到寻找到点的周围点中有目标点,这样寻找的路径就通过指针一一连接起来了,最后通过输出这些点就是寻找的路径了。(ps:在寻找最佳路径时可能会改变部分点的指针指向)
本文主要通过以下几个方面来逐步分析A星算法的寻路过程:
1、将实际问题抽象化为矩阵表示
抽象出的矩阵如上图所示(来源已在文章末尾标注清楚),其中绿色区域表示起始点,红色区域表示目标点,中间蓝色区域表示障碍物(如不可通过的高山或是河流),黑色区域表示可产生路径的区域。
2、以起点为中心寻找到下一节点
如上图所示,以起点为中心与之紧密相邻的8个点是其所寻路径上可到的下一点,且都以指针的形式将中间当前点作为与其紧邻的周围点的父节点。对于这8个点,应该选择哪一点作为寻路的下一个起点呢,A星算法中建立了两个列表,一个为开启列表(用于存储所有当前点的可到点(除去已经在关闭列表中的点、障碍物点)),另一个为关闭列表(里面存储已经到过的点,已经在关闭列表中的点在下一次寻路的过程中是不会再次检查的,这也说明寻路的线路不会有相交的可能)。
3、 选择下一节点
将起点加入关闭列表,在以后的寻路过程中不再对其进行检查,接下来就是在这8个点中选择一个作为下一路径点,选择的原则是在其中寻找路径耗费最小的节点,
其权值用F表示,F=G+H
其中G表示从起点开始,沿着产生的路径,移动到指定方格上的路径耗费;如下图所示,以起点为中心其紧邻周围点有上下左右、对角线方向上的8个点,以上下左右移动路径耗费为10,对角线耗费为√2*10,约为14。
其中H表示从路径所在的当前点到终点的移动路径耗费,计算方法为当前点到目的点之间水平和垂直的方格的数量总和,然后把结果乘以10。
从上图可以看出,起始点右边点的权值F最小,故将其作为下一路径点。
4、继续搜索
把路径点从开启列表中删除,并添加到关闭列表中。检查与此点紧邻的8个点(忽略在关闭列表中或者不可通过的点),把他们添加进开启列表,如果存在还有点没有添加进开启列表,则将路径点作为此类点的父节点并添加进开启列表。
如果所有可行的紧邻点已经在开启列表中,对每一紧邻点检查目前这条路径到是否比上一路径点到这一紧邻点的路径耗费要小,如果不是则什么都不用做(如下图所示)从原始起点到其紧邻的右下方的点,按照新产生路径G值:G1=10+10=20,而原始路径G值G2=14,即新产生路径的G值比原始路径的G值大,而它们的H值相同(为同一点)故原始路径的F值比新产生路径的F值要小,不做任何处理,继续下一步寻路。如果是,那就把相邻方格的父节点改为目前选中的方格,说明新产生的路径的移动耗费更小。
5、重复上一搜索过程直至结束
搜寻过程结束分为两种情况:一种是目标点加入关闭列表,搜索正常结束,找到路径!另一种情况是目标点未找到但开启列表已经为空,意味着没有找到从起始点到目标点的路径,搜索结束。
搜索过程如下图所示,从中可以看出从起点到目标点之间有指针指向一致的一条路径,这便A星算法是搜寻到的路径。
在路径点上添加红色点突出显示(如下图所示),此即为从起始点到终点的一条路径
整个寻路过程整理如下:
1、起始格加入开启列表;
2、重复如下的工作:
a) 寻找开启列表中F值最低的点。我们称它为当前点;
b) 把它加入关闭列表;
c) 对紧邻的8格中的每一点
* 如果它不可通过或者已经在关闭列表中,略过它。反之如下:
* 如果它不在开启列表中,把它添加进去。把当前点作为这一格的父节点。记录这一点的F、G和H值;
* 如果它已经在开启列表中,用G值为参考检查新的路径是否更好。更低的G值意味着更好的路径。如果是这样,就把这一点的父节 点改成当前点,并且重新计算这一点的G和F值。改变之后需要重新对开启列表按F值大小排序。如果不是则不需要做后面改变指针指向并重新计算G、F值的工作;
3、停止搜索,分为两种情况:
* 当目标点添加入了关闭列表,这时候路径被找到,搜索正常结束;
* 没有找到目标点,但开启列表已经空了,此时未找到合适的路径,搜索结束;
4、保存路径。从目标点开始,沿着每一点的指针指向移动,直到回到起始点,输出路径。
具体算法实现(此处用C语言实现):
1 #include <stdio.h> 2 3 #define STARTNODE 1 //起始点 4 #define ENDNODE 2 //终点 5 #define BARRIER 3 //障碍物 6 7 typedef struct AStarNode //定义一个具有A星节点特点的结构体 8 { 9 int s_x; // 坐标 10 int s_y; 11 int s_g; // 起点到此点的路径耗费( 由g和h可以得到f,此处f省略,f=g+h ) 12 int s_h; // 启发函数预测的当前点到终点的路径耗费 13 int s_style; // 结点类型:1表示起始点,2表示目标点,3表示障碍物(如不可通过的高山或是河流),0表示可产生路径的区域 14 struct AStarNode * s_parent; //指向父节点的指针 15 int s_is_in_closetable; // 是否在close表中 16 int s_is_in_opentable; // 是否在open表中 17 }AStarNode, *pAStarNode; 18 19 AStarNode map_maze[6][8]; // 结点数组 20 pAStarNode open_table[100]; // open表 21 pAStarNode close_table[100]; // close表 22 int open_node_count; // open表中节点数量 23 int close_node_count; // close表中结点数量 24 pAStarNode path_stack[100]; // 保存路径的栈 25 int top = -1; // 栈顶 26 27 // 交换两个元素 28 29 void swap( int idx1, int idx2 ) 30 { 31 pAStarNode tmp = open_table[idx1]; 32 open_table[idx1] = open_table[idx2]; 33 open_table[idx2] = tmp; 34 } 35 36 // 堆调整,此处用到小顶堆的算法,对开启列表进行堆调整后其第一个元素必是该列表中值最小的元素 37 38 /////////////////////////////////////////////////////////////////////////// 39 ////////////////////////////////////////////////////////////////////////// 40 // 41 void adjust_heap( int nIndex ) 42 { 43 int curr = nIndex; 44 int child = curr * 2 + 1; // 得到左孩子idx( 下标从0开始,所以左孩子是curr*2+1 ) 45 int parent = ( curr - 1 ) / 2; // 得到双亲idx 46 47 if (nIndex < 0 || nIndex >= open_node_count) 48 { 49 return; 50 } 51 52 // 往下调整( 要比较左右孩子和cuur parent ) 53 // 54 while ( child < open_node_count ) 55 { 56 // 小根堆是双亲值小于孩子值 57 // 58 if ( child + 1 < open_node_count && open_table[child]->s_g + open_table[child]->s_h > open_table[child+1]->s_g + open_table[child+1]->s_h ) 59 { 60 ++child;// 判断左右孩子大小 61 } 62 63 if (open_table[curr]->s_g + open_table[curr]->s_h <= open_table[child]->s_g + open_table[child]->s_h) 64 { 65 break; 66 } 67 else 68 { 69 swap( child, curr ); // 交换节点 70 curr = child; // 再判断当前孩子节点 71 child = curr * 2 + 1; // 再判断左孩子 72 } 73 } 74 75 if (curr != nIndex) 76 { 77 return; 78 } 79 80 // 往上调整( 只需要比较cuur child和parent ) 81 // 82 while (curr != 0) 83 { 84 if (open_table[curr]->s_g + open_table[curr]->s_h >= open_table[parent]->s_g + open_table[parent]->s_h) 85 { 86 break; 87 } 88 else 89 { 90 swap( curr, parent ); 91 curr = parent; 92 parent = (curr-1)/2; 93 } 94 } 95 } 96 ////////////////////////////////////////////////////////////////////////// 97 ////////////////////////////////////////////////////////////////////////// 98 99 // 判断邻居点是否可以进入open表 100 // 101 void insert_to_opentable( int x, int y, pAStarNode curr_node, pAStarNode end_node, int w ) 102 { 103 int i; 104 105 if ( map_maze[x][y].s_style != BARRIER ) // 不是障碍物 106 { 107 if ( !map_maze[x][y].s_is_in_closetable ) // 不在闭表中 108 { 109 if ( map_maze[x][y].s_is_in_opentable ) // 在open表中 110 { 111 // 需要判断是否是一条更优化的路径 112 // 113 if ( map_maze[x][y].s_g > curr_node->s_g + w ) // 如果更优化 114 { 115 map_maze[x][y].s_g = curr_node->s_g + w; 116 map_maze[x][y].s_parent = curr_node; //此处将中心点与其周围8个点通过指针连接起来 117 118 for ( i = 0; i < open_node_count; ++i ) 119 { 120 if ( open_table[i]->s_x == map_maze[x][y].s_x && open_table[i]->s_y == map_maze[x][y].s_y ) 121 { 122 break; 123 } 124 } 125 126 adjust_heap( i ); // 下面调整点 127 } 128 } 129 else // 不在open中 130 { 131 map_maze[x][y].s_g = curr_node->s_g + w; 132 map_maze[x][y].s_h = abs(end_node->s_x - x ) + abs(end_node->s_y - y); 133 map_maze[x][y].s_parent = curr_node; 134 map_maze[x][y].s_is_in_opentable = 1; 135 open_table[open_node_count++] = &(map_maze[x][y]); 136 } 137 } 138 } 139 } 140 141 // 查找邻居 142 // 对上下左右8个邻居进行查找 143 // 144 void get_neighbors( pAStarNode curr_node, pAStarNode end_node ) 145 { 146 int x = curr_node->s_x; 147 int y = curr_node->s_y; 148 149 // 下面对于8个邻居进行处理! 150 // 151 if((x+1)>=0&&(x+1)<6&&y>=0&&y<8) 152 { 153 insert_to_opentable(x+1,y,curr_node,end_node,10); 154 } 155 156 if((x-1)>=0&&(x-1)<6&&y>=0&&y<8) 157 { 158 insert_to_opentable(x-1,y,curr_node,end_node,10); 159 } 160 161 if(x>=0&&x<6&&(y+1)>=0&&(y+1)<8) 162 { 163 insert_to_opentable(x,y+1,curr_node,end_node,10); 164 } 165 166 if(x>=0&&x<6&&(y-1)>=0&&(y-1)<8) 167 { 168 insert_to_opentable(x,y-1,curr_node,end_node,10); 169 } 170 171 if((x+1)>=0&&(x+1)<6&&(y+1)>=0&&(y+1)<8) 172 { 173 insert_to_opentable(x+1,y+1,curr_node,end_node,14); 174 } 175 176 if((x+1)>=0&&(x+1)<6&&(y-1)>=0&&(y-1)<8) 177 { 178 insert_to_opentable(x+1,y-1,curr_node,end_node,14); 179 } 180 181 if((x-1)>=0&&(x-1)<6&&(y+1)>=0&&(y+1)<8) 182 { 183 insert_to_opentable(x-1,y+1,curr_node,end_node,14); 184 } 185 186 if((x-1)>=0&&(x-1)<6&&(y-1)>=0&&(y-1)<8) 187 { 188 insert_to_opentable(x-1,y-1,curr_node,end_node,14); 189 } 190 } 191 192 int main() 193 { 194 // 地图数组的定义 195 // 196 AStarNode *start_node; // 起始点 197 AStarNode *end_node; // 结束点 198 AStarNode *curr_node; // 当前点 199 int is_found; // 是否找到路径 200 int maze[6][8] ={ // 仅仅为了好赋值给map_maze 201 {0,0,0,0,0,0,0,0}, 202 {0,0,0,0,3,0,0,0}, 203 {0,0,1,0,3,0,2,0}, 204 {0,0,0,0,3,0,0,0}, 205 {0,0,0,0,0,0,0,0}, 206 {0,0,0,0,0,0,0,0}, 207 }; 208 int i,j,x; 209 210 // 下面准备点 211 // 212 for( i = 0; i < 6; ++i ) 213 { 214 for ( j = 0; j < 8; ++j ) 215 { 216 map_maze[i][j].s_g = 0; 217 map_maze[i][j].s_h = 0; 218 map_maze[i][j].s_is_in_closetable = 0; 219 map_maze[i][j].s_is_in_opentable = 0; 220 map_maze[i][j].s_style = maze[i][j]; 221 map_maze[i][j].s_x = i; 222 map_maze[i][j].s_y = j; 223 map_maze[i][j].s_parent = NULL; 224 225 if ( map_maze[i][j].s_style == STARTNODE ) // 起点 226 { 227 start_node = &(map_maze[i][j]); 228 } 229 else if( map_maze[i][j].s_style == ENDNODE ) // 终点 230 { 231 end_node = &(map_maze[i][j]); 232 } 233 234 printf("%d ", maze[i][j]); 235 } 236 237 printf("\n"); 238 } 239 240 // 下面使用A*算法得到路径 241 // 242 open_table[open_node_count++] = start_node; // 起始点加入open表 243 244 start_node->s_is_in_opentable = 1; // 加入open表 245 start_node->s_g = 0; 246 start_node->s_h = abs(end_node->s_x - start_node->s_x) + abs(end_node->s_y - start_node->s_y); 247 start_node->s_parent = NULL; 248 249 if ( start_node->s_x == end_node->s_x && start_node->s_y == end_node->s_y ) 250 { 251 printf("起点==终点!\n"); 252 return 0; 253 } 254 255 is_found = 0; 256 257 while( 1 ) 258 { 259 260 curr_node = open_table[0]; // open表的第一个点一定是f值最小的点(通过堆排序得到的) 261 open_table[0] = open_table[--open_node_count]; // 最后一个点放到第一个点,然后进行堆调整, 262 //即将最小F值的点选择为路径点后就将其从开启列表中删除(此处是用列表中最后一个点来覆盖, 263 //节省了很多时间,省去了对元素的移位操作) 264 265 adjust_heap( 0 ); // 调整堆 266 267 close_table[close_node_count++] = curr_node; // 当前点加入close表 268 curr_node->s_is_in_closetable = 1; // 已经在close表中了 269 270 if ( curr_node->s_x == end_node->s_x && curr_node->s_y == end_node->s_y )// 终点在close中,结束 271 { 272 is_found = 1; 273 break; 274 } 275 276 get_neighbors( curr_node, end_node ); // 对邻居的处理 277 278 if ( open_node_count == 0 ) // 没有路径到达 279 { 280 is_found = 0; 281 break; 282 } 283 } 284 if ( is_found ) 285 { 286 curr_node = end_node; 287 while( curr_node ) 288 { 289 ///////////////////////////////////绕过斜线两边有障碍物/////////////////////////////////////// 290 //此处绕过障碍物的方法是在寻找到路径点后通过简单地绕点来达到目的,找到的路径不是最佳的,需要进一步优化 291 AStarNode* temp=curr_node->s_parent; 292 if(temp!=NULL) 293 { 294 if((curr_node->s_x-1==temp->s_x&&curr_node->s_y+1==temp->s_y)||(curr_node->s_x+1==temp->s_x&&curr_node->s_y-1==temp->s_y)) 295 { 296 if(curr_node->s_y>temp->s_y) 297 { 298 if(map_maze[curr_node->s_x][curr_node->s_y-1].s_style==3) 299 { 300 AStarNode* temp1=&map_maze[curr_node->s_x+1][curr_node->s_y]; 301 temp1->s_parent=curr_node->s_parent; 302 curr_node->s_parent=temp1; 303 } 304 else if(map_maze[curr_node->s_x+1][curr_node->s_y].s_style==3) 305 { 306 AStarNode* temp1=&map_maze[curr_node->s_x][curr_node->s_y-1]; 307 temp1->s_parent=curr_node->s_parent; 308 curr_node->s_parent=temp1; 309 } 310 } 311 312 else 313 { 314 if(map_maze[curr_node->s_x][curr_node->s_y+1].s_style==3) 315 { 316 AStarNode* temp1=&map_maze[curr_node->s_x-1][curr_node->s_y]; 317 temp1->s_parent=curr_node->s_parent; 318 curr_node->s_parent=temp1; 319 } 320 321 else if(map_maze[curr_node->s_x-1][curr_node->s_y].s_style==3) 322 { 323 AStarNode* temp1=&map_maze[curr_node->s_x][curr_node->s_y+1]; 324 temp1->s_parent=curr_node->s_parent; 325 curr_node->s_parent=temp1; 326 } 327 328 } 329 } 330 331 else 332 { 333 if((curr_node->s_x+1==temp->s_x&&curr_node->s_y+1==temp->s_y)||(temp->s_x+1==curr_node->s_x&&temp->s_y+1==curr_node->s_y)) 334 { 335 if(curr_node->s_x<temp->s_x) 336 { 337 if(map_maze[curr_node->s_x][curr_node->s_y+1].s_style==3) 338 { 339 AStarNode* temp1=&map_maze[curr_node->s_x+1][curr_node->s_y]; 340 temp1->s_parent=curr_node->s_parent; 341 curr_node->s_parent=temp1; 342 } 343 else if(map_maze[curr_node->s_x+1][curr_node->s_y].s_style==3) 344 { 345 AStarNode* temp1=&map_maze[curr_node->s_x][curr_node->s_y+1]; 346 temp1->s_parent=curr_node->s_parent; 347 curr_node->s_parent=temp1; 348 } 349 } 350 else if(curr_node->s_x>temp->s_x) 351 { 352 if(map_maze[curr_node->s_x-1][curr_node->s_y].s_style==3) 353 { 354 AStarNode* temp1=&map_maze[curr_node->s_x][curr_node->s_y-1]; 355 temp1->s_parent=curr_node->s_parent; 356 curr_node->s_parent=temp1; 357 } 358 else if(map_maze[curr_node->s_x][curr_node->s_y-1].s_style==3) 359 { 360 AStarNode* temp1=&map_maze[curr_node->s_x-1][curr_node->s_y]; 361 temp1->s_parent=curr_node->s_parent; 362 curr_node->s_parent=temp1; 363 } 364 } 365 } 366 } 367 } 368 ///////////////////////////////////绕过斜线两边有障碍物/////////////////////////////////////// 369 ////////////////////////////////////////////////////////////////////////// 370 path_stack[++top] = curr_node; 371 curr_node = curr_node->s_parent; 372 } 373 374 while( top >= 0 ) // 输出路径 375 { 376 if ( top > 0 ) 377 { 378 printf("(%d,%d)-->", path_stack[top]->s_x, path_stack[top--]->s_y); 379 } 380 else 381 { 382 printf("(%d,%d)", path_stack[top]->s_x, path_stack[top--]->s_y); 383 } 384 } 385 } 386 else 387 { 388 printf("没有找到路径"); 389 } 390 391 puts(""); 392 393 return 0; 394 }
运行效果如下图所示:
此代码还存在改进的地方,此处仅作参考
注:本文章图形及主要参考文章来自下面的网址
http://www.policyalmanac.org/games/aStarTutorial.htm