Karlsruher Institut für Technologie Institut für Theoretische Informatik Prof. Dr. Peter Sanders G.V. Batz, C. Schulz, J. Speck 11. Übungsblatt zu Algorithmen I im SS 2010 http://algo2.iti.kit.edu/AlgorithmenI.php {sanders,batz,christian.schulz,speck}@kit.edu Musterlösungen Aufgabe 1 (Negative Kreise in gerichteten Graphen, 2 + 2 + 2 Punkte) Wir betrachten hier die Ausführung des Algorithmus von Bellman-Ford auf einem gewichteten und gerichteten Graphen G. Der Graph habe n Knoten und der Startknoten für Bellman-Ford sei mit s bezeichent. Alle Knoten im Graphen seien von s erreichbar. Mit d[v]k wird hier der Wert von d[v] nach der k-ten Runde bezeichnet. a) Zeigen Sie: Ein negativer Kreis existiert genau dann, wenn in der n-ten Runde von Bellman-Ford d[v] für einen Knoten verkleinert wird. Hinweis: Siehe Übung. b) Es seien nun n Runden von Bellman-Ford auf G angewendet und alle Datenstrukturen entsprechend aufgebaut (insbesondere der Array der zu jedem Knoten seinen Vorgänger speichert). Zudem ist ein Knoten u, mit d[u]n < d[u]n−1 gegeben. Es gibt also nach a) einen Kreis mit negativem Gewicht. Geben Sie einen Algorithmus mit Laufzeit in O(n) an, der einen negativen Kreis in G findet und begründen Sie die Laufzeit. Hinweis: Was passiert wenn man von u aus immer wieder zum Vorgänger geht? c) Zeigen Sie, dass das was Ihr Algorithmus findet tatsächlich ein negativer Kreis ist. Hinweis: Sie dürfen davon ausgehen, dass es keinen Kreis mit Gewicht 0 gibt. Musterlösung: a) ∃v : d[v]n < d[v]n−1 ⇒ es existiert ein negativer Kreis: In der Übung wurde gezeigt: Wenn kein negativer Kreis existiert, findet Bellman-Ford zu jedem Knoten v einen kürzesten Weg von s nach v. Da dieser Weg keine Kreise enthält, gibt es maximal n − 2 Knoten und n − 1 Kanten auf dem kürzesten Weg zwischen s und v. Insbesondere hat Bellman-Ford den kürzesten Weg nach maximal n − 1 Runden gefunden. Damit ist in der nten Runde keine weitere Verkleinerung von d[v] mehr möglich. Deshalb muss im Falle einer Verkleinerung in der n-ten Runde ein negativer Kreis existieren. Es existiert ein negativer Kreis ⇒ ∃v : d[v]n < d[v]n−1 : Es sei C ein Kreis der Länge k in G mit einem Gesamtgewicht der Kanten von −c < 0. Nach t ≤ n − 1 Runden gelte für alle Knoten v ∈ C d[v] < ∞. Da ein negativer Kreis nie statisch wird, existiert in jeder Runde ` ab der t + 1-ten mindestens ein Knoten v ∈ C mit d[v]` < d[v]`−1 . b) Algorithmus: 1. Starte mit Knoten u. 2. Markiere den aktuellen Knoten und gehe zum Elterknoten. 3. Falls der Elterknoten nicht markiert ist, gehe zu 2.. 4. Wenn der Elterknoten markiert ist, hat man einen Kreis gefunden. 5. Man merkt sich den gerade erreichten Knoten. 1 6. Gib den aktuellen Knoten aus und gehe zum Elterknoten. 7. Wenn der Elterknoten nicht der gemerkte Knoten ist, gehe zu 6.. Der Algorithmus terminiert nach maximal O(n) Schritten: Nach n Runden von Bellman-Ford haben alle Knoten v außer s einen Elterknoten u 6= v. Wenn man beim Rückwärtslaufen“ in s ankommt und s sich selbst als Elterknoten hätte, dann wäre ” man einen kürzesten Pfad von s nach u zurückgelaufen. Dieser hätte jedoch in der n-ten Runde von Bellman-Ford nicht kürzer werden können, daher kann dieser Fall nicht auftreten. Deshalb muss man irgendwann immer auf einen schon markierten Knoten stoßen. Da es aber maximal n Knoten gibt, die markiert werden können, passiert dies spätestens nach n Schritten. Die Ausgabe des Kreises benötigt auch maximal n Schritte. Damit werden insgesamt nur O(n) Schritte benötigt. c) Es sei v1 , . . . , vk mit Kanten e1 = (v1 , v2 ), . . . , ek = (vk , v1 ) der in b) gefundene Kreis C. Wobei vi jeweils der Elter von vi+1 ist, der Elter von v1 ist vk . Wegen der n-ten Runde (oder der vorherigen) von Bellman-Ford gilt d[vi+1 ]n = c(ei ) + d[vi ]n−1 . Summiert man die obige Formel über alle i so erhält man: k k X X d[vi ]n = (c(ei ) + d[vi ]n−1 ) i=1 i=1 Für jedes i gilt zudem d[vi ]n−1 ≥ d[vi ]n , und daher: k X k X d[vi ]n ≥ i=1 i=1 k X 0 ≥ (c(ei ) + d[vi ]n ) c(ei ) i=1 Damit ist C entweder Kreis vom Gewicht 0 (wurde durch den Hinweis ausgeschlossen) oder ein Kreis von negativem Gewicht. Aufgabe 2 (Jarnı́k-Prim und Kruskals Algorithmus, 3 + 3 Punkte) Berechnen Sie einen MST des angegebenen Graphen mit dem Algorithmus von Jarnı́k-Prim und dem Algorithmus von Kruskal. Geben Sie jeweils die Kanten des MST in der Reihenfolge an, in der sie der Algorithmus auswählt. Um Eindeutigkeit herzustellen, verwenden Sie Knoten a als Startknoten von Jarnı́k-Prim und sortieren Sie die Kanten bei beiden Algorithmen bei Unentschieden nach den Nummern der Endknoten. b d 4 8 c a 1 10 5 7 5 e 3 f 5 g 4 3 13 11 1 i 3 j 2 9 h 7 6 Musterlösung: Jarnı́k-Prim: {a, b}, {a, h}, {h, i}, {i, f }, {f, e}, {i, j}, {b, c}, {f, g}, {g, d} Kruskal: {a, b}, {h, i}, {a, h}, {e, f }, {i, f }, {i, j}, {b, c}, {f, g}, {g, d} 2 Aufgabe 3 (Minimale Spannbäume, 3 + 3 Punkte) a) Sei G = (V, E) ein zusammenhängender ungerichteter gewichteter Graph und T ein MST in G. Für V 0 ⊆ V sei G0 der von V 0 induzierte Teilgraph von G und T 0 der von V 0 induzierte Teilgraph von T . Zeigen Sie: Wenn T 0 zusammenhängend ist, dann ist T 0 ein MST in G0 . b) Sei G = (V, E) ein zusammenhängender ungerichteter gewichteter Graph mit Gewichtsfunktion c : E → R>0 . Sei T ein MST in G bzgl. der Gewichtsfunktion c. Sei e0 eine Kante in T und q0 ∈ R mit c(e0 ) ≥ q0 ≥ 0. Wir definieren nun eine weitere Gewichtsfunktion c0 : E → R>0 mit c0 (e) := c(e) für e 6= e0 und mit c0 (e0 ) := c(e0 ) − q0 . Zeigen Sie: T ist auch bzgl. der Gewichtsfunktion c0 ein MST in G. Musterlösung: a) T 0 ist Baum, da es nach Vorraussetzung zusammenhängend ist und keine Kreise enthält. Anngenommen T 0 enthielte einen Kreis: Da T 0 Teilgraph von T ist, würde dann aber auch T einen Kreis enthalten, was ein Widerspruch zur Baumeigenschaft von T ist. Außerdem ist T 0 spannender Baum, da es als von V 0 induzierter Teilgraph in G alle Knoten von V 0 berührt. Weiter ist T 0 minimaler Spannbaum. Andernfalls gäbe es einen anderen Spannbaum T 00 in G0 mit Gewicht c(T 00 ) < c(T 0 ) und wir könnten einen Graph T̃ bilden, indem wir alle Kanten von T 0 aus T entfernen und stattdessen alle Kanten von T 00 einfügen. Da alle Bäume in G0 gleich viele Kanten haben (siehe Übung), haben T 0 und T 00 auch gleichviele Kanten. Weiter ist T 0 ein Teilgraph von T . Folglich werden beim bilden von T̃ gleich viele Kanten weggenommen wie hinzugefügt. Also haben T und T̃ gleich viele Kanten. Außerdem sind T und T 00 zusammenhängend und somit auch T̃ . Also ist T̃ iebenfalls spannender Baum in G. Es ist aber c(T̃ ) = c(T ) + c(T 00 ) − c(T 0 ) < c(T ) | {z } <0 im Widerspruch zur Minimalität von T . b) Wäre T bezüglich der Gewichtsfunktion c0 kein MST in G, dann gäbe es einen MST T 0 in G mit c0 (T 0 ) < c0 (T ). Dabei gehört e0 entweder zu T 0 dazu oder nicht. Fall 1: e0 gehört zu T 0 dazu. Da sich c und c0 nur in der Bewertung von e0 unterscheiden und e0 sowohl in T als auch T 0 enthalten ist gilt sowohl c0 (T ) = c(T ) − q0 als auch c0 (T 0 ) = c(T 0 ) − q0 . Wir haben also c0 (T 0 ) < c0 (T ) ⇔ c(T 0 ) − q0 < c(T ) − q0 ⇔ c(T 0 ) < c(T ) im Widerspruch zur Minimalität von T bzgl. c. Fall 2: e0 gehört nicht zu T 0 dazu. In diesem Fall gilt c(T 0 ) = c0 (T 0 ) < c0 (T ) = c(T ) − q0 < c(T ) , was erneut ein Widerspruch zur Minimalität von T bzgl. c ist. 3 Aufgabe 4 (Anwendungsproblem, 1 + 5 Punkte) Man hat ihnen ein Bild der folgenden Art gegeben: Zunächst wurden die Hintergrundpixel völlig zufällig gewählt, d.h. jedes Pixel hat eine zufällige Farbe zugewiesen bekommen. Anschließend wurden kleine Objekte jeweils komplett mit der gleichen Farbe auf das Bild gezeichnet. Weiter bekommen Sie folgende Graphmodellierung an die Hand gegeben: G = (V, E) mit V die Pixel des Bildes und e = {p1 , p2 } ∈ E genau dann wenn p1 und p2 sind benachbart und haben die gleiche Farbe. a) Argumentieren Sie, dass das Graphmodell höchstens O(n) Kanten hat. b) Geben Sie einen Algorithmus an, der nur mit Hilfe des Graphmodells die Regionen/Objekte identifiziert die mehr als R Pixel haben und ausgibt. Der Algorithmus soll die Laufzeit von O(nαT (m, m)) nicht überschreiten. Musterlösung: a) Ein Pixel hat entweder 4 oder 8 Nachbarn (je nach Definition). Damit können im Graphen höchstens 4n Kanten existieren. b) Aufgrund der Definition des Graphen reduziert sich das Problem auf das Finden von Zusammenhangskomponenten in G. Zusammenhangskomponenten kann man in Zeit O(mαT (m, m)) unter Verwendung einer Union-Find Datenstruktur finden (Union-by-rank + Pfadkompression). Der Algorithmus sieht folgendermaßen aus: 1: procedure connectedComponents(G = (V, E)) 2: for v ∈ V do initSet(v) 3: for e = {u, v} ∈ E do 4: if find(u) 6= find(v) then 5: union(u, v) 6: return Allerdings müssen wir uns noch über die Größe der Regionen Gedanken machen. Dazu bekommt jede Menge ein Attribut size und wir modifizieren die union operation, so dass der Repräsentant (der Knoten der als Parent sich selbst hat) der neuen vereinigten Menge die Summe der beiden vereinigten Mengen als Wert erhält. Initialisiert werden die Size Attribute mit 1. Weiter führen wir eine neue Operation size(i : 1..n) ein, welche uns die Größe der Menge liefert, in der sich der Knoten i befindet. Diese Operation kann man folgendermaßen implementieren: 1: function size(i : 1..n) 2: return size[find(i)]; Zum Ausgeben der Objekte formulieren wir Breitensuchen, die in den jeweiligen Zusammenhangskomponenten nur einmal starten können: 1: procedure findeObjekte(G, R) 2: connectedComponents(G) 3: unmark all nodes 4: forall v ∈ V do 4 if size(v) ≥ R and v not marked then 6: output ”found new object larger than R” 7: perform a bfs starting at v and mark all visited nodes; print them 8: return 5: Wir haben höchstens O(m) Find und Union Operationen, die ausgeführt werden müssen, um Zusammenhangskomponenten zu finden. Da m ∈ O(n) ergibt sich die geforderte Laufzeit für das Finden der Zusammenhangskomponenten. Die modifizierte Breitensuche zum Ausgeben der Komponenten, hat ebenfalls die geforderte Laufzeit. Man beachte das in der size Funktion eine find Operation steckt. Zusatzaufgabe 1 (Bottleneck Shortest Path, 2 + 2 + 1 + 1 Punkte) Sei G = (V, E) ein zusammenhängender ungerichteter gewichteter Graph und s, t ∈ V . Ein kreisfreier Pfad P zwischen s und t heiße ein Bottleneck Shortest Path für s und t, wenn das größte in P auftretende Kantengewicht minimal ist für alle Pfade zwischen s und t. a) Zeigen Sie: Ist T ein MST in G, dann ist der in T eindeutige Pfad P zwischen zwei Knoten s, t ∈ V ein Bottleneck Shortest Path in G für s und t. b) Geben Sie einen Algorithmus an, der für gegebenes G = (V, E), gegebene s, t ∈ V und einen gegebenen MST T in G einen Bottleneck Shortest Path P zwischen s und t ausgibt. Die Laufzeit soll dabei höchstens in O(|P |) liegen (mit |P | die Anzahl Kanten von P ). Nehmen Sie an T wäre vom Jarnı́k-Prim Algorithmus geliefert worden und liege in Form des Arrays parent vor. Hinweis: Es genügt nicht nacheinander die Vorgänger von s und t abzulaufen, da die Laufzeit sonst schlimmstenfalls in Ω(|V |) liegt. c) Argumentieren Sie kurz warum Ihr Algorithmus korrekt ist. d) Argumentieren Sie kurz warum Ihr Algorithmus das geforderte Laufzeitverhalten aufweist. Musterlösung: a) Angenommen, P ist kein Bottleneck Shortest Path in G, dann gibt es einen anderen Pfad P 0 zwischen s und t in G, mit max e Kante auf P 0 c(e) < max e Kante auf P c(e) . Sei e0 = {u, v} eine Kanten auf P mit c(e0 ) = max e Kante auf P c(e) . Offenbar gilt c(e) < c(e0 ) für alle Kanten e auf P 0 , somit liegt e0 nicht auf P 0 . Bzgl. e0 zerfällt T in die Teilbäume Tu und Tv , wobei s und t jeweils in einem von beiden liegen (sonst gäbe es in T mehr als einen einfachen Pfad zwischen ihnen). Da P 0 aber s und t verbindet und e0 nicht auf P 0 liegt, muss es ein Kante e00 = {u0 , v 0 } = 6 e0 geben mit u0 in Tu und v 0 in Tv . Nun können 0 wir einen neuen spannenden Baum T in G bilden, indem wir e0 in T durch e00 ersetzen. Wegen c(e00 ) < c(e0 ) gilt aber c(T 0 ) = c(T ) + c(e00 ) − c(e0 ) < c(T ) . | {z } <0 Wir haben also einen spannenden Baum T 0 in G mit geringeren Gesamtgewicht als T . Dies widerspricht aber der Tatsache, dass T MST ist in G. 5 b) Ausgehend von den Knoten s und t springe man jeweils zum Vorgängerknoten des aktuellen Knoten, was mit Hilfe von parent möglich ist. Dabei werde jeder besuchte Knoten als gesehen“ ” markiert. Irgendwann treffen sich diese beiden Prozesse, was man daran erkennt, dass ein bereits markierter Knoten besucht wird. Beide Prozesse merken sich bei ihrer Arbeit jeweils das umgekehrte“ parent, der eine in einem ” durch eine Liste implementierten Stack, der andere in einer durch eine Liste implementierten Queue. Sobald sich beide Prozesse getroffen haben, kann man zuerst die Queue und dann den Stack leer räumen und so die Kanten des Pfades ausgeben (der Stack oder die Queue enthält möglicherweise ein paar Knoten zuviel, welche man beim entnehmen verwerfen muss). Wichtig ist, dass man die beiden Prozesse nicht nacheinander ablaufen lässt sondern alternierend. D.h., man lässt die beiden Prozesse jeweils abwechselnd zum nächsten Vorgängerknoten springen (sonst kann Laufzeit O(Pfadlänge) nicht garantiert werden). c) Da es in T zwischen zwei Knoten genau einen Pfad gibt, und der Jarnı́ck-Prim den Baum ausgehend von einem Startknoten r wachsen lässt, kommt man durch das Ablaufen des parent irgnedwann zu diesem r. Beide Knoten Prozesse müssen sich daher irgendwann treffen.Dabei sind zwei Fälle möglich: Fall 1: s und t liegen nicht auf dem selben gerichteten Pfad nach r. Dann treffen sich beide gerichtete Pfade in einem Knoten v ∗ , spätestens aber in r. D.h. einer der beiden Prozesse trifft zuerst auf v ∗ und markiert v ∗ , der zweite erkennt dies sobald er ebenfalls auf v ∗ trifft und der Algorithmus terminiert. Fall 2: s und t liegen auf dem selben gerichteten Pfad nach r. Liege o.B.d.A. t zwischen s und r. Dann erreicht die von s gestartete Prozess irgendwann t und wir sind fertig. In diesem Fall sei v ∗ := t. Da in T alle einfachen Pfade eindeutig sind, ist der einfache Pfad zwischen s und t über v ∗ die richtige Lösung. d) Da die beiden Prozesse alternierend ablaufen, stoppen sie jeweils spätestens nach Pfadlänge + 1 Schritten. Das Aufbauen und Abräumen der Stacks besteht in höchstens O(Pfadlänge) Stackoperationen. Sowohl alle Zugriffe auf parent als auch alle Stack- und Queueoperationen (Listenimplementierung!) benötigen nur konstante Zeit. Insgesamt haben wir also O(Pfadlänge) Laufzeit. 6