BFS: die wenigsten Haltestellen
Kurz gesagt: BFS (Breitensuche) erkundet einen Graphen Schicht für Schicht mit einer Warteschlange und findet den Weg mit den wenigsten Kanten in O(V + E).
Problem
Sechs Straßen statt vier
Der Disponent des Kurierdienstes will eine Route vom Lager zum Krankenhaus mit den wenigsten Straßen. In dieser Stadt bedeutet jede Straße eine Ampel, eine Abbiegung und einen Halt, darum zählt die Zahl der Straßen mehr als die Meter.
Ein Kollege hat eine Suche geschrieben, die die erste freie Straße nimmt und weitermacht, bis sie ankommt. Bleibt sie stecken, geht sie zurück und probiert die nächste:
def any_route(graph, at, target, seen): if at == target: return [at] seen.add(at) for nxt in graph[at]: if nxt in seen: continue rest = any_route(graph, nxt, target, seen) if rest is not None: return [at] + rest return Noneroute = any_route(graph, "Lager", "Krankenhaus", set())print(" → ".join(route) + ": " + str(len(route) - 1) + " Straßen")Lager → Bahnhof → Platz → Markt → Park → Stadion → Krankenhaus: 6 StraßenEs gibt eine Route, aber sie hat 6 Straßen. Sieh dir die Karte aus der vorigen Lektion an: Lager → Bahnhof → Platz → Kino → Krankenhaus sind 4. Die Suche hat keinen Fehler gemacht: Sie hat nie nach der kürzesten gefragt, sie hat die erste genommen, die sie fand.
Die Idee
Eine Welle vom Lager
Stell dir eine Welle vor, die das Lager verlässt. Zuerst erreicht sie alle Haltestellen, die eine Straße entfernt sind. Dann von ihnen aus alle neuen Haltestellen, die zwei Straßen entfernt sind. Dann drei. Eine Haltestelle, die die Welle erreicht hat, ist vergeben: Die nächsten Wellen fassen sie nicht an.
Eine Haltestelle bekommt ihre Entfernung also in dem Moment, in dem die Welle sie zum ersten Mal berührt, und das ist die kleinste mögliche.
Die Reihenfolge „erst die näheren“ hält eine Warteschlange: Gefundene Haltestellen reihen sich hinten ein, und die, die gerade bearbeitet wird, geht vorn weg. Das ist BFS (Breitensuche, englisch breadth-first search).
distspeichert die Entfernung jeder gefundenen Haltestelle. Es ist zugleich die Menge der gesehenen Haltestellen: Eine Haltestelle mit einemdistkommt nicht ein zweites Mal in die Warteschlange.prevspeichert, von wo jede Haltestelle gefunden wurde. Damit wird später die Route selbst aufgebaut.
Sieh es dir an
Schicht für Schicht
from collections import deque
def bfs(graph, start): dist = {start: 0} prev = {} queue = deque([start]) while queue: stop = queue.popleft() for nxt in graph[stop]: if nxt in dist: continue dist[nxt] = dist[stop] + 1 prev[nxt] = stop queue.append(nxt) return dist, prevdef bfs(graph, start): dist = {start: 0} prev = {} queue = deque([start]) while queue: stop = queue.popleft() for nxt in graph[stop]: if nxt in dist: continue dist[nxt] = dist[stop] + 1 prev[nxt] = stop queue.append(nxt) return dist, prev
def path_to(prev, target): path = [] at = target while at is not None: path.append(at) at = prev.get(at) path.reverse() return pathSchritt für Schritt, in Worten
- Wir starten bei Lager: dist = 0, und es ist die einzige Haltestelle in der Warteschlange.
- Lager verlässt die Warteschlange. Die Nachbarn Bahnhof und Markt sind neu: dist = 1, prev = Lager, und sie reihen sich hinten ein.
- Bahnhof: Lager hat schon ein dist und wird übersprungen. Platz bekommt dist = 2.
- Markt sieht auch Platz, aber Platz ist schon mit dist = 2 in der Warteschlange. Jede Haltestelle kommt nur einmal hinein. Park: dist = 2.
- Schicht 2 verlässt die Warteschlange und öffnet Schicht 3: Kino hinter der Brücke und Stadion. In der Warteschlange stehen immer höchstens zwei Schichten, die nähere vorn.
- Kino findet Krankenhaus: dist = 4. Erreicht auch Stadion sie, hat Krankenhaus schon ein dist: Die erste Ankunft ist die kürzeste.
- Um Krankenhaus gibt es nichts Neues. Die Warteschlange ist leer: Jede Haltestelle hat ihre Entfernung vom Lager.
- pathTo geht entlang prev zurück: Krankenhaus ← Kino ← Platz ← Bahnhof ← Lager. Umgedreht ist das die Route: 4 Straßen.
Das Programm lässt BFS vom Lager laufen und gibt die Entfernung zu jeder Haltestelle und die Route zum Krankenhaus aus:
dist, prev = bfs(graph, "Lager")for stop in stops: print(stop + ": " + str(dist[stop]))path = path_to(prev, "Krankenhaus")print(" → ".join(path) + ": " + str(len(path) - 1) + " Straßen")Lager: 0Bahnhof: 1Markt: 1Platz: 2Park: 2Kino: 3Krankenhaus: 4Stadion: 3Lager → Bahnhof → Platz → Kino → Krankenhaus: 4 Straßendeque aus collections ist eine Warteschlange: append legt hinten ab, popleft nimmt vorn weg. Wie die Warteschlange innen funktioniert, steht in der Lektion Stapel und Warteschlange.
Probier es aus
Bring die Zeilen in die richtige Reihenfolge
Das ist das Herz von BFS: ein Durchlauf der Schleife. Bring die Zeilen in die richtige Reihenfolge:
prev[nxt] = stopfor nxt in graph[stop]:dist[nxt] = dist[stop] + 1stop = queue.popleft()queue.append(nxt)continueif nxt in dist:
Probier es aus
Sag die Ausgabe voraus
Dasselbe BFS, aber es startet beim Kino. Was gibt das Programm für Lager, Stadion und Park aus?
dist, prev = bfs(graph, "Kino")print(*(dist[s] for s in ["Lager", "Stadion", "Park"]))Die Idee
Die Route rückwärts
dist sagt, wie viele Straßen es sind, aber nicht, welche. Die Route steckt in prev: Das Krankenhaus wurde vom Kino aus gefunden, das Kino vom Platz, der Platz vom Bahnhof, der Bahnhof vom Lager. Das Lager hat kein prev, weil dort alles begann.
Darum wird die Route von hinten gelesen: vom Ziel entlang prev bis zum Start und am Ende umgedreht.
def path_to(prev, target): path = [] at = target while at is not None: path.append(at) at = prev.get(at) path.reverse() return pathat = target
while at is not None:
path.append(at)
at = Die nächste Haltestelle zurück ist die, von der aus die aktuelle gefunden wurde: prev.get(at). Am Start gibt es kein prev, der Wert ist None, und die Schleife hört auf.
Genauer, für Neugierige
Die Wege in prev bilden einen Baum: Jede Haltestelle zeigt auf genau eine andere, die, von der aus sie gefunden wurde, und die Wurzel ist das Lager. Das heißt BFS-Baum. Jeder Ast von der Wurzel zu einer Haltestelle ist die kürzeste Route dorthin. Darum liefert ein BFS vom Lager aus die kürzesten Routen zu allen Haltestellen auf einmal, nicht nur zum Krankenhaus.
Wurde eine Haltestelle nie gefunden (es gibt keine Route), hat sie kein dist. Prüfe das, bevor du path_to aufrufst: Sonst bekommst du eine „Route“ aus einer Haltestelle, dem Ziel selbst.
Die Idee
Warum die erste Ankunft die kürzeste ist
Die Warteschlange enthält höchstens zwei Schichten: d und d + 1, die nähere vorn. Eine Haltestelle, die d + 1 Straßen entfernt ist, wird von einer gefunden, die d Straßen entfernt ist, und Haltestellen mit d + 2 kommen erst, wenn die mit d + 1 anfangen zu gehen. Eine Haltestelle bekommt ihr dist also von der kürzesten Route, und jede spätere Route zu ihr ist mindestens genauso lang und wird übersprungen.
Wichtig ist auch, wann eine Haltestelle als gesehen markiert wird: wenn sie in die Warteschlange kommt, nicht wenn sie sie verlässt. Sonst käme der Platz, den man sowohl vom Bahnhof als auch vom Markt aus sieht, zweimal hinein.
Genauer, für Neugierige
Der Beweis in einem Satz: durch Induktion über d verlassen alle Haltestellen mit Entfernung d die Warteschlange vor allen mit Entfernung d + 1, und jede bekommt dist = d. Das gilt nur, wenn jede Kante 1 kostet. Haben die Straßen verschiedene Längen, heißt weniger Straßen nicht weniger Meter. Dann wird die Warteschlange durch eine Prioritätswarteschlange nach der zurückgelegten Strecke ersetzt, und das ist Dijkstra (die Dijkstra-Lektion in diesem Modul).
Die Idee
Was es kostet
Jede Haltestelle kommt höchstens einmal in die Warteschlange und verlässt sie einmal. Beim Verlassen sieht sie sich ihre Straßen an. Eine Straße in beide Richtungen wird von beiden Enden aus gesehen. Insgesamt: V Abgänge und 2E Blicke, O(V + E).
Das gilt für eine Adjazenzliste. Bei einer Matrix ist „wer sind die Nachbarn“ die ganze Zeile, und BFS macht V Blicke pro Haltestelle: O(V²). Lass beide auf einer Stadt mit n Kreuzungen im Raster laufen:
- BFS mit einer Adjazenzliste
O(n) - BFS mit einer Matrix
O(n²)
| Algorithmus | n = 10 | n = 1.000 | n = 5.000 | n = 100.000 |
|---|---|---|---|---|
| BFS mit einer Adjazenzliste | 36 | 4.872 | 24.716 | 498.734 |
| BFS mit einer Matrix | 110 | 1.001.000 | 25.005.000 | ≈ 10.002.000.000 |
def bfs(graph, start): dist = {start: 0} prev = {} queue = deque([start]) while queue: stop = queue.popleft() for nxt in graph[stop]: if nxt in dist: continue dist[nxt] = dist[stop] + 1 prev[nxt] = stop queue.append(nxt) return dist, prevGezählt wird jede Haltestelle, die die Warteschlange verlässt, und jeder Blick auf einen Nachbarn. Bei 1000 Kreuzungen macht die Liste 4872 Operationen und die Matrix 1.001.000. Bei 100.000 Kreuzungen macht die Liste 498.734, etwa 5 pro Kreuzung: ein Abgang und bis zu 4 Straßen. Die Matrix liegt bei 5000 schon bei 25.005.000.
Probier es aus
Finde den Fehler
Ein Kollege hat entschieden, dass die Entfernung einer neuen Haltestelle „die nächste Zahl“ ist. Das Programm gibt aus:
def bfs(graph, start): dist = {start: 0} prev = {} queue = deque([start]) while queue: stop = queue.popleft() for nxt in graph[stop]: if nxt in dist: continue dist[nxt] = len(dist) prev[nxt] = stop queue.append(nxt) return dist, prevLager: 0Bahnhof: 1Markt: 2Platz: 3Park: 4Kino: 5Krankenhaus: 7Stadion: 6Lager → Bahnhof → Platz → Kino → Krankenhaus: 4 StraßenDie Route stimmt, 4 Straßen, und doch kommt das Krankenhaus mit 7 heraus. In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
Die Entfernung einer neuen Haltestelle ist die Entfernung der, von der aus du sie gefunden hast, plus eine Straße: dist[stop] + 1. Die Zahl der gefundenen Haltestellen ist nur ihre Position in der Warteschlange. Beide stimmen nur überein, solange jede Schicht eine Haltestelle hat.
Genauer
Die Warteschlange in jeder Sprache
collections.deque nimmt vorn in O(1) weg. list.pop(0) verschiebt alle übrigen Elemente um einen Platz: O(n) bei jedem Entnehmen, und BFS wird selbst mit einer Adjazenzliste O(V²). Das ist der häufigste Grund für ein langsames BFS in Python. queue.Queue ist für Threads gedacht und wegen der Sperren langsamer.
Übung
Die Schichten der Stadt
Löse die Aufgabe in deinem Editor.
Schichten und Brücken. Gib mit BFS vom Lager aus die Haltestellen nach Schichten aus (die Haltestellen jeder Schicht in der Reihenfolge von stops) und die am weitesten entfernte Haltestelle. Dann sperre beide Brücken: Entferne sie aus der Adjazenzliste, lass BFS noch einmal laufen und gib aus, welche Haltestellen nicht mehr erreichbar sind.
bridges = [ ("Markt", "Park"), ("Platz", "Kino"),]Schicht 0: LagerSchicht 1: Bahnhof, MarktSchicht 2: Platz, ParkSchicht 3: Kino, StadionSchicht 4: KrankenhausAm weitesten: Krankenhaus (4 Straßen)Ohne die Brücken nicht erreichbar: Park, Kino, Krankenhaus, StadionWeitere Aufgaben
- Die nächste Apotheke. Einige Haltestellen haben eine Apotheke. Lass ein BFS laufen, das von allen Apotheken gleichzeitig startet (alle stehen mit
dist0 in der Warteschlange), und gib für jede Haltestelle aus, wie viele Straßen die nächste entfernt ist. - Ein Labyrinth aus Regalen. Das Lager ist ein Raster aus Regalen (
#) und Gängen (.). Finde die wenigsten Schritte vom Eingang zum Ausgang, wobei die Nachbarn einer Zelle oben, unten, links und rechts sind. - Alle kürzesten. Zähle, wie viele verschiedene kürzeste Routen es vom Lager zu jeder Haltestelle gibt.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Big-O: wie die Arbeit wächst · Speicherkomplexität · Stapel und Warteschlange · Graph: Adjazenzliste und Matrix
- Geht weiter in: DFS: Inseln und Zyklen · Topologische Sortierung · Dijkstra · Navigation in der Stadt
- Aufgaben zum Thema: Graphen