如果原始算法是 BFS,您正在寻找最短路径中的最小路径,其中“最小”是根据边缘上的某些总顺序 Ord 引起的字典顺序(当然“最短”是根据到路径长度)。
amit 建议的调整权重的想法很自然,但我认为这不是很实用,因为权重需要具有与路径长度相当的位数以避免丢弃信息,这会使算法变慢几个数量级。
幸运的是,这仍然可以通过对 A* 的两个简单且廉价的修改来完成:
- 一旦我们到达目标,我们应该继续访问节点,直到路径长度增加,而不是返回任意最短路径到目标,以便我们访问所有属于最短路径的节点。
- 在重建路径时,我们会构建有助于最短路径的节点集。该集合在考虑所有最短路径边时具有 DAG 结构,现在很容易在此 DAG 中找到从
start 到 goal 的词典最小路径,这是理想的解决方案。
从示意图上看,经典 A* 是:
path_length = infinity for every node
path_length[start] = 0
while score(goal) > minimal score of unvisited nodes:
x := any unvisited node with minimal score
mark x as visited
for y in unvisited neighbors of x:
path_length_through_x = path_length[x] + d(x,y)
if path_length[y] > path_length_through_x:
path_length[y] = path_length_through_x
ancestor[y] = x
return [..., ancestor[ancestor[goal]], ancestor[goal], goal]
其中score(x) 代表path_length[x] + heuristic(x, goal)。
我们简单地将严格的while 循环不等式变为非严格不等式,并添加路径重构阶段:
path_length = infinity for every node
path_length[start] = 0
while score(goal) >= minimal score of unvisited nodes:
x := any unvisited node with minimal score
mark x as visited
for y in unvisited neighbors of x:
path_length_through_x = path_length[x] + d(x,y)
if path_length[y] > path_length_through_x:
path_length[y] = path_length_through_x
optimal_nodes = [goal]
for every x in optimal_nodes: // note: we dynamically add nodes in the loop
for y in neighbors of x not in optimal_nodes:
if path_length[x] == path_length[y] + d(x,y):
add y to optimal_nodes
path = [start]
x = start
while x != goal:
z = undefined
for y in neighbors of x that are in optimal_nodes:
if path_length[y] == path_length[x] + d(x,y):
z = y if (x,y) is smaller than (x,z) according to Ord
x = z
append x to path
return path
警告:引用 Knuth 的话,我只是证明它是正确的,没有尝试过。
至于性能影响,应该是最小的:搜索循环只访问得分比经典 A* 高 1 个单位的节点,重建阶段在属于 a 的节点数量上是准线性的最短路径。如果正如您所暗示的那样,在大多数情况下只有一条最短路径,则影响较小。您甚至可以针对这种特殊情况进行优化,例如通过记住一个ancestor 节点,就像在经典情况下一样,当有多个祖先时(即path_length[y] == path_length_through_x),您将其设置为一个特殊的错误值。搜索循环结束后,您将尝试检索通过 ancestor 的路径,就像在经典 A* 中一样;如果在构建路径时遇到错误值,则只需要执行完整路径重构。