【问题标题】:Is there a way to keep direction priorities in A*? (ie. Generating the same path as breadth-first)有没有办法在 A* 中保持方向优先级? (即生成与广度优先相同的路径)
【发布时间】:2012-07-16 14:01:28
【问题描述】:

我有一个应用程序可以从使用 A* 中受益;但是,由于遗留原因,当有多个最佳路径可供选择时,我需要继续生成 完全与以前相同的路径。

例如,考虑这个迷宫

...X FX.S …… S = 开始 F = 完成 X = 墙 . = 空白空间

方向优先向上;正确的;下;左。使用广度优先,我们将找到路径 DLLLU;然而,使用 A*,我们立即向左走,最终找到路径 LULLD。

我尝试过确保在打破关系时始终向正确的方向扩展;并在从更重要的方向移动时覆盖PreviousNode 指针,但在该示例中均无效。有没有办法做到这一点?

【问题讨论】:

  • 我认为您所描述的行为与 heuristic A* 使用有关。在你的情况下 - 我假设欧几里德/曼哈顿距离。请注意,例如,如果您设置 h(v) in [0,1] - A* 的行为将与 BFS 完全相同,除了最后一步..
  • @amit:我知道如何让它像 BFS 一样表现,但这对我来说完全没用。我希望它表现 像 A* (不会失去速度优势),但生成与 BFS 相同的路径。我已经让它在 99.9% 的情况下工作,但上述情况失败了。真的没有办法在上面的迷宫中走同样的路吗?
  • 我怀疑它可以通过向边缘添加权重来完成,这样w(u,v) = 1 + f(u)*prio(u,v) 使得:(1)所有边缘的总和不超过 1。(2)@987654324 @ 是根据与源的距离(可能是启发式的?)计算的,并且是单调递减的,这样每个 f(u) 都“更重要”,然后所有节点都会以最短的路径到达目标 [可能将具有芝诺属性并呈指数衰减]。但是我想不出一种方法来证明它有效,并且还怀疑它仍然可能会遗漏一些细节。
  • 提交截止日期已过! ;o)
  • @andrewcooke: ...抱歉,什么?

标签: algorithm graph path-finding a-star


【解决方案1】:

我想出了两种方法来做到这一点。两者都需要继续算法,而队列顶部的距离到开始 g-value admissable,这保证了属于 some best-path 的每个节点最终都会被扩展。


第一种方法是,当我们遇到冲突时(即,我们发现两个具有相同 f 值的节点,它们都可能是沿最佳路径的某个节点的父节点) ,我们通过回溯到他们遇到的路径上的第一个点来解决它(我们可以在O(path-length) 中轻松做到这一点)。然后,我们只需检查两条路径的方向优先级,然后选择在 BFS 搜索中具有更高优先级的路径。


第二种方法仅适用于每个节点接触水平和垂直(可能对角)相邻节点的网格(即 4 连接网格图)。我们做和以前一样的事情,但不是回溯来解决冲突,而是从一开始就比较沿路径的节点,并找到它们不同的第一个地方。它们的第一个不同之处将是与以前相同的关键节点,我们可以从中检查方向优先级。

我们通过为每个节点存储迄今为止的最佳路径来做到这一点。通常这会很麻烦,但由于我们有一个 4 连通图,我们可以通过存储沿路径的每个方向来非常有效地做到这一点。这将只需要每个节点 2 位。因此,我们基本上可以使用整数对路径进行编码——使用 32 位寄存器,我们可以一次比较 16 个节点; 32个节点,64位寄存器;和 64(!) 个节点,同时具有 128 位寄存器(如 x86 和 x64 处理器中的 SSE 寄存器),即使对于具有 100 个节点的路径,这种搜索也非常便宜。


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

  • @generic 人类算法的运行速度比普通 A* 慢 120% 左右
  • 我的回溯算法运行速度比正常 A* 慢 55% 左右
  • 整数编码算法的运行速度仅比 A* 慢不到 10%

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


我复制了一封写给教授 here 的电子邮件。它包括更详细的算法描述,以及它们工作的证明草图。

【讨论】:

    【解决方案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 的 on 发生变化时,它会更大,因此 g 函数在运行深度优先时总是会升高。

    注意:如果深度优先算法成功,则选择g()值最小的路径

    注意:g()

    将 g 添加到 A* 的启发式函数不会干扰最短路径长度,因为 min edge length>=1

    这样修改的 A* 将找到的第一个解决方案将是深度优先找到的解决方案

    为你举例:

    h_bread=g(DLLLU) = (23330)_4 * c
    h_astar=g(LULLD) = (30332)_4 * c
    

    ()_4 是base4 c 是一个常数 (4^{-5})

    为你举例:h_bread

    【讨论】:

    • +1 这与我已经提出的解决方案之一几乎相同(尚未发布),但需要注意的是更有效地保持 g 值和完整路径的编码分离。此外,需要注意的是,此解决方案仅适用于每个节点有 4 个(或 8 个)方向的网格图,而通用 BFS/Djikstra 和 A* 的适用范围更广。
    • 正如我在帖子中提到的,如果您的路径可以增长很长,这种方法将不起作用。如果前 30 步中两条路径重合,则当表示为 double 浮点变量时,它们的 g 值将相等。所以你无法知道哪条路径在字典上是最小的。
    • base 4可以用最大顶点连接代替,浮点可以用二级比较逻辑来限定
    • @ZoltánNagy:当然,但这意味着如果你的路径在最坏的情况下可以有 3000 步,那么你需要将路径权重存储在至少 100 个机器字中。因此,在这种情况下,您的算法将比标准 A* 慢 100 倍!
    【解决方案3】:

    如果原始算法是 BFS,您正在寻找最短路径中的最小路径,其中“最小”是根据边缘上的某些总顺序 Ord 引起的字典顺序(当然“最短”是根据到路径长度)。

    amit 建议的调整权重的想法很自然,但我认为这不是很实用,因为权重需要具有与路径长度相当的位数以避免丢弃信息,这会使算法变慢几个数量级。

    幸运的是,这仍然可以通过对 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 个单位的节点,重建阶段在属于 a 的节点数量上是准线性的最短路径。如果正如您所暗示的那样,在大多数情况下只有一条最短路径,则影响较小。您甚至可以针对这种特殊情况进行优化,例如通过记住一个ancestor 节点,就像在经典情况下一样,当有多个祖先时(即path_length[y] == path_length_through_x),您将其设置为一个特殊的错误值。搜索循环结束后,您将尝试检索通过 ancestor 的路径,就像在经典 A* 中一样;如果在构建路径时遇到错误值,则只需要执行完整路径重构。

    【讨论】:

    • 我不确定这个解决方案。它仍然不会返回所有路径(否则我们会得到与 A* 运行多项式与边和顶点数量相矛盾的事实,因为路径的数量是指数级的)。您能否提供解释/证明指南来说明为什么我们不会使用这种方法“丢失”首选路径?
    • 谢谢!我自己提出了一个完全不同的解决方案(当你有一个可能冲突的 PreviousNode 值时涉及回溯) - 我一直在等待发布它,直到我测试了它与 BFS 的速度,但是我也会将您的解决方案添加到我的测试中。谢谢!!
    • @amit:我相当肯定这会奏效。这个解决方案本质上是,找到我们知道属于最佳路径的所有节点,然后使用我们的方向优先级从开始走到结束,考虑那些节点并且总是g值上升。最佳路径的数量在n 中可能是指数的,但属于最佳路径的节点数量是线性的。
    • 没错。我们构建的 DAG 包含 所有 最短路径(因此它们的数量是指数的),但 DAG 本身是原始图的子集,因此它是线性大小的。而且我们不会建立每条最短路径的详尽列表:我们只需要操作每条边一定次数即可找到字典上最小的路径。
    • 我发现了更好的方法来做到这一点(见下文),但我会给你赏金,因为除了我之外,你是唯一一个想出解决问题的方法。谢谢!
    【解决方案4】:

    一般来说,没有重要的方法可以做到这一点:

    广度优先搜索找到由考虑顶点的顺序确定的最低顺序的最短路径。并且在断开等长路径之间的联系时,此顺序必须优先于任何其他因素。

    示例:如果按照 A、B、C 的顺序考虑节点,则为 Node A < Node C。因此,如果以 A 开头的最短路径和以 C 开头的最短路径之间存在平局,则将找到以 A 开头的最短路径。

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

    【讨论】:

    • “找到它的唯一方法是 BFS。” - 这句话从何而来?这是猜测吗?我不明白为什么我们在运行 A* 时不能以某种方式跟踪/计算字典顺序。
    • 这是因为启发式是特定节点的函数。它不能依赖于诸如“我来自哪个方向?”之类的外部因素。因此,要计算启发式,您最终将不得不运行 BFS。我的直觉是 BFS 是这一步的最佳算法。对不起,如果这个解释有点不清楚。我将尝试在以后发布正式证明(现在很忙:P)。
    猜你喜欢
    • 2010-10-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多