1 Rot-Schwarz-Bäume Rot-Schwarz-Bäume stellen eine Implementierung von binären Suchbäumen dar, mit einer Worst-Case-Laufzeit von O(log n). Jeder Knoten hat eine Farbe (rot oder schwarz). Ein Rot-Schwarz-Baum ist balanciert, da durch die Farbregeln sichergestellt wird, das kein Pfad mehr als doppelt so lang ist, wie ein anderer. 1.1 Eigenschaften von Rot-Schwarz-Bäumen - jeder Knoten ist rot oder schwarz die Wurzel des Baumes ist schwarz jedes Blatt (NIL) ist schwarz die Kinder von roten Knoten sind schwarz alle Blätter haben die selbe Schwarztiefe (der Knoten an dem man sich befindet zäht nicht mit) Jeder neu eingefügte Knoten ist erstmal rot, danach wird durch Rotationen die Rot-Schwarz-Eigenschaften wieder hersgestellt, falls diese beim Einfügen des neuen Knotens verletzt wurden. Der Sentinel-Knoten ist immer schwarz. 1.2 Rot-Schwarz-Bäume <-> (2,4)-Bäume a) RS-Baum -> (2,4)-Baum jeder rote Knoten wird in seinen Vaterknoten gemischt b) (2,4)-Baum -> RS-Baum siehe Skript 1.3 Rotation Die Rotation dient zur Wiederherstellung der Rot-Schwarz-Eigenschaften des Baumes, sie werden ggf. nach Insert- oder Delete-Anweisungen notwendig. Rotationen sind lokale Operationen, sie verändern die SuchbaumEigenschaften nicht. 1.4 RB-Insert-Fixup(T,x) - neuen Knoten einfügen und rot färben welche rs-Eigenschaften wird verletzt; beim Einfügen von Knoten können nur Bedingung 2 oder 4 verletzt werden. Bedingung 2 ist verletzt, wenn der eingefügte Knoten die Wurzel ist Bedingung 4 ist verletzt, wenn der Vater des eingefügten Knotens rot ist 2 Treaps Problem: einzufügende Elemente treffen gut sortiert nacheinander ein, dadurch degeneriert der Baum. Ein Treap ist ein binärer Suchbaum mit einer modifizierten Knotenordnung Jeder Treap-Knoten wird gekennzeichtnet durch einen Schlüssel und durch einen Wert Priorität, der für jeden Knoten unabhängig zufällig gewählt wird. Die Treap-Knoten sind so geordnet, das Ihre Schlüssel der binären Sucheingenschaft genügen und ihre Prioritäten der Min-Heap-Eigenschaft: - ob Knoten rechtes oder linkes Kind ist bestimmt der Schlüssel - nach unterhin stegende Prioritäten Rotiert wird bei Verletztung der Min-Heap-Eigenschaft 3 Persistente dynamische Mengen Ziel ist es, frühere Versionen von Mengen zu erhalten (Persistent). Eine Möglichkeit wäre, vor jeder verändernden Operation die Menge zu kopieren. Dies benötigt allerdings viel Zeit und Speicherplatz. Eine schnellere Möglichkeit ist, nur einen Teil der Menge zu kopieren und dann die neuen Elemente einzufügen. Ein teil der Knoten stammen dann noch aus dem original Baum. Probleme: Angenommen, die Knoten der Menge speichern nur die Felder key, left, right; somit keine Referenz auf den Vater. 1. Man muss alle Knoten identifizieren, welche bei einer verändernden Operation betroffen sind. 2. 3. Man muss eine Methode schreiben, welche aus dem Baum T einen neuen Baum T’ generiert, in welchen der neue Schlüssel k eingefügt wird. Welche Zeit- und Speicherkomplexität hat diese Methode? 4 Erweitern von Datenstrukturen Um Standard-Datenstrukturen an reale Probleme anzupassen, müssen diese oft angepasst werden. Meistens reicht es, Operationen hinzuzufügen. 4.1 Dynamische Ordnungsstatistik Zwei häufige benötigte Informationen über Elemente dynamischer Mengen sind: 1) den Rang eines Elementes in einer total geordneten Menge zu bestimmen 2) ein Element mit einem bestimmten Rang zu finden Es gibt verschiedene Lösungsnöglichkeiten, welche sich besonders in der Laufzeit unterscheiden. Die einfache Brute-Force-Methode löst anfragen in O(n2)-Laufzeit. Ein schnellerer Algorithmus löst das Problem nicht-deterministisch (zufallsgesteuert) mit einem Decrease & Conquer-Verfahren in O(n) Zeit. Durch die Verwenung geeigneter Datenstrukturen, wie z.B. Rot-SchwarzBäume, kann das i-te Element in O(log n)-Zeit gefunden werden. Die Abbildung zeigt eine Datenstruktur, die schnelle ordnungsstatistische Operationen unterstützt. Das Feld „Größe“ enthält die Knotenanzahl des jeweiligen Teilbaum. Verwendet man einen Rot-Schwarz-Baul als Struktur, werden folgende Informationen in den Knoten gespeichert: - key[x] color[x] p[x] left[x] right[x], zusätzlich noch size[x], enthält die Anzahl innerer Knoten (inkl.x) Die Größe des Sentinel-Knoten (NIL) definieren wir mit 0, somit ergibt sich für die Berechnung der Größe eines Knotens folgende Formel: size[x] = size [left[x]] + size[right[x]] + 1 4.1.1 Suchen eines Elementes im Baum Nun entwickelt man einen Algorithmus, welcher das I-kleinste Element der Menge sucht ( OS-Select(x,i) ). In Zeile 1 wird der Rang von x berechnet und dann in Zeile 2 mit dem gesuchten Element verglichen. Stimmen beide Wert überein, haben wir das ikleinste Element schon gefunden. Ist i kleiner als der Rang von x, wird im linken Teilbeum weiter gesucht. Ist i größer als r, wird im rechten Teilbaum gesucht. Hierbei ist x das i-rkleinste Element im rechten Teilbaum. Ein Beispiel gibt es im Skript, Folie K2.4.1 Seite 5) 4.1.2 Rang eines Elementes Bestimmen Der folgende Algorithmus dient zum bestimmes des Ranges eines Elementes (lineare Position bei In-Order-Traversierung). Rang x = Anzahl der Knoten die bei In-Order-Traversierung vor dem Knoten liegen + 1. Der Algortithmus arbeitet sich vom Knoten x zur Wurzel hoch. Da jede Iteration der while-Schleife O(1) Zeit benötigt, ist die Laufzeit des Algortithmus proportional zur Baumhöhe, als O(log n). Neuberechnung der Baumgröße Damit die beiden oben genannten Algorithmen eine gute Performance haben ist es wichtig, die Baumgröße bei Änderungen effizient zu aktualisieren. Die Insert-Operation bei Rot-Schwarz-Büumen besteht aus zwei Phasen: - Suchen der Einfügestelle und Einfügen des Knotens - Den Suchpfad zurückverfolgen und die Rot-Schwarz-Eigenschaften ggf. wieder herstellen. Um die größe der Teilbäume zu aktualisieren, wird beim Einfügen auf dem Suchpfad der Wert size[x] bei jedem Knoten inkrementiert. Bei den Rotationen in der zweiten Phase können höchstens zwei size-Werte inkorrekt werden. Diese werden bei der Rotation aktuakisiert. Da beim Einfügen in Rot-Schwarz-Bäumen höchstens zwei Rotationen nötig sind und das Ändern der size-Felder in O(1) statt findet, benötigt das Einfügen weiterhin nur O(log n)-Zeit. Das Löschen findet dazu analog auch in O(log n)-Zeit statt. 4.1.3 Erweitern von Datenstrukuren 1. Auswählen einer geeigneten Datenstruktur 2. Entscheiden, welche zusätzlichen Informationen in die BasisDatenstruktur aufgenommen werden müssen. 3. Verifizieren, das die zusätzlichen Informationen von den bestehenden Basisoperationen korrekt und effizient verwaltet werden. 4. Entwickeln neuer Operationen. 4.2 Intervall-Bäume Besonders geeingnet für die Darstellung von Zeitperioden Geschlossene Intervalle enthalten ihre Grenzen. Intervall-Trichometrie, d.h. es gelten folgende Eigenschaften: - i und i’ überlappen sich - i liegt links von i’ - i liegt rechts von i’ Drei Operationen auf Intervallbäumen: - Intervall-Insert(T,x) - Intervall-Delete(T,x) - Intervall-Search(T,x) 4.2.1 Struktur und Knoteninformationen Damit die Operationen auf dem Intervall-Baum logarithmische Laufzeit haben, werden dem Rot-Schwarz-Baum folgende zusätzliche Informationen hinzugefügt: - Intervall int[x] - Anfangspunkt left[x] - Endpunkt right[x] - Maximaler Endpunkt max[x] Als Schlüssel des Knoten x wählen wir den kleinsten Wert des Intervalls (lox endpoint). Somit liefert eine In-Order-Traversierung des Baumes die Intervalle in aufsteigender Sortierung. Der max[x]-Wert eines Knotens speichert den maximalen Endpunkt (right[x]) von dem Teilbaum unter x. 4.2.2 Intervall-Search Einzige neue Methode ist Intervall-Search(T,i), welche einen Knoten im Baum findet, dessen Intervall sich mit dem Intervall i überschneidet. Die Suche nach dem Intervall beginnt bei der Baumwurzel und steigt dann den Baum hinab. Die Suche endet, wenn kein überlappendes Intervall gefunden wurde, beim Sentinel-Knoten. Da auch hier jede Iteration der while-Schleife O(1)-Zeit benötigt, läuft Intervall-Search im Worst-Case mit O(log n) ab. Die Suche beginnt bei der Wurzel. Ist der low[x]-Wert kleiner als der max[x]-Wert des linken Knotens, wird die Suche im linken Teilbaum fortgesetzt. Andernfalls im rechten Teilbaum. Überschneiden sich nun die beiden Intervalle, wird der Knoten zurückgegeben, ansonsten wird die Suche nach selbem Schema fortgesetzt. Grundidee von Intervall-Search Die Grundidee liegt darin, das es bei der Suche immer nur eine „sichere“ Richtung gibt, in der die Suche fortgesetzt wird. 5 Dynamische Programmierung Die Grund idee der dynamischen Programmierung besteht darin, ein Problem durch Teilprobleme unter Benutzung kleiner Integer-Indizes-Mengen zu charakterisieren. Das Zeil dieser Art der Problemcharakterisierung ist es, eine optimale Subproblemlösung durch die Kombination der (möglicherweise sich überlappenden) Lösungen kleinerer Probleme zu definieren. Im Gegensatz zu D&C-Algorithmen sind bei der dynamischen Programmierung die Subprobleme voneinander abhängig, d.h. Subprobleme teilen sich gemeinsam Subsubprobleme. 5.1 Vier Schritte eines DP-Algorithmus - Charakterisiere die Struktur einer optimalen Lösung Definiere rekursiv den Wert einer optimalen Lösung Berechne bottom-up den Wert einer optimalen Lösung Konstruiere eine optimale Lösung aus den berechneten Informationen Die Schritte 1-3 sind die Basis für eine DP-Lösung eines Problems. Schritt 4 entfällt, wenn nur der Wert einer optimalen Lösung gsucht wird. Für Schritt 4 müssen manchmal zusätzlice Informationen während der Ausführung von Schritt 3 bereitgestellt werden, um die Konstruktion einer optimalen Lösung zu erleichtern. //TODO 5.2 Elemente der dynamischen Programmierung Für die dynamische Programmierung sind zwei Schlüsseleigenschaften von bedeutung: - optimale Substruktur sich überlappende Subprobleme 5.2.1 Optimale Substruktur Ein Problem besitzt dann eine optimale Substruktur, wenn eine optimale Lösung des Originalproblems die optimalen Lösungen von Subproblemen enthält. D.h. man trifft eine Auswahl, welche das Originalproblem teilt. Diese Auswahl führt dann zu einem oder mehreren zu lösenden Subproblemen. In diesem Schritt wird aber noch nichts darüber gesagt, wie die Auswahl getroffen wird. Es wird lediglich angenommen, das diese Auswahl zu einer optimalen Lösung führt. Danach wird geprüft, welche Subprobleme aus der Auswahl folgen und wie der resultierende Subproblem-Raum am besten beschrieben wird. Nun muss man zeigen, das die in der optimalen Lösung benutzten Subproblemlösungen selbst optimal sind (Verwendung der „cut-and-paste“ – Methode) „Cut-and-Paste“ – Methode In dieser Technik nimmt man an, das jede Subproblemlösung nicht optimal ist und leitet aus dieser Annahme einen Widerspruch her. Durch das Ausschneiden nicht-optimaler Subproblemlösungen („cutting out“) und das Einsetzen einer optimalen Lösung („pasting in“) zeigt man, dass man zu einer besseren Lösung des Originalproblems kommt, was aber dann zum Widerspruch zu der Annahme einer optimalen Lösung führt. Die optimale Substruktur kann im Problemraum auf zwei Arten variieren: 1. Wie viele Subprobleme werden in der optimalen Lösung des Originalporblems genutzt? 2. Wie viele Auswahlmöglichkeiten gibt es bei der Entscheidung, welche Subprobleme für die optimale Lösung genutzt werden? Beispiele: Beim Fertigungsstrassen-Scheduling wird für die optimale Lösung nur ein Subproblem (schnellster Weg zur vorherigen Station) benutzt, aber es müssen zwei Auswahlmöglichkeiten (vorherige Station auf Band A oder B) betrachtet werden. Bei der Matrixketten-Multiplikation werden zwei Subprobleme (Problem wird in zwei Teile aufgeteilt) betrachtet und i-j Auswahlen (es gibt i-j Splittpunkte k). 5.2.2 Laufzeit von DP-Algorithmen Die Laufzeit von DP-Algorithmen hängt vom Produkt zweier Faktoren ab: - der Gesamtzahl der Subprobleme und der Anzahl der Auswahlmöglichkeiten, die bei jedem Subproblem zu betrachten sind. Beim Scheduling-Beispiel gibt es O(n) Subprobleme und für jedes Subproblem zwei Auswahlmöglichkeiten, also O(n). Bei der Matrix-Multiplikation gibt es O(n2) Subprobleme und für jedes Subproblem n-1 Auswahlmöglichkeiten, also O(n3). 5.2.3 DP-Programmierung vs. Greedy-Algortithmen Greedy-Algorithmen haben viele Ähnlichkeiten mit DP-Algorithmen. Beide Vorgehensweisen lösen ihr Problem über optimale Substrukturen. Bei Greedy-Algorithmen wird die optimale Substruktur allerdings in einer „top-down“ – Art benutzt. Statt zuerst optimale Lösungen für Subprobleme zu finden und dann eine Auswahl zu treffen, treffen Greedy-Algorithmen zuerst eine Auswahl (nach dem Greedy-Kriterium). Es wird diejenige Auswahl getroffen, die momentan am günstigsten erscheint. Mit der getroffenen Auswahl wird dann ein resultierendes Subproblem gelösst. DP-Algorithmen dagegen nutzen einen „bottom-up“ – Ansatz. Zuerst werden die optimalen Lösungen der Subprobleme gesucht. Nachdem diese gelöst sind, wird die optimale Lösung des Originalproblems konstruiert. 5.2.4 Überlappende Subprobleme Die zweite wichtige Eigenschaft, die ein Optimierungsproblem haben muss, um für eine Lösung durch dynamische Programmierung in Frage zu kommen, ist die, dass der Raum der möglichen Subprobleme „klein“ sein soll. Ein rekursiver Algorithmus löst für das Problem die Subprobleme immer wieder von Neuem, statt neue Subprobleme zu generieren. Überlappen sich nun zwei Subprobleme, ist es sinnvoll, DP zu verwenden. Dagegen generier ein D & C – Algorithmus in jedem Rekursionsschritt völlig neue Subprobleme. Ein DP-Algorithmus nutzt sich überlappende Subprobleme aus, um jedes Subproblem genau einmal zu lösen und die Lösung in einer Tabelle zu speichern, auf die in konstanter Zeitz zugegriffen werden kann, wenn ein Wert benötigt wird. Vergleicht man den rekursiven „top-down“ – Algorithmis mit dem „bottom-up“ – DPAlgoritmus, stellt man fest, das letzterer effizienter ist. Dies liegt daran, da er die Vorteile von sich überlappenden Subproblemen ausnutzt, indem er sich die Lösungen von Subproblemen in einer Tabelle merkt. Der rekursive Algorithmus hingegen löst jedes Subproblem mehrmals und zwar jedes Mal, wenn es im Rekursionsbaum vorkommt. Faustregel: Immer dann, wenn ein Rekursionsbaum für die natürliche rekursive Lösung eines Problems dieselben Subprobleme mehrmals enthält, sollte man prüfen, ob eine Lösung durch dynamische Programmierung in Frage kommt. 5.2.5 Rekonstruktion einer optimalen Lösung Aus praktischen Gründen ist es sinnvoll, auch die Entscheidungen, die bei der Lösung der Subprobleme gemacht werden, in einer Tabelle zu speichern (vgl. die l-Tabelle beim Scheduling). Somit müssen die Auswahl-Informationen nicht rekonstruiert werden, sondern stehen in O(1) zur Verfügung. Eine weitere Idee der dynamischen Programmierung ist es, einen Algorithmus mit einem Gedächtnis zu verstehen. Somit erhölt man die Effizienz des DP-Ansatzes bei gleichzeitiger Anwendung der „top-down“ – Strategie. Beim Matrix-Problem füllt man einfach die Tabelle bei Beginn mit einen bestimmten Wert, der angibt das das Ergebnis noch nicht berechtet wurde. Wird ein Subproblem nun das erste Mal gelöst, wird es in der Tabelle gespeichert und steht ab jetzt für weitere Berechnungen zu Verfügung. 5.3 Schlussfolgerungen Wenn alle Subprobleme mindestens einmal gelöst werden müssen, dann ist ein „botom-up“ – DP-Algorithmus um einen konmstanten Faktor besser als ein „Gedächtnis“-Algorithmus, weil es keinen Rekursions-Overhead und weniger Verwaltungs-Overhead für die Tabelle gibt. Wenn allerdings einige Subprobleme im Subproblemraum überhaupt nicht gelöst werden müssen, dann hat die „Gedächtnis“-Methode Vorteile, weil nur diejenigen Subprobleme zu lösen sind, die auch definitiv gebraucht werden. 6 Greedy – Algorithmen Die Idee der Greedy-Algorithmen besteht darin, aus einer Menge von optimalen Teillösungen eine optimale Lösung für das Originalproblem zu konstruieren. Um zu einer optimalen Lösung zu gelangen, wird eine Folge von Teillösungen ermittelt, die aus lokaler Sicht optimal sind. Der Greedy-Ansatz muss nicht notwendigerweise zu einer optimalen Lösung führen. Es gibt Probleme, deren Lösung nicht optimal sein muss. Begnügt man sich mit einer suboptimalen Lösung, so kann diese mit einem oft sehr schnellen Greedy-Verfahren erreicht werden. 6.1 Dynamische Programmierung vs. Greedy-Algorithmus Bei der dynamischen Programmierung werden für eine optimale Lösung mehrere Subprobleme betrachtet. Beim Greedy-Verfahren wird hingegen nur ein Subproblem betrachtet; man nennt das ausgewählte Subproblem auch Greedy-Auswahl. Nach der Greedy-Auswahl bleibt nur ein eiziges nicht-leeres Subproblem übrig. 6.1.1 Greedy-Lösung In der Greedy-Lösung wird nur noch ein Subproblem in der optimalen Lösung betrachtet. Das anbdere ist garantiert leer, das es schon gelöst wurde und somit der optimalen Gesamtlösung hinzugefügt würde. Somit kann jedes Subproblem mit einer „top-down“-Strategie gelöst werden. Ein Greedy-Algorithmus trifft eine Entscheidung nach einer heuristischen Strategie. Es wird diejenige Auswahl getroffen, die im Moment am besten erscheint. Diese Strategie liefert aber nicht immer die optimale Lösung. Bei der Suche nach einer geeigneten Strategie sollte man auf zwei Schlüsseleigenschaften besonders achten: - die Greedy-Auswahl - die optimale Substruktur Der Bottom-Up-Ansatz bei der dynamischen Programmierung verlangt, das eine optimale Lösung des Original-Problems erst dann konstruiert werden kann, wenn die optimalen Lösungen der Subprobleme bekannt sind. Durch den Top-Down-Ansatz bei Greedy-Algorithmen kann ein Objekt zur optimalen Lösung des Original-Problems hinzugefügt werden schon bevor die optimalen Lösungen der Subprobleme bekannt sind. Die Voraussetzung für eine Greedy-Auswahl schafft in vielen Fällen eine Ordnung der Objekte. 7 Flüsse in Netzwerken 7.1 Der Fluss im Netzwerk Ein Fluss in einem Netzwerk hat die Kapazitätsfunktion c, eine Quelle q und eine Senke s. Eigenschaften: - Kapazitätsbeschränkung Erhaltung des Flusses, d.h. die Kapazitäten die in einen Knoten rein fliessen müssen ihn auch wieder verlassen. 7.1.1 Wert eines Flusses Der Wert eines Flusses ist definiert als die Summe der Flusswerte aller die Quelle q verlassenden Pfeile 7.1.2 Maximaler Fluss Ein maximaler Fluss in einem Graphen G ist ein Fluss f mit maximalem Wert |f| unter allen Flüssen in G. 7.2 Trennender Schnitt ??? 7.2.1 Kapazität des Schnittes Die Kapazität des Schnittes ist die Summer der Pfeile, die von Q nach S führen. 7.2.2 Minimaler Schnitt Ein Schnitt mit kleinster Kapazität unter allen möglichen Schnitten heißt minimaler Schnitt. 7.2.3 Fluss über einen Schnitt Die Summe aller Pfeile die von Q nach S führen abzüglich der Summe aller Pfeile, die von S nach Q führen. 7.3 Der Satz von Ford und Fulkerson Der Fluss in einem Netzwerk kann niemals größer sein, als der Fluss über einen beliebigen Schnitt im Netzwerk. Daraus folgt, das der minimale Schnitt gleichzeitig auch der maximale Fluss in einem Netzwerk ist. Min Cut = Max Flow 7.3.1 Zunehmender Weg Ein Weg von q nach s, auf dem ohne Rücksicht auf die Pfeilrichtungen (ein ungerichteter Weg) der Fluss erhöht werden kann, heißt zunehmender Weg. Also alle Pfeile, auf denen der Fluss erhöht (f(e) < c(e)) bzw. verringert (f(e) > 0) werden kann. 7.3.2 Restgraph Der Restgraph enthält alle zunehmende Wege eines Graphen. Somit beschreibt der Restgraph alle Möglichkeiten der Flussvergrößerung: - er enthält einen Pfeil e, wenn der rest(e) > 0 ist er enthält den e entgegen gerichteten Pfeil, wenn f(e) > 0. Der Restgraph enthält damit die Informationen, um wieviel der Fluss zwischen Knoten x und y noch erhöht und/oder gesenkt werden kann. WICHTIG: Man kann nun zeigen, das ein Fluss f genau dann maximal ist, wenn es für f keinen zunehmenden Weg gibt, und dass genau dann der Wert des Flusses f der Kapazität des Minimalen Schnittes entspricht. 7.3.3 Laufzeit von Ford und Fulkerson Die Laufzeit des Algorithmus hängt von den Knoten, Kanten und der Kapazität ab. Dadurch zeigt sich, das der Algorithmus sehr ineffizient arbeitet, weil er möglicherweise zunehmenden Wegen folgt, die nur eine geringe Flusserhöhung zulassen. Verbesserungsidee: Bei der Flussvergrößerung wählt man nicht beliebige zunehmende Wege, sonder immer den kürzesten zunehmenden Weg. Somit wird nach höchstens |E| Schleifendurchläufen die Anzahl der Pfeile um min. 1 erhöht. Die maximale Länge des kürzesten Weges ist |V| - 1 (sonst gäbe es Zykel). 7.3.4 Blockierender Fluss Ein Fluss, der auf einem zunehmendem Weg nicht mehr zu vergrößern ist, heißt blockierender Fluss. Man betrachtet somit nicht mehr einen beliebigen Weg im Restgraph zur Flusserhöhung, sondern immer nur den/die kürzesten Wege. Diese werde im Niveagraphen dargestellt. 7.4 Niveagraph (Edmonds-Karp) Der Niveagraph ist ein Teilgraph des Restgraphen und enthält nur solche Pfeile, die von q aus erreichbar sind und auf dem kürzesten Weg liegen. Der Niveagraph enthält jeden kürzesten vergrößernden Weg, aber nicht unbedingt jeden vergrößernden Weg. Die Wege lassen sich mit der Breitensuche in O(|E|) Zeit konstruieren. Laufzeit: Gegeben sei ein Netzwerk mit n Knoten und m Kanten. Der Edmonds-Karp Algorithmus berechnet den maximalen Fluss in O( n * m 2) Zeit. Wie findet man am schnellsten einen blockierenden Fluss?? Man wählt einen Weg von q nach s und erhöht den Fluss so, das eine Kante auf dem Weg gesättigt ist. Dann entfernt man alle gesättigten Pfeile. Man wiederholt dies solange, bis s nicht mehr von q erreichbar ist. Das finden eines Weges von q nach s ist eine Tiefensuche. Der gefundene Weg ist höchstens |V| - 1 Kanten lang. Die Flusserhöhung ist nun die kleinste Restkapazität der Pfeile auf dem Weg. Alle Restkapazitäten der Pfeile auf dem Weg werden angepasst, die gesättigte Kante wird entfernt. Da bei jeder Flussvergrößerung mindestens eine Kante entfernt wird, entsteht nach höchstens |E| Flussvergrößerungen ein blockierender Fluss.