Wikipedia Un algoritmo di * pathfinding richiede molto tempo


9

Ho implementato con successo A * pathfinding in C # ma è molto lento e non capisco perché. Ho anche provato a non ordinare l'elenco openNodes ma è sempre lo stesso.

La mappa è 80x80 e ci sono 10-11 nodi.

Ho preso lo pseudocodice da qui Wikipedia

E questa è la mia implementazione:

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

Ecco l'euristica che sto usando:

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

Che cosa sto facendo di sbagliato? È un giorno intero che continuo a guardare lo stesso codice.


2
Senza euristica dovrebbe (di solito) impiegare più tempo mentre attraversi più nodi fino a trovare la fine. Inoltre, prova a utilizzare un elenco ordinato che rimane ordinato (preferibilmente un set ordinato, in questo modo non devi controllare se esiste un elemento nell'elenco puoi semplicemente aggiungerlo)
Elva

Risposte:


10

Vedo tre cose, una sbagliata, due sospette.

1) Stai ordinando ogni iterazione. Non farlo. Utilizzare una coda di priorità o almeno eseguire una ricerca lineare per trovare il minimo. In realtà non hai bisogno che l'intero elenco sia ordinato in ogni momento!

2) openNodes.Contains () è probabilmente lento (non sono sicuro dei dettagli dell'elenco di C #, ma scommetto che fa una ricerca lineare). È possibile aggiungere un flag a ciascun nodo e farlo in O (1).

3) GetNeighborNodes () potrebbe essere lento.


2
2) Sì, Contains () sarà piuttosto lento. Anziché memorizzare tutti i nodi negli elenchi, utilizzare un dizionario <int, PGNode>. Quindi ottieni il tempo di ricerca O (1) e puoi ancora iterare l'elenco di un elenco. Se i nodi hanno un campo ID, usalo per la chiave, altrimenti PGNode.GetHashCode () funzionerà.
Trattamento favorevole

2
@Leniency: il dizionario <PGNode, PGNode> non sarebbe migliore? Due oggetti possono avere lo stesso codice hash ma non essere uguali. "Di conseguenza, l'implementazione predefinita di questo metodo non deve essere utilizzata come identificatore univoco di oggetti per scopi di hashing." msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx - .NET 3.5 fornisce HashSet, che è meglio - msdn.microsoft.com/en-us/library/bb359438.aspx .

Buon punto, ho dimenticato HashSet.
Trattamento favorevole

9

Oltre al punto già sottolineato che dovresti usare un heap prioritario, hai frainteso l'euristico. Hai

if (isCostBetter)
{
    ...
    neighbour.H = GetManhattanHeuristic (attuale, vicino);
}
Ma l'euristica dovrebbe essere una stima della distanza dalla destinazione. Dovresti impostarlo una volta, quando aggiungi per la prima volta il vicino:
if (openNodes.Contains (neighbour) == false)
{
    neighbour.H = GetHeuristic (neighbour, mEnd);
    ...
}

E come ulteriore punto secondario, è possibile semplificare A * filtrando i nodi impraticabili in GetNeighbourNodes().


+1, mi sono concentrato sulla complessità algoritmica e ho perso completamente l'uso sbagliato dell'euristico!
ggambett,

4

La meta-risposta: non dovresti mai passare una giornata a guardare il codice alla ricerca di problemi di prestazioni. Cinque minuti con un profiler ti mostrerebbero esattamente dove si trovano i colli di bottiglia. Puoi scaricare una traccia gratuita della maggior parte dei profiler e collegarla alla tua app in pochi minuti.


3

Non è chiaro cosa si sta confrontando quando si confronta la F di nodi diversi. F è una proprietà definita come G + H? Dovrebbe essere. (Side-rant: questo è un esempio del perché il principio di accesso uniforme è una schifezza.)

Ancora più importante, riordini i nodi in ogni frame. A * richiede l'uso di una coda prioritaria , che consente un efficiente - O (lg n) - inserimento ordinato di un singolo elemento e un set, che consente controlli rapidi per nodi chiusi. Come hai scritto l'algoritmo, hai O (n lg n) insertion + sort, che aumenta il tempo di esecuzione a proporzioni inutili.

(Potresti ottenere O (n) inserimento + ordinamento se C # ha un buon algoritmo di ordinamento. È ancora troppo. Usa una vera coda di priorità.)


2

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

  • Ad un estremo, se h (n) è 0, solo g (n) gioca un ruolo e A * si trasforma nell'algoritmo di Dijkstra, che è garantito per trovare un percorso più breve.
  • Se h (n) è sempre inferiore (o uguale a) al costo del passaggio da n all'obiettivo, allora A * è garantito per trovare il percorso più breve. Più la h (n) è bassa, più il nodo A * si espande, rendendolo più lento.
  • Se h (n) è esattamente uguale al costo del passaggio da n all'obiettivo, allora A * seguirà solo il percorso migliore e non espanderà mai nient'altro, rendendolo molto veloce. Sebbene non sia possibile farlo accadere in tutti i casi, è possibile renderlo esatto in alcuni casi speciali. È bello sapere che, date le informazioni perfette, A * si comporterà perfettamente.
  • Se h (n) è talvolta maggiore del costo del passaggio da n all'obiettivo, allora A * non è garantito per trovare un percorso più breve, ma può correre più veloce.
  • All'altro estremo, se h (n) è molto elevato rispetto a g (n), allora solo h (n) svolge un ruolo e A * si trasforma in Best-First-Search.

Stai usando 'distanza manhatten'. Questa è quasi sempre una cattiva euristica. Inoltre, guardando quelle informazioni dalla pagina collegata, puoi immaginare che la tua euristica sia inferiore al costo reale.


-1, il problema non è l'euristico, ma l'implementazione.

2

Oltre alle altre risposte principali (che sono senza dubbio più significative di questo suggerimento), un'altra ottimizzazione è quella di cambiare la "lista" chiusa in una sorta di tabella hash. Non è necessario che sia una raccolta ordinata, solo per poter aggiungere rapidamente valori e vedere rapidamente se esistono nella raccolta.


1

Il tuo costo e la tua euristica devono avere una relazione. Dovrebbe essere un indizio che H viene calcolato in due punti diversi ma a cui non si accede mai.


Ciò presuppone che la proprietà sia implementata in modo errato, il che è possibile poiché la sua definizione non è mostrata, ma ci sono altri due problemi immediati con il codice.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.