有没有办法在A*算法中保持方向优先级?(例如,生成与广度优先相同的路径)

10

我有一个应用程序,使用A*算法会使它受益匪浅;然而由于历史原因,当出现多条最佳路径时,我需要它继续生成完全相同的路径

例如,考虑这个迷宫:

...X
FX.S
....
S = 起点 F = 终点 X = 墙 . = 空地

假定我们按照上、右、下、左的方向优先级来寻路,在广度优先算法中,我们将找到路径DLLLU;然而,在使用A*算法时,我们会立即朝左移动,并最终找到路径LULLD。

我尝试过在选择方向时始终展开正确的方向,以及在从较重要的方向移动时覆盖PreviousNode指针,但在该示例中均不起作用。是否有一种方法可以解决这个问题?


1
我认为你所描述的行为与A算法使用的启发式有关。在你的情况下,我假设是欧几里得/曼哈顿距离。请注意,例如,如果你设置h(v)在[0,1]范围内,A将会像BFS一样运行,除了最后一步... - amit
@amit:我知道如何让它的行为像BFS,但对我来说完全没有用。我希望它的行为像A*(不失去速度优势),但生成与BFS相同的路径。我已经在99.9%的情况下使其正常工作,但上述情况失败了。难道真的没有办法在上述迷宫中获得相同的路径吗? - BlueRaja - Danny Pflughoeft
1
我怀疑可以通过给边添加权重来实现,使得 w(u,v) = 1 + f(u)*prio(u,v),其中:(1) 所有边的加权总和不超过1。(2) f(u) 是基于源点到目标点的距离(可能是启发式的?)计算出来的,并且单调递减,这样每个 f(u) 都比在最短路径中跟随它的所有节点“更重要”[可能具有 Zeno 性质并呈指数衰减]。然而,我无法想出一种证明它有效的方法,并且还怀疑它可能会漏掉一些细节。 - amit
提交截止日期已经过了!;o) - andrew cooke
@andrewcooke:抱歉,什么? - BlueRaja - Danny Pflughoeft
显示剩余2条评论
4个回答

5
如果原始算法是BFS,则您正在寻找最短路径中的最小路径,其中“最小”是根据边缘上的某个总序列Ord引起的词典顺序(当然,“最短”是按照路径长度)。调整权重的想法是自然的,但我认为这不是很实用,因为权重需要具有与路径长度可比的位数,以避免丢弃信息,这将使算法变得慢得多。幸运的是,这仍然可以通过对A*进行两个简单而廉价的修改来完成:
1.一旦到达目标,我们应该继续访问节点,直到路径长度增加,这样我们就可以访问所有属于最短路径的节点。
2.在重建路径时,我们构建贡献于最短路径的节点集。当考虑所有最短路径边缘时,此集合具有DAG结构,现在可以在此DAG中找到从startgoal的字典顺序最小路径,这是所需的解决方案。
经典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个单位的节点,并且重构阶段在属于最短路径的节点数几乎线性。如果像你所暗示的那样,在大多数情况下只有一条最短路径,则影响更小。甚至可以针对这种特殊情况进行优化,例如通过记住一个祖先节点(如经典情况),当存在多个祖先时(即当path_length[y] == path_length_through_x 时)将其设置为特殊错误值。一旦搜索循环结束,您尝试通过ancestor检索路径,就像经典A*一样;仅在构建路径时遇到错误值时才需要执行完整的路径重构。


我对这个解决方案不太确定。它仍然不能返回所有路径(否则我们就会得到一个矛盾的事实,即A*算法在边和顶点数量上是多项式级别的,因为路径的数量是指数级别的)。你能提供解释/证明指南来说明为什么我们不会通过这种方法“失去”首选路径吗? - amit
谢谢!我已经自己想出了一个完全不同的解决方案(涉及到当你有一个可能冲突的PreviousNode值时进行回溯)- 我一直在等待测试它的速度与BFS相比如何,但我也会将您的解决方案添加到我的测试中。谢谢! - BlueRaja - Danny Pflughoeft
@amit:我相当确定这会起作用。这个解决方案本质上是找到所有我们知道属于最佳路径的节点,然后使用我们的方向优先级从起点到终点进行遍历,仅考虑那些节点并始终以g值增加的方式前进。最佳路径的数量可能在n中呈指数增长,但属于最佳路径的节点数量是线性的。 - BlueRaja - Danny Pflughoeft
确切地说,我们构建的DAG包含所有最短路径(因此有指数数量的路径),但是DAG本身是原始图形的子集,因此它的大小是线性的。而且我们并不建立每个最短路径的详尽列表:我们只需要有限次操作每个边来找到词典序最小的路径。 - Generic Human
我已经发现了更好的方法来解决这个问题(见下文),但是由于除了我自己之外,你是唯一提出解决方案的人,所以我会给你奖励。谢谢! - BlueRaja - Danny Pflughoeft

2

我会将路径顺序的偏好直接建立在启发式函数中。

我会首先查看广度优先算法。

为广度优先算法选择的每条路径定义一个函数:

假设我们正在运行深度优先算法,并且它在第n层。算法之前所做的决策为:x_i \in {U,R,D,L},其中U=0,R=1,D=2,L=3。

然后定义:

g(x_1,..,x_n) = sum_{i=1}^n x_i * (1/4)^i

让我们将此步骤的g值设为g'。当算法访问比这个节点更深的节点时,g()函数会变大。
当{1..n} x_i中的任意一个改变时,在未来的每一步中,它都会变得更大,因此在运行深度优先搜索时,g函数总是增加的。
注意:如果深度优先算法成功,则选择具有最小g()值的路径。
注意:g() < 1,因为max(L,R,U,D)=3。
将g添加到A*的启发式函数中不会干扰最短路径长度,因为最小边长>=1。
像这样修改的A*找到的第一个解决方案将是深度优先搜索找到的解决方案。
以您的示例为例:
h_bread=g(DLLLU) = (23330)_4 * c
h_astar=g(LULLD) = (30332)_4 * c

()_4是基于4进制的 c是一个常数(4^{-5})

以您的例子为例:h_bread < h_astar


+1 这几乎与我已经想出的解决方案相同(尚未发布),但要注意的是,将 g 值和到目前为止完整路径的编码分开可能更有效率。此外,需要注意的是,此解决方案仅适用于每个节点具有 4(或 8)个方向的网格图,而一般的 BFS/Djikstra 和 A* 更加广泛适用。 - BlueRaja - Danny Pflughoeft
正如我在帖子中提到的,如果你的路径可能会变得非常长,这种方法就不起作用了。如果两个路径在前30步相遇,则它们的g值在表示为“double”浮点变量时将相等。因此,你无法知道哪个路径在词典顺序上最小。 - Generic Human
基数4可以用最大顶点连接替换,浮点数可以通过二级比较逻辑消除。 - Zoltán Haindrich
@ZoltánNagy:当然,但这意味着如果您的路径在最坏情况下可能有3000个步骤,则需要至少将路径权重存储在100个机器字中。因此,在这种情况下,您的算法比标准A *慢100倍! - Generic Human

1
我想到了两种方法来实现这个。两种方法都需要在队列顶部的距离起点的g值<= g(终节点)的情况下继续算法。由于A*中使用的启发式函数是可接受的,这保证了属于某些最佳路径的每个节点最终都会被扩展。
第一种方法是:当我们遇到冲突时(例如,我们发现两个节点具有相同的f值,并且可能都是沿着最佳路径的某个节点的父节点),我们通过回溯到它们相遇的路径上的第一个点来解决它(在路径长度为O时,我们可以轻松地做到这一点)。然后,我们只需要检查两条路径的方向优先级,并选择在BFS搜索中具有更高优先级的路径。
第二种方法仅适用于每个节点都与水平和垂直(可能还有对角线)相邻节点接触的网格图(即4连通网格图)。我们执行与之前相同的操作,但不是回溯以解决冲突,而是比较沿着从起点开始的路径的节点,并找到它们第一次不同的地方。它们第一次不同的地方将是与之前相同的关键节点,从该节点我们可以检查方向优先级。
我们通过为每个节点存储到目前为止的最佳路径来实现这一点。通常这会很麻烦,但由于我们有一个4连通图,因此我们可以通过存储沿路径采取的每个方向来相当有效地完成此操作。每个节点只需要2位。因此,我们可以使用整数编码路径 - 使用32位寄存器,我们可以一次比较16个节点;使用64位寄存器可以比较32个节点;使用128位寄存器(如x86和x64处理器中的SSE寄存器)可以一次比较64(!)个节点,使得即使对于具有数百个节点的路径,此搜索也非常廉价。

我实现了这两个算法,以及@generic human的算法,来测试速度。在一个50x50的网格上有400个塔。

  • @generic human的算法比正常的A*慢了大约120%
  • 我的回溯算法比正常的A*慢了大约55%
  • 整数编码算法只比A*慢了不到10%

因此,由于我的应用程序使用4连通图,似乎整数编码算法对我最好。


我复制了一封我写给教授的电子邮件在这里。它包括更详细的算法描述,以及证明它们有效性的草图。


0

一般来说,没有非平凡的方法可以做到这一点:

广度优先搜索找到的是按照顶点被考虑的顺序确定的最低阶路径。当在等长路径之间进行抉择时,这个顺序必须优先考虑于其他任何因素。

例如:如果节点按照A、B、C的顺序被考虑,则节点A < 节点C。因此,如果有一个以A开头的最短路径和一个以C开头的最短路径之间存在抉择,那么将选择以A开头的路径。

另一方面,A*搜索将找到由节点的启发式值确定的最低阶路径。因此,启发式函数必须考虑到每个节点的最低词典路径。而找到这个路径的唯一方法就是BFS。


“而找到它的唯一方法是BFS。”- 这个陈述来自哪里?这是一个猜测吗?我不明白为什么我们不能在运行A*时跟踪/计算词典顺序。 - BlueRaja - Danny Pflughoeft
这是因为启发式函数是特定节点的函数。它不能依赖于外部因素,比如“我从哪个方向来?”。因此,为了计算启发式函数,你最终将不得不运行BFS算法。我的直觉是,BFS算法是这一步的最优算法。如果这个解释有点不清楚,很抱歉。我会尽快发布正式证明(现在有点忙:P)。 - tskuzzy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接