我已经在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);
}
我做错了什么?我整整一天都在看同样的代码。
答案 0 :(得分:16)
首先,使用分析器。用工具告诉你什么是慢的;什么是慢的往往令人惊讶。并且使用调试器。制作一个包含五个节点的地图,并在尝试查找路径时逐步执行代码的每一行。什么意外发生了?如果是这样,请弄清楚发生了什么。
其次,抛开你的性能问题,我认为你也有正确性问题。你能解释一下为什么你认为曼哈顿距离是一个合理的启发式算法吗?
(对于那些不熟悉度量标准的人来说,“曼哈顿距离”或“出租车距离”是指如果你住在一个网格上的城市,你必须旅行的两点之间的距离。是的,要到东北方向14英里,你必须向北行驶10英里,然后向东行驶10英里,行驶总共20英里。)
A *算法要求其正确性,启发式总是低估在两点之间行进所需的实际距离。如果图中有任何“对角快捷方式”街道,则曼哈顿距离高估这些路径上的距离,因此算法不一定会找到最短路径。
为什么不使用 Euclidean 距离作为启发式算法?
您是否尝试使用“始终为零”作为启发式算法?这保证是低估的。 (这样做可以实现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);
}
}
我们的想法是维护一个路径优先级队列;也就是说,路径队列始终能够以最小距离告诉您到目前为止的路径。然后我们检查一下我们是否已经到达目的地;如果是这样,我们就完成了。如果没有,那么我们根据他们(低于)到目标的估计距离排队一堆新路径。
第五,维基百科中的伪代码可以改进。事实上,我的实际代码在很多方面比伪代码更容易理解,这表明假代码中可能存在太多细节。
答案 1 :(得分:5)
一对夫妇注意到:
List<T>
未针对删除第一个元素进行优化。更好的是以相反的顺序排序并采取最后一个元素。或者使用Stack<T>
或Queue<T>
。
List.Remove(current)
效率极低。您已经知道要删除的索引,不要在整个列表中搜索该元素。
通过在正确的位置插入新节点来保持openNodes
排序应该比连续使用整个列表快得多。跳过列表排序会通过删除重要的不变量来破坏整个算法。你需要更快地排序,而不是跳过它。
您在closedNodes
上执行的主要操作是状态测试,closedNodes.Contains()
。使用针对此优化的数据结构,例如Set<T>
。或者更好的是,在每个节点中放置一个封闭的标志字段,并在每次传递开始时将它们全部清除。这比在每次迭代中通过closedNodes
进行线性搜索要快得多。
您最初不应在solutionNodes
中放置任何内容,mEnd
和mStart
都会在遍历路径的最终循环中添加。
neighborNodes
可以是IEnumerable<T>
而不是List<T>
,因为您从不需要一次完整列表。使用foreach
也会比按列表枚举列表稍快一些。
答案 2 :(得分:1)
您可以将其与quickgraph library中的A *实施进行比较(或仅使用):
QuickGraph.Algorithms.ShortestPath.AStarShortestPathAlgorithm<TVertex,TEdge>
答案 3 :(得分:0)
什么是内存消耗?下载Red Gate工具。使用Performance Profiler查看花费最多时间的位置和Memory Profiler,以确定是否存在内存泄漏或对象没有足够快地处理的任何问题。
正如@Rodrigo指出的那样,你可以有一张大地图来处理。预计嵌套循环不会有效。
答案 4 :(得分:0)
您计算遍历节点成本如下:
cost = current.G + 10;
然而对于启发式方法,你有一个简单的距离。为什么不在这里使用相同的距离?根据您的节点当前的距离,您的启发式可能会太低。
可能错误的另一个“细节”:current.GetNeighborNodes
。这是如何实现的?它是否返回相同位置的相同节点,以便共享不同路径上的相同节点,或者是否始终使用new分配新节点?
答案 5 :(得分:-1)
您是否使用网格进行地形表示?如果是这样,那么在这种情况下最好的启发式是Octile:
启发式成本=(min(x中的差异,y中的差异)* 2 + max的平方根(x中的差异,y中的差异) - min(x中的差异,y中的差异)
对于网格,这将始终是最佳的。不幸的是,这种启发式方法并不为人所知。
另一个有用的提示是为您的打开列表选择一个数据结构以适合您的地图大小。如果您的地图相对较小(100 x 100),那么未分类的矢量将是最快的方式。要删除元素,只需执行最后一个元素和要删除的元素的迭代器交换,然后调用pop_back。如果您有更大的地图,请使用堆。您只关心最便宜的节点,因此排序其他所有节点都没有任何好处。堆插入并按复杂度log n排序,适用于中型和大型数据集,但对于小型数据集则很慢。
最后,如果速度是一个很大的问题,请实施Jump Point Search。它平均比寻路A *快20到30倍,没有内存开销(或者研究论文声称没有找到任何关于那个的证据)。它基本上会用“查找后继者”替换A *的“查找邻居”步骤,因此将其合并到您的代码中应该相对简单。
希望有所帮助。