1 Projektbeschreibung

1.1 Aufgabenstellung

Es sollte ein Programm geschrieben werden, welches den günstigsten Weg zwischen einem Start- und einem Zielpunkt (um Hindernisse herumführend) mit dem A*-Algorithmus berechnet, und sowohl die Punkte als auch den Weg graphisch ausgibt.

1.1.1 Spezifizierung

1.1.2 Erbrachte Zusatzleistung

Optimale Wegsuche bei unterschiedlich gewichteten Pfaden, zum Beispiel in Computerspielen: Berücksichtigung des Blickwinkels von Spieler und Gegner, „Schleichen und Eilen“. 

2 Der A*-Algorithmus

2.1 Allgemeine Beschreibung

Der A*-Algorithmus ist ein informierter Suchalgorithmus. Er berechnet den kürzesten Pfad zwischen zwei Knoten eines Graphen.
Er findet immer die optimale Lösung, falls eine Lösung existiert.
Erstmals wurde er 1968 von Peter Hardt, Nils Nilsson und Bertram Raphael beschrieben.

2.2 Anwendungsgebiete

2.3 Funktionsweise

idee.png

2.3.1 Dijkstra-Algorithmus und Schätzfunktion h

onlycost  onlyheuristic

Abb. 1: Darstellung der untersuchten Zellen für einen Weg ohne Hindernis über Dijkstra-Algorithmus (links) und nur der Schätzfunktion h. (rot: Startpunkt, blau: Zielpunkt)

Der Dijkstra-Algorithmus (Abb. 1 links) nutzt keine Information über die Position des Zielpunktes bei der Suche. Er sucht gleichmäßig in alle Richtungen bis er das Ziel findet. Die dabei untersuchten Knoten sind hier gelblich unterlegt, der gefundene Pfad gelb markiert.

Mit weit weniger untersuchten Knoten kommt ein Algorithmus basierend auf einer Distanz-Schätzfunktion (Abb. 1 rechts) aus. Er muss aber über die Position des Zielpunktes informiert sein.

Allerdings liefert der Algorithmus auf Basis der Schätzfunktion im Normalfall keine optimale Lösung, Dijkstra hingegen schon, wie man am nachfolgenden Beispiel sehen kann:

Dijkstra with obstacleBFS with obstacle

Abb. 2: Darstellung der untersuchten Zellen für einen Weg mit Hindernis über Dijkstra-Algorithmus (links) und nur der Schätzfunktion h. Der Dijkstra-Algorithmus liefert den optimalen Weg.

Der A*-Algorithmus liefert wie der Dijkstra-Algorithmus eine optimale Lösung, muss dazu aber im Durchschnitt weniger Knoten untersuchen. Im besten Falle sind dies genau so viele Knoten, wie der Algorithmus nur über die Schätzfunktion untersucht. Dieser Fall tritt dann auf, wenn kein Hindernis auf der direkten Verbindungsstrecke zwischen Start- und Zielpunkt liegt. Im schlechtesten Fall sind dies ebenso viele, wie der Dijkstra-Algorithmus analysieren muss, zum Beispiel in einem Labyrinth.

2.3.2 Open List und Closed List (Der Algorithmus)

Die drei Knotenkategorien:

Vorgehensweise des Algorithmus

1. Schreibe alle Nachbarzellen der aktuellen Zelle mit ihrem Wert und einem Zeiger auf die aktuelle Zelle in die Open List. (Aktualisieren oder überspringen, falls nötig)

2. Sortiere die Open List. Zelle mit kleinstem Wert wird zu aktueller Zelle, in die Closed List eingetragen (mit Vorgängerzeiger) und aus der Open List gelöscht.

3. Wiederhole 1. und 2. bis Zielzelle als Nachbarzelle gefunden.

4. Erstelle Pfad = gebe relevanten Teil von der Closed List rückwärts aus (folge den Vorgängerzeigern)

3 Implementierung des Algorithmus


3.1 Funktionsumfang des Programms


3.2 GUI-Design: Die Qt 4-Bibliothek

gui.png

Abb. 3: Unser GUI auf Basis von Qt 4.3


3.3 Implementierung der Open List

Für die Open List wurde der Datentyp Vector aus der Standard Template Library (STL) verwendet. Diese Art von Array bietet den Vorteil, während der Laufzeit dynamisch erweiterbar zu sein. Innerhalb des Vektors bestehen die Elemente aus dem selbstdefinierten abstrakten Datentyp cell, in dem folgende Werte gespeichert werden:

Da in jedem Iterationsschritt fünf neue Zellen analysiert werden müssen und die jeweils nächstgünstigste Zelle ausgewählt werden muss, bietet sich eine Sortierung der Open List hinsichtlich der Gesamtkosten an.

Um die Sortierung möglichst effizient zu gestalten, wurde sie als eine Prioritätswarteschlange über einen binären Heap implementiert.

Binärer Heap

Ein binärer Heap ist eine Datenstruktur in Form eines Binärbaums, für den folgende Bedingungen gelten müssen:

binary_heap

Abb. 4 - Schematische Darstellung der Sortierung eines binären Heap nach dem Maximalwert-Kriterium (Max-Heap).

Im average case sortiert ein binärer Heap ein Array mit zufällig angeordneten Zahlen mit einer Komplexität von O(n*log(n)).

Ein binärer Heap und der Heapsort-Algorithmus ist bereits in der STL integriert und kann direkt auf einen STL-Vector angewandt werden. Die einzig notwendige Anpassung ist die Definition eines Sortierkriteriums, wobei in unserem Fall absteigend nach den Gesamtkosten f sortiert wurde. Da die Gesamtkosten während der Laufzeit monoton fallen und bei einem dynamischen Array das Einfügen weiterer Elemente an der Anfangsposition zu rechenzeitaufwändig ist (da alle folgenden Elemente verschoben werden müssen), wurde die Open List in umgekehrter Reihenfolge gespeichert. Hier steht also das kostengünstigste Element an letzter Stelle. Dies hat auch den Vorteil, dass während des Identifizierens der neuen Nachbarn nur unter den letzten sechs Elementen nach bereits besuchten Knoten gesucht werden muss.

3.4 Implementierung der Closed List

Die Closed List wurde ebenfalls als STL-Vector implementiert. Sie wird erst nach der vollständigen Erstellung der Open List mit Werten gefüllt. Da innerhalb des Datentyps cell jeweils ein Zeiger auf die Vorgängerzelle gespeichert wird, welche, ausgehend vom Zielpunkt, durch die Heap-Sortierung auch die kostengünstigste Zelle ist, kann man so sehr leicht den günstigsten Weg vom Zielpunkt zum Startpunkt zurückverfolgen. Wenn man sich die Open List als Binärbaum vorstellt, entspricht das dem dem Verfolgen des insgesamt kostengünstigsten Asts vom Blatt (Ziel) bis zur Wurzel hin (Start).


3.5 Die heuristische Funktion h

Die heuristische Funktion schätzt die Kosten des Weges von einer Zelle zum Zielknoten. An sie wird die Anforderung gestellt, dass diese niemals die tatsächlichen Kosten überschätzen darf. Unter dieser Bedingung würde schon der euklidische Abstand als Schätzfunktion ausreichen (bei minimalen Knotengewichten >=1). Da unsere Anwendung allerdings auf einem regulären Gitter arbeitet, konnten wir die heuristische Funktion strenger formulieren:

heuristic function

Das Kantengewicht zwischen zwei direkt benachbarten Felden wird mit 1 abgeschätzt, das Gewicht zwischen diagonal benachbarten Feldern, entsprechend des euklidischen Abstands zweier Einheitszellen, mit sqrt(2). 

only_heuristic.png

Abb. 5: Darstellung eines Wegs vom Start zum Ziel nur unter Berücksichtigung der heuristischen Funktion. Da nur der Abstand zwischen den untersuchten Knoten und dem Zielknoten in die Kostenfunktion eingeht und die Hindernisse hier nicht berücksichtigt werden, wird nicht der optimale Weg gefunden.

3.6 Die Kostenfunktion g

    ngvonx

Die Kostenfunktion g speichert, ausgehend vom Startpunkt, die Kosten des bisher gelaufenen Wegs. Dabei werden einfach die Kosten von der Vorgängerzelle mit den aktuellen Kosten c(x) addiert. c(x) ist abhängig von der Gewichtung der Zellen, die durch eventuell vorhandene Hindernisse definiert wird: Für Felder ohne Hindernisse wurde sie auf den Wert 1 gesetzt, für Felder mit begehbaren Hindernissen entsprechend der Hindernisstärke auf 1.8, 3.0, 4.0, 5.0 oder 6.0. Undurchdringbare Hindernisse wurden mit den Kosten 9999 bewertet. Die Stärke der Hindernisse ließe sich auch beliebig anders wählen, diese Werte haben sich aber in unseren Testläufen als praktikabel erwiesen.

In Diagonalrichtung werden die Zellen zusätzlich noch mit dem Faktor sqrt(2) gewichtet, der dem euklidischen Abstand diagonal benachbarter Einheitszellen entspricht.

a_star2.pnga_star.png

Abb. 6: Darstellung der analysierten Zellen und des optimalen Wegs für den Dijkstra-Algorithmus (links) und den A-Stern-Algorithmus (rechts).
 

4 Literaturverzeichnis


Last modified: 2008-10-07 by Christoph Hoppe