Navigation in der Stadt
Kurz gesagt: Ein Algorithmen-Projekt: Ein Programm liest die Stadtkarte als Graph, findet mit BFS die Route mit den wenigsten Straßen und mit Dijkstra die schnellste und zeigt, wo sie sich unterscheiden.
Problem
Der Kurier fragt nach einer Route
Der Kurierdienst aus dem Modul „Graphen“ hat ein Lager in der Stadt. Die Karte ist eine Textdatei mit einer Straße pro Zeile: von wo, nach wo und wie viele Minuten man darauf fährt. - ist eine Straße in beide Richtungen, -> eine Einbahnstraße:
Lager - Bahnhof 4Lager - Markt 3Bahnhof - Platz 3Markt - Platz 2Markt -> Park 4Platz - Kino 5Kino - Krankenhaus 3Park - Stadion 2Stadion - Krankenhaus 2Lager - Stadion 16Der Disponent braucht eine Route vom Lager zum Krankenhaus. Der Kurier auf dem Fahrrad fragt nach der Route mit den wenigsten Kreuzungen, denn an jeder hält er an. Der Kurier im Lieferwagen fragt nach der schnellsten. Die Antworten sind nicht dieselben.
Dieses Projekt ist das Programm, das die Karte liest und beide Fragen beantwortet. Du baust es in Schritten, jeder mit einer Ausgabe, mit der du deine vergleichen kannst.
Die Idee
Der Plan
- Die Karte als Graph. Kreuzungen sind Knoten, Straßen sind Kanten mit Gewicht: den Minuten. Wie in „Graph: Adjazenzliste und Matrix“ hältst du eine Adjazenzliste: eine Map von einer Kreuzung zu einer Liste von Straßen.
- Wenigste Straßen. BFS vom Lager aus in Schichten: zuerst die Kreuzungen, die eine Straße entfernt sind, dann zwei.
- Die Minuten einer Route. Die Summe der Minuten ihrer Straßen, um die beiden Antworten zu vergleichen.
- Am schnellsten. Dijkstra mit einer Prioritätswarteschlange: Sie nimmt immer die Kreuzung mit den bisher wenigsten Minuten heraus.
- Der Rückweg. Dasselbe Programm vom Krankenhaus zum Lager. Die Einbahnstraße macht den Rückweg anders.
Das Programm liest die Karte von der Standardeingabe, damit es mit jeder Karte funktioniert. Du startest es mit der Datei: java Main.java < map.txt.
Übung
Schritt 1: die Karte
Lies die Karte in eine Map von einer Kreuzung zu einer Liste von Straßen (zu welcher Kreuzung und wie viele Minuten). Eine Straße in beide Richtungen kommt in die Listen beider Enden, eine Einbahnstraße nur in die Liste ihres Anfangs. Eine Kreuzung ohne ausgehende Straßen muss ebenfalls in der Map sein, mit einer leeren Liste.
Gib die Zahl der Kreuzungen und die Nachbarn von Lager, Markt und Park aus, in der Reihenfolge der Zeilen in der Datei:
Kreuzungen: 8Lager: Bahnhof 4, Markt 3, Stadion 16Markt: Lager 3, Platz 2, Park 4Park: Stadion 2Lösung
record Street(String to, int minutes) {}
record Stop(String place, int minutes) {}
static void addStreet(Map<String, List<Street>> city, String a, String b, int minutes, boolean twoWay) { city.computeIfAbsent(a, k -> new ArrayList<>()).add(new Street(b, minutes)); city.computeIfAbsent(b, k -> new ArrayList<>()); if (twoWay) city.get(b).add(new Street(a, minutes));}
static Map<String, List<Street>> readMap(String text) { Map<String, List<Street>> city = new HashMap<>(); for (String line : text.strip().split("\n")) { String[] p = line.trim().split("\\s+"); addStreet(city, p[0], p[2], Integer.parseInt(p[3]), p[1].equals("-")); } return city;}
static String neighbors(Map<String, List<Street>> city, String place) { List<String> parts = new ArrayList<>(); for (Street s : city.get(place)) parts.add(s.to() + " " + s.minutes()); return place + ": " + String.join(", ", parts);}
String text = new String(System.in.readAllBytes(), StandardCharsets.UTF_8);Map<String, List<Street>> city = readMap(text);System.out.println("Kreuzungen: " + city.size());System.out.println(neighbors(city, "Lager"));System.out.println(neighbors(city, "Markt"));System.out.println(neighbors(city, "Park"));import java.io.IOException;, import java.nio.charset.StandardCharsets; und import java.util.*;, und main deklariert throws IOException wegen readAllBytes.Der Park hat einen Nachbarn, das Stadion: Markt -> Park ist eine Einbahnstraße, und der Markt steht nicht in der Liste des Parks.
Übung
Schritt 2: wenigste Straßen
Schreibe fewestStreets(city, start, end): ein BFS von start, das anhält, sobald es end herausnimmt, und die Route als Liste von Kreuzungen zurückgibt. Um die Route zu bauen, merke dir für jede Kreuzung, woher du gekommen bist (prev), gehe dann von end rückwärts und drehe die Liste um.
wenigste Straßen: Lager → Stadion → KrankenhausLösung
static List<String> route(Map<String, String> prev, String end) { List<String> places = new ArrayList<>(); for (String at = end; at != null; at = prev.get(at)) places.add(at); Collections.reverse(places); return places;}
static List<String> fewestStreets( Map<String, List<Street>> city, String start, String end) { Map<String, String> prev = new HashMap<>(); Set<String> seen = new HashSet<>(List.of(start)); Deque<String> queue = new ArrayDeque<>(List.of(start)); while (!queue.isEmpty()) { String at = queue.poll(); if (at.equals(end)) return route(prev, end); for (Street s : city.get(at)) { if (seen.add(s.to())) { prev.put(s.to(), at); queue.add(s.to()); } } } return List.of();}
String text = new String(System.in.readAllBytes(), StandardCharsets.UTF_8);Map<String, List<Street>> city = readMap(text);System.out.println("wenigste Straßen: " + String.join(" → ", fewestStreets(city, "Lager", "Krankenhaus")));Eine Kreuzung wird als gesehen markiert, wenn sie in die Warteschlange kommt, nicht wenn sie herauskommt: Sonst kommt eine Kreuzung mehrmals hinein.
Übung
Schritt 3: wie viele Minuten
Füge minutesOf(city, route) hinzu, das die Minuten der Straßen entlang der Route addiert, und describe(city, route), das die Route, die Zahl der Straßen und die Minuten zurückgibt. Gib die Route aus Schritt 2 mit describe aus:
wenigste Straßen: Lager → Stadion → Krankenhaus (2 Straßen, 18 Min.)Lösung
static int minutesOf(Map<String, List<Street>> city, List<String> route) { int total = 0; for (int i = 1; i < route.size(); i++) { for (Street s : city.get(route.get(i - 1))) { if (s.to().equals(route.get(i))) { total += s.minutes(); break; } } } return total;}
static String describe(Map<String, List<Street>> city, List<String> route) { return String.join(" → ", route) + " (" + (route.size() - 1) + " Straßen, " + minutesOf(city, route) + " Min.)";}
String text = new String(System.in.readAllBytes(), StandardCharsets.UTF_8);Map<String, List<Street>> city = readMap(text);System.out.println("wenigste Straßen: " + describe(city, fewestStreets(city, "Lager", "Krankenhaus")));Zwei Straßen, aber 18 Minuten: sechzehn davon auf der Ringstraße.
Sieh es dir an
Zwei Routen auf einer Karte
static List<String> fastest( Map<String, List<Street>> city, String start, String end) { Map<String, Integer> dist = new HashMap<>(); Map<String, String> prev = new HashMap<>(); PriorityQueue<Stop> queue = new PriorityQueue<>( Comparator.comparingInt(Stop::minutes)); dist.put(start, 0); queue.add(new Stop(start, 0)); while (!queue.isEmpty()) { Stop stop = queue.poll(); String at = stop.place(); if (stop.minutes() > dist.get(at)) continue; if (at.equals(end)) return route(prev, end); for (Street s : city.get(at)) { int t = stop.minutes() + s.minutes(); Integer best = dist.get(s.to()); if (best == null || t < best) { dist.put(s.to(), t); prev.put(s.to(), at); queue.add(new Stop(s.to(), t)); } } } return List.of();}static List<String> fewestStreets( Map<String, List<Street>> city, String start, String end) { Map<String, String> prev = new HashMap<>(); Set<String> seen = new HashSet<>(List.of(start)); Deque<String> queue = new ArrayDeque<>(List.of(start)); while (!queue.isEmpty()) { String at = queue.poll(); if (at.equals(end)) return route(prev, end); for (Street s : city.get(at)) { if (seen.add(s.to())) { prev.put(s.to(), at); queue.add(s.to()); } } } return List.of();}Schritt für Schritt, in Worten
- Die Karte: 8 Kreuzungen und 10 Straßen mit ihren Minuten. Markt → Park ist eine Einbahnstraße, die Ringstraße von Lager bis Stadion dauert 16 Min.
- BFS, eine Straße von Lager entfernt: Bahnhof, Markt, Stadion. Die Minuten interessieren ihn nicht.
- Zwei Straßen entfernt: Platz, Park, Krankenhaus. Krankenhaus ist schon in der Warteschlange, über Stadion.
- Wenigste Straßen: Lager → Stadion → Krankenhaus, 2 Straßen, aber 18 Min.: 16 davon sind die Ringstraße.
- Dijkstra: Die Warteschlange ist nach Minuten geordnet. Lager 0, seine Nachbarn kommen in die Warteschlange: Bahnhof 4, Markt 3, Stadion 16.
- Das Schnellste in der Warteschlange ist Markt, 3 Min. Neu: Platz 5, Park 7.
- Bahnhof, 4 Min.: darüber wird nichts schneller.
- Das Schnellste in der Warteschlange ist Platz, 5 Min. Neu: Kino 10.
- Park mit 7 Min. Darüber sinkt Stadion von 16 auf 9.
- Das Schnellste in der Warteschlange ist Stadion, 9 Min. Neu: Krankenhaus 11.
- Kino, 10 Min.: darüber wird nichts schneller.
- Krankenhaus kommt mit 11 Min. heraus: nichts in der Warteschlange ist schneller, also ist das die Antwort.
- Am schnellsten: Lager → Markt → Park → Stadion → Krankenhaus, 4 Straßen, 11 Min.
- Zwei Antworten auf zwei Fragen: 2 Straßen und 18 Min. oder 4 Straßen und 11 Min. Für den Kurier zählen die Minuten.
BFS sieht nur die Zahl der Straßen und wählt die Ringstraße. Dijkstra nimmt die Kreuzungen nach Minuten heraus und erreicht das Krankenhaus in 11 Min. über den Markt, die Einbahnstraße und das Stadion. Das Stadion hat zuerst 16 Min. über die Ringstraße, dann sinkt es über den Park auf 9.
Übung
Schritt 4: am schnellsten
Schreibe fastest(city, start, end) mit Dijkstra: dist hält die bisher wenigsten Minuten, die Warteschlange ist nach Minuten geordnet, und prev hält die Route. Gib beide Routen aus:
wenigste Straßen: Lager → Stadion → Krankenhaus (2 Straßen, 18 Min.)am schnellsten: Lager → Markt → Park → Stadion → Krankenhaus (4 Straßen, 11 Min.)Lösung
static List<String> fastest( Map<String, List<Street>> city, String start, String end) { Map<String, Integer> dist = new HashMap<>(); Map<String, String> prev = new HashMap<>(); PriorityQueue<Stop> queue = new PriorityQueue<>( Comparator.comparingInt(Stop::minutes)); dist.put(start, 0); queue.add(new Stop(start, 0)); while (!queue.isEmpty()) { Stop stop = queue.poll(); String at = stop.place(); if (stop.minutes() > dist.get(at)) continue; if (at.equals(end)) return route(prev, end); for (Street s : city.get(at)) { int t = stop.minutes() + s.minutes(); Integer best = dist.get(s.to()); if (best == null || t < best) { dist.put(s.to(), t); prev.put(s.to(), at); queue.add(new Stop(s.to(), t)); } } } return List.of();}
String text = new String(System.in.readAllBytes(), StandardCharsets.UTF_8);Map<String, List<Street>> city = readMap(text);System.out.println("wenigste Straßen: " + describe(city, fewestStreets(city, "Lager", "Krankenhaus")));System.out.println("am schnellsten: " + describe(city, fastest(city, "Lager", "Krankenhaus")));PriorityQueue<Stop> mit Comparator.comparingInt(Stop::minutes) nimmt den Eintrag mit den wenigsten Minuten heraus. record Stop ist das Paar aus Kreuzung und Minuten. dist.get kommt in einen Integer, weil es für eine neue Kreuzung null ist.Probier es aus
Ergänze
Eine Kreuzung kann mehrmals in die Warteschlange kommen, wenn die Minuten bis zu ihr sinken. Der alte Eintrag bleibt. Was macht das Programm, wenn es ihn herausnimmt?
if (stop.minutes() > dist.get(at)) ;continue: Die Kreuzung ist schon mit weniger Minuten herausgekommen, und ihre Nachbarn wurden angesehen. Der alte Eintrag wird übersprungen. So musst du nie Einträge aus der Warteschlange löschen.
Probier es aus
Sag den Rückweg voraus
Der Kurier kehrt vom Krankenhaus zum Lager zurück. Das Programm fragt fastest(city, "Krankenhaus", "Lager"). Was gibt es aus?
String text = new String(System.in.readAllBytes(), StandardCharsets.UTF_8);Map<String, List<Street>> city = readMap(text);System.out.println("zurück: " + describe(city, fastest(city, "Krankenhaus", "Lager")));Markt -> Park ist eine Einbahnstraße: Vom Park zum Markt geht es nicht, darum funktioniert die Route von 11 Min. rückwärts nicht. Am schnellsten ist es über das Kino und den Platz: 3 + 5 + 2 + 3 = 13 Min. Die Route über die Ringstraße dauert 18 Min.
Übung
Schritt 5: das ganze Programm
Füge den Rückweg hinzu. Das ganze Programm gibt aus:
wenigste Straßen: Lager → Stadion → Krankenhaus (2 Straßen, 18 Min.)am schnellsten: Lager → Markt → Park → Stadion → Krankenhaus (4 Straßen, 11 Min.)zurück: Krankenhaus → Kino → Platz → Markt → Lager (4 Straßen, 13 Min.)Das ganze Programm
import java.io.IOException;import java.nio.charset.StandardCharsets;import java.util.*;
record Street(String to, int minutes) {}
record Stop(String place, int minutes) {}
public class Main { static void addStreet(Map<String, List<Street>> city, String a, String b, int minutes, boolean twoWay) { city.computeIfAbsent(a, k -> new ArrayList<>()).add(new Street(b, minutes)); city.computeIfAbsent(b, k -> new ArrayList<>()); if (twoWay) city.get(b).add(new Street(a, minutes)); }
static Map<String, List<Street>> readMap(String text) { Map<String, List<Street>> city = new HashMap<>(); for (String line : text.strip().split("\n")) { String[] p = line.trim().split("\\s+"); addStreet(city, p[0], p[2], Integer.parseInt(p[3]), p[1].equals("-")); } return city; }
static List<String> route(Map<String, String> prev, String end) { List<String> places = new ArrayList<>(); for (String at = end; at != null; at = prev.get(at)) places.add(at); Collections.reverse(places); return places; }
static List<String> fewestStreets( Map<String, List<Street>> city, String start, String end) { Map<String, String> prev = new HashMap<>(); Set<String> seen = new HashSet<>(List.of(start)); Deque<String> queue = new ArrayDeque<>(List.of(start)); while (!queue.isEmpty()) { String at = queue.poll(); if (at.equals(end)) return route(prev, end); for (Street s : city.get(at)) { if (seen.add(s.to())) { prev.put(s.to(), at); queue.add(s.to()); } } } return List.of(); }
static int minutesOf(Map<String, List<Street>> city, List<String> route) { int total = 0; for (int i = 1; i < route.size(); i++) { for (Street s : city.get(route.get(i - 1))) { if (s.to().equals(route.get(i))) { total += s.minutes(); break; } } } return total; }
static String describe(Map<String, List<Street>> city, List<String> route) { return String.join(" → ", route) + " (" + (route.size() - 1) + " Straßen, " + minutesOf(city, route) + " Min.)"; }
static List<String> fastest( Map<String, List<Street>> city, String start, String end) { Map<String, Integer> dist = new HashMap<>(); Map<String, String> prev = new HashMap<>(); PriorityQueue<Stop> queue = new PriorityQueue<>( Comparator.comparingInt(Stop::minutes)); dist.put(start, 0); queue.add(new Stop(start, 0)); while (!queue.isEmpty()) { Stop stop = queue.poll(); String at = stop.place(); if (stop.minutes() > dist.get(at)) continue; if (at.equals(end)) return route(prev, end); for (Street s : city.get(at)) { int t = stop.minutes() + s.minutes(); Integer best = dist.get(s.to()); if (best == null || t < best) { dist.put(s.to(), t); prev.put(s.to(), at); queue.add(new Stop(s.to(), t)); } } } return List.of(); }
public static void main(String[] args) throws IOException { String text = new String(System.in.readAllBytes(), StandardCharsets.UTF_8); Map<String, List<Street>> city = readMap(text); System.out.println("wenigste Straßen: " + describe(city, fewestStreets(city, "Lager", "Krankenhaus"))); System.out.println("am schnellsten: " + describe(city, fastest(city, "Lager", "Krankenhaus"))); System.out.println("zurück: " + describe(city, fastest(city, "Krankenhaus", "Lager"))); }}Probier es aus
Finde den Fehler
Ein Kollege hat addStreet gekürzt. Jetzt kommt der Rückweg mit 11 Min. heraus:
zurück: Krankenhaus → Stadion → Park → Markt → Lager (4 Straßen, 11 Min.)Wähle die Zeile mit dem Fehler.
Die Gegenrichtung wird für jede Straße eingetragen, auch für die Einbahnstraße. Markt -> Park wird zur Straße in beide Richtungen, und der Rückweg fährt darauf gegen den Verkehr. Die Gegenstraße darf nur eingetragen werden, wenn twoWay wahr ist.
Sieh es dir an
Miss: die Stadt wächst
Eine echte Stadt hat Tausende Kreuzungen. Das Raster ist ein Geschwindigkeitstest: n Kreuzungen in einem Quadrat, Straßen zwischen Nachbarn, Minuten nach einer Formel. Der Kurier startet in der oberen linken Ecke und fährt zur unteren rechten:
static Map<String, List<Street>> gridCity(int size) { Map<String, List<Street>> city = new HashMap<>(); for (int r = 0; r < size; r++) { for (int c = 0; c < size; c++) { String at = r + ":" + c; if (c + 1 < size) addStreet(city, at, r + ":" + (c + 1), 1 + (r * 7 + c * 13) % 9, true); if (r + 1 < size) addStreet(city, at, (r + 1) + ":" + c, 1 + (r * 11 + c * 5) % 9, true); } } return city;}
Map<String, List<Street>> city = gridCity(100);System.out.println("Kreuzungen: " + city.size());List<String> few = fewestStreets(city, "0:0", "99:99");System.out.println("wenigste Straßen: " + (few.size() - 1) + " Straßen, " + minutesOf(city, few) + " Min.");List<String> fast = fastest(city, "0:0", "99:99");System.out.println("am schnellsten: " + minutesOf(city, fast) + " Min.");Kreuzungen: 10000wenigste Straßen: 198 Straßen, 990 Min.am schnellsten: 693 Min.Lass beide Algorithmen im Browser laufen. BFS sieht sich jede Straße einmal an: O(n). Dijkstra auch, aber jeder Eintrag im Heap kostet O(log n): O(n log n). Bei n = 1000 sind das 3.966 gegen 15.778 Operationen, und bei n = 100.000 sind es 398.158 gegen 2.348.779:
- BFS: die wenigsten Straßen
O(n) - Dijkstra: am schnellsten
O(n log n)
| Algorithmus | n = 10 | n = 1.000 | n = 100.000 |
|---|---|---|---|
| BFS: die wenigsten Straßen | 22 | 3.966 | 398.158 |
| Dijkstra: am schnellsten | 34 | 15.778 | 2.348.779 |
static List<String> fastest( Map<String, List<Street>> city, String start, String end) { Map<String, Integer> dist = new HashMap<>(); Map<String, String> prev = new HashMap<>(); PriorityQueue<Stop> queue = new PriorityQueue<>( Comparator.comparingInt(Stop::minutes)); dist.put(start, 0); queue.add(new Stop(start, 0)); while (!queue.isEmpty()) { Stop stop = queue.poll(); String at = stop.place(); if (stop.minutes() > dist.get(at)) continue; if (at.equals(end)) return route(prev, end); for (Street s : city.get(at)) { int t = stop.minutes() + s.minutes(); Integer best = dist.get(s.to()); if (best == null || t < best) { dist.put(s.to(), t); prev.put(s.to(), at); queue.add(new Stop(s.to(), t)); } } } return List.of();}Genauer, für Neugierige
Dijkstra ist langsamer, aber es beantwortet die Frage nach den Minuten, und BFS kann das nicht. Beide halten an, sobald sie das Ziel herausnehmen: Bei einem nahen Ziel sehen sie sich nur einen kleinen Teil der Stadt an.
Navigation für ganze Länder lässt nicht von jedem Ort aus ein einfaches Dijkstra laufen. Sie verarbeitet die Karte vorab (zum Beispiel mit Contraction Hierarchies) und sucht mit A*, das eine Schätzung hinzufügt, wie weit das Ziel noch entfernt ist. Die Grundlage bleibt Dijkstra.
Übung
Straßenbau
Löse diese Aufgabe in deinem Editor.
Eine gesperrte Straße. Die Straße Park – Stadion ist wegen Bauarbeiten gesperrt. Gib readMap eine Menge gesperrter Straßen ("Park Stadion"), die diese Zeilen in beide Richtungen überspringt. Gib beide Routen vom Lager zum Krankenhaus aus:
wenigste Straßen: Lager → Stadion → Krankenhaus (2 Straßen, 18 Min.)am schnellsten: Lager → Markt → Platz → Kino → Krankenhaus (4 Straßen, 13 Min.)Weitere Aufgaben
- Nur die Minuten. Schreibe eine Variante von
fastest, die die Minuten bis zu jeder Kreuzung zurückgibt, ohne am Ziel anzuhalten. Welche Kreuzung ist am weitesten vom Lager entfernt? - Ohne Kreuzung. Der Platz ist wegen einer Veranstaltung gesperrt. Wie lässt du ihn weg, ohne die Karte anzufassen?
- Eine leere Ringstraße. Nachts ist die Ringstraße leer und dauert 6 Min. Ändere die Minuten in der Karte: Welche Route ist jetzt die schnellste, und ist es dieselbe wie die von BFS?
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Prioritätswarteschlange · Graph: Adjazenzliste und Matrix · BFS: die wenigsten Haltestellen · Dijkstra
- Aufgaben zum Thema: Projekt: Navigation in der Stadt