DFS: Inseln und Zyklen
Kurz gesagt: DFS (Tiefensuche) folgt einem Weg, so weit er reicht, und geht dann zurück, mit Rekursion oder mit einem Stapel; sie findet Zusammenhangskomponenten und Zyklen in einem Graphen in O(V + E).
Problem
Eine Stadt, vom Fluss zerschnitten
Die Stadt sperrt beide Brücken wegen Bauarbeiten. Die Kuriere können den Fluss nicht mehr überqueren, und die Disposition muss wissen, in wie viele Teile die Stadt zerfällt und welche Haltestellen in welchem Teil liegen: Jeder Teil bekommt einen eigenen Lieferwagen.
Aus der BFS-Lektion weißt du, wie man findet, was vom Lager aus erreichbar ist. Es kann aber viele Teile geben, und jeder muss ganz gefunden werden. Du brauchst einen Gang, der bei jeder Haltestelle ohne Teil startet und alles markiert, was er erreicht.
In derselben Stadt gibt es ein zweites Problem. Der Lieferplan hat Regeln „A wird vor B beliefert“. Jemand hat eine falsche Regel dazugeschrieben, und jetzt gibt es keine Reihenfolge, die alle einhält. Welche Regel ist schuld?
Die Idee
So weit die Straße reicht
DFS (Tiefensuche) geht wie ein Mensch im Labyrinth: Sie nimmt die erste Straße zu einer unbekannten Haltestelle, von dort die erste zur nächsten unbekannten, und so weiter, bis sie eine Haltestelle erreicht, um die alles gesehen wurde. Dann geht sie eine Haltestelle zurück und probiert deren nächste Straße.
Bei der Rekursion passiert das Zurückgehen von selbst: visit für eine Haltestelle ruft visit für ihren ersten unbekannten Nachbarn auf, und wenn der fertig ist, läuft die Schleife mit dem nächsten weiter. Der Aufrufstapel merkt sich, wie weit jede Haltestelle ist.
- Jede Haltestelle bekommt die Nummer ihrer Insel, sobald sie betreten wird. Das ist zugleich die Menge der gesehenen Haltestellen: Eine Haltestelle mit Nummer wird nicht ein zweites Mal besucht.
- Eine Insel (eine Zusammenhangskomponente) ist alles, was ein
visiterreicht. Die Schleife über alle Haltestellen startet von jeder Haltestelle ohne Nummer ein neuesvisit.
Sieh es dir an
Hinunter und zurück
static void visit(Map<String, List<String>> graph, String stop, int island, Map<String, Integer> islandOf) { islandOf.put(stop, island); for (String next : graph.get(stop)) { if (!islandOf.containsKey(next)) { visit(graph, next, island, islandOf); } }}
static Map<String, Integer> islands( Map<String, List<String>> graph, String[] stops) { Map<String, Integer> islandOf = new HashMap<>(); int count = 0; for (String stop : stops) { if (islandOf.containsKey(stop)) continue; count++; visit(graph, stop, count, islandOf); } return islandOf;}static void visit(Map<String, List<String>> graph, String stop, int island, Map<String, Integer> islandOf) { islandOf.put(stop, island); for (String next : graph.get(stop)) { if (!islandOf.containsKey(next)) { visit(graph, next, island, islandOf); } }}
static Map<String, Integer> islands( Map<String, List<String>> graph, String[] stops) { Map<String, Integer> islandOf = new HashMap<>(); int count = 0; for (String stop : stops) { if (islandOf.containsKey(stop)) continue; count++; visit(graph, stop, count, islandOf); } return islandOf;}Schritt für Schritt, in Worten
- Beide Brücken sind gesperrt. islands geht die Haltestellen der Reihe nach durch: Lager hat noch keine Insel, also count = 1, und visit startet dort.
- visit(Lager) gibt der Haltestelle Insel 1 und geht sofort in den ersten Nachbarn ohne Insel: Bahnhof. Lager wartet grau auf dem Aufrufstapel.
- Immer tiefer: Bahnhof → Platz → Markt. Vier Aufrufe sind gleichzeitig offen, und jeder wartet auf den darüber.
- Um Markt hat schon alles eine Insel. visit kehrt zurück, und Markt wird schwarz. Zurück im Stapel: Auch bei Platz und Bahnhof gibt es nichts Neues.
- Lager ist wieder oben. Sein letzter Nachbar Markt ist schon gesehen, also endet der erste Gang: Insel 1 ist Lager, Bahnhof, Markt, Platz. Die gesperrten Brücken haben ihn am Fluss gestoppt.
- Die Schleife in islands geht weiter: Bahnhof, Markt und Platz haben schon eine Insel und werden übersprungen. Park hat keine: count = 2.
- Derselbe Gang am anderen Ufer: Park → Stadion → Krankenhaus → Kino, dann zurück zu Park.
- Zwei Inseln: 4 Haltestellen an jedem Ufer. Jede Haltestelle bekam einmal eine Nummer, und jede Straße wurde von beiden Enden angesehen: O(V + E).
Das Programm sperrt die Brücken, startet islands und gibt die Inseln aus:
String[][] closed = { {"Markt", "Park"}, {"Platz", "Kino"},};for (String[] street : closed) { graph.get(street[0]).remove(street[1]); graph.get(street[1]).remove(street[0]);}Map<String, Integer> islandOf = islands(graph, stops);int count = Collections.max(islandOf.values());System.out.println("Inseln: " + count);for (int i = 1; i <= count; i++) { List<String> members = new ArrayList<>(); for (String stop : stops) { if (islandOf.get(stop) == i) members.add(stop); } System.out.println("Insel " + i + ": " + String.join(", ", members));}Inseln: 2Insel 1: Lager, Bahnhof, Markt, PlatzInsel 2: Park, Kino, Krankenhaus, StadionDie Nummer der Insel steht in einer HashMap<String, Integer>, und ihre Zahl ist die größte Nummer darin.
Probier es aus
Sag die Ausgabe voraus
Die Brücke Markt – Park ist offen, aber um das Kino sind Bauarbeiten: Platz – Kino und Kino – Krankenhaus sind gesperrt. Wie viele Inseln gibt das Programm aus?
String[][] closed = { {"Platz", "Kino"}, {"Kino", "Krankenhaus"},};for (String[] street : closed) { graph.get(street[0]).remove(street[1]); graph.get(street[1]).remove(street[0]);}Map<String, Integer> islandOf = islands(graph, stops);int count = Collections.max(islandOf.values());System.out.println("Inseln: " + count);Die Idee
Ein eigener Stapel statt Rekursion
Hier haben die Inseln je 4 Haltestellen, und die Rekursion ist 4 Aufrufe tief. In einer Rasterstadt mit 100.000 Kreuzungen kann ein Gang in die Tiefe zehntausende Ebenen hinabsteigen, und jede Ebene ist ein Frame auf dem Aufrufstapel. Der Stapel ist klein und läuft über. Wie aus der Rekursion eine Schleife wird, steht in der Lektion „Von der Rekursion zur Schleife“. Hier ist es dasselbe: Der Stapel wird zu einer gewöhnlichen Sammlung.
static List<String> walk(Map<String, List<String>> graph, String start) { List<String> order = new ArrayList<>(); Set<String> seen = new HashSet<>(); Deque<String> stack = new ArrayDeque<>(); stack.push(start); while (!stack.isEmpty()) { String stop = stack.pop(); if (!seen.add(stop)) continue; order.add(stop); List<String> next = graph.get(stop); for (int i = next.size() - 1; i >= 0; i--) { if (!seen.contains(next.get(i))) stack.push(next.get(i)); } } return order;}Zwei Dinge sind anders als bei der Rekursion:
- Eine Haltestelle kann zweimal auf den Stapel kommen: von zwei Nachbarn aus, bevor sie besucht wird. Darum wird die Prüfung „schon gesehen?“ auch beim Herausnehmen gemacht: Beim zweiten Mal wird sie übersprungen.
- Die Nachbarn kommen in umgekehrter Reihenfolge hinein. Was zuletzt hineinkommt, geht zuerst heraus, also muss der erste Nachbar oben liegen. So ist die Reihenfolge dieselbe wie bei der Rekursion:
Map<String, Integer> islandOf = new LinkedHashMap<>();visit(graph, "Lager", 1, islandOf);System.out.println("Rekursion: " + String.join(", ", islandOf.keySet()));System.out.println("Eigener Stapel: " + String.join(", ", walk(graph, "Lager")));Rekursion: Lager, Bahnhof, Platz, Markt, Park, Stadion, Krankenhaus, KinoEigener Stapel: Lager, Bahnhof, Platz, Markt, Park, Stadion, Krankenhaus, KinoHashMap merkt sich die Reihenfolge des Einfügens nicht, darum liegt die Insel hier in einer LinkedHashMap: keySet() gibt die Haltestellen in der Reihenfolge ihres Besuchs.Genauer, für Neugierige
Wenn du die Nachbarn in der Reihenfolge der Liste hineinlegst, ist der Gang immer noch eine Tiefensuche und findet dieselben Inseln, nur die Reihenfolge der Besuche ist anders. Für die Inseln spielt die Reihenfolge keine Rolle. Für die Zyklen in einem gerichteten Graphen weiter unten schon: Dort musst du wissen, wann jede Haltestelle fertig wird, und mit einem Stapel aus Haltestellen ist das schwerer. Darum ist findCycle rekursiv, und bei tiefen Graphen hält der Stapel ein Paar (Haltestelle, wie weit sie in ihrer Liste ist).
Die Idee
Ein Zyklus im Lieferplan
Die Regeln des Plans sind ein gerichteter Graph: Der Pfeil Markt → Park heißt „Markt vor Park“. Bilden die Pfeile einen Zyklus, gibt es keine Reihenfolge, die sie einhält: Jede Haltestelle im Zyklus müsste vor sich selbst kommen.
„Gesehen“ allein reicht nicht, um einen Zyklus zu fangen. Markt → Platz führt zu einer gesehenen Haltestelle, aber es gibt keinen Zyklus: Von Platz kommst du nicht zurück zu Markt. Darum haben Haltestellen drei Farben:
- weiß: noch nicht gesehen;
- grau: auf dem aktuellen Pfad, ihr Aufruf ist noch offen;
- schwarz: alles danach ist geprüft, und es gibt keinen Zyklus.
Ein Pfeil auf eine graue Haltestelle führt zurück in den eigenen Pfad: Das ist der Zyklus.
static List<String> findCycle( Map<String, List<String>> plan, String stop, Map<String, String> color, List<String> path) { color.put(stop, "grey"); path.add(stop); for (String next : plan.get(stop)) { String c = color.getOrDefault(next, "white"); if (c.equals("grey")) { var cycle = new ArrayList<String>(path); cycle.subList(0, path.indexOf(next)).clear(); cycle.add(next); return cycle; } if (c.equals("white")) { var found = findCycle(plan, next, color, path); if (found != null) return found; } } color.put(stop, "black"); path.remove(path.size() - 1); return null;}static List<String> findCycle( Map<String, List<String>> plan, String stop, Map<String, String> color, List<String> path) { color.put(stop, "grey"); path.add(stop); for (String next : plan.get(stop)) { String c = color.getOrDefault(next, "white"); if (c.equals("grey")) { var cycle = new ArrayList<String>(path); cycle.subList(0, path.indexOf(next)).clear(); cycle.add(next); return cycle; } if (c.equals("white")) { var found = findCycle(plan, next, color, path); if (found != null) return found; } } color.put(stop, "black"); path.remove(path.size() - 1); return null;}Schritt für Schritt, in Worten
- Jeder Pfeil ist eine Regel: Die Haltestelle, von der er ausgeht, wird zuerst beliefert. Alle Haltestellen sind weiß, keine wurde gesehen. Der Gang startet bei Lager.
- Grau heißt auf dem aktuellen Pfad: Lager → Bahnhof → Platz. Diese Aufrufe sind noch offen.
- Von Platz geht keine Regel aus: Platz wird schwarz, alles danach ist geprüft, und die Haltestelle verlässt den Pfad. Auch Bahnhof ist fertig.
- Auch Markt zeigt auf Platz, aber Platz ist schwarz: Von dort gibt es keinen Weg zurück zu Markt. Zwei Regeln auf dieselbe Haltestelle sind kein Zyklus.
- Hinter dem Fluss wächst der Pfad: Park → Kino → Krankenhaus → Stadion, alle grau.
- Stadion → Park: Park ist grau, also noch auf dem Pfad. Der Pfeil führt zurück in den Pfad selbst, und das ist ein Zyklus.
- Der Zyklus ist der Pfad von Park bis zum Ende: Park → Kino → Krankenhaus → Stadion → Park. Keine Reihenfolge dieser vier Haltestellen hält alle Regeln ein, also muss die Regel Stadion → Park weg.
Die Schleife startet findCycle von jeder weißen Haltestelle, so wie islands visit startet:
static List<String> anyCycle(Map<String, List<String>> plan, String[] stops) { Map<String, String> color = new HashMap<>(); for (String stop : stops) { if (color.containsKey(stop)) continue; var found = findCycle(plan, stop, color, new ArrayList<>()); if (found != null) return found; } return null;}String[][] rules = { {"Lager", "Bahnhof"}, {"Bahnhof", "Platz"}, {"Lager", "Markt"}, {"Markt", "Platz"}, {"Markt", "Park"}, {"Park", "Kino"}, {"Kino", "Krankenhaus"}, {"Krankenhaus", "Stadion"}, {"Stadion", "Park"},};List<String> cycle = anyCycle(plan, stops);System.out.println(cycle == null ? "Kein Zyklus" : "Zyklus: " + String.join(" → ", cycle));Zyklus: Park → Kino → Krankenhaus → Stadion → ParkEntferne die letzte Regel, und das Programm gibt Kein Zyklus aus. Dann ist die Reihenfolge, in der die Haltestellen schwarz werden, die umgekehrte Lieferreihenfolge. Darauf baut die topologische Sortierung auf, die nächste Lektion.
static List<String> findCycle(
Map<String, List<String>> plan, String stop,
Map<String, String> color, List<String> path) {
color.put(stop, "grey");
path.add(stop);
for (String next : plan.get(stop)) {
String c = color.getOrDefault(next, "white");
if (c.equals("grey")) {
var cycle = new ArrayList<String>(path);
cycle.subList(0, path.indexOf(next)).clear();
cycle.add(next);
return cycle;
}
if (c.equals("white")) {
var found = findCycle(plan, next, color, path);
if (found != null) return found;
}
}
color.put(stop, );
path.remove(path.size() - 1);
return null;
}Ist alles nach der Haltestelle geprüft und gibt es keinen Zyklus, wird sie "black": Ein späterer Pfeil auf sie ist kein Zyklus. Bliebe sie grau, hielte man den nächsten Pfeil auf sie für einen Zyklus.
Genauer, für Neugierige
In einem ungerichteten Graphen ist jede Straße ein Pfeil in beide Richtungen, und der Nachbar, von dem du kamst, ist immer „gesehen“. Dort gibt es einen Zyklus, wenn du eine gesehene Haltestelle erreichst, die nicht die ist, von der du gerade kommst. Die drei Farben braucht man für einen gerichteten Graphen.
Die Idee
Was es kostet
Jede Haltestelle bekommt einmal eine Nummer und wird einmal besucht. Beim Besuch sieht sie ihre Straßen an, und eine Straße in beide Richtungen wird von beiden Enden gesehen. Insgesamt: V Besuche und 2E Blicke, O(V + E), wie bei BFS.
Linear ist es nur, wenn die Inselnummer vom ganzen Gang geteilt wird. Fragst du für jede Haltestelle „auf welcher Insel bin ich?“ mit einem neuen Gang und einer leeren Menge gesehener Haltestellen, machst du n Gänge. Lass beide in einer Rasterstadt mit n Kreuzungen laufen, die der Fluss in zwei Teile schneidet:
- Ein gemeinsames islandOf für die ganze Stadt
O(n) - Ein neuer Durchlauf für jede Kreuzung
O(n²)
| Algorithmus | n = 10 | n = 1.000 | n = 2.000 | n = 100.000 |
|---|---|---|---|---|
| Ein gemeinsames islandOf für die ganze Stadt | 28 | 4.808 | 9.730 | 498.100 |
| Ein neuer Durchlauf für jede Kreuzung | 148 | 2.405.440 | 9.730.980 | ≈ 24.327.450.000 |
static Map<String, Integer> islands( Map<String, List<String>> graph, String[] stops) { Map<String, Integer> islandOf = new HashMap<>(); int count = 0; for (String stop : stops) { if (islandOf.containsKey(stop)) continue; count++; visit(graph, stop, count, islandOf); } return islandOf;}Gezählt wird jede Haltestelle, die vom Stapel genommen wird, und jeder Blick auf einen Nachbarn. Beide benutzen einen eigenen Stapel: Bei 100.000 Kreuzungen stürzt die Rekursion im Browser mit einem Stapelüberlauf ab. Bei 1000 Kreuzungen macht die geteilte Nummer 4.808 Operationen und ein neuer Gang für jede Kreuzung 2.405.440. Bei 2000 steht der neue Gang schon bei 9.730.980, während die geteilte Nummer bei 100.000 nur 498.100 braucht.
Probier es aus
Finde den Fehler
Ein Kollege hat entschieden, dass in visit die Insel mit jedem Schritt wachsen soll. Das Programm gibt aus:
static void visit(Map<String, List<String>> graph, String stop, int island, Map<String, Integer> islandOf) { islandOf.put(stop, island); for (String next : graph.get(stop)) { if (!islandOf.containsKey(next)) { visit(graph, next, island + 1, islandOf); } }}Inseln: 5Insel 1: LagerInsel 2: Bahnhof, ParkInsel 3: Platz, StadionInsel 4: Markt, KrankenhausInsel 5: KinoEs sind fünf Inseln, und Bahnhof und Park sind „auf einer Insel“, obwohl die Brücken gesperrt sind. In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
Alle Haltestellen, die ein visit erreicht, liegen auf einer Insel: Die Nummer wird unverändert weitergegeben. Mit island + 1 wird die Nummer zur Tiefe im Gang, und Haltestellen in derselben Tiefe auf verschiedenen Inseln bekommen dieselbe Nummer.
Genauer
Wie tief Rekursion gehen kann
Der Stapel eines Threads in Java ist standardmäßig etwa 1 MB groß (einstellbar mit -Xss), das reicht für einige tausend bis zehntausende Aufrufe von visit. Ein Überlauf wirft StackOverflowError. Du kannst ihn abfangen, aber der Gang ist schon kaputt. Für große Graphen nimm ArrayDeque als Stapel: push und pop.
Übung
Die kritischen Straßen
Löse die Aufgabe in deinem Editor.
Kritische Straßen. Die Brücke Markt – Park ist für einen Monat gesperrt, und Platz – Kino ist noch offen. Eine Straße ist kritisch, wenn ihre Sperrung die Stadt in mehr Inseln teilt. Sperre die Brücke und gib aus, wie viele Inseln es gibt. Dann nimm für jede offene Straße sie weg, zähle die Inseln mit islands und lege sie zurück. Gib die kritischen Straßen in der Reihenfolge von streets aus.
String[][] closed = { {"Markt", "Park"},};Inseln: 1Kritisch: Platz - Kino, Kino - Krankenhaus, Park - Stadion, Krankenhaus - StadionWeitere Aufgaben
- Die größte Insel. Gib die Insel mit den meisten Haltestellen aus und wie viele Lieferwagenstunden pro Woche sie braucht, wenn jede Haltestelle 3 Stunden dauert.
- Der Zyklus im Plan. Schreibe
findCyclemit einem eigenen Stapel: Leg ein Paar (Haltestelle, Index des nächsten Nachbarn) auf den Stapel, und die Haltestelle wird schwarz, wenn der Index das Ende ihrer Liste erreicht. - Inseln auf der Karte. Die Lagerkarte ist ein Raster aus
#(Regal) und.(frei). Zähle die freien Zonen, wobei die Nachbarn einer Zelle oben, unten, links und rechts sind.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Der Aufrufstapel von innen · Von der Rekursion zur Schleife · Graph: Adjazenzliste und Matrix · BFS: die wenigsten Haltestellen
- Geht weiter in: Topologische Sortierung · Minimaler Spannbaum · Architektur-Review
- Aufgaben zum Thema: Graphen