关于A*算法的研究总结
重要公式: f ( n ) = g ( n ) + h ( n ) f(n)=g(n)+h(n) f(n)=g(n)+h(n)
其中:
- f(n)是节点n的综合优先级。当我们选择下一个要遍历的节点时,我们总会选取综合优先级最高(值最小)的节点。
- g(n) 是节点n距离起点的代价。
- h(n)是节点n距离终点的预计代价,这也就是A*算法的启发函数。关于启发函数我们在下面详细讲解。
A*算法在运算过程中,每次从优先队列中选取f(n)值最小(优先级最高)的节点作为下一个待遍历的节点。
另外,A*算法使用两个集合来表示待遍历的节点,与已经遍历过的节点,这通常称之为open_set
和close_set
。
完整的A*算法描述如下:
* 初始化open_set和close_set;
* 将起点加入open_set中,并设置优先级为0(优先级最高);
* 如果open_set不为空,则从open_set中选取优先级最高的节点n:
* 如果节点n为终点,则:
* 从终点开始逐步追踪parent节点,一直达到起点;
* 返回找到的结果路径,算法结束;
* 如果节点n不是终点,则:
* 将节点n从open_set中删除,并加入close_set中;
* 遍历节点n所有的邻近节点:
* 如果邻近节点m在close_set中,则:
* 跳过,选取下一个邻近节点
* 如果邻近节点m也不在open_set中,则:
* 设置节点m的parent为节点n
* 计算节点m的优先级
* 将节点m加入open_set中
启发函数
上面已经提到,启发函数会影响A*算法的行为。
- 在极端情况下,当启发函数h(n)始终为0,则将由g(n)决定节点的优先级,此时算法就退化成了Dijkstra算法。
- 如果h(n)始终小于等于节点n到终点的代价,则A*算法保证一定能够找到最短路径。但是当h(n)的值越小,算法将遍历越多的节点,也就导致算法越慢。
- 如果h(n)完全等于节点n到终点的代价,则A*算法将找到最佳路径,并且速度很快。可惜的是,并非所有场景下都能做到这一点。因为在没有达到终点之前,我们很难确切算出距离终点还有多远。
- 如果h(n)的值比节点n到终点的代价要大,则A*算法不能保证找到最短路径,不过此时会很快。
- 在另外一个极端情况下,如果h(n)相较于g(n)大很多,则此时只有h(n)产生效果,这也就变成了最佳优先搜索。
由上面这些信息我们可以知道,通过调节启发函数我们可以控制算法的速度和精确度。因为在一些情况,我们可能未必需要最短路径,而是希望能够尽快找到一个路径即可。这也是A*算法比较灵活的地方。
对于网格形式的图,有以下这些启发函数可以使用:
-
如果图形中只允许朝上下左右四个方向移动,则可以使用曼哈顿距离(Manhattan distance)。
h ( n ) = D ∗ ( a b s ( c . x − e n d . x ) + a b s ( c . y − e n d . y ) ) h(n)=D*(abs(c.x-end.x)+abs(c.y-end.y)) h(n)=D∗(abs(c.x−end.x)+abs(c.y−end.y)) -
如果图形中允许朝八个方向移动,则可以使用对角距离。
有两种情况:
1,假设行走的直线和对角线的代价都为D,则:
h ( n ) = D ∗ M A X ( a b s ( c . x − e n d . x ) , a b s ( c . y − e n d . y ) ) h(n)=D*MAX(abs(c.x-end.x),abs(c.y-end.y)) h(n)=D∗MAX(abs(c.x−end.x),abs(c.y−end.y))
2,假设行走的直线代价为D,对角线代价为 $ \sqrt{2} $D,则:h − d i a g o n a l ( n ) = M I N ( a b s ( c . x − e n d . x ) , a b s ( c . y − e n d . y ) ) h-diagonal(n)=MIN(abs(c.x-end.x),abs(c.y-end.y)) h−diagonal(n)=MIN(abs(c.x−end.x),abs(c.y−end.y))
h − s t r a i n g h t ( n ) = a b s ( c . x − e n d . x ) + a b s ( c . y − e n d . y ) h-strainght(n)=abs(c.x-end.x)+abs(c.y-end.y) h−strainght(n)=abs(c.x−end.x)+abs(c.y−end.y)
h ( n ) = 2 D ∗ h − d i a g o n a l ( n ) + D ∗ ( h − s t r a i n g h t ( n ) − 2 ∗ h − d i a g o n a l ( n ) ) h(n) =\sqrt{2}D*h-diagonal(n)+D*(h-strainght(n)-2*h-diagonal(n)) h(n)=2D∗h−diagonal(n)+D∗(h−strainght(n)−2∗h−diagonal(n)) -
如果图形中允许朝任何方向移动,则可以使用欧几里得距离(Euclidean distance)
h ( n ) = D ∗ s q r t ( ( c . x − e n d . x ) 2 + ( c . y − e n d . y ) 2 ) h(n)=D*sqrt((c.x-end.x)^2+(c.y-end.y)^2) h(n)=D∗sqrt((c.x−end.x)2+(c.y−end.y)2)
1,Q:从当前节点的周围节点中如何选取第一个检查节点?
A:具有最小 F 值的那个。
/**
* A*算法简易版实现
* @author Xxx
*
*/
public class Astar {
public final static String BAR = "|"; // 障碍值
public final static String PATH = "●"; // 路径
public final static int DIRECT_VALUE = 10; // 横竖移动代价
public final static int OBLIQUE_VALUE = 14; // 斜移动代价
// 全局变量
static Queue<Node> open_list = new PriorityQueue<Node>();
static List<Node> close_list = new ArrayList<Node>();
static long startTime = 0;
static long endTime = 0;
public static void main(String[] args) {
String[][] maps = {
{ "□", "□", "□", "□", "□", "□", "", "□", "□", "□", "□", "□", "□", "□", "□" },
{ "□", "□", "□", "□", "|", "|", "|", "|", "|", "|", "|", "□", "|", "|", "□" },
{ "□", "|", "|", "|", "|", "□", "□", "□", "□", "|", "|", "|", "|", "|", "□" },
{ "□", "□", "□", "|", "□", "□", "□", "□", "□", "|", "|", "□", "□", "□", "□" },
{ "□", "□", "□", "|", "□", "□", "□", "□", "□", "|", "□", "□", "□", "□", "□" },
{ "□", "□", "□", "|", "□", "□", "□", "□", "|", "□", "□", "□", "□", "□", "□" },
{ "□", "□", "□", "|", "□", "□", "□", "□", "|", "□", "□", "□", "□", "□", "□" }
};
MapInfo info=new MapInfo(maps,maps[0].length, maps.length,new Node(1, 6), new Node(4,3));
startTime = System.currentTimeMillis();
start(info);
printMap(maps);
endTime = System.currentTimeMillis() - startTime;
System.out.println("花费的时间:" + endTime);
}
public static void start(MapInfo mapinfo) {
open_list.clear();
close_list.clear();
// 起点加入open_list中来了
open_list.add(mapinfo.start);
// 判断起点和终点是否一致
if((mapinfo.start.coord.x == mapinfo.end.coord.x)
&&(mapinfo.start.coord.y == mapinfo.end.coord.y)) {
System.out.println("起点就是终点,不用寻找路径了!");
return;
}
/**
* 如果不一致的话
* 1,将当前节点(起点)从open_list弹出,并加入到close_list中
* 2,将当前节点(起点)周围的节点加入到open_list来【不可达节点不用加,已在open_list中的节点不用加,已在close_list中的节点不用加】,并且重新计算G和F值
*/
// Node current = open_list.poll();
// close_list.add(current);
while (!open_list.isEmpty())
{
// 一旦终点加入到了close_list中去,立马回溯起点,就可以找到路径了
if (isCoordInClose(mapinfo.end.coord))
{
// 根据父节点回溯直至找到起点
drawPath(mapinfo.maps, mapinfo.end);
break;
}
// 弹出当前节点加入到close_list中
Node current = open_list.poll();
close_list.add(current);
// 将当前节点的周围节点添加到open_list中
// 上
addNode2Open(mapinfo, current,current.coord.x,current.coord.y+1,DIRECT_VALUE);
// 右
addNode2Open(mapinfo, current,current.coord.x+1,current.coord.y,DIRECT_VALUE);
// 下
addNode2Open(mapinfo, current,current.coord.x,current.coord.y-1,DIRECT_VALUE);
// 左
addNode2Open(mapinfo, current,current.coord.x-1,current.coord.y,DIRECT_VALUE);
// 左上
addNode2Open(mapinfo, current,current.coord.x-1,current.coord.y+1,OBLIQUE_VALUE);
// 右上
addNode2Open(mapinfo, current,current.coord.x+1,current.coord.y+1,OBLIQUE_VALUE);
// 右下
addNode2Open(mapinfo, current,current.coord.x+1,current.coord.y-1,OBLIQUE_VALUE);
// 左下
addNode2Open(mapinfo, current,current.coord.x-1,current.coord.y-1,OBLIQUE_VALUE);
}
}
/**
* 在二维数组中绘制路径
*/
private static void drawPath(String[][] maps, Node end)
{
if(end==null||maps==null) return;
System.out.println("总代价:" + end.G);
while (end != null)
{
Coord c = end.coord;
maps[c.y][c.x] = PATH;
end = end.parent;
}
}
/**
* 将周围的邻节点加入到open_list中,并且重新计算G和F值
* 按照:上->右->下->左 左上->右上->右下->左下的顺序依次添加
* @param end
* @param current
* @param x
* @param y
* @param value
*/
public static void addNode2Open(MapInfo mapinfo, Node current, int x, int y, int value) {
if(canAddNodeToOpen(mapinfo,x,y)) {
Node end=mapinfo.end;
// 当前邻节点的坐标
Coord coord = new Coord(x, y);
// 计算邻结点的G值(离起点的权值)
int G = current.G + value;
Node child = findNodeInOpen(coord);
// child 不在open列表中的话
if (child == null)
{
// 以下两种方式的路径不一样,但是花费的总代价是一样的
int H = calcH(end.coord,coord)*DIRECT_VALUE; // 计算H值(离终点的预估权值)
// 判断当前节点是否是终点
if(isEndNode(end.coord,coord))
{
child = end;
child.parent = current;
child.G = G;
child.H = H;
}
else
{
// 创建该节点并准备加入open列表中
child = new Node(coord, current, G, H);
}
open_list.add(child);
}else if (child.G > G)
{
child.G = G;
child.parent = current;
open_list.add(child);
}
}
}
/**
* 判断结点能否放入Open列表:
* 1,不再地图中的节点不要放
* 2,不可达的节点不要放
* 3,在close_list中的节点不要放
*/
private static boolean canAddNodeToOpen(MapInfo mapInfo,int x, int y)
{
// 是否在地图中
if (x < 0 || x >= mapInfo.width || y < 0 || y >= mapInfo.hight) return false;
// 判断是否是不可通过的结点
if (mapInfo.maps[y][x] == BAR) return false;
// 判断结点是否存在close表
if (isCoordInClose(x, y)) return false;
return true;
}
/**
* 判断坐标是否在close表中
*/
private static boolean isCoordInClose(Coord coord)
{
return coord!=null&&isCoordInClose(coord.x, coord.y);
}
/**
* 判断坐标是否在close表中
*/
private static boolean isCoordInClose(int x, int y)
{
if (close_list.isEmpty()) return false;
for (Node node : close_list)
{
if (node.coord.x == x && node.coord.y == y)
{
return true;
}
}
return false;
}
/**
* 从Open列表中查找结点
*/
private static Node findNodeInOpen(Coord coord)
{
if (coord == null || open_list.isEmpty()) return null;
for (Node node : open_list)
{
if (node.coord.equals(coord))
{
return node;
}
}
return null;
}
/**
* 计算H的估值:“曼哈顿”法,坐标分别取差值相加
*/
private static int calcH(Coord end,Coord coord)
{
return Math.abs(end.x - coord.x)
+ Math.abs(end.y - coord.y);
}
/**
* 判断结点是否是最终结点
*/
private static boolean isEndNode(Coord end,Coord coord)
{
return coord != null && end.equals(coord);
}
/**
* 打印地图
*/
public static void printMap(String[][] maps)
{
for (int i = 0; i < maps.length; i++)
{
for (int j = 0; j < maps[i].length; j++)
{
System.out.print(maps[i][j] + " ");
}
System.out.println();
}
}
}