【问题标题】:Wikipedia A* pathfinding algorithm takes a lot of time维基百科 A* 寻路算法需要大量时间
【发布时间】:2011-02-01 16:09:59
【问题描述】:

我已经在 C# 中成功实现了 A* 寻路,但是速度很慢,我不明白为什么。我什至尝试不对 openNodes 列表进行排序,但还是一样。

地图为80x80,有10-11个节点。

我从这里获取了伪代码Wikipedia

这是我的实现:

 public static List<PGNode> Pathfind(PGMap mMap, PGNode mStart, PGNode mEnd)
    {
        mMap.ClearNodes();

        mMap.GetTile(mStart.X, mStart.Y).Value = 0;
        mMap.GetTile(mEnd.X, mEnd.Y).Value = 0;

        List<PGNode> openNodes = new List<PGNode>();
        List<PGNode> closedNodes = new List<PGNode>();
        List<PGNode> solutionNodes = new List<PGNode>();

        mStart.G = 0;
        mStart.H = GetManhattanHeuristic(mStart, mEnd);

        solutionNodes.Add(mStart);
        solutionNodes.Add(mEnd);

        openNodes.Add(mStart); // 1) Add the starting square (or node) to the open list.

        while (openNodes.Count > 0) // 2) Repeat the following:
        {
            openNodes.Sort((p1, p2) => p1.F.CompareTo(p2.F));

            PGNode current = openNodes[0]; // a) We refer to this as the current square.)

            if (current == mEnd)
            {
                while (current != null)
                {
                    solutionNodes.Add(current);
                    current = current.Parent;
                }

                return solutionNodes;
            }

            openNodes.Remove(current);
            closedNodes.Add(current); // b) Switch it to the closed list.

            List<PGNode> neighborNodes = current.GetNeighborNodes();
            double cost = 0;
            bool isCostBetter = false;

            for (int i = 0; i < neighborNodes.Count; i++)
            {
                PGNode neighbor = neighborNodes[i];
                cost = current.G + 10;
                isCostBetter = false;

                if (neighbor.Passable == false || closedNodes.Contains(neighbor))
                    continue; // If it is not walkable or if it is on the closed list, ignore it.

                if (openNodes.Contains(neighbor) == false)
                {
                    openNodes.Add(neighbor); // If it isn’t on the open list, add it to the open list.
                    isCostBetter = true;
                }
                else if (cost < neighbor.G)
                {
                    isCostBetter = true;
                }

                if (isCostBetter)
                {
                    neighbor.Parent = current; //  Make the current square the parent of this square. 
                    neighbor.G = cost;
                    neighbor.H = GetManhattanHeuristic(current, neighbor);
                }
            }
        }

        return null;
    }

这是我正在使用的启发式方法:

    private static double GetManhattanHeuristic(PGNode mStart, PGNode mEnd)
    {
        return Math.Abs(mStart.X - mEnd.X) + Math.Abs(mStart.Y - mEnd.Y);
    }

我做错了什么?我整天都在看相同的代码。

【问题讨论】:

  • 你的地图尺寸是多少?扩展的数量取决于到目标节点的距离,这取决于地图的大小。
  • 列表中有多少个节点?究竟什么是“慢”——做某事需要多长时间?
  • 共有 11 个房间和 10 个寻路连接。 Pathfind 方法大约需要 8-10 秒。
  • 11 个节点需要 10 秒?即使您的启发式方法很糟糕,这也绝对是太多了。您应该在最坏的情况下用几毫秒很好地完成所有节点。在这种情况下,我真的建议使用 stackoverflow.com/questions/266373/… 中描述的穷人分析器
  • 请;如果我错了,有人纠正我,但你有 if (current == mEnd) { while (current != null) { solutionNodes.Add(current);当前=当前。父母; } 返回解决方案节点; mEnd 和 mStart 不是已经在 solutionNodes 列表中了吗?你的逻辑不会再次添加它们吗?

标签: c# .net optimization path-finding a-star


【解决方案1】:

首先,使用分析器。用工具告诉你什么是慢;什么是缓慢的往往令人惊讶。并且使用调试器。制作一个包含五个节点的地图,并在代码的每一行 尝试查找路径时单步执行。有没有发生什么意想不到的事情?如果是这样,请弄清楚发生了什么。

其次,撇开你的性能问题不谈,我认为你也有一个正确性问题。你能向我们解释为什么你认为曼哈顿距离是一个合理的启发式吗?

(对于那些不熟悉该指标的读者来说,“曼哈顿距离”或“出租车距离”是指如果您居住在网格上的城市中,您必须经过两点之间的距离。那也就是说,要向东北行驶 14 英里,您必须向北行驶 10 英里,然后向东行驶 10 英里,总共行驶 20 英里。)

A* 算法的正确性要求启发式算法始终低估两点之间行进所需的实际距离。如果图中存在任何“对角线捷径”街道,则曼哈顿距离高估这些路径上的距离,因此 算法不一定会找到最短路径

您为什么不使用 欧几里得 距离作为启发式方法?

您是否尝试过使用“始终为零”作为启发式算法?这肯定是低估了。 (这样做会给你一个 Dijkstra 算法的实现。)

第三,你似乎在这个实现中做了很多排序。当然,您可以使用优先级队列;这比排序便宜很多。

第四,我的博客上有一个C#3中A*的实现,欢迎大家使用;没有任何明示或暗示的保证,使用风险自负。

http://blogs.msdn.com/b/ericlippert/archive/tags/astar/

我的代码非常简单;我的实现中的算法如下所示:

var closed = new HashSet<Node>();
var queue = new PriorityQueue<double, Path<Node>>();
queue.Enqueue(0, new Path<Node>(start));
while (!queue.IsEmpty)
{
    var path = queue.Dequeue();
    if (closed.Contains(path.LastStep)) continue;
    if (path.LastStep.Equals(destination)) return path;
    closed.Add(path.LastStep);
    foreach(Node n in path.LastStep.Neighbours)
    {
        double d = distance(path.LastStep, n);
        var newPath = path.AddStep(n, d);
        queue.Enqueue(newPath.TotalCost + estimate(n), newPath);
    }
}

这个想法是我们维护一个路径的优先级队列;也就是说,一个路径队列总是能够以最短的距离告诉你到目前为止的路径。然后我们检查我们是否已经到达目的地;如果是这样,我们就完成了。如果没有,那么我们会根据它们(低估)到目标的距离将一堆新路径排入队列。

第五,维基百科中的伪代码可以改进。我的实际代码在很多方面都比伪代码更容易理解,这表明伪代码中可能有太多细节。

【讨论】:

  • “您能向我们解释一下为什么您认为曼哈顿距离是合理的启发式方法吗?”由于他没有移动对角线(扩展四个邻居,请参阅stackoverflow.com/questions/4864945/… 的评论),曼哈顿是正确的。
  • 什么是距离使用,什么是估计使用?他们都使用欧几里得吗?
  • @Vee: "distance" 是任何需要两个节点并计算它们之间的精确距离的函数; “估计”是一个函数,它采用一个节点并估计从该节点到目标节点的距离。这些方法如何工作与 A* 的实现无关,只要它们正确有效地工作。如果有意义的话,它们可能是欧几里得距离。请记住,A* 可用于在 任意 图中查找路径;它不一定是真实世界的地图。欧几里得度量只在坐标系中有意义。
  • 如果允许对角线运动的成本与网格运动相同,则欧几里得距离(2-范数)不是正确的度量。无穷范数 (max(abs(x1-x2),abs(y1-y2))) 是。当不允许对角线运动时,曼哈顿距离(1-norm,abs(x1-x2)+abs(y1-y2))是正确的。
【解决方案2】:

几个注意事项:

List&lt;T&gt; 未针对删除第一个元素进行优化。最好以相反的顺序排序并取最后一个元素。或者使用Stack&lt;T&gt;Queue&lt;T&gt;

List.Remove(current) 效率极低。您已经知道要删除的索引,不要在整个列表中搜索该元素。

通过在正确位置插入新节点来保持openNodes 排序应该比不断地重新排列整个列表要快得多。跳过列表排序会通过删除重要的不变量来破坏整个算法。您需要加快排序速度,而不是跳过它。

您在closedNodes 上执行的主要操作是存在测试closedNodes.Contains()。使用为此优化的数据结构,例如Set&lt;T&gt;。或者更好的是,在每个节点中放置一个封闭的标志字段,并在每次传递开始时将它们全部清除。这将比在每次迭代中通过closedNodes 进行线性搜索要快得多。

您最初不应在 solutionNodes 中添加任何内容,mEndmStart 将在遍历路径的最终循环中添加。

neighborNodes 可以是IEnumerable&lt;T&gt; 而不是List&lt;T&gt;,因为您永远不需要一次整个列表。使用foreach 也会比按索引枚举列表稍快。

【讨论】:

    【解决方案3】:

    您可以将其与quickgraph library 中的 A* 实现进行比较(或仅使用):

    QuickGraph.Algorithms.ShortestPath.AStarShortestPathAlgorithm<TVertex,TEdge>
    

    【讨论】:

    • 我没有找到任何关于如何使用它的文档。 TVertex 应该是我的 PGNode,对吧? TEdge 呢?
    • @Vee:您应该在图表中转换您的地图(基本上每个图块(节点)都通过一条边连接到可访问的邻居,例如new Edge&lt;int&gt;(source,target)
    【解决方案4】:

    您可以这样计算遍历节点成本:

    cost = current.G + 10;
    

    但对于启发式方法,您有一个简单的距离。为什么即使在这里也不使用相同的距离?根据您的节点当前的距离,您的启发式方法可能太低了。

    另一个可能是错误的“细节”:current.GetNeighborNodes。这是如何实施的?它是返回相同位置的相同节点,以便共享不同路径上的相同节点,还是总是使用 new 分配一个新节点?

    【讨论】:

    • current.GetNeighborNodes 返回 4 个相邻的已存在节点。使用地图创建一个 80x80 的节点数组。
    • 好的。启发式呢?如果节点之间的距离为 1,成本为 10,则启发式应该是距离 * 10,而不是距离。由于低估了启发式算法,您的搜索将接近 Djisktra。
    【解决方案5】:

    内存消耗如何?下载红门工具。使用 Performance Profiler 查看花费最多的时间,并使用 Memory Profiler 确定您是否有任何内存泄漏问题或对象处理速度不够快。

    正如@Rodrigo 指出的那样,您可以处理一张大地图。嵌套循环永远不会被期望是高性能的。

    【讨论】:

      【解决方案6】:

      您是否使用网格来表示地形?如果是这样,那么在这种情况下最好的启发式方法是 Octile:

      启发式成本= (min(x 中的差异,y 中的差异) * 2 的平方根 + max(x 中的差异,y 中的差异) - min(x 中的差异,y 中的差异))

      对于网格,这将始终是最佳的。不幸的是,这种启发式方法并不为人所知。

      另一个有用的提示是为您的打开列表选择适合地图大小的数据结构。如果您的地图相对较小(100 x 100),那么未排序的向量将是最快的方法。要删除元素,只需对最后一个元素和要删除的元素进行迭代器交换,然后调用 pop_back。如果您有更大的地图,请使用堆。您只关心最便宜的节点,因此对其他所有内容进行排序将没有任何好处。具有复杂度 log n 的堆插入和排序,非常适合中型和大型数据集,但对于小型数据集来说速度较慢。

      最后,如果速度如此重要,请实施跳转点搜索。平均而言,它比寻路 A* 快 20 到 30 倍,而且没有内存开销(或者研究论文声称,没有找到任何关于这一点的证据)。它基本上将 A* 的“查找邻居”步骤替换为“查找继任者”,因此将其合并到您的代码中应该相对简单。

      希望对您有所帮助。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-07-11
        • 2019-08-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-05-24
        相关资源
        最近更新 更多