Wikipedia Algorytm poszukiwania ścieżki * zajmuje dużo czasu

9

Z powodzeniem zaimplementowałem wyszukiwanie ścieżek A * w języku C #, ale jest to bardzo wolne i nie rozumiem dlaczego. Próbowałem nawet nie sortować listy openNodes, ale nadal jest taka sama.

Mapa ma wymiary 80 x 80 i jest 10–11 węzłów.

Wziąłem pseudokod z tutaj Wikipedii

A to moja realizacja:

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

Oto heurystyka, której używam:

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

Co ja robię źle? Cały dzień patrzę na ten sam kod.

Vee
źródło
2
Bez heurystyki powinno to (zwykle) trwać dłużej, gdy przechodzisz przez więcej węzłów, aż znajdziesz koniec. Spróbuj także użyć posortowanej listy, która pozostaje posortowana (najlepiej posortowanego zestawu, w ten sposób nie musisz sprawdzać, czy element istnieje na liście, możesz go po prostu dodać)
Elva

Odpowiedzi:

10

Widzę trzy rzeczy, jedną błędną, dwie podejrzaną.

1) Sortujesz według każdej iteracji. Nie rób Użyj kolejki priorytetowej lub przynajmniej przeprowadź wyszukiwanie liniowe, aby znaleźć minimum. W rzeczywistości przez cały czas nie potrzebujesz sortować całej listy!

2) openNodes.Contains () jest prawdopodobnie wolny (nie jestem pewien, co do specyfiki listy C #, ale założę się, że wykonuje wyszukiwanie liniowe). Możesz dodać flagę do każdego węzła i zrobić to w O (1).

3) GetNeighborNodes () może być powolny.

ggambett
źródło
2
2) Tak, Contains () będzie dość wolny. Zamiast przechowywać wszystkie węzły na listach, użyj Dictionary <int, PGNode>. Następnie otrzymujesz czas wyszukiwania O (1) i nadal możesz iterować listę. Jeśli węzły mają pole identyfikatora, użyj tego dla klucza, w przeciwnym razie zadziała PGNode.GetHashCode ().
Leniency
2
@Leniency: Czy słownik <PGNode, PGNode> nie byłby lepszy? Dwa obiekty mogą mieć ten sam kod skrótu, ale nie mogą być równe. „W związku z tym domyślnej implementacji tej metody nie można używać jako unikalnego identyfikatora obiektu do celów mieszania.” msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx - .NET 3.5 zapewnia HashSet, co jest lepsze - msdn.microsoft.com/en-us/library/bb359438.aspx .
Dobra uwaga, zapomniałem o HashSet.
Leniency
9

Oprócz tego, że już powiedziałeś, że powinieneś używać sterty priorytetowej, źle zrozumiałeś heurystykę. Ty masz

if (isCostBetter)
{
    ...
    neighbour.H = GetManhattanHeuristic (bieżący, sąsiad);
}
Ale heurystyka ma być szacunkiem odległości do miejsca docelowego. Powinieneś ustawić to raz, kiedy dodajesz sąsiada po raz pierwszy:
if (openNodes.Contains (neighbour) == false)
{
    neighbour.H = GetHeuristic (neighbour, meEnd);
    ...
}

Kolejnym drobnym punktem jest uproszczenie A * poprzez odfiltrowanie nieprzekraczalnych węzłów GetNeighbourNodes().

Peter Taylor
źródło
+1, skupiłem się na złożoności algorytmu i całkowicie przegapiłem niewłaściwe użycie heurystyki!
ggambett
4

Meta-odpowiedź: nigdy nie powinieneś spędzać dnia wpatrując się w kod w poszukiwaniu problemów z wydajnością. Pięć minut z profilerem pokaże dokładnie, gdzie są wąskie gardła. Możesz pobrać bezpłatną ścieżkę większości profilerów i podłączyć ją do swojej aplikacji w ciągu kilku minut.

hojny
źródło
3

Nie jest jasne, co porównujesz, porównując F różnych węzłów. Czy F jest właściwością zdefiniowaną jako G + H? Powinno być. (Side-rant: Jest to przykład tego, dlaczego zasada jednolitego dostępu to bzdura.)

Co ważniejsze, ponownie sortujesz węzły w każdej ramce. A * wymaga użycia kolejki priorytetowej , która umożliwia wydajne - O (lg n) - posortowane wstawianie pojedynczego elementu oraz zestawu, który umożliwia szybkie sprawdzanie zamkniętych węzłów. Gdy napisałeś algorytm, masz O (n lg n) wstawianie + sortowanie, co podnosi czas działania do bezużytecznych proporcji.

(Możesz uzyskać wstawienie O (n) + sortowanie, jeśli C # ma dobry algorytm sortowania. To wciąż za dużo. Użyj prawdziwej kolejki priorytetowej.)


źródło
2

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

  • W jednym ekstremum, jeśli h (n) wynosi 0, to tylko g (n) odgrywa rolę, a A * zamienia się w algorytm Dijkstry, który gwarantuje znalezienie najkrótszej ścieżki.
  • Jeśli h (n) jest zawsze niższe niż (lub równe) koszt przejścia od n do celu, A * gwarantuje znalezienie najkrótszej ścieżki. Im niższa h (n), tym bardziej węzeł A * rozszerza się, przez co jest wolniejszy.
  • Jeśli h (n) jest dokładnie równe kosztowi przejścia od n do celu, A * podąży tylko najlepszą ścieżką i nigdy nie rozszerzy niczego innego, co czyni ją bardzo szybką. Chociaż nie możesz tego zrobić we wszystkich przypadkach, możesz to zrobić dokładnie w niektórych szczególnych przypadkach. Miło wiedzieć, że przy doskonałej informacji A * będzie się zachowywać idealnie.
  • Jeśli h (n) jest czasami wyższy niż koszt przejścia od n do celu, A * nie ma gwarancji znalezienia najkrótszej ścieżki, ale może biegać szybciej.
  • Z drugiej strony, jeśli h (n) jest bardzo wysokie w stosunku do g (n), wtedy tylko h (n) odgrywa rolę, a A * zamienia się w Najlepsze wyszukiwanie.

Używasz „dystansu manhattena”. Jest to prawie zawsze zła heurystyka. Ponadto, patrząc na te informacje z połączonej strony, możesz zgadywać, że heurystyka jest niższa niż rzeczywisty koszt.

Będzie
źródło
-1, problemem nie jest heurystyka, ale implementacja.
2

Oprócz innych najważniejszych odpowiedzi (które są niewątpliwie ważniejsze niż ta sugestia), kolejną optymalizacją jest zmiana zamkniętej „listy” w pewnego rodzaju tablicę skrótów. Nie musisz być kolekcją uporządkowaną, aby móc szybko dodawać wartości i szybko sprawdzać, czy istnieją w kolekcji.

Kylotan
źródło
1

Twój koszt i heurystyka muszą mieć związek. Powinno być wskazówką, że H jest obliczane w dwóch różnych punktach, ale nigdy nie jest dostępne.

Jay Bell
źródło
Zakłada się, że właściwość jest niepoprawnie zaimplementowana, co jest możliwe, ponieważ jej definicja nie jest wyświetlana, ale istnieją jeszcze dwa bezpośrednie problemy z kodem.