# A*-Algorithmus

Der A*-Algorithmus („A Stern“ oder englisch „a star“, auch A*-Suche) gehört zur Klasse der informierten Suchalgorithmen. Er dient in der Informatik der Berechnung eines kürzesten Pfades zwischen zwei Knoten in einem Graphen mit positiven Kantengewichten. Er wurde das erste Mal 1968 von Peter Hart, Nils J. Nilsson und Bertram Raphael beschrieben. Der Algorithmus gilt als Verallgemeinerung und Erweiterung des Dijkstra-Algorithmus, in vielen Fällen kann aber umgekehrt A* auch auf Dijkstra reduziert werden.
Im Gegensatz zu uninformierten Suchalgorithmen verwendet der A*-Algorithmus eine Schätzfunktion (Heuristik), um zielgerichtet zu suchen und damit die Laufzeit zu verringern. Der Algorithmus ist vollständig und optimal. Das heißt, dass immer eine optimale Lösung gefunden wird, falls eine existiert.


## Idee des Algorithmus
Der A*-Algorithmus untersucht immer die Knoten zuerst, die wahrscheinlich schnell zum Ziel führen. Um den vielversprechendsten Knoten zu ermitteln, wird allen bekannten Knoten 
  
    
      
        x
      
    
    {\displaystyle x}
  
 jeweils durch eine Metrik ein Wert 
  
    
      
        f
        (
        x
        )
      
    
    {\displaystyle f(x)}
  
 zugeordnet, der eine Abschätzung angibt, wie lang der Pfad vom Start zum Ziel unter Verwendung des betrachteten Knotens im günstigsten Fall ist. Der Knoten mit dem niedrigsten 
  
    
      
        f
      
    
    {\displaystyle f}
  
-Wert wird als nächster untersucht.

  
    
      
        f
        (
        x
        )
        =
        g
        (
        x
        )
        +
        h
        (
        x
        )
      
    
    {\displaystyle f(x)=g(x)+h(x)}
  

Für einen Knoten 
  
    
      
        x
      
    
    {\displaystyle x}
  
 bezeichnet 
  
    
      
        g
        (
        x
        )
      
    
    {\displaystyle g(x)}
  
 die bisherigen Kosten vom Startknoten aus, um 
  
    
      
        x
      
    
    {\displaystyle x}
  
 zu erreichen. 
  
    
      
        h
        (
        x
        )
      
    
    {\displaystyle h(x)}
  
 bezeichnet die geschätzten Kosten von 
  
    
      
        x
      
    
    {\displaystyle x}
  
 bis zum Zielknoten. Die verwendete Heuristik darf die Kosten nie überschätzen. Für das Beispiel der Wegsuche ist die Luftlinie eine geeignete Schätzung: Die tatsächliche Strecke ist nie kürzer als die direkte Verbindung.


## Funktionsweise
Die Knoten werden während der Suche in drei verschiedene Klassen eingeteilt:

unbekannte Knoten: Diese Knoten wurden während der Suche noch nicht gefunden. Zu ihnen ist noch kein Weg bekannt. Jeder Knoten (außer dem Startknoten) ist zu Beginn des Algorithmus unbekannt.
bekannte Knoten: Zu diesen Knoten ist ein (möglicherweise suboptimaler) Weg bekannt. Alle bekannten Knoten werden zusammen mit ihrem 
  
    
      
        f
      
    
    {\displaystyle f}
  
-Wert in der sogenannten Open List gespeichert. Aus dieser Liste wird immer der vielversprechendste Knoten ausgewählt und untersucht. Die Implementierung der Open List hat großen Einfluss auf die Laufzeit und wird oft als einfache Prioritätswarteschlange (z. B. binärer Heap) realisiert. Zu Beginn ist nur der Startknoten bekannt.
abschließend untersuchte Knoten: Zu diesen Knoten ist der kürzeste Weg bekannt. Die abschließend untersuchten Knoten werden in der sogenannten Closed List gespeichert, damit sie nicht mehrfach untersucht werden. Um effizient entscheiden zu können, ob sich ein Element auf der Closed List befindet, wird diese oft als Menge implementiert. Die Closed List ist zu Beginn leer.
Jeder bekannte oder abschließend besuchte Knoten enthält einen Zeiger auf seinen (bisher besten) Vorgängerknoten. Mit Hilfe dieser Zeiger kann der Pfad bis zum Startknoten rückverfolgt werden.
Wird ein Knoten 
  
    
      
        x
      
    
    {\displaystyle x}
  
 abschließend untersucht (auch expandiert oder relaxiert), so werden seine Nachfolgeknoten in die Open List eingefügt und 
  
    
      
        x
      
    
    {\displaystyle x}
  
 in die Closed List aufgenommen. Für neu eingefügte Nachfolgeknoten werden die Vorgängerzeiger auf 
  
    
      
        x
      
    
    {\displaystyle x}
  
 gesetzt. Ist ein Nachfolgeknoten bereits auf der Closed List, so wird er nicht erneut in die Open List eingefügt und auch sein Vorgängerzeiger nicht geändert. Ist ein Nachfolgeknoten bereits auf der Open List, so wird der Knoten nur aktualisiert (
  
    
      
        f
      
    
    {\displaystyle f}
  
-Wert und Vorgängerzeiger), wenn der neue Weg dorthin kürzer ist als der bisherige.
Falls der Zielknoten abschließend untersucht wird, terminiert der Algorithmus. Der gefundene Weg wird mit Hilfe der Vorgängerzeiger rekonstruiert und ausgegeben. Falls die Open List leer ist, gibt es keine Knoten mehr, die untersucht werden könnten. In diesem Fall terminiert der Algorithmus, da es keine Lösung gibt.
Bedingt durch die Vorgängerzeiger wird der gefundene Weg vom Ziel ausgehend rückwärts bis zum Start ausgegeben. Um den Weg in der richtigen Reihenfolge zu erhalten, kann z. B. vor der Wegsuche Start und Ziel vertauscht werden. Somit wird vom eigentlichen Ziel zum Start gesucht und die Wegausgabe beginnt beim ursprünglichen Startknoten.

Anmerkung: Die Closed List kann auch indirekt implementiert werden. Dazu speichern auch die abschließend untersuchten Knoten ihren 
  
    
      
        f
      
    
    {\displaystyle f}
  
-Wert. Wird nun ein abschließend untersuchter Knoten erneut gefunden, ist sein alter 
  
    
      
        f
      
    
    {\displaystyle f}
  
-Wert geringer als der neue, da der kürzeste Weg zu diesem Knoten bereits gefunden wurde. Der Knoten wird also nicht erneut in die Open List eingefügt.
Wird keine Closed List benutzt, muss anderweitig sichergestellt werden, dass Knoten nicht mehrfach untersucht werden. Ansonsten wird die worst-case Laufzeit schlechter als quadratisch. Außerdem terminiert der Algorithmus nicht mehr, wenn es keine Lösung gibt. Die Knoten werden dann unendlich oft untersucht, da die Open List nie leer wird.
declare openlist as PriorityQueue with Nodes // Prioritätenwarteschlange
declare closedlist as Set with Nodes

program a-star
    // Initialisierung der Open List, die Closed List ist noch leer
    // (die Priorität bzw. der f-Wert des Startknotens ist unerheblich)
    openlist.enqueue(startknoten, 0)
    // diese Schleife wird durchlaufen bis entweder
    // - die optimale Lösung gefunden wurde oder
    // - feststeht, dass keine Lösung existiert
    repeat
        // Knoten mit dem geringsten f-Wert aus der Open List entfernen
        currentNode := openlist.removeMin()
        // Wurde das Ziel gefunden?
        if currentNode == zielknoten then
            return PathFound
        // Der aktuelle Knoten soll durch nachfolgende Funktionen
        // nicht weiter untersucht werden, damit keine Zyklen entstehen
        closedlist.add(currentNode)
        // Wenn das Ziel noch nicht gefunden wurde: Nachfolgeknoten
        // des aktuellen Knotens auf die Open List setzen
        expandNode(currentNode)
    until openlist.isEmpty()
    // die Open List ist leer, es existiert kein Pfad zum Ziel
    return NoPathFound
end

// überprüft alle Nachfolgeknoten und fügt sie der Open List hinzu, wenn entweder
// - der Nachfolgeknoten zum ersten Mal gefunden wird, oder
// - ein besserer Weg zu diesem Knoten gefunden wird
function expandNode(currentNode)
    foreach successor of currentNode
        // wenn der Nachfolgeknoten bereits auf der Closed List ist – tue nichts
        if closedlist.contains(successor) then
            continue
        // g-Wert für den neuen Weg berechnen: g-Wert des Vorgängers plus
        // die Kosten der gerade benutzten Kante
        tentative_g = g(currentNode) + c(currentNode, successor)
        // wenn der Nachfolgeknoten bereits auf der Open List ist,
        // aber der neue Weg nicht besser ist als der alte – tue nichts
        if openlist.contains(successor) and tentative_g >= g(successor) then
            continue
        // Vorgängerzeiger setzen und g Wert merken oder anpassen
        successor.predecessor := currentNode
        g(successor) = tentative_g
        // f-Wert des Knotens in der Open List aktualisieren
        // bzw. Knoten mit f-Wert in die Open List einfügen
        f := tentative_g + h(successor)
        if openlist.contains(successor) then
            openlist.decreaseKey(successor, f)
        else
            openlist.enqueue(successor, f)
    end
end


## Anwendungsgebiete
Der A*-Algorithmus findet allgemein einen optimalen Pfad zwischen zwei Knoten in einem Graphen. Optimal kann dabei am kürzesten, am schnellsten oder auch am einfachsten bedeuten, je nach Art der Gewichtungsfunktion, die den Kanten ihre Länge zuordnet. Theoretisch kann der Algorithmus alle Probleme lösen, die durch einen Graphen dargestellt werden können und bei denen eine Schätzung über die Restkosten bis zum Ziel gemacht werden kann. Zu den Beschränkungen von A* siehe den Abschnitt Nachteile.
A* wird oft zur Wegsuche bei Routenplanern oder in Computerspielen benutzt. Als Heuristik wird dabei meist der Luftlinienabstand zum Ziel verwendet, da er auf Grund der Dreiecksungleichung stets optimistisch schätzt. Weitere Anwendungsgebiete sind Spiele wie das 15-Puzzle oder das Damenproblem, bei dem ein vorgegebener Zielzustand erreicht werden soll. Als Heuristik kann dabei die Anzahl der falsch platzierten Steine verwendet werden.


## Beispiel
## Heuristiken
Es gibt zwei Arten von Heuristiken für den A*-Algorithmus: Zulässige und monotone Heuristiken. Jede monotone Heuristik ist auch zulässig. Monotonie ist also eine stärkere Eigenschaft als die der Zulässigkeit. Im Allgemeinen werden monotone Heuristiken verwendet. Die Heuristik zur Abschätzung der Entfernung zweier Städte – die Luftlinie – ist zum Beispiel monoton.


### Zulässige Heuristik
Eine Heuristik ist zulässig, wenn die Kosten nie überschätzt werden. Das heißt, die geschätzten Kosten müssen stets im Intervall 
  
    
      
        [
        0
        ;
        k
        ]
      
    
    {\displaystyle [0;k]}
  
 liegen, wenn 
  
    
      
        k
      
    
    {\displaystyle k}
  
 die tatsächlichen Kosten bezeichnet. Ist die verwendete Heuristik nur zulässig, aber nicht monoton, dann ist zu einem expandierten Knoten nicht notwendigerweise der kürzeste Weg bekannt. Daher muss es möglich sein, einen Knoten mehrfach zu expandieren. Es darf also keine Closed List benutzt werden.
Im Diagramm rechts ist ein Beispielgraph und eine zulässige (aber nicht monotone) Heuristik dargestellt. Der beste Weg vom Start zum Ziel hat Kosten 40 und führt über die Knoten K1 und K2. Der Weg über den Umwegknoten U hat Kosten 45. Beim Startknoten schätzt die Heuristik Kosten von 40 und beim Knoten K1 Kosten von 30, was jeweils den tatsächlichen Kosten entspricht. Bei den Knoten K2, U und Ziel werden Kosten von 0 geschätzt. Da eine Heuristik nicht überschätzen darf, werden für einen Zielknoten immer Kosten von 0 geschätzt.
Beispiel mit Closed List

Im ersten Schritt wird der Startknoten expandiert. Die beiden Nachfolgeknoten K1 und U werden gefunden und mit den Werten 
  
    
      
        
          f
          
            K
            1
          
        
        =
        10
        +
        30
        =
        40
      
    
    {\displaystyle f_{K1}=10+30=40}
  
 und 
  
    
      
        
          f
          
            U
          
        
        =
        25
        +
        0
        =
        25
      
    
    {\displaystyle f_{U}=25+0=25}
  
 in die Open List eingetragen.
Im zweiten Schritt wird der Knoten U expandiert, da sein 
  
    
      
        f
      
    
    {\displaystyle f}
  
-Wert am niedrigsten ist. Der Nachfolgeknoten K2 wird gefunden und mit dem Wert 
  
    
      
        
          f
          
            K
            2
          
        
        =
        35
        +
        0
        =
        35
      
    
    {\displaystyle f_{K2}=35+0=35}
  
 in die Open List eingetragen.
Im dritten Schritt besteht die Open List aus den Knoten K2 mit 
  
    
      
        
          f
          
            K
            2
          
        
        =
        35
      
    
    {\displaystyle f_{K2}=35}
  
 und dem Knoten K1 mit 
  
    
      
        
          f
          
            K
            1
          
        
        =
        40
      
    
    {\displaystyle f_{K1}=40}
  
. Also wird der Knoten K2 expandiert. Die beiden Nachfolgeknoten K1 und Ziel werden gefunden. Der neue Wert 
  
    
      
        
          f
          
            K
            1
          
          ′
        
        =
        55
        +
        30
        =
        85
      
    
    {\displaystyle f_{K1}'=55+30=85}
  
 ist größer als der alte Wert. Daher wird der 
  
    
      
        f
      
    
    {\displaystyle f}
  
-Wert für den Knoten K1 in der Open List nicht aktualisiert. Der Zielknoten wird mit 
  
    
      
        
          f
          
            Z
            i
            e
            l
          
        
        =
        45
        +
        0
        =
        45
      
    
    {\displaystyle f_{Ziel}=45+0=45}
  
 in die Open List eingefügt.
Im vierten Schritt befinden sich die Knoten K1 mit 
  
    
      
        
          f
          
            K
            1
          
        
        =
        40
      
    
    {\displaystyle f_{K1}=40}
  
 und Ziel mit 
  
    
      
        
          f
          
            Z
            i
            e
            l
          
        
        =
        45
      
    
    {\displaystyle f_{Ziel}=45}
  
 in der Open List. Die Expansion von K1 erzeugt keine Nachfolgeknoten, da sich sowohl Start als auch K2 bereits auf der Closed List befinden. Die Verwendung der Closed List verhindert hier, dass die optimale Lösung gefunden wird.
Im fünften Schritt wird der einzige Knoten auf der Open List expandiert: der Zielknoten. Die gefundene Lösung ist also Start–U–K2–Ziel und mit Kosten 45 nicht optimal.
Beispiel ohne Closed List

Im ersten Schritt wird der Startknoten expandiert. Die beiden Nachfolgeknoten K1 und U werden gefunden und mit den Werten 
  
    
      
        
          f
          
            K
            1
          
        
        =
        10
        +
        30
        =
        40
      
    
    {\displaystyle f_{K1}=10+30=40}
  
 und 
  
    
      
        
          f
          
            U
          
        
        =
        25
        +
        0
        =
        25
      
    
    {\displaystyle f_{U}=25+0=25}
  
 in die Open List eingetragen.
Im zweiten Schritt wird der Knoten U mit 
  
    
      
        
          f
          
            U
          
        
        =
        25
      
    
    {\displaystyle f_{U}=25}
  
 expandiert. Die Nachfolgeknoten K2 und Start werden gefunden und mit den Werten 
  
    
      
        
          f
          
            K
            2
          
        
        =
        35
        +
        0
        =
        35
      
    
    {\displaystyle f_{K2}=35+0=35}
  
 und 
  
    
      
        
          f
          
            S
            t
            a
            r
            t
          
        
        =
        50
        +
        40
        =
        90
      
    
    {\displaystyle f_{Start}=50+40=90}
  
 in die Open List eingetragen.
Im dritten Schritt wird der Knoten K2 expandiert. Die Nachfolgeknoten K1 (
  
    
      
        
          f
          
            K
            1
          
        
        =
        55
        +
        30
        =
        85
      
    
    {\displaystyle f_{K1}=55+30=85}
  
), U (
  
    
      
        
          f
          
            U
          
        
        =
        45
        +
        0
        =
        45
      
    
    {\displaystyle f_{U}=45+0=45}
  
) und Ziel (
  
    
      
        
          f
          
            Z
            i
            e
            l
          
        
        =
        45
        +
        0
        =
        45
      
    
    {\displaystyle f_{Ziel}=45+0=45}
  
) werden gefunden.
Im vierten Schritt werden die Nachfolgeknoten von K1 (
  
    
      
        
          f
          
            K
            1
          
        
        =
        40
      
    
    {\displaystyle f_{K1}=40}
  
) in die Open List eingefügt: Start mit 
  
    
      
        
          f
          
            S
            t
            a
            r
            t
          
        
        =
        20
        +
        40
        =
        60
      
    
    {\displaystyle f_{Start}=20+40=60}
  
 und K2 mit 
  
    
      
        
          f
          
            K
            2
          
        
        =
        30
        +
        0
        =
        30
      
    
    {\displaystyle f_{K2}=30+0=30}
  
.
Im fünften Schritt wird K2 zum zweiten Mal expandiert, jetzt mit 
  
    
      
        
          f
          
            K
            2
          
        
        =
        30
      
    
    {\displaystyle f_{K2}=30}
  
. Die Knoten K1 (
  
    
      
        
          f
          
            K
            1
          
        
        =
        50
        +
        30
        =
        80
      
    
    {\displaystyle f_{K1}=50+30=80}
  
), U (
  
    
      
        
          f
          
            U
          
        
        =
        40
        +
        0
        =
        40
      
    
    {\displaystyle f_{U}=40+0=40}
  
) und Ziel (
  
    
      
        
          f
          
            Z
            i
            e
            l
          
        
        =
        40
        +
        0
        =
        40
      
    
    {\displaystyle f_{Ziel}=40+0=40}
  
) werden gefunden.
Im sechsten Schritt befinden sich 8 Knoten auf der Open List (
  
    
      
        
          f
          
            Z
            i
            e
            l
          
        
        =
        40
      
    
    {\displaystyle f_{Ziel}=40}
  
, 
  
    
      
        
          f
          
            U
          
        
        =
        40
      
    
    {\displaystyle f_{U}=40}
  
, 
  
    
      
        
          f
          
            U
          
        
        =
        45
      
    
    {\displaystyle f_{U}=45}
  
, 
  
    
      
        
          f
          
            Z
            i
            e
            l
          
        
        =
        45
      
    
    {\displaystyle f_{Ziel}=45}
  
, 
  
    
      
        
          f
          
            S
            t
            a
            r
            t
          
        
        =
        60
      
    
    {\displaystyle f_{Start}=60}
  
, 
  
    
      
        
          f
          
            K
            1
          
        
        =
        80
      
    
    {\displaystyle f_{K1}=80}
  
, 
  
    
      
        
          f
          
            K
            1
          
        
        =
        85
      
    
    {\displaystyle f_{K1}=85}
  
, 
  
    
      
        
          f
          
            S
            t
            a
            r
            t
          
        
        =
        90
      
    
    {\displaystyle f_{Start}=90}
  
). Die beiden Knoten U und Ziel haben jeweils mit 40 den niedrigsten 
  
    
      
        f
      
    
    {\displaystyle f}
  
-Wert. Welcher Knoten expandiert wird hängt vom Zufall (genauer: Implementierungsdetails) ab. Auf die gefundene Lösung hat dies jedoch keinen Einfluss. Sobald der Zielknoten expandiert wird (entweder in diesem oder im nächsten Schritt), ist mit dem Pfad Start–K1–K2–Ziel die optimale Lösung gefunden.


### Monotone Heuristik
Damit eine Heuristik monoton (oder konsistent) ist, muss sie zwei Bedingungen erfüllen:

Die Kosten dürfen nie überschätzt werden (Bedingung für eine zulässige Heuristik).
Für jeden Knoten 
  
    
      
        k
      
    
    {\displaystyle k}
  
 und jeden Nachfolgeknoten 
  
    
      
        
          k
          ′
        
      
    
    {\displaystyle k'}
  
 von 
  
    
      
        k
      
    
    {\displaystyle k}
  
 muss gelten 
  
    
      
        h
        (
        k
        )
        ≤
        c
        (
        k
        ,
        
          k
          ′
        
        )
        +
        h
        (
        
          k
          ′
        
        )
      
    
    {\displaystyle h(k)\leq c(k,k')+h(k')}
  
. Hierbei bezeichnet 
  
    
      
        c
        (
        k
        ,
        
          k
          ′
        
        )
      
    
    {\displaystyle c(k,k')}
  
 die tatsächlichen Kosten, um von 
  
    
      
        k
      
    
    {\displaystyle k}
  
 nach 
  
    
      
        
          k
          ′
        
      
    
    {\displaystyle k'}
  
 zu kommen.
Die zweite Bedingung ist eine Form der Dreiecksungleichung: Die geschätzten Kosten von einem Knoten 
  
    
      
        k
      
    
    {\displaystyle k}
  
 dürfen nicht größer sein als die tatsächlichen Kosten zu einem Nachfolgeknoten 
  
    
      
        
          k
          ′
        
      
    
    {\displaystyle k'}
  
 plus die geschätzten Kosten dieses Knotens.
Die zulässige Heuristik aus dem Beispiel verletzt diese Bedingung: Die Kosten von Knoten K1 werden mit 30 geschätzt. Bewegt man sich nun 20 in Richtung des Ziels zu Knoten K2 werden plötzlich keine Kosten mehr geschätzt. Hier gilt 
  
    
      
        30
        ≰
        20
        +
        0
      
    
    {\displaystyle 30\not \leq 20+0}
  
.
In vielen Fällen ist z. B. der euklidische Abstand (die Luftlinie) zum Ziel eine monotone Heuristik, beispielsweise für die Fahrtstrecke mit einem Auto. 
Wenn eine Heuristik monoton ist, so kann sie in die Kostenfunktion integriert werden, und die A*-Suche wird zu einem Dijkstra-Algorithmus.


## Eigenschaften
Der A*-Algorithmus ist

vollständig: Falls eine Lösung existiert, wird sie gefunden.
optimal: Es wird immer die optimale Lösung gefunden. Existieren mehrere optimale Lösungen, wird eine davon gefunden (abhängig von Implementierungsdetails).
optimal effizient: Es gibt keinen anderen Algorithmus, der die Lösung unter Verwendung der gleichen Heuristik schneller findet. (genauer: A* expandiert eine minimale Anzahl an Knoten.)


### Optimalität
Der A*-Algorithmus ist optimal, falls eine monotone Heuristik verwendet wird. Wenn keine Closed List verwendet wird, bleibt die Optimalität auch bei einer zulässigen Heuristik erhalten. Im Folgenden wird die Optimalität unter Verwendung einer monotonen Heuristik bewiesen.
Behauptung: Der A*-Algorithmus findet immer eine optimale Lösung.
Beweis: Sei 
  
    
      
        
          L
          
            1
          
        
      
    
    {\displaystyle L_{1}}
  
 eine optimale Lösung mit Kosten 
  
    
      
        
          K
          
            ∗
          
        
      
    
    {\displaystyle K^{*}}
  
 und 
  
    
      
        
          L
          
            2
          
        
      
    
    {\displaystyle L_{2}}
  
 eine suboptimale Lösung. Da die Heuristik die Kosten bis zum Ziel nie überschätzt, gilt für jeden Zielknoten und insbesondere für 
  
    
      
        
          L
          
            2
          
        
      
    
    {\displaystyle L_{2}}
  
:

  
    
      
        h
        (
        
          L
          
            2
          
        
        )
        
        
        =
        0
      
    
    {\displaystyle h(L_{2})\!\,=0}
  

Da 
  
    
      
        
          L
          
            2
          
        
      
    
    {\displaystyle L_{2}}
  
 eine suboptimale Lösung ist, gilt für die Kosten 
  
    
      
        
          K
          
            2
          
        
      
    
    {\displaystyle K_{2}}
  
:

  
    
      
        
          K
          
            2
          
        
        
        
        =
        f
        (
        
          L
          
            2
          
        
        )
        =
        g
        (
        
          L
          
            2
          
        
        )
        +
        h
        (
        
          L
          
            2
          
        
        )
        =
        g
        (
        
          L
          
            2
          
        
        )
        >
        
          K
          
            ∗
          
        
      
    
    {\displaystyle K_{2}\!\,=f(L_{2})=g(L_{2})+h(L_{2})=g(L_{2})>K^{*}}
  

Die Heuristik schätzt die tatsächlichen Kosten oder unterschätzt sie. Also gilt für einen beliebigen Knoten 
  
    
      
        x
      
    
    {\displaystyle x}
  
 auf dem kürzesten Pfad zur optimalen Lösung 
  
    
      
        
          L
          
            1
          
        
      
    
    {\displaystyle L_{1}}
  
:

  
    
      
        f
        (
        x
        )
        =
        g
        (
        x
        )
        +
        h
        (
        x
        )
        ≤
        
          K
          
            ∗
          
        
      
    
    {\displaystyle f(x)=g(x)+h(x)\leq K^{*}}
  

Damit gilt:

  
    
      
        f
        (
        x
        )
        ≤
        
          K
          
            ∗
          
        
        <
        f
        (
        
          L
          
            2
          
        
        )
      
    
    {\displaystyle f(x)\leq K^{*}<f(L_{2})}
  

Dies bedeutet, dass der A*-Algorithmus die suboptimale Lösung 
  
    
      
        
          L
          
            2
          
        
      
    
    {\displaystyle L_{2}}
  
 nicht wählt, solange sich Knoten eines optimalen Pfades in der Open List befinden (da bei jedem Schritt der Knoten mit minimalem 
  
    
      
        f
      
    
    {\displaystyle f}
  
-Wert erweitert wird). Eine suboptimale Lösung würde also erst gewählt werden, nachdem die Knoten jeder optimalen Lösung besucht worden wären. Dazu kommt es nicht, da der A*-Algorithmus stets die erste gefundene Lösung ausgibt und dann terminiert.


### Zeitkomplexität
Die hier beschriebene Zeitkomplexität (oder asymptotische Laufzeit) hat nur geringe Bedeutung, da die Stärke des A*-Algorithmus im zielgerichteten Suchen liegt und im Vergleich zur Gesamtanzahl der Knoten meist nur ein kleiner Teil davon untersucht wird. Bei Labyrinthen ist dies jedoch oft nicht möglich und die tatsächliche Laufzeit nähert sich der angegebenen worst-case Laufzeit an. Die Zeitkomplexität wird hier nur unter Verwendung von vereinfachenden Annahmen abgeschätzt. Sie hängt sehr stark von zwei Faktoren ab:

Genauigkeit der verwendeten Heuristik: Ist die Heuristik nicht monoton, wird die Laufzeit exponentiell, da Knoten mehrfach expandiert werden. Je genauer die Kostenabschätzung ist, desto weniger Knoten werden untersucht.
Implementierung der Open und Closed List: Die kostenintensiven Operationen im A*-Algorithmus sind die Methoden, um Elemente in die Listen einzufügen und zu entfernen, sowie Elemente in der Open List zu ändern. Diese müssen von den verwendeten Datenstrukturen effizient unterstützt werden, um eine kurze Laufzeit zu ermöglichen.
Im Folgenden wird die Menge der Knoten des zu Grunde liegenden Graphen mit 
  
    
      
        V
      
    
    {\displaystyle V}
  
 bezeichnet. Es wird angenommen, dass alle Knoten und Kanten im Voraus bekannt sind. (Dies ist bei einer Wegsuche meist der Fall.) Die verwendete Heuristik ist monoton. Die Open List wird als binärer Heap implementiert, die Closed List als Array. (Jeder Knoten besitzt ein Flag, ob er auf der Liste ist oder nicht.) Der A*-Algorithmus hat damit eine quadratische worst-case Laufzeit:

  
    
      
        
          
            O
          
        
        (
        |
        V
        
          |
          
            2
          
        
        )
      
    
    {\displaystyle {\mathcal {O}}(\vert V\vert ^{2})}
  

Diese Laufzeit ergibt sich wie folgt: Die Hauptschleife des Algorithmus wird für jeden Knoten maximal einmal ausgeführt. Die Funktionen openlist.removeMin, expandNode und closedlist.add werden also maximal 
  
    
      
        |
        V
        |
      
    
    {\displaystyle \vert V\vert }
  
-mal aufgerufen. openlist.removeMin enthält maximal 
  
    
      
        |
        V
        |
      
    
    {\displaystyle \vert V\vert }
  
 Knoten und benötigt bei einem binären Heap logarithmische Laufzeit. closedlist.add benötigt bei einem Array nur konstante Laufzeit. Dadurch ergibt sich eine vorläufige Laufzeit von:

  
    
      
        
          
            O
          
        
        
          
            (
          
        
        |
        V
        |
        ⋅
        (
        log
        ⁡
        |
        V
        |
        +
        
          
            
              O
            
          
          
            
              expandNode
            
          
        
        )
        
          
            )
          
        
      
    
    {\displaystyle {\mathcal {O}}{\big (}\vert V\vert \cdot (\log \vert V\vert +{\mathcal {O}}_{\textit {expandNode}}){\big )}}
  

Die Laufzeit von expandNode setzt sich zusammen aus: closedlist.contains hat konstante Laufzeit. openlist.contains hat ebenfalls konstante Laufzeit, wenn man zu jedem Knoten auch ein Open List Flag speichert. Der Aufruf von openlist.find kann entfallen, wenn jeder Knoten auch seinen 
  
    
      
        f
      
    
    {\displaystyle f}
  
-Wert speichert. Es wird entweder openlist.decreaseKey (benötigt lineare Laufzeit, um das entsprechende Element zu finden) oder openlist.enqueue (benötigt logarithmische Laufzeit) aufgerufen. Dabei wird die logarithmische von der linearen Laufzeit dominiert. Ein Schleifendurchlauf innerhalb von expandNode benötigt somit lineare Laufzeit.
Alle Funktionen werden für jeden Nachfolgeknoten aufgerufen. Es wird angenommen, dass jeder Knoten nur maximal 
  
    
      
        d
      
    
    {\displaystyle d}
  
 ausgehende Kanten hat. Die Anzahl der Schleifendurchläufe innerhalb von expandNode ist somit konstant und kann bei der asymptotischen Laufzeitbetrachtung vernachlässigt werden. Diese Annahme gilt nicht für Graphen, in denen jeder Knoten mit fast jedem anderen Knoten verbunden ist.

  
    
      
        
          
            
              O
            
          
          
            
              expandNode
            
          
        
        =
        
          
            O
          
        
        (
        d
        ⋅
        |
        V
        |
        )
        =
        
          
            O
          
        
        (
        |
        V
        |
        )
      
    
    {\displaystyle {\mathcal {O}}_{\textit {expandNode}}={\mathcal {O}}(d\cdot \vert V\vert )={\mathcal {O}}(\vert V\vert )}
  

Die Gesamtlaufzeit ergibt sich als:

  
    
      
        
          
            O
          
        
        
          
            (
          
        
        |
        V
        |
        ⋅
        (
        log
        ⁡
        |
        V
        |
        +
        |
        V
        |
        )
        
          
            )
          
        
        =
        
          
            O
          
        
        (
        |
        V
        |
        ⋅
        |
        V
        |
        )
      
    
    {\displaystyle {\mathcal {O}}{\big (}\vert V\vert \cdot (\log \vert V\vert +\vert V\vert ){\big )}={\mathcal {O}}(\vert V\vert \cdot \vert V\vert )}
  

Optimierungspotential in Bezug auf die worst-case Laufzeit besteht vor allem bei openlist.decreaseKey. Das teure Suchen im Heap entfällt, wenn jeder Knoten die Position seines entsprechenden Eintrages im Heap speichert. Damit würde sich die Laufzeit von decreaseKey zu logarithmisch und die Gesamtlaufzeit zu linear-logarithmisch reduzieren:

  
    
      
        
          
            O
          
        
        (
        |
        V
        |
        ⋅
        log
        ⁡
        |
        V
        |
        )
      
    
    {\displaystyle {\mathcal {O}}(\vert V\vert \cdot \log \vert V\vert )}
  


### Nachteile
Der begrenzende Faktor bei A* ist oft nicht die Rechenzeit, sondern der Speicherplatz. Da alle bekannten Knoten im Speicher gehalten werden (Open und Closed List), ist A* für viele Probleme nicht geeignet. Schon beim einfachen 15-Puzzle hat der komplette Graph bereits 
  
    
      
        16
        !
        =
        20.922.789.888.000
      
    
    {\displaystyle 16!=20.922.789.888.000}
  
 Knoten. Bei einem entsprechend langen Lösungsweg reicht der verfügbare Speicher nicht aus und A* kann keine Lösung finden. Im Abschnitt Verwandte Algorithmen finden sich ähnliche Algorithmen, die versuchen, den Speicherplatzverbrauch zu beschränken.


## Verwandte Algorithmen
Der A*-Algorithmus ist verwandt mit dem Dijkstra-Algorithmus und ein Greedy-Algorithmus. Der Algorithmus von Dijkstra verwendet keine Heuristik (
  
    
      
        h
        =
        0
      
    
    {\displaystyle h=0}
  
, also 
  
    
      
        f
        =
        g
      
    
    {\displaystyle f=g}
  
) und wählt Knoten nur anhand der bisherigen Kosten aus. Ist die Heuristik des A*-Algorithmus monoton, so kann man sie in die Kostenfunktion einberechnen (also 
  
    
      
        
          g
          
            neu
          
        
        (
        x
        ,
        y
        )
        =
        g
        (
        x
        ,
        y
        )
        +
        h
        (
        y
        )
        −
        h
        (
        x
        )
      
    
    {\displaystyle g_{\text{neu}}(x,y)=g(x,y)+h(y)-h(x)}
  
, 
  
    
      
        
          h
          
            neu
          
        
        =
        0
      
    
    {\displaystyle h_{\text{neu}}=0}
  
) und äquivalent den Algorithmus von Dijkstra verwenden. Ein Greedy-Algorithmus hingegen beachtet die Kosten nicht (
  
    
      
        g
        =
        0
      
    
    {\displaystyle g=0}
  
, also 
  
    
      
        f
        =
        h
      
    
    {\displaystyle f=h}
  
) und wählt Knoten nur anhand der geschätzten Restkosten aus. Für das Beispiel der Wegsuche ist der Dijkstra-Algorithmus besser geeignet, falls das Ziel nicht bereits vor der Wegsuche bekannt ist (z. B. nächste Tankstelle), und daher die Verwendung einer Heuristik nicht möglich ist. 
Viele zu A* ähnliche Algorithmen versuchen das Speicherproblem zu lösen. Unter anderem sind dies:

IDA* (Iterative Deepening A*), eine Variante der iterativen Tiefensuche
RBFS (Recursive Best-First Search), beschränkt den Speicherplatzverbrauch linear zur Länge der Lösung
MA* (Memory-Bounded A*), SMA* (Simplified MA*), benutzen jeweils eine fest vorgegebene Menge an Speicherplatz
Diese Algorithmen beschränken den Speicherplatzverbrauch auf Kosten der Laufzeit. Dadurch können unter Umständen nicht mehr alle benötigten Knoten im Speicher gehalten werden. Schlechte Knoten werden dann vergessen und müssen später eventuell neu generiert werden. Bei Verwendung einer monotonen Heuristik, und unter der Voraussetzung, dass genügend Speicher zur Verfügung steht, sind alle diese Algorithmen optimal. Ist die Speicherplatzbeschränkung zu restriktiv, kann die optimale Lösung unerreichbar sein. In diesem Fall wird eine suboptimale Lösung oder überhaupt keine Lösung gefunden.
Eine Erweiterung von A* ist der D*-Algorithmus (dynamischer A*). Dieser Algorithmus ist in der Lage, neue Informationen über die Umgebung effizient zu verarbeiten. Ist zum Beispiel eine Brücke entgegen der anfänglichen Annahme unpassierbar, so wird der Weg nur teilweise neu geplant, anstatt A* erneut auf den leicht geänderten Graphen anzuwenden. Besonders in dynamischen oder unbekannten Umgebungen (Roboter bewegt sich durch ein Katastrophengebiet) kann eine wiederholte Neuplanung des bereits gefundenen Weges erforderlich sein. D* ist wie A* optimal und optimal effizient.
Andere graphbasierte Algorithmen sind der Bellman-Ford-Algorithmus (erlaubt negative Kantengewichte) oder der Algorithmus von Floyd und Warshall (berechnet die kürzesten Pfade zwischen allen Knotenpaaren).


## Literatur
P. E. Hart, N. J. Nilsson, B. Raphael: A Formal Basis for the Heuristic Determination of Minimum Cost Paths. IEEE Transactions on Systems Science and Cybernetics SSC4 (2), 1968, S. 100–107.
P. E. Hart, N. J. Nilsson, B. Raphael: Correction to „A Formal Basis for the Heuristic Determination of Minimum Cost Paths“. SIGART Newsletter, 37, 1972, S. 28–29.
Stuart Russell, Peter Norvig: Künstliche Intelligenz – Ein moderner Ansatz. 2004, Prentice Hall, ISBN 3-8273-7089-2.


## Weblinks
Ein interaktives Applet zum Lernen, Ausprobieren und Demonstrieren des Algorithmus
Seite über A*-Algorithmus und Wegfindung im Allgemeinen (englisch)
Praktische Beschreibung und Java-Applet zum A*-Algorithmus
A*-Library für TypeScript und Javascript Projekte (Github)