A*算法(二)启发式算法
1. 启发式函数的使用
启发式函数
h
(
n
)
h(n)
h(n)告诉A*从任意结点n到目标点的最小代价评估值
因此,选择一个好的启发式函数是重要的
情况 | 函数 | 结果 |
---|---|---|
h ( n ) h(n) h(n) = 0 | A* 演变成 Dijkstra算法 | 保证能找到最短路径 |
h ( n ) h(n) h(n) ≤ 实际代价 | h ( n ) h(n) h(n)越小,A*扩展的结点越多,运行就得越慢 | 保证能找到一条最短路径,但运算更快了 |
h ( n ) h(n) h(n) = 实际代价 | 仅仅寻找最佳路径而不扩展别的任何结点 | 保证能找到一条最短路径,并且运算非常快 |
h ( n ) h(n) h(n) > 实际代价 | 寻找最佳路径且扩展别的任何结点 | 不能保证找到一条最短路径,但运算更快了 |
h ( n ) h(n) h(n)>> g ( n ) g(n) g(n) | A*演变成BFS算法 | 不能保证找到一条最短路径,但运算非常快 |
所以得到一个很有趣的情况,那就是可以决定想要从A* 中获得什么
理想情况下,想最快地得到最短路径
如果目标太低,仍会得到最短路径,不过速度变慢了
如果目标太高,那就放弃了最短路径,但A* 运行得更快
A*的这个特性非常有用,例如
在某些情况下,希望得到一条好的路径(“good” path)而不是一条完美的路径(“perfect” path)
为了权衡
g
(
n
)
g(n)
g(n)和
h
(
n
)
h(n)
h(n),可以修改任意一个的权重
在学术上,如果启发式函数值是对实际代价的低估,A* 算法被称为 简单的A算法(simply A)
然而,继续称之为A*,因为在实现上是一样的,并且在编程领域并不区别A和A*
2. 权衡代价函数
A* 改变它自己行为的能力基于启发式代价函数
在速度和精确度之间取得折衷将会让搜索运行得更快
在搜索中,并不真正需要得到最好的路径,仅需要近似的就足够了
而需要什么则取决于路径搜索中发生着什么,或者运行路径搜索的机器有多快
假设路径搜索的地图中有两种地形:平原和山地
有可能有一条沿着平原到山地的路径
在平原中的移动代价是1,而在山地则是3
把两个邻接点之间的评估距离设为1.5可以加速A* 的搜索过程
然后A* 会将3和1.5比较,这并不比把3和1比较差
速度和精确度之间的选择前不是静态的
可以基于CPU的速度、用于路径搜索的时间片数、地图上物体的数量、物体的重要性、组(group)的大小、难度或者其他任何因素来进行动态的选择
取得动态的折衷的一个方法是:
建立一个启发式函数用于假定通过一个网格空间的最小代价是1
然后建立一个代价函数(cost function)用于测量(scales):
g
′
(
n
)
=
1
+
a
l
p
h
a
∗
(
g
(
n
)
–
1
)
g'(n) = 1 + alpha * ( g(n) – 1 )
g′(n)=1+alpha∗(g(n)–1)
改变最短路径搜索算法的代价
如果alpha是0,则改进后的代价函数的值总是1,地形代价被完全忽略,简单地判断一个网格可否通过
如果alpha是1,则最初的最短路径搜索算法代价函数将起作用,然后得到了A*的所有优点
也可以考虑对启发式函数的返回值做选择:绝对最小代价或者期望最小代价
例如,如果地图大部分地形是代价为2的草地,其它一些地方是代价为1的道路
那么可以考虑让启发式函数不考虑道路,而只返回2*距离
当然,速度和精确度之间的选择并不是全局的
3. 衡量单位
A* 计算
f
(
n
)
=
g
(
n
)
+
h
(
n
)
f(n) = g(n) + h(n)
f(n)=g(n)+h(n)
为了对这两个值进行相加,这两个值必须使用相同的衡量单位
如果
g
(
n
)
g(n)
g(n)用小时来衡量而
h
(
n
)
h(n)
h(n)用米来衡量,那么A* 将会认为
g
g
g或者
h
h
h太大或者太小
因而将不能得到正确的路径,同时A* 算法将运行得更慢
4. 精确的启发式函数
如果启发式函数精确地等于实际最佳路径(optimal path)
此时A*扩展的结点将非常少
A* 算法内部发生的事情是:
在每一结点它都计算
f
(
n
)
=
g
(
n
)
+
h
(
n
)
f(n) = g(n) + h(n)
f(n)=g(n)+h(n)
当
h
(
n
)
h(n)
h(n)精确地和
g
(
n
)
g(n)
g(n)匹配时,
f
(
n
)
f(n)
f(n)的值在沿着该路径时将不会改变
不在最短路径上,所有结点的
f
(
n
)
f(n)
f(n)值均大于最短路径上的
f
(
n
)
f(n)
f(n)值
如果已经有较低
f
(
n
)
f(n)
f(n)值的结点,A* 将不考虑
f
(
n
)
f(n)
f(n)值较高的结点
因此它肯定不会偏离最短路径
4.1 预计算的精确启发式函数
构造精确启发函数的一种方法:
预先计算任意一对结点之间最短路径的长度
然后添加一个启发函数
h
′
h'
h′用于评估从任意位置n到达邻近导航点(waypoints)的代价
最终的启发式函数:
h
(
n
)
=
h
′
(
n
,
w
1
)
+
d
i
s
t
a
n
c
e
(
w
1
,
w
2
)
+
h
′
(
w
2
,
g
o
a
l
)
h(n) = h'(n, w1) + distance(w1, w2) + h'(w2, goal)
h(n)=h′(n,w1)+distance(w1,w2)+h′(w2,goal)
或者如果希望一个更好但更昂贵的启发式函数
则分别用靠近结点和目标的 所有的w1,w2对 对上式进行求值
4.2 线性精确启发式算法
在特殊情况下,可以不通过预计算而让启发式函数很精确
如果有一个不存在障碍物和平缓地形
那么从初始点到目标的最短路径应该是一条直线
如果正使用简单的启发式函数(不知道地图上的障碍物)
则它应该和精确的启发式函数相符合
如果不是这样,则会遇到衡量单位的问题,或者所选择的启发函数类型的问题
5. 网格地图中的启发式算法
在网格地图中,有一些众所周知的启发式函数
5.1 曼哈顿距离
标准的启发式函数是曼哈顿距离(Manhattan distance)
考虑代价函数并找到从一个位置移动到邻近位置的 最小代价D
曼哈顿距离——两点在南北方向上的距离加上在东西方向上的距离
即 D(I,J)= |XI - XJ| + |YI - YJ|
因此,启发式函数应该是曼哈顿距离的D倍:
H(n) = D * (abs ( n.x – goal.x ) + abs ( n.y – goal.y ) )
5.2 对角线距离
如果在地图中允许对角运动,那么需要一个不同的启发函数
(4 east, 4 north)的曼哈顿距离将变成8* D,然而可以简单地移动(4 northeast)代替
所以启发函数应该是4* D
假设直线和对角线的代价都是D:
h(n) = D * max(abs(n.x - goal.x), abs(n.y - goal.y))
如果对角线运动的代价不是D,但类似于D2 = sqrt(2) * D
则上面的启发函数不准确,需要一些更准确的东西:
D2 = sqrt(2) * D
h_diagonal(n) = min(abs(n.x - goal.x), abs(n.y - goal.y))
h_straight(n) = (abs(n.x - goal.x) + abs(n.y - goal.y))
h(n) = D2 * h_diagonal(n) + D * (h_straight(n) - 2*h_diagonal(n)))
h_diagonal(n):沿着斜线可以移动的步数
h_straight(n):曼哈顿距离
然后合并这两项,让所有的斜线步都乘以 D2
剩下的所有直线步(注意这里是曼哈顿距离的步数减去2倍的斜线步数)都乘以D
因为一个斜线用直折线走就得2步
5.3 欧几里得距离
如果单位可以沿着任意角度移动(而不是网格方向),那么也许应该使用直线距离:
h(n) = D * sqrt(pow(n.x - goal.x, 2) + pow(n.y - goal.y, 2))
然而,如果是这样的话,直接使用A* 时将会遇到麻烦
因为代价函数
g
(
n
)
g(n)
g(n)不会匹配启发函数
h
(
n
)
h(n)
h(n)
因为欧几里得距离比曼哈顿距离和对角线距离都短
仍可以得到最短路径,不过A*将运行得更久一些
5.4 平方后的欧几里得距离
一些A*的网页提到通过使用距离的平方而避免欧几里得距离中昂贵的平方根运算:
h(n) = D * (pow(n.x - goal.x, 2) + pow(n.y - goal.y, 2))
千万不要这样做!这明显地导致衡量单位的问题
当A* 计算
f
(
n
)
=
g
(
n
)
+
h
(
n
)
f(n) = g(n) + h(n)
f(n)=g(n)+h(n)
h
(
n
)
h(n)
h(n)将比
g
(
n
)
g(n)
g(n)的代价大很多,并且会因为启发式函数评估值过高而停止
对于更长的距离,A*退化成BFS
5.5 Breaking ties Breaking ties
导致低性能的一个原因来自于启发函数的结点处理
当某些路径具有相同的
f
(
n
)
f(n)
f(n) 值的时候,它们都会被搜索,尽管只需要搜索其中的一条:
为了解决这个问题,可以为启发函数添加一个附加值
附加值对于结点必须是确定性的(也就是说,不能是随机的数),而且它必须让
f
(
n
)
f(n)
f(n)值体现区别
一种,对 f ( n ) f(n) f(n)值排序,让 f ( n ) f(n) f(n)值不同意味着只有一个等价(equivalent)的 f ( n ) f(n) f(n)值会被检测
另一种添加附加值的方式是 稍微改变
h
(
n
)
h(n)
h(n)的衡量单位
如果减少
h
(
n
)
h(n)
h(n)的权重,那么当朝着目标移动的时候
f
(
n
)
f(n)
f(n)将逐渐增加
很不幸,这意味着A*倾向于扩展到靠近初始点的结点,而不是靠近目标的结点
可以增加衡量单位 p(甚至是0.1%),A* 就会倾向于扩展到靠近目标的结点
heuristic *= (1.0 + p)
选择因子p使得p < 移动一步(step)的最小代价的最长路径长度
假设不希望路径超过1000步,可以使 p = 1 / 1000
添加这个附加值的结果是,A*比以前搜索的结点更少了
当存在障碍物时,当然仍要在它们周围寻找路径
但要意识到,当绕过障碍物以后,A*搜索的区域非常少
一个更直截了当的方法是把
h
(
n
)
h(n)
h(n)传递到比较函数(comparison function)
当
f
(
n
)
f(n)
f(n)值相等时,比较函数检查
h
(
n
)
h(n)
h(n),然后添加附加值
一个不同的添加附加值的方法是,倾向于从初始点到目标点的连线(直线):
dx1 = current.x - goal.x
dy1 = current.y - goal.y
dx2 = start.x - goal.x
dy2 = start.y - goal.y
cross = abs(dx1 * dy2 - dx2 * dy1)
heuristic += cross*0.001
这段代码计算初始-目标向量(start to goal vector)和当前-目标向量(current point to goal vector)的向量叉积(vector cross-product)
结果是,这段代码选择的路径稍微倾向于从初始点到目标点的直线
当没有障碍物时,A*不仅搜索很少的区域,而且它找到的路径看起来非常棒
然而,因为这种附加值倾向于从初始点到目标点的直线路径,当出现障碍物时将会出现奇怪的结果
注意这条路径仍是最佳的,只是看起来很奇怪
然而,另一种添加附加值的方法是,小心地构造A*优先队列
使新插入的具有特殊
f
(
n
)
f(n)
f(n)值的结点总是比那些以前插入的具有相同
f
(
n
)
f(n)
f(n)值的旧结点要好一些
5.6 区域搜索
如果想搜索邻近目标的任意不确定结点,而不是某个特定的结点
应该建立一个启发函数
h
′
(
x
)
h'(x)
h′(x),使得
h
′
(
x
)
h'(x)
h′(x)为 h1(x), h2(x), h3(x)…的最小值
而这些h1, h2, h3是邻近结点的启发函数
然而,一种更快的方法是让A*仅搜索目标区域的中心
一旦从开放且未考察的区域OPEN集合中取得任意一个邻近目标的结点
就可以停止搜索并建立一条路径了
参考:
相关推荐:
谢谢!