相关文章:
启发式搜索算法1 – 最佳优先搜索算法
A* 算法
最佳优先搜索算法的效果非常依赖估价函数,而估价函数又不是这么容易设置,那么能不能折中一下,添加其他确定性参数来平衡这个估价函数的不确定性,这就是A算法。
A算法(A-Star)是一种静态路网中求解最短路径最有效的直接搜索方法,也是解决许多搜索问题的有效算法。它是基于使用启发式方法来实现最佳性和完整性的,它是最佳优先算法的一种变体。因为它不但具备启发式搜索特点,而且能够保证找到最佳的解决方案。许多游戏和基于Web的地图都使用此算法非常有效地找到最短路径(近似值)。
为什么这个算法这么厉害,主要是它在进行搜索时候,会计算到达相邻结点的成本函数f(n),挑选f(n)最小结点。这个成本函数公式为f(n)=g(n)+h(n),g(n)函数是从起点状态到当前状态n的实际代价,这是一个确定值,h(n)是从当前状态n到目标状态的最佳路径的估计代价,换言之f(n)函数包含了一部分确定值和一部分估计值的估价函数,也可以说是一种启发式搜索算法。为了保证找到最短路径(最优解),关键在于估价函数f(n)的选取(或者说h(n)的选取),也可以说是g(n)和h(n)值的比重分配。以d(n)表达状态n到目标状态的实际距离,那么h(n)的选取大致有如下三种情况:
(1)如果h(n)<d(n)到目标状态的实际距离,或者说h(n)的比重过小,这种情况下,搜索的点数多,搜索范围大,效率低。但能得到最优解。在极端情况下,h(n)值过小可以忽略,f(n)约等于g(n),算法变回了盲目搜索。
(2)理想状态是h(n)=d(n),即距离估计h(n)等于实际最短距离,那么搜索效率是最高的。
(3)如果h(n)>d(n),或者说h(n)的比重过大,这种情况下搜索的点数少,搜索范围小,效率高,但不能保证得到最优解。在极端情况下,h(n)值过大,使得f(n)约等于h(n),算法变回了最佳优先算法。
举一个例子,参照上一个的例子,这次是一个有权有向图,【D-E】边值变成5,如图所示。
尝试用A*算法在图结构上的直接实现。为了简单起见,所有结点的启发式函数h(n)均定义为1,分析过程如下表所示。
每次挑选结点,都是在开放列表中选择f(n)最小值的结点,直到找到目标路径。从表中看到,f(F)的值经过一次修改,在选择【E】结点的时候,因为g(f)值减少了,f(F)也就比原来的值小,因此更新了【F】结点的f(n)值,以下是用代码来表示此算法运算。
class Graph(object):
def __init__(self, graph):
self.graph = graph
def get_neighbors(self, v):
return self.graph[v]
def h(self, n):
# 估价函数,我们简化了这个函数,假设每个结点的距离为1
H = {
'A': 1,
'B': 1,
'C': 1,
'D': 1,
'E': 1,
'F': 1,
'G': 1,
'H': 1,
}
return H[n]
def a_star_algorithm(self, start_node, target_node):
# A*算法主程序
open_list = set([start_node]) # 在开放列表中,是结点已被访问,但邻接的结点未被访问
closed_list = set([]) # 那是结点已访问,邻接的结点也都全部访问
g = {} # 记录所有结点到开始结点的距离,若没有记录就当成无穷大
g[start_node] = 0 # 开始结点与自身距离为零
parents = {} # 记录结点的邻接结点
parents[start_node] = start_node # 第一个结点为开始结点
while len(open_list) > 0: # 直到开放列表为空,跳出循环
n = None
for v in open_list: # 寻找f(n)的最小值
if n == None or g.get(v,MAX) + self.h(v) < g.get(n,MAX) + self.h(n):
n = v
if n == None: # 找不到下一个结点,证明路径不存在
print('两个结点没有路径相连')
return None
print("挑选结点:", n)
# 如果到达目标结点,开始重建回路
if n == target_node:
reconst_path = []
while parents[n] != n: # 直到找到开始结点,跳出循环
reconst_path.append(n)
n = parents[n]
reconst_path.append(start_node) # 补充开始结点
reconst_path.reverse() # 翻转列表
print('最佳路径为: {}'.format(reconst_path))
return reconst_path
# 遍历该结点的所有邻接结点
for (neighbor_node, value) in self.get_neighbors(n):
if neighbor_node not in open_list and neighbor_node not in closed_list:
# 该结点不在开放列表和关闭列表,则加入开放列表
open_list.add(neighbor_node)
parents[neighbor_node] = n # 记录其父亲结点,便于构建路径
g[neighbor_node] = g[n] + value # 记录此结点到开始结点的代价
else:
# 新的路径代价比原来小则更新路径
if g[neighbor_node] > g[n] + value:
parents[neighbor_node] = n
g[neighbor_node] = g[n] + value
# 如果该结点在关闭列表,让他重新回到开放列表
if neighbor_node in closed_list:
closed_list.remove(neighbor_node)
open_list.add(neighbor_node)
open_list.remove(n) # 所有邻接结点访问完,移除该结点
closed_list.add(n) # 放到关闭列表
# 尝试所有可能后,若没有找到路径,说明不存在
print('两个结点没有路径相连')
return None
创建一个【Graph】类,继续使用邻接列表来表示图,属性【graph】保存图的邻接列表,属性【parents】是记录结点的父结点,用于从目标结点回溯构建最优解的路径。a_star_algorithm()函数是A*算法主程序,算法实现步骤如下。
(1)初始化开放列表【open_list】,把起点结点放入开放列表。
(2)初始化关闭列表【closed_list】为空列表。
(3)若开放列表不为空,进入循环来到第四步,否则程序结束到第十一步。
(4)在开放列表中挑选f(n)最小的结点n。
(5)若n是目标结点,就结束循环,如不是则进入第六步。
(6)遍历n结点的所有邻接结点neighbor_node,进入第七步,遍历结束来到第十步。
(7)若邻接结点不在开放列表也不在关闭列表,进入第八步,否则进入第九步。
(8)把邻接结点放进开放列表,更新g(neighbor_node)的值。
(9)若新的g(neighbor_node)小于原来的值,则更新值,若此结点在关闭列表,则把它从关闭列表移除,并放到开放列表中。
(10)把结点n从开放列表移除,添加到关闭列表。
(11)结束程序。
程序的关键部分是第四步,计算f(n)的值,找到最小值的结点。这里主要是介绍算法思路,所以简化了f(n)的计算,h(n)变成一个固定值1,现在来测试一下程序。
graph_list = {
"A": [("B",3), ("D",2), ("G",12)],
"B": [("H",9)],
"D": [("C",4), ("E",5)],
"C": [("F",3)],
"E": [("F",1)],
}
graph = Graph(graph_list)
graph.a_star_algorithm('A', 'F')
# ---------结果-------------
挑选结点: A
挑选结点: D
挑选结点: B
挑选结点: C
挑选结点: E
挑选结点: F
最佳路径为: ['A', 'D', 'E', 'F']
这里打印了程序运行过程,挑选结点的顺序和手动调试是一致的,而且最后结果也是符合预期。这个例子中的h(n)估计函数的作用不是很大,但也能体现算法的思想。当遇到实际问题的时候,要根据实际情况来设置h(n),比如在一个方格上的寻找路径,如下图所示。
h(n)的值可以是两个坐标值的曼哈顿距离(Manhattan Distance),h(n)为目标的x和y坐标与当前单元格的x和y坐标之差的绝对值之和,正如图中的黑色线条,或者取目标的x和y坐标与当前单元格的x和y坐标之差的绝对值的最大值,正如图中的蓝色线条,又或者计算两个坐标点的直线距离,如图中的红色线条。
注意:在选择h(n)的时候也要考虑运算效率,比如红色线条的计算涉及平方和开方,相对其他两种只有加减运算就复杂了。
更多内容
想获取完整代码或更多相关图的算法内容,请查看我的书籍:《数据结构和算法基础Python语言实现》