Wikipedia Ein * Pfadfindungsalgorithmus nimmt viel Zeit in Anspruch

9

Ich habe A * Pathfinding erfolgreich in C # implementiert, aber es ist sehr langsam und ich verstehe nicht warum. Ich habe sogar versucht, die openNodes-Liste nicht zu sortieren, aber sie ist immer noch dieselbe.

Die Karte ist 80x80 und es gibt 10-11 Knoten.

Ich habe den Pseudocode von hier Wikipedia genommen

Und das ist meine Implementierung:

 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;
    }

Hier ist die Heuristik, die ich verwende:

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

Was mache ich falsch? Es ist ein ganzer Tag, an dem ich immer wieder denselben Code betrachte.

Vee
quelle
2
Ohne Heuristik sollte es (normalerweise) länger dauern, wenn Sie mehr Knoten durchlaufen, bis Sie das Ende finden. Versuchen Sie auch, eine sortierte Liste zu verwenden, die sortiert bleibt (vorzugsweise eine sortierte Gruppe, damit Sie nicht überprüfen müssen, ob ein Element in der Liste vorhanden ist, sondern können es einfach hinzufügen)
Elva

Antworten:

10

Ich sehe drei Dinge, eines falsch, zwei verdächtig.

1) Sie sortieren nach jeder Iteration. Tu es nicht. Verwenden Sie entweder eine Prioritätswarteschlange oder führen Sie zumindest eine lineare Suche durch, um das Minimum zu finden. Sie müssen nicht immer die gesamte Liste sortieren!

2) openNodes.Contains () ist wahrscheinlich langsam (nicht sicher über die Besonderheiten der C # -Liste, aber ich wette, es wird eine lineare Suche durchgeführt). Sie können jedem Knoten ein Flag hinzufügen und dies in O (1) tun.

3) GetNeighborNodes () kann langsam sein.

ggambett
quelle
2
2) Ja, Contains () wird ziemlich langsam sein. Verwenden Sie ein Wörterbuch <int, PGNode>, anstatt alle Ihre Knoten in Listen zu speichern. Dann erhalten Sie O (1) Suchzeit und können trotzdem eine Liste iterieren. Wenn Knoten ein ID-Feld haben, verwenden Sie dieses für den Schlüssel, andernfalls funktioniert PGNode.GetHashCode ().
Kronzeugenregelung
2
@Leniency: Wäre Dictionary <PGNode, PGNode> nicht besser? Zwei Objekte haben möglicherweise denselben Hashcode, sind jedoch nicht gleich. "Folglich darf die Standardimplementierung dieser Methode nicht als eindeutige Objektkennung für Hashing-Zwecke verwendet werden." msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx - .NET 3.5 bietet HashSet, das besser ist - msdn.microsoft.com/en-us/library/bb359438.aspx .
Guter Punkt, HashSet vergessen.
Kronzeugenregelung
9

Zusätzlich zu dem bereits erwähnten Punkt, dass Sie einen Prioritätshaufen verwenden sollten, haben Sie die Heuristik falsch verstanden. Du hast

if (isCostBetter)
{
    ...
    neighbour.H = GetManhattanHeuristic (aktuell, Nachbar);
}}
Die Heuristik soll jedoch eine Schätzung für die Entfernung zum Ziel sein. Sie sollten es einmal einstellen, wenn Sie den Nachbarn zum ersten Mal hinzufügen:
if (openNodes.Contains (Nachbar) == false)
{
    neighbour.H = GetHeuristic (Nachbar, mEnd);
    ...
}}

Und als weiteren kleinen Punkt könnten Sie das A * vereinfachen, indem Sie unpassierbare Knoten herausfiltern GetNeighbourNodes().

Peter Taylor
quelle
+1, ich habe mich auf algorithmische Komplexität konzentriert und die falsche Verwendung der Heuristik völlig übersehen!
Ggambett
4

Die Meta-Antwort: Sie sollten niemals nur einen Tag damit verbringen, auf Code zu starren, um nach Leistungsproblemen zu suchen. Fünf Minuten mit einem Profiler zeigen Ihnen genau, wo die Engpässe liegen. Sie können einen kostenlosen Trail der meisten Profiler herunterladen und ihn in wenigen Minuten an Ihre App anschließen.

großartig
quelle
3

Es ist nicht klar, was Sie vergleichen, wenn Sie das F verschiedener Knoten vergleichen. Ist F eine Eigenschaft, die als G + H definiert ist? Es sollte sein. (Side-Rant: Dies ist ein Beispiel dafür, warum das Prinzip des einheitlichen Zugangs Mist ist.)

Wichtiger ist jedoch, dass Sie die Knoten in jedem Frame neu sortieren. A * erfordert die Verwendung einer Prioritätswarteschlange , die das effiziente Einfügen eines einzelnen Elements nach O (lg n) ermöglicht, und einer Menge, die eine schnelle Überprüfung auf geschlossene Knoten ermöglicht. Während Sie den Algorithmus geschrieben haben, haben Sie O (n lg n) Einfügung + Sortierung, was die Laufzeit auf nutzlose Proportionen erhöht.

(Sie erhalten möglicherweise O (n) Einfügung + Sortierung, wenn C # über einen guten Sortieralgorithmus verfügt. Es ist immer noch zu viel. Verwenden Sie eine Warteschlange mit echter Priorität.)


quelle
2

http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html

  • In einem Extremfall, wenn h (n) 0 ist, spielt nur g (n) eine Rolle, und A * wird zum Dijkstra-Algorithmus, der garantiert einen kürzesten Weg findet.
  • Wenn h (n) immer niedriger als (oder gleich) den Kosten für den Übergang von n zum Ziel ist, findet A * garantiert einen kürzesten Weg. Je niedriger h (n) ist, desto mehr expandiert der Knoten A *, wodurch er langsamer wird.
  • Wenn h (n) genau den Kosten für den Übergang von n zum Ziel entspricht, folgt A * nur dem besten Pfad und erweitert niemals etwas anderes, was es sehr schnell macht. Obwohl Sie dies nicht in allen Fällen erreichen können, können Sie es in einigen speziellen Fällen genau festlegen. Es ist schön zu wissen, dass sich A * bei perfekten Informationen perfekt verhält.
  • Wenn h (n) manchmal größer ist als die Kosten für den Übergang von n zum Ziel, kann A * nicht garantiert einen kürzesten Weg finden, kann aber schneller laufen.
  • Im anderen Extremfall spielt nur h (n) eine Rolle, wenn h (n) relativ zu g (n) sehr hoch ist, und A * wird zur Best-First-Search.

Sie verwenden "Manhatten-Abstand". Dies ist fast immer eine schlechte Heuristik. Wenn Sie sich diese Informationen auf der verlinkten Seite ansehen, können Sie außerdem erraten, dass Ihre Heuristik niedriger ist als die tatsächlichen Kosten.

Wille
quelle
-1, das Problem ist nicht die Heuristik, sondern die Implementierung.
2

Zusätzlich zu den anderen Top-Antworten (die zweifellos wichtiger sind als dieser Vorschlag) besteht eine weitere Optimierung darin, die geschlossene "Liste" in eine Art Hash-Tabelle zu ändern. Sie müssen keine geordnete Sammlung sein, nur um schnell Werte hinzufügen und schnell feststellen zu können, ob sie in der Sammlung vorhanden sind.

Kylotan
quelle
1

Ihre Kosten und Ihre Heuristik müssen eine Beziehung haben. Es sollte ein Hinweis darauf sein, dass H an zwei verschiedenen Stellen berechnet wird, aber nie abgerufen wird.

Jay Bell
quelle
Dies setzt voraus, dass die Eigenschaft falsch implementiert ist, was möglich ist, da ihre Definition nicht angezeigt wird, es jedoch zwei weitere unmittelbare Probleme mit dem Code gibt.