Zustandsraumsuche: BFS, DFS, UCS und A* mit Animationen
KI-Grundlagen anschaulich: was ein Zustandsraum ist, wie BFS, DFS, UCS, gierige Suche und A* funktionieren, was eine Heuristik ist und was das mit maschinellem Lernen zu tun hat.
Serie KI-Grundlagen Teil 21 von 52
- Landkarte der künstlichen Intelligenz: was was ist und wie alles zusammenpasst
- Eine kurze Geschichte der künstlichen Intelligenz: von Turing bis zu Sprachmodellen
- Daten, Modell, Training, Inferenz: die Grundbegriffe an einem einzigen Beispiel
- Mathematische Notation lesen: Symbole, Indizes, Summen und Quantoren
- Mengen und Relationen: der Baukasten, aus dem Definitionen in der KI bestehen
- Funktionen und Abbildungen: warum jedes KI-Modell eine Funktion ist
- Logik und Beweise: die Regeln, nach denen KI schließt
- Graphen und Bäume: die Struktur, auf der Suche und neuronale Netze stehen
- Lineare Algebra für maschinelles Lernen: Vektoren, Matrizen und warum KI vor allem multipliziert
- Ableitungen, Gradient und Kettenregel: wie ein Modell herausfindet, in welche Richtung es lernen soll
- Gradientenabstieg: wie ein Modell lernt
- Was ist Wahrscheinlichkeit: vom Würfelwurf zu den Kolmogorow-Axiomen
- Bedingte Wahrscheinlichkeit und der Satz von Bayes: wie du deine Einschätzung änderst, wenn du etwas Neues erfährst
- Zufallsvariable und Verteilungsfunktion: eine Funktion, die alles über eine Verteilung verrät
- Dichtefunktion und Wahrscheinlichkeitsfunktion: warum eine stetige Zufallsvariable in einem Punkt die Wahrscheinlichkeit null hat
- Erwartungswert, Varianz und Kovarianz: wo eine Verteilung liegt, wie breit sie ist und was womit zusammenhängt
- Überblick über Wahrscheinlichkeitsverteilungen: Bernoulli-, Binomial-, Poisson-, Gleich-, Normal- und Exponentialverteilung
- Gesetz der großen Zahlen und zentraler Grenzwertsatz: Warum sich Durchschnitte so vorhersagbar verhalten
- Statistik und Parameterschätzung: Maximum Likelihood, MAP und Konfidenzintervalle
- Informationstheorie: Entropie, Kreuzentropie und KL-Divergenz — woher Verlustfunktionen kommen
- Zustandsraumsuche: BFS, DFS, UCS und A* mit Animationen
- Spiele und Gegner: Minimax, Alpha-Beta-Suche und Monte Carlo Tree Search
- Constraint Satisfaction: Backtracking, Constraint-Propagierung und AC-3 an Sudoku und einer Karte tschechischer Regionen
- Logik und wissensbasierte Systeme: Vorwärts- und Rückwärtsverkettung, Resolution und Expertensysteme
- Planung: STRIPS, die Blockwelt, Heuristiken aus Relaxierung und Planung in KI-Agenten
- Lokale Suche und simulierte Abkühlung: ein Handlungsreisender durch die tschechischen Regionalhauptstädte
- Genetische Algorithmen: Population, Kreuzung, Mutation und Selektion am Rucksack für die Wanderung
- Evolutionsstrategien und genetische Programmierung: von der 1/5-Erfolgsregel zum Keplerschen Gesetz
- Ameisenkolonieoptimierung: wie eine Pheromonspur einen kurzen Weg findet
- Partikelschwarmoptimierung: Trägheit, eigene Erfahrung und die Erfahrung des Schwarms
- Neuroevolution: Evolution der Gewichte und der Gestalt neuronaler Netze und der Algorithmus NEAT
- Fuzzy-Mengen und Zugehörigkeitsfunktionen: wie man „etwas warm“ aufschreibt
- Fuzzy-Logik und Wahrscheinlichkeit: warum „etwas warm“ nicht „wahrscheinlich warm“ bedeutet
- Fuzzy-Logik: t-Normen, Implikationen und WENN–DANN-Regeln
- Fuzzy-Regelung: Mamdani und Sugeno vom Ventilator bis zum Pendel auf dem Wagen
- Fuzzy-Clustering: der Algorithmus fuzzy c-means und Punkte zwischen den Clustern
- Neuro-Fuzzy-Systeme (ANFIS): Fuzzy-Regeln, die aus Daten lernen
- Markow-Ketten: Übergangsmatrix, stationäre Verteilung und PageRank
- Hidden-Markov-Modelle: der Viterbi-Algorithmus und das unehrliche Kasino
- Bayessche Netze: warum das Gras nass ist und was das für den Rasensprenger bedeutet
- Der Kalman-Filter: wie man aus ungenauen Messungen schätzt, wo ein Auto ist und wie schnell es fährt
- Arten des maschinellen Lernens: überwacht, unüberwacht, halbüberwacht und mehr
- Prompts, Kontext und RAG: wie man einem Modell die richtigen Informationen gibt
- Variational Autoencoder (VAE): neue Bilder aus einem stetigen latenten Raum
- Generative Adversarial Networks (GAN): der Fälscher gegen den Detektiv
- Diffusionsmodelle: wie aus reinem Rauschen ein Bild wird
- Markow-Entscheidungsprozesse: Zustände, Aktionen, Belohnungen und Bellman-Gleichungen
- Q-Learning und DQN: den Wert von Aktionen lernen, von der Tabelle bis zum neuronalen Netz
- Policy Gradient und PPO: die Strategie direkt lernen
- Wie ein KI-Agent funktioniert: ein Sprachmodell in einer Schleife mit Werkzeugen
- Programmieren mit KI-Agenten: Auftrag, Tests und Kontrolle
- Bewertung und Sicherheit von KI-Systemen: wie man misst und wie man überwacht
Der ganze Lehrplan der Serie 52 von 83 Themen fertig
- Wie man eine Aufgabe formal als Wegsuche in einem Zustandsgraphen aufschreibt.
- Worin sich BFS, DFS, UCS, gierige Suche und A* unterscheiden — und warum es im Kern eine einzige Codezeile ist.
- Was eine Heuristik ist, wann sie zulässig ist und warum A* mit ihr den günstigsten Weg mit dem wenigsten Aufwand findet.
Vorher gut zu wissen: Landkarte der künstlichen Intelligenz: was was ist und wie alles zusammenpasst, Mathematische Notation lesen: Symbole, Indizes, Summen und Quantoren
Wenn heute von künstlicher Intelligenz die Rede ist, denken die meisten an neuronale Netze und große Sprachmodelle. Doch lange bevor Maschinen anfingen, aus Daten zu lernen, konnten sie suchen. Navigation auf einer Karte, ein Zug im Schach, der Plan eines Roboters im Lager — all das ist die Suche nach einem Weg durch einen Raum von Möglichkeiten. Die Zustandsraumsuche ist das erste Kapitel jedes KI-Lehrbuchs, und ihren Ideen begegnest du auch im maschinellen Lernen: Heuristiken, Optimierung, die Suche nach der besten Lösung.
Jeden Begriff findest du hier zweimal: zuerst eine präzise mathematische Definition, direkt darunter dasselbe in einfachen Worten. Alle Animationen sind ein echter Lauf des Algorithmus — nichts davon ist von Hand gezeichnet.
1. Was ein Zustandsraum ist
Bevor wir irgendetwas suchen, müssen wir die Aufgabe so beschreiben, dass ein Computer sie versteht. Dazu dient das Suchproblem:
Definition — Suchproblem
Ein Suchproblem ist ein Sechstupel
P = (S, s0, A, T, G, c),
das Folgendes erfüllt:
- S ist eine nichtleere Menge; ihre Elemente nennen wir Zustände;
- s0 ∈ S; diesen Zustand nennen wir den Anfangszustand;
- A ordnet jedem Zustand s ∈ S eine endliche Menge A(s) der im Zustand s anwendbaren Aktionen zu; sei 𝒟 = {(s, a) : s ∈ S, a ∈ A(s)} die Menge aller Paare „Zustand, darin anwendbare Aktion“;
- T : 𝒟 → S ist die Übergangsfunktion; für (s, a) ∈ 𝒟 ist der Zustand s′ = T(s, a) ein Nachfolger des Zustands s (nach der Aktion a);
- G ⊆ S ist die Menge der Zielzustände;
- c : 𝒟 → ℝ+ = (0, ∞) sind die Aktionskosten: c(s, a) > 0 sind die Kosten der Anwendung der Aktion a im Zustand s.
Das Problem sagt also genau, wo wir beginnen, welche Züge uns in jedem Zustand zur Verfügung stehen, wohin welcher Zug führt, was er kostet und wann wir am Ziel sind.
Ein Zustand ist eine „Momentaufnahme der Welt“ — wo das Männchen im Labyrinth steht, wie die Plättchen eines Puzzles liegen. Eine Aktion ist ein Zug, den du machen kannst. Die Übergangsfunktion sagt, wohin dich dieser Zug bringt. Das Ziel ist das, wo du hinwillst, und die Kosten sagen, was dich welcher Zug kostet — Zeit, Treibstoff, Schritte.
Definition — Pfad, Lösung, Optimum
Sei P = (S, s0, A, T, G, c) ein Suchproblem und k ∈ ℕ0. Ein Pfad der Länge k vom Anfangszustand ist eine Folge
π = (s0, a1, s1, a2, …, ak, sk),
sodass für jedes i = 1, …, k gilt: ai ∈ A(si−1) und si = T(si−1, ai). (Für k = 0 ist der Pfad nur (s0).)
Die Pfadkosten von π sind die Summe der Kosten seiner Aktionen:
g(π) = ∑i=1k c(si−1, ai).
Ein Pfad π ist eine Lösung genau dann, wenn sk ∈ G. Sei Π die Menge aller Lösungen. Ist Π ≠ ∅, so sind die optimalen Kosten
C* = min {g(π) : π ∈ Π}
(sofern das Minimum existiert; bei Problemen mit endlich vielen Zuständen wie unserem Labyrinth existiert es immer), und eine Lösung π ist optimal genau dann, wenn g(π) = C*. Eine optimale Lösung ist also eine Lösung, die nicht teurer ist als irgendeine andere Lösung.
Ein Pfad ist eine Liste von Zügen, die aufeinander folgen. Eine Lösung ist ein Pfad, der ans Ziel kommt. Die optimale Lösung ist die günstigste von allen — und genau die wollen wir meistens finden.
Durch Aktionen verbundene Zustände bilden einen gerichteten Graphen: Zustände sind die Knoten, Aktionen die Kanten mit dem Gewicht c. Eine Lösung zu suchen heißt also, im Graphen einen Weg von s0 zu einem Knoten aus G zu finden. Das klassische Beispiel ist das 8-Puzzle: Plättchen 1–8 in einem 3 × 3-Rahmen und ein leeres Feld. Ein Zustand ist die Anordnung der Plättchen, eine Aktion das Verschieben des leeren Feldes und das Ziel die geordneten Plättchen:
Das sieht harmlos aus, aber das 8-Puzzle hat 181 440 erreichbare Zustände und sein großer Bruder, das 15-Puzzle, über zehn Billionen. Deshalb bauen wir den Graphen nie vorab vollständig auf. Der Algorithmus entdeckt ihn nach und nach — aus der Übergangsfunktion berechnet er immer nur die Nachfolger des Zustands, auf dem er gerade steht.
Beispiel — woher die 181 440 kommen
Acht Plättchen und das leere Feld lassen sich auf 9! = 9 · 8 · 7 · 6 · 5 · 4 · 3 · 2 · 1 = 362 880 Arten auf die neun Felder verteilen. Durch Züge erreichst du aber nur die Hälfte davon: Schreibst du die Plättchen zeilenweise auf (ohne das leere Feld), erhält jeder Zug, ob in dieser Reihenfolge die Zahl der Paare „größeres vor kleinerem“ gerade oder ungerade ist. Ein waagerechter Zug ändert die Reihenfolge überhaupt nicht, ein senkrechter Zug lässt ein Plättchen über zwei andere springen und ändert die Zahl solcher Paare um 0 oder um ±2. In einen Zustand mit entgegengesetzter Parität kommt man deshalb nicht. Dass genau die Hälfte der Zustände erreichbar ist (beim 15-Puzzle mit einer etwas komplizierteren Invariante, in die auch die Zeile des leeren Feldes eingeht), bewiesen W. W. Johnson und W. E. Story im Jahr 1879.
8-Puzzle: 9!/2 = 362 880/2 = 181 440 Zustände.
15-Puzzle: 16!/2 = 20 922 789 888 000/2 = 10 461 394 944 000 ≈ 1,05 · 1013 Zustände — über zehn Billionen.
2. Die Aufgabe: ein Weg durchs Labyrinth
Alle Algorithmen zeigen wir an derselben Aufgabe. Wir haben ein Labyrinth aus 9 × 14 Feldern und wollen so günstig wie möglich vom Start ans Ziel. Zuerst, was welches Feld bedeutet:
- Freies Feld — es zu betreten kostet 1
- Schlamm — man kommt durch, aber das Betreten kostet 5
- Wand — kann nicht betreten werden
- S Start — der Anfangszustand, Feld (0, 0)
- G Ziel (goal) — Feld (8, 13)
- Untersuchtes Feld — der Algorithmus hat es expandiert
- Gefundener Weg — das Ergebnis des Algorithmus
Formale Aufgabenstellung
Ein Feld schreiben wir als Paar (r, s) — Zeile r ∈ {0, …, 8}, Spalte s ∈ {0, …, 13}. Sei Z die Menge der Wände und B die Menge der Schlammfelder.
- Zustände: S = ({0, …, 8} × {0, …, 13}) ∖ Z — alle Felder außer den Wänden.
- Anfangszustand: s0 = (0, 0). Ziel: G = {(8, 13)}.
- Verschiebungen: ↑ = (−1, 0), → = (0, 1), ↓ = (1, 0), ← = (0, −1). Im Zustand (r, s) ∈ S anwendbar sind die Aktionen, die auf ein Feld innerhalb des Gitters führen, das keine Wand ist: A((r, s)) = {(Δr, Δs) ∈ {↑, →, ↓, ←} : (r + Δr, s + Δs) ∈ S}.
- Übergang: T((r, s), (Δr, Δs)) = (r + Δr, s + Δs), z. B. T((r, s), →) = (r, s + 1).
- Kosten: c(x, a) = 5, falls T(x, a) ∈ B, sonst c(x, a) = 1 — bezahlt wird für das Feld, das man betritt.
- Aufgabe: eine Lösung mit den geringsten Kosten C* finden.
Das Männchen steht oben links und will nach unten rechts. Es geht nur nach oben, unten, links und rechts, durch eine Wand kommt es nicht, und jeder Schritt kostet es 1 — außer einem Schritt in den Schlamm, der kostet 5. Wir suchen den Weg, der es am wenigsten kostet.
Der Schlamm liegt absichtlich auf der direktesten Route. Wir werden sehen, dass ihn nicht jeder Algorithmus bemerkt: Manche zählen nur Schritte, andere die echten Kosten.
3. Das gemeinsame Gerüst aller Algorithmen
Alle Algorithmen in diesem Artikel haben dasselbe Gerüst. Sie unterscheiden sich nur in einer einzigen Entscheidung.
Definition — Randmenge, geschlossene Zustände, Expansion
Sei P = (S, s0, A, T, G, c) ein Suchproblem. Für einen Zustand s ∈ S bezeichnen wir die Menge seiner Nachfolger mit
N(s) = {T(s, a) : a ∈ A(s)}.
Ein Suchalgorithmus hält zu jedem Zeitpunkt seines Laufs zwei Mengen von Zuständen:
- die Randmenge (frontier) F ⊆ S — Zustände, die der Algorithmus schon entdeckt hat (zu ihnen führt ein gefundener Pfad von s0), aber noch nicht expandiert hat; am Anfang ist F = {s0};
- die geschlossenen Zustände E ⊆ S — Zustände, die er schon expandiert hat; am Anfang ist E = ∅.
Einen Zustand s ∈ F zu expandieren heißt, diese beiden Änderungen (in dieser Reihenfolge) vorzunehmen:
E ← E ∪ {s}, F ← (F ∖ {s}) ∪ (N(s) ∖ E).
Der expandierte Zustand wandert also aus der Randmenge zu den geschlossenen, und in die Randmenge kommen diejenigen seiner Nachfolger hinzu, die noch nicht expandiert wurden.
Die Randmenge ist eine Liste von Orten, von denen du weißt, dass ein Weg dorthin führt, wo du aber noch nicht nachgeschaut hast. Geschlossene Zustände sind Orte, an denen du schon warst — dank ihnen läufst du nicht im Kreis. Einen Zustand expandieren heißt, dich auf ihn zu stellen und umzuschauen, wohin es weitergeht.
rand = [start]
geschlossen = {}
solange rand nicht leer ist:
zustand = wähle_aus(rand) # ← hier unterscheiden sich die Algorithmen
wenn zustand das Ziel ist: fertig
füge zustand zu geschlossen hinzu
für jeden Nachfolger von zustand:
wenn er noch nicht geschlossen ist: füge ihn zu rand hinzu
Der ganze Unterschied zwischen den Algorithmen steckt in einer einzigen Zeile: welchen Zustand aus der Randmenge wir als Nächstes wählen. Eine Warteschlange, ein Stapel oder eine Prioritätswarteschlange nach irgendeiner Zahl. Darauf beruht alles andere.
4. Breitensuche (BFS)
Definition — BFS
Eingabe: ein Suchproblem P = (S, s0, A, T, G, c). Ausgabe: eine Lösung (ein Pfad von s0 zu einem Zustand aus G) oder die Meldung „es gibt keine Lösung“.
Die Breitensuche (BFS) ist das gemeinsame Gerüst von oben, in dem die Randmenge F eine Warteschlange ist (FIFO, first in — first out):
- F ← (s0), E ← ∅.
- Ist F leer, halte an: Es gibt keine Lösung.
- Entnimm aus F den Zustand s, der als erster hineingekommen ist.
- Ist s ∈ G, halte an und gib den Pfad von s0 nach s zurück.
- Expandiere s: Füge ihn zu E hinzu und reihe jeden Nachfolger s′ ∈ N(s), der weder in E noch in F ist, ans Ende von F ein. Fahre mit Schritt 2 fort.
Eigenschaften. Sei d(s) die Tiefe des Zustands s — die kleinste Länge eines Pfads von s0 nach s. Die BFS expandiert die Zustände in nicht fallender Reihenfolge von d(s) und gibt deshalb die Lösung mit den wenigsten Aktionen zurück. Sei b = maxs ∈ S |A(s)| der Verzweigungsfaktor (die größte Zahl von Nachfolgern eines Zustands) und d die Tiefe der flachsten Lösung. Dann erzeugt die BFS höchstens b + b2 + … + bd = O(bd) Zustände und hält ebenso viele im Speicher.
Wer zuerst kommt, ist zuerst dran — wie an der Kasse im Laden. Zuerst untersuchst du alles, was einen Schritt vom Start entfernt ist, dann alles in zwei Schritten, dann in drei. Das breitet sich aus wie eine Welle auf dem Wasser, und deshalb hat der erste Weg, der das Ziel erreicht, die wenigsten Schritte. Der Haken: Du musst dir die ganze Welle merken, und die wächst furchtbar schnell.
Im Labyrinth ist die Welle gut zu sehen — sie breitet sich in alle Richtungen aus, bis sie auf das Ziel trifft:
Beachte aber, dass der Weg mitten durch den Schlamm führt. Die BFS zählt Aktionen, nicht Kosten — für sie ist ein Schritt in den Schlamm dasselbe wie ein Schritt auf trockenem Boden. Der kürzeste Weg kam so auf 25 Schritte, aber Kosten von 37. Die BFS ist nur dann optimal, wenn alle Aktionen gleich viel kosten.
Beispiel — die Kosten des BFS-Wegs und wie schnell die Welle wächst
Der BFS-Weg hat 25 Schritte, davon 3 in den Schlamm: g = 22 · 1 + 3 · 5 = 22 + 15 = 37.
Die Größe der Welle für den Verzweigungsfaktor b = 10: In der Tiefe d = 5 sind es 10 + 100 + 1 000 + 10 000 + 100 000 = 111 110 Zustände, in der Tiefe d = 10 schon 11 111 111 110 ≈ 1,1 · 1010. Würde ein Zustand 1 kB Speicher belegen, bräuchte die Welle in der Tiefe 10 über 11 TB. Jede weitere Etage verzehnfacht das.
from collections import deque
def bfs(start, goal, successors):
frontier = deque([start])
parent = {start: None}
while frontier:
state = frontier.popleft() # Warteschlange: wer zuerst kommt, geht zuerst
if state == goal:
return path(parent, goal)
for nxt in successors(state):
if nxt not in parent:
parent[nxt] = state
frontier.append(nxt)
return None
def path(parent, state):
result = []
while state is not None:
result.append(state)
state = parent[state]
return result[::-1]
5. Tiefensuche (DFS)
Definition — DFS
Eingabe: ein Suchproblem P = (S, s0, A, T, G, c). Ausgabe: eine Lösung oder die Meldung „es gibt keine Lösung“.
Die Tiefensuche (DFS) ist das gemeinsame Gerüst von oben, in dem die Randmenge F ein Stapel ist (LIFO, last in — first out):
- F ← (s0), E ← ∅.
- Ist F leer, halte an: Es gibt keine Lösung.
- Entnimm aus F den Zustand s, der zuletzt hineingekommen ist (die Spitze des Stapels).
- Ist s ∈ G, halte an und gib den Pfad von s0 nach s zurück.
- Expandiere s: Füge ihn zu E hinzu und lege jeden Nachfolger s′ ∈ N(s) ∖ E oben auf F. Fahre mit Schritt 2 fort.
Eigenschaften. Sei b der Verzweigungsfaktor und m die größte Tiefe des Zustandsraums (die größte Länge eines Pfads ohne wiederholte Zustände, gegebenenfalls ∞). In der Baumvariante (ohne die Menge E, Wiederholungen werden nur auf dem aktuellen Zweig geprüft) hält die DFS nur den aktuellen Zweig und seine Geschwister im Speicher: O(b · m) Zustände. Die Zeit beträgt im schlimmsten Fall O(bm). Die DFS ist nicht optimal und in einem unendlichen Raum nicht einmal vollständig — sie kann endlos absteigen.
Wie wenn du im Labyrinth immer der rechten Hand nach gehst: Du wählst einen Gang und folgst ihm bis zum Ende. Erst in einer Sackgasse kehrst du zur letzten Abzweigung zurück und probierst eine andere. Das braucht wenig Speicher — du merkst dir nur den Weg, den du gerade gehst. Aber den kürzesten Weg findest du so nur zufällig.
Im Baum sieht man es an der Reihenfolge: A, B, D, H, I — und erst dann E. Warum, zeigt die Randmenge Schritt für Schritt. Das Gerüst ist bei beiden gleich, es unterscheidet sich nur, von welchem Ende der Randmenge genommen wird: BFS nimmt von links den ältesten Zustand (den Kopf der Warteschlange), DFS von links den neuesten (die Spitze des Stapels).
| Schritt | BFS expandiert | Warteschlange danach | DFS expandiert | Stapel danach |
|---|---|---|---|---|
| 1 | A | B C | A | B C |
| 2 | B | C D E | B | D E C |
| 3 | C | D E F G | D | H I E C |
| 4 | D | E F G H I | H | I E C |
| 5 | E | F G H I | I | E C |
| 6 | F | G H I | E | C |
| 7 | G | H I J | C | F G |
| 8 | H | I J | F | G |
| 9 | I | J | G | J |
| 10 | J | — | J | — |
Im zweiten Schritt fügen beide die Kinder des Knotens B hinzu, also D und E. Die BFS reiht sie ans Ende der Warteschlange hinter C ein und nimmt als Nächstes C, weil es am längsten wartet. Die DFS legt sie oben auf den Stapel über C und nimmt D, weil es zuletzt gekommen ist — und die Kinder H, I kommen dann sogar vor E und C an die Reihe. Die Warteschlange wächst auf bis zu fünf Knoten (E F G H I), der Stapel höchstens auf vier (H I E C): Bei BFS liegt in der Randmenge eine ganze Etage, bei DFS nur ein Zweig und die Geschwister seiner Knoten. Bei einem so kleinen Baum ist das ein Unterschied von einem Knoten, mit jeder Etage wächst er aber. Im Labyrinth:
Hier hatte die DFS Glück und kam ans Ziel, ohne viele Felder zu untersuchen. Der Weg ist aber unnötig lang — 31 Schritte statt 25 und Kosten von 59.
Definition — iterative Vertiefung (IDDFS)
Eingabe: ein Suchproblem P = (S, s0, A, T, G, c). Ausgabe: eine Lösung mit den wenigsten Aktionen (sofern es eine Lösung gibt).
Für ℓ ∈ ℕ0 bezeichne DLS(ℓ) (depth-limited search) die Tiefensuche, die keine Zustände in der Tiefe ℓ oder größer expandiert — Pfade länger als ℓ untersucht sie also nicht. Iterative Vertiefung (IDDFS):
- ℓ ← 0.
- Starte DLS(ℓ).
- Hat DLS(ℓ) eine Lösung gefunden, halte an und gib sie zurück.
- ℓ ← ℓ + 1 und fahre mit Schritt 2 fort.
Abbruchbedingung: das erste ℓ, für das DLS(ℓ) eine Lösung findet; das ist genau die Tiefe d der flachsten Lösung. (Gibt es keine Lösung und ist der Raum unendlich, hält der Algorithmus nicht an.)
Eigenschaften. Sei b der Verzweigungsfaktor und d die Tiefe der flachsten Lösung. Zustände in der Tiefe i ∈ {1, …, d} werden (d − i + 1)-mal erzeugt, insgesamt ∑i=1d (d − i + 1) · bi = O(bd) Zustände — genauso wie bei der BFS, aber mit einem Speicher von nur O(b · d).
Du startest eine DFS, verbietest ihr aber, tiefer als einen Schritt zu gehen. Nichts gefunden? Dann zwei Schritte, dann drei… Die oberen Etagen gehst du immer wieder durch, aber die meisten Zustände liegen immer in der tiefsten, also kostet die Wiederholung fast nichts. Du bekommst den kürzesten Weg wie bei der BFS und den kleinen Speicher wie bei der DFS.
NIDDFS = ∑i=1d (d − i + 1) · bi
- NIDDFS
- wie viele Zustände die ganze iterative Vertiefung erzeugt — daran misst man ihre Zeit
- ∑i=1d
- wir summieren über die Etagen des Baums, von der Tiefe 1 bis zur Tiefe der Lösung d — tiefer geht es nie
- (d − i + 1)
- wie oft die Etage i durchlaufen wird: in jeder Runde mit dem Limit ℓ = i, i + 1, …, d — die flachen Etagen wiederholen sich am häufigsten
- bi
- wie viele Zustände in der Etage i liegen — mit der Tiefe wächst das exponentiell, sodass die tiefste Etage, die nur einmal durchlaufen wird, alles andere überwiegt
def iddfs(start, is_goal, successors):
limit = 0
while True: # ℓ = 0, 1, 2, …
path = dls(start, is_goal, successors, limit)
if path is not None:
return path
limit += 1
def dls(state, is_goal, successors, limit): # DFS mit begrenzter Tiefe
if is_goal(state):
return [state]
if limit == 0:
return None # tiefer dürfen wir nicht
for nxt in successors(state):
path = dls(nxt, is_goal, successors, limit - 1)
if path is not None:
return [state] + path
return None
6. Kostengesteuerte Suche (UCS)
Definition — UCS
Eingabe: ein Suchproblem P = (S, s0, A, T, G, c). Ausgabe: eine Lösung mit den geringsten Kosten oder die Meldung „es gibt keine Lösung“.
Während des Laufs ordnet der Algorithmus jedem entdeckten Zustand n ∈ S eine Zahl g(n) zu — die Kosten des günstigsten bisher gefundenen Pfads von s0 nach n. Die Randmenge F ist eine Prioritätswarteschlange, geordnet nach g. Kostengesteuerte Suche (UCS, uniform-cost search):
- F ← {s0}, g(s0) ← 0, E ← ∅.
- Ist F = ∅, halte an: Es gibt keine Lösung.
- Entnimm aus F den Zustand n mit dem kleinsten g(n).
- Ist n ∈ G, halte an und gib den gefundenen Pfad nach n zurück (seine Kosten sind g(n)).
- Füge n zu E hinzu. Für jede Aktion a ∈ A(n) und n′ = T(n, a) ∉ E: Ist n′ ∉ F oder g(n) + c(n, a) < g(n′), setze g(n′) ← g(n) + c(n, a), merke dir n als Vorgänger von n′ und füge n′ in F ein. Fahre mit Schritt 2 fort.
Eigenschaften. Es gebe ein ε > 0, sodass c(s, a) ≥ ε für alle s ∈ S, a ∈ A(s). Dann expandiert die UCS die Zustände in nicht fallender Reihenfolge von g und ist deshalb vollständig und optimal. Die Zahl der expandierten Zustände ist O(b1 + ⌊C*/ε⌋), wobei b der Verzweigungsfaktor und ⌊x⌋ die untere Gaußklammer (Abrundung) der Zahl x ist.
Statt Schritte zählst du Geld. Du machst immer an dem Ort weiter, den du bisher am günstigsten erreicht hast. Wenn du am Ziel ankommst, weißt du sicher, dass es nicht günstiger geht — alles Günstigere hast du schon untersucht. In allen Graphen mit positiven Kosten macht das der bekannte Dijkstra-Algorithmus; die UCS ist seine Variante, die am Ziel anhält.
Die UCS ist um den Schlamm herumgegangen. Sie hat einen Weg mit derselben Zahl an Schritten gefunden wie die BFS, aber mit Kosten von 25 statt 37 — und der ist garantiert der günstigste, also C* = 25. Der Preis dafür: Sie sucht blind in alle Richtungen. Sie weiß nicht, wo das Ziel liegt, und breitet sich deshalb auch von ihm weg aus.
Beispiel — in welcher Reihenfolge die UCS expandiert und was die Komplexitätsabschätzung sagt
Die ersten zehn Expansionen mit dem Wert g: (0, 0) g = 0; (0, 1) g = 1; (1, 0) g = 1; (0, 2) g = 2; (2, 0) g = 2; (0, 3) g = 3; (3, 0) g = 3; (1, 3) g = 4; (4, 0) g = 4; (2, 3) g = 5. Bis sie auf Schlamm trifft, expandiert die UCS genauso wie die BFS, denn jeder Schritt kostet 1 und g ist einfach die Tiefe. Der Schlamm „verschiebt“ einen Zustand dann um 5 statt um 1 — in den Schlamm schaut die UCS erst, wenn sie alles Günstigere erschöpft hat.
Der gefundene Weg: 25 Schritte auf trockenem Boden, C* = 25 · 1 = 25. Die kleinsten Aktionskosten sind ε = 1 und der Verzweigungsfaktor b = 4, die obere Abschätzung ist also b1 + ⌊C*/ε⌋ = 426 ≈ 4,5 · 1015 Zustände. In Wirklichkeit hat die UCS 49 Felder expandiert: Die Abschätzung rechnet mit einem unendlichen Baum, in dem sich nichts wiederholt, während das Labyrinth nur 83 begehbare Felder hat und geschlossene Zustände nicht noch einmal expandiert werden.
7. Heuristik: eine Schätzung, wo es zum Ziel geht
Die Algorithmen oben sind uninformiert — über das Ziel wissen sie nur, dass sie es erkennen, sobald sie darauf treten. Informierte Algorithmen bekommen zusätzlich eine Schätzung, wie viel noch fehlt.
Definition — Heuristik
Sei P = (S, s0, A, T, G, c) ein Suchproblem. Für einen Zustand n ∈ S bezeichne h*(n) ∈ ℝ≥0 ∪ {∞} die kleinsten Kosten (genauer das Infimum der Kosten) der Pfade vom Zustand n zu irgendeinem Zielzustand (ein Pfad ist genauso definiert wie oben, er beginnt nur in n statt in s0; führt von n kein Pfad nach G, ist h*(n) = ∞). Eine Heuristik ist eine Funktion
h : S → ℝ≥0 mit ∀n ∈ G : h(n) = 0.
Den Wert h(n) verstehen wir als Schätzung von h*(n). Eine Heuristik h ist
- zulässig (admissible) genau dann, wenn ∀n ∈ S : h(n) ≤ h*(n);
- konsistent (monoton) genau dann, wenn ∀n ∈ S ∀a ∈ A(n) : h(n) ≤ c(n, a) + h(T(n, a)).
Eine zulässige Heuristik überschätzt die tatsächlichen Restkosten also nie; eine konsistente Heuristik sinkt nach keinem Schritt um mehr, als dieser Schritt gekostet hat. Jede konsistente Heuristik ist zulässig (Satz unten).
Eine Heuristik ist eine fundierte Schätzung — wie wenn du in einer fremden Stadt weißt, dass der Bahnhof „irgendwo im Norden“ liegt. Zulässig heißt optimistisch: Sie behauptet nie, es sei weiter, als es wirklich ist. Konsistent heißt, dass sich die Schätzung nach einem Schritt nicht widerspricht — sie kann sich nicht stärker verbessern, als du für diesen Schritt bezahlt hast.
h(n) ≤ c(n, a) + h(n′)
- h(n)
- was die Heuristik jetzt, im Zustand n, verspricht — dieses Versprechen darf nicht übertrieben sein
- c(n, a)
- die tatsächlichen Kosten eines Schritts zum Nachfolger; für diesen Schritt bezahlen wir wirklich
- h(n′)
- die Schätzung des Rests ab dem Nachfolger. Wäre die Summe rechts kleiner als h(n), führte über n′ ein günstigerer Pfad, als h(n) behauptet, und eine der beiden Schätzungen wäre Unsinn
Satz — eine konsistente Heuristik ist zulässig
Sei P = (S, s0, A, T, G, c) ein Suchproblem und h : S → ℝ≥0 eine konsistente Heuristik. Dann ist h zulässig, d. h. h(n) ≤ h*(n) für jedes n ∈ S.
Beweis. Sei n ∈ S. Ist h*(n) = ∞, gilt die Ungleichung trivialerweise. Andernfalls nehmen wir einen beliebigen Pfad von n zum Ziel: n = n0, n1, …, nk ∈ G, wobei ni = T(ni−1, ai) und ci = c(ni−1, ai) für i = 1, …, k. Die Konsistenz wenden wir k-mal hintereinander an:
h(n0) ≤ c1 + h(n1) ≤ c1 + c2 + h(n2) ≤ … ≤ c1 + … + ck + h(nk).
Im Ziel ist h(nk) = 0, also ist h(n) höchstens gleich den Kosten dieses Pfads. Das gilt für jeden Pfad von n zum Ziel, also auch für den günstigsten, dessen Kosten h*(n) sind. Also h(n) ≤ h*(n). ∎
Umgekehrt gilt das nicht: Eine zulässige Heuristik muss nicht konsistent sein.
In unserem Labyrinth bietet sich die Manhattan-Distanz an — die Zahl der Felder waagerecht plus senkrecht, als würdest du durch die rechtwinkligen Straßen Manhattans gehen:
h((r, s)) = |r − 8| + |s − 13|
- h((r, s))
- die Schätzung der Restkosten vom Feld in Zeile r und Spalte s
- |r − 8|
- wie viele Zeilen bis zum Ziel (8, 13) fehlen. Der Betrag deshalb, weil die Entfernung gleich gezählt wird, ob das Ziel über oder unter uns liegt; ein senkrechter Zug nimmt höchstens eine davon weg
- |s − 13|
- wie viele Spalten fehlen. Wir addieren, weil man nicht diagonal gehen kann: Jede Zeile und jede Spalte kostet mindestens einen eigenen Zug
Du zählst, wie viele Felder das Ziel weiter unten und wie viele weiter rechts liegt, und addierst beides. Wände und Schlamm können den Weg nur verlängern oder verteuern, nie verkürzen — die Schätzung ist also immer optimistisch, genau wie sie sein soll.
8. Gierige Suche
Definition — gierige Suche
Eingabe: ein Suchproblem P = (S, s0, A, T, G, c) und eine Heuristik h : S → ℝ≥0. Ausgabe: eine Lösung oder die Meldung „es gibt keine Lösung“.
Die gierige Suche (greedy best-first search) ist das gemeinsame Gerüst von oben, in dem die Randmenge F eine Prioritätswarteschlange ist nach
f(n) = h(n):
- F ← {s0}, E ← ∅.
- Ist F = ∅, halte an: Es gibt keine Lösung.
- Entnimm aus F den Zustand n mit dem kleinsten h(n) — den Zustand, der der Schätzung nach dem Ziel am nächsten ist.
- Ist n ∈ G, halte an und gib den Pfad von s0 nach n zurück.
- Expandiere n: Füge ihn zu E hinzu und füge jeden Nachfolger n′ ∈ N(n), der weder in E noch in F ist, in F ein. Fahre mit Schritt 2 fort.
Die bisherigen Kosten g(n) werden nicht berücksichtigt. Eigenschaften: Die gierige Suche ist nicht optimal; mit Kontrolle wiederholter Zustände (der Menge E) ist sie in einem endlichen Raum vollständig.
Du gehst immer dorthin, wo es am nächsten zum Ziel aussieht, und kümmerst dich gar nicht darum, was dich der Weg bisher gekostet hat. Das zielt schnell in die richtige Richtung, lässt sich aber leicht hereinlegen — etwa von Schlamm, der „in der richtigen Richtung“ liegt.
Die gierige Suche untersucht wenige Felder, geht aber dorthin, wo es näher aussieht, und landet auf einem langen, teuren Weg — 31 Schritte, Kosten 59.
Beispiel — die ersten zehn Schritte der gierigen Suche
Expandierte Felder mit dem Wert h: (0, 0) 21 → (0, 1) 20 → (0, 2) 19 → (0, 3) 18 → (1, 3) 17 → (2, 3) 16 → (2, 2) 17 → (3, 2) 16 → (4, 2) 15 → (4, 3) 14.
Die gierige Suche geht direkt nach rechts und unten, solange h sinkt. Im siebten Schritt steigt h kurz an (von 16 auf 17), weil von (2, 3) weder nach rechts noch nach unten ein Weg weiterführt (Wände) und in der Randmenge kein Feld mit kleinerem h mehr liegt. Zwei Schritte später betritt sie den Schlamm auf (4, 2), weil h dort wieder sinkt. Die Kosten 5 für den Schritt in den Schlamm sieht sie überhaupt nicht: Die Priorität ist nur h, kein g.
9. A*: das Beste aus beiden Welten
Definition — A*
Eingabe: ein Suchproblem P = (S, s0, A, T, G, c) und eine Heuristik h : S → ℝ≥0. Ausgabe: eine Lösung oder die Meldung „es gibt keine Lösung“.
A* ist die UCS (Schritte 1–5 ihrer Definition oben, einschließlich der Pflege von g), in der die Randmenge F eine Prioritätswarteschlange ist nach
f(n) = g(n) + h(n)
statt nach g(n): In Schritt 3 wird aus F der Zustand mit dem kleinsten f(n) entnommen. Der Wert f(n) ist eine Schätzung der Kosten des ganzen Pfads vom Start über n zum Ziel. Der Algorithmus hält an, sobald er einen Zielzustand aus F entnimmt (oder wenn F leer ist).
Satz. Sei h eine konsistente Heuristik und seien die Aktionskosten ≥ ε für ein ε > 0. Dann fallen die Werte von f entlang jedes Pfads nie, A* expandiert die Zustände in nicht fallender Reihenfolge von f, und der erste expandierte Zielzustand liegt auf einem optimalen Pfad mit den Kosten C*. Dabei expandiert A* jeden Zustand mit f(n) < C* und keinen mit f(n) > C*.
Für h ≡ 0 ist A* dasselbe wie UCS; ohne g wäre es die gierige Suche.
A* addiert für jeden Ort, wie viel du schon bezahlt hast und wie viel du der Schätzung nach noch bezahlen wirst. Dank des ersten Teils fällt A* nicht auf eine teure Abkürzung herein, dank des zweiten verschwendet es keine Zeit mit Wegen, die vom Ziel wegführen. Ist die Schätzung optimistisch, findet A* immer den günstigsten Weg — und untersucht dabei nur, was es untersuchen muss.
f(n) = g(n) + h(n)
- f(n)
- die Priorität in der Randmenge: eine Schätzung der Kosten der ganzen Lösung, die über n führt. Expandiert wird das kleinste, weil das der vielversprechendste Kandidat für das Optimum ist
- g(n)
- was der Weg vom Start nach n tatsächlich schon gekostet hat. Ohne diesen Teil wüsste A* nichts vom Schlamm, durch den es gegangen ist, und verhielte sich wie die gierige Suche
- h(n)
- die Schätzung, wie viel bis zum Ziel noch fehlt. Ohne diesen Teil wüsste A* nicht, wo das Ziel liegt, und breitete sich wie die UCS in alle Richtungen aus
Satz — mit einer konsistenten Heuristik fällt f entlang eines Pfads nicht
Sei h eine konsistente Heuristik, f = g + h, n ∈ S, a ∈ A(n), n′ = T(n, a), und der Pfad nach n′ führe über n, sodass g(n′) = g(n) + c(n, a). Dann gilt f(n′) ≥ f(n).
Beweis. f(n′) =g(n) + c(n, a) + h(n′) ≥ g(n) + h(n) = f(n),
wobei die Ungleichung genau die Konsistenz h(n) ≤ c(n, a) + h(n′) ist. ∎
Daraus folgt (hier nur skizziert), dass A* die Zustände in nicht fallender Reihenfolge von f expandiert: Würde es später einen Zustand mit kleinerem f expandieren, müsste zu ihm von der Randmenge aus ein Zustand mit noch kleinerem f führen, der früher an der Reihe gewesen wäre. Wenn also zum ersten Mal das Ziel expandiert wird (wo h = 0 und f = g), bleibt kein günstigerer Pfad mehr übrig.
Wie A* vorgeht, zeigt die Tabelle der ersten Schritte. Bei jedem expandierten Feld stehen g, h und f, rechts die Randmenge nach der Expansion in der Reihenfolge, in der die Warteschlange sie herausgibt (die Zahl hinter dem Feld ist sein f; bei Gleichstand kommt zuerst, was früher hinzukam; Farben nach der Formel oben):
| Schritt | expandiert | g | h | f | Randmenge danach |
|---|---|---|---|---|---|
| 1 | (0, 0) | 0 | 21 | 21 | (0, 1) 21, (1, 0) 21 |
| 2 | (0, 1) | 1 | 20 | 21 | (1, 0) 21, (0, 2) 21 |
| 3 | (1, 0) | 1 | 20 | 21 | (0, 2) 21, (2, 0) 21 |
| 4 | (0, 2) | 2 | 19 | 21 | (2, 0) 21, (0, 3) 21 |
| 5 | (2, 0) | 2 | 19 | 21 | (0, 3) 21, (3, 0) 21 |
| 6 | (0, 3) | 3 | 18 | 21 | (3, 0) 21, (1, 3) 21 |
| … | … | ||||
| 23 | (8, 9) | 17 | 4 | 21 | (2, 2) 23, (8, 1) 23, (7, 5) 23, (7, 9) 23, (6, 3) 25, (5, 2) 27 |
| 24 | (2, 2) | 6 | 17 | 23 | (8, 1) 23, (7, 5) 23, (7, 9) 23, (3, 2) 23, (6, 3) 25, (5, 2) 27 |
| 25 | (8, 1) | 11 | 12 | 23 | (7, 5) 23, (7, 9) 23, (3, 2) 23, (6, 3) 25, (8, 0) 25, (5, 2) 27 |
Die ersten 23 Schritte haben f = 21: A* geht über Felder, auf denen jeder Schritt näher ans Ziel führt, sodass, was bei g hinzukommt, bei h wegfällt. Auf dem Feld (8, 9) endet der Gang an einer Wand, und erst jetzt kommen die Felder mit f = 23 an die Reihe. Ein Schritt auf trockenem Boden, der sich vom Ziel entfernt, erhöht g um 1 und h ebenfalls um 1, also f um 2; das Umgehen einer Wand hebt f deshalb in Zweierschritten an. Insgesamt hat es 23 Felder mit f = 21, 5 Felder mit f = 23, 9 Felder mit f = 25 expandiert. Genau wie der Satz sagt: Alle 28 Felder mit f < C* = 25 musste es expandieren, von den Feldern mit f = 25 nur so viele, bis es auf das Ziel stieß, und keines mit f > 25.
Dieselben Kosten von 25 wie bei der UCS, aber A* hat 37 Felder untersucht statt 49. In einem kleinen Labyrinth ist das ein Unterschied von ein paar Feldern; auf einer Karte mit Millionen von Kreuzungen ist es der Unterschied zwischen einer sofortigen Antwort und einer Minute Warten. Deshalb findest du A* (und seine Varianten) in Navigationen, in Spielen und in der Bewegungsplanung von Robotern.
import heapq
def a_star(start, goal, successors, h):
frontier = [(h(start), 0, start)] # (f, g, Zustand)
g = {start: 0}
parent = {start: None}
while frontier:
_, cost, state = heapq.heappop(frontier)
if state == goal:
return path(parent, goal), cost
if cost > g[state]:
continue # veralteter Eintrag, wir kennen schon einen günstigeren
for nxt, step in successors(state):
new = cost + step
if new < g.get(nxt, float("inf")):
g[nxt] = new
parent[nxt] = state
heapq.heappush(frontier, (new + h(nxt), new, nxt))
return None
10. Vergleich der Algorithmen
Definition — Eigenschaften eines Algorithmus
Sei 𝒜 ein Algorithmus, der als Eingabe ein Suchproblem P bekommt und eine Lösung oder die Meldung „es gibt keine Lösung“ zurückgibt. Der Algorithmus 𝒜 ist
- vollständig genau dann, wenn 𝒜 für jedes Problem P, das mindestens eine Lösung hat (Π ≠ ∅), nach endlich vielen Schritten anhält und eine Lösung π ∈ Π zurückgibt;
- optimal genau dann, wenn er für jedes Problem P mit Π ≠ ∅ eine Lösung π mit den Kosten g(π) = C* zurückgibt.
Die Parameter, von denen Zeit und Speicher abhängen (für ein Problem P = (S, s0, A, T, G, c)):
- b = maxs ∈ S |A(s)| — der Verzweigungsfaktor;
- d — die Tiefe der flachsten Lösung (die kleinste Länge eines Pfads von s0 nach G);
- m — die größte Tiefe des Raums (die größte Länge eines Pfads von s0 ohne wiederholte Zustände, gegebenenfalls ∞);
- C* — die optimalen Kosten und ε > 0 — eine untere Schranke der Aktionskosten.
Für Funktionen u, v ≥ 0 dieser Parameter schreiben wir u = O(v) genau dann, wenn es Konstanten K > 0 und x0 gibt, sodass u(x) ≤ K · v(x) für alle x ≥ x0. O(·) gibt also an, wie Zeit (die Zahl der erzeugten Zustände) oder Speicher (die Zahl der gleichzeitig gehaltenen Zustände) mit diesen Parametern höchstens wachsen, bis auf einen konstanten Faktor.
Vollständig = wenn es einen Weg gibt, findet er ihn. Optimal = er findet den günstigsten. O(bd) bedeutet „mit jedem weiteren Schritt in die Tiefe vervielfacht sich die Arbeit“ — deshalb kommt es so sehr auf den Speicher an.
| Algorithmus | Vollständig | Optimal | Zeit | Speicher | In unserem Labyrinth |
|---|---|---|---|---|---|
| BFS | ja | bei gleichen Kosten | O(bd) | O(bd) | 75 Felder, Kosten 37 |
| DFS | nein* | nein | O(bm) | O(b·m) | 32 Felder, Kosten 59 |
| IDDFS | ja | bei gleichen Kosten | O(bd) | O(b·d) | — |
| UCS | ja | ja | O(b1+⌊C*/ε⌋) | O(b1+⌊C*/ε⌋) | 49 Felder, Kosten 25 |
| Gierig | nein* | nein | O(bm) | O(bm) | 33 Felder, Kosten 59 |
| A* | ja | ja** | abhängig von der Heuristik | abhängig von der Heuristik | 37 Felder, Kosten 25 |
* In einem endlichen Raum mit Kontrolle wiederholter Zustände finden sie immer eine Lösung. ** Mit einer konsistenten Heuristik.
11. Lokale Suche: wenn es nicht um den Weg geht
Bei manchen Aufgaben interessiert uns der Weg nicht — wir wollen nur einen guten Endzustand. Einen Stundenplan, die Platzierung von Bauteilen auf einer Platine, die Einstellung von Parametern.
Definition — lokale Suche
Sei S eine nichtleere Menge von Zuständen, f : S → ℝ eine Funktion (die Zielfunktion) und N : S → 2S eine Abbildung, die jedem Zustand s ∈ S eine endliche nichtleere Menge N(s) ⊆ S ∖ {s} zuordnet (die Nachbarschaft des Zustands s, ihre Elemente sind die Nachbarn). Das Problem der lokalen Suche besteht darin, einen Zustand
s* ∈ argmaxs ∈ S f(s) = {s ∈ S : ∀s′ ∈ S : f(s) ≥ f(s′)}
zu finden, oder zumindest einen Zustand mit möglichst großem Wert von f. Ein Zustand s ∈ S ist
- ein lokales Maximum genau dann, wenn ∀s′ ∈ N(s) : f(s) ≥ f(s′);
- ein globales Maximum genau dann, wenn ∀s′ ∈ S : f(s) ≥ f(s′).
Jedes globale Maximum ist auch ein lokales, umgekehrt gilt das nicht.
Bergsteigeralgorithmus (hill climbing). Eingabe: S, f, N wie oben und ein Anfangszustand s ∈ S. Ausgabe: ein lokales Maximum.
- Wähle s′ ∈ argmaxt ∈ N(s) f(t) (den besten Nachbarn).
- Ist f(s′) ≤ f(s), halte an und gib s zurück — es ist ein lokales Maximum.
- Sonst s ← s′ und fahre mit Schritt 1 fort.
Die Zielfunktion sagt, wie gut eine Lösung ist — je höher, desto besser. Die Nachbarschaft sind die Lösungen, die du durch eine kleine Änderung bekommst. Der Bergsteiger steht im Nebel auf einem Hügel, sieht nur ein Stück um sich herum und geht immer dorthin, wo es bergauf geht. Wenn es ringsum überall bergab geht, bleibt er stehen — auch wenn ganz in der Nähe ein viel höherer Berg stehen kann.
def hill_climbing(s, neighbours, f):
while True:
best = max(neighbours(s), key=f) # der höchste Nachbar
if f(best) <= f(s):
return s # ringsum ist nichts höher: ein Gipfel (vielleicht nur ein lokaler)
s = best
Beispiel — der Bergsteiger aus der Abbildung Schritt für Schritt
Ein Zustand ist eine Zahl s ∈ [0, 1], die Nachbarschaft sind die zwei Punkte 0,02 links und rechts: N(s) = {s − 0,02, s + 0,02}. Die Landschaft hat zwei Gipfel (einen lokalen bei s = 0,25, den globalen bei 0,72), und der Bergsteiger startet bei s = 0,05.
| Schritt | s | f(s − 0,02) | f(s) | f(s + 0,02) |
|---|---|---|---|---|
| 0 | 0,05 | 0,060 | 0,070 | 0,087 |
| 1 | 0,07 | 0,070 | 0,087 | 0,115 |
| 2 | 0,09 | 0,087 | 0,115 | 0,157 |
| 3 | 0,11 | 0,115 | 0,157 | 0,216 |
| 4 | 0,13 | 0,157 | 0,216 | 0,289 |
| 5 | 0,15 | 0,216 | 0,289 | 0,373 |
| 6 | 0,17 | 0,289 | 0,373 | 0,457 |
| 7 | 0,19 | 0,373 | 0,457 | 0,531 |
| 8 | 0,21 | 0,457 | 0,531 | 0,582 |
| 9 | 0,23 | 0,531 | 0,582 | 0,600 |
| 10 | 0,25 | 0,582 | 0,600 | 0,582 |
In jeder Zeile ist der rechte Nachbar der höchste, also geht es nach rechts. In der letzten Zeile sind beide Nachbarn (0,582) niedriger als f(0,25) = 0,600, und der Algorithmus hält an — auf einem lokalen Maximum, obwohl das globale Maximum f(0,72) = 0,950 hat.
Gegen das Hängenbleiben helfen zufällige Neustarts (den Bergsteiger an verschiedenen Stellen loslassen), genetische Algorithmen, die eine ganze Population von Lösungen kreuzen und mutieren, oder simulierte Abkühlung:
Definition — simulierte Abkühlung
Eingabe: S, f, N wie bei der lokalen Suche, ein Anfangszustand s ∈ S, eine Anfangstemperatur T0 > 0, ein Abkühlungskoeffizient q ∈ (0, 1) und eine Mindesttemperatur Tmin ∈ (0, T0). Ausgabe: ein Zustand s ∈ S.
- T ← T0.
- Ist T ≤ Tmin, halte an und gib s zurück.
- Wähle zufällig (gleichverteilt) einen Nachbarn s′ ∈ N(s) und berechne Δ = f(s′) − f(s).
- Ist Δ > 0, setze s ← s′. Ist Δ ≤ 0, setze s ← s′ mit der Wahrscheinlichkeit exp(Δ/T), sonst behalte s.
- T ← q · T und fahre mit Schritt 2 fort.
Die „Temperatur“ T sinkt also allmählich gegen null und mit ihr die Bereitschaft, einen schlechteren Nachbarn anzunehmen. (Die Abkühlung T ← q · T ist die gebräuchlichste Wahl, es werden auch andere Abkühlungspläne verwendet.)
Am Anfang erlaubt sich der Bergsteiger ab und zu, auch bergab zu gehen — so kommt er von einem kleinen Gipfel zu einem größeren. Je länger er sucht, desto vorsichtiger wird er, bis er am Ende nur noch steigt.
P(s′ annehmen) = exp(Δ/T), falls Δ ≤ 0
- P(s′ annehmen)
- die Wahrscheinlichkeit, dass ein schlechterer Nachbar trotzdem angenommen wird — dank ihr kann man von einem lokalen Gipfel wieder absteigen
- exp
- die Exponentialfunktion exp(x) = ex macht aus einer beliebigen negativen Zahl einen Wert zwischen 0 und 1, also eine gültige Wahrscheinlichkeit; übernommen aus der Physik, wo dieselbe Form (der Boltzmann-Faktor) angibt, wie wahrscheinlich ein Zustand mit höherer Energie ist
- Δ = f(s′) − f(s)
- um wie viel der Nachbar schlechter ist (eine negative Zahl). Je schlechter, desto geringer die Chance: Eine kleine Verschlechterung wird leicht angenommen, ein großer Absturz fast nie
- T
- die Temperatur — wie viel Risiko wir eingehen wollen. Ein hohes T verwischt die Unterschiede (der Exponent liegt nahe bei 0, P nahe bei 1), ein niedriges vergrößert sie
def annealing(s, neighbour, f, T=1.0, cooling=0.995, T_min=1e-3):
while T > T_min:
s2 = neighbour(s) # ein zufälliger Nachbar, nicht der beste
delta = f(s2) - f(s)
if delta > 0 or random() < exp(delta / T):
s = s2 # besser immer, schlechter nur manchmal
T *= cooling # die Temperatur sinkt langsam
return s
Und hier schließt sich der Kreis zum maschinellen Lernen. Das Training eines neuronalen Netzes ist lokale Suche im Raum seiner Gewichte θ: Der Gradientenabstieg wiederholt θ ← θ − η ∇L(θ) — einen Schritt der Größe η in die Richtung, in der die Verlustfunktion L am schnellsten fällt. Er ist ein auf den Kopf gestellter Bergsteiger, der statt des höchsten Gipfels das tiefste Tal sucht — und mit denselben Problemen kämpft: lokalen Minima und Plateaus.
12. Zusammenfassung
- Ein Suchproblem ist ein Sechstupel (S, s0, A, T, G, c). Eine Lösung ist ein Pfad im Zustandsgraphen, die optimale Lösung der günstigste davon.
- Die Algorithmen unterscheiden sich nur darin, welchen Zustand sie aus der Randmenge wählen: Warteschlange (BFS), Stapel (DFS), kleinstes g (UCS), kleinstes h (gierig), kleinstes g + h (A*).
- BFS findet die wenigsten Schritte, UCS die geringsten Kosten, A* die geringsten Kosten und untersucht dabei am wenigsten — sofern es eine konsistente Heuristik hat.
- Wenn es nicht um den Weg, sondern um das Ergebnis geht, kommt die lokale Suche ins Spiel — und von dort ist es nur noch ein Schritt zum Gradientenabstieg und zum maschinellen Lernen.
Willst du die Algorithmen selbst ausprobieren? Lass einen KI-Agenten deine eigene Visualisierung schreiben — und such dann im Code die eine Zeile, in der sie sich unterscheiden.
Siehst du das anders? Schreib an support@coden-app.com.