http://www.mpi-sb.mpg.de/~sschmitt/info5-ss01 IS UN R S SS 2001 E R SIT S Schmitt, Schömer SA IV A Grundlagen zu Datenstrukturen und Algorithmen A VIE N Lösungsvorschläge für das 12. Übungsblatt Letzte Änderung am 6. Juli 2001 Aufgabe 1 Zu zeigen: ∀ (u, v) ∈ E : f (v) < f (u) Bei einer Tiefensuche wird v von u aus entdeckt, da v Nachfolger von u ist. Es gilt folgende Fälle zu betrachten: 1) v ist weiß: v wurde zum ersten mal entdeckt. Nach der Rekursionsvorschrift muß die Bearbeitung von v vor der von u abgeschlossen werden. ⇒ f (v) < f (u) 2) v ist schwarz: die Bearbeitung von v ist schon abgeschlossen, die von u noch nicht. ⇒ f (v) < d(u) <= f (u) 3) v kann nicht grau sein, denn sonst befänden wir uns in einem Zyklus. Die topologische Sortierung erfolgt umgekehrt der ’Reihenfolge’ der f -Werte, also höchster f -Wert = ’Startknoten’ der Topologie. Aufgabe 2 Es existieren mehrere Möglichkeiten zur Bestimmung des kürzesten Weges, wir werden 2 davon vorstellen. Zum einen kann man die Matrix in einen Graphen überführen, und anschließend eine Breitensuche durchführen, da jede Kanten gleich weit führt. Baue den Graphen wie folgt: • Die Zellen der Matrix mit Inhalt 1 repräsentieren die Knoten des Graphen. • S repräsentiert den Startknoten, Z den Zielknoten. • Eine Kante e = (u, v) existiert genau dann, wenn u und v Matrix-Zellen entsprechen, die horizontal oder vertikal benachbart sind und deren beider Inhalt 1, S oder Z ist. Anschließend starten wir eine Breitensuche, und bestimmen somit den kürzesten Weg zu Z. Laufzeit: Breitensuche hat eine Laufzeit von O(|V | + |E|). Es gilt, daß |V | = n2 und pro Knoten maximal 4 Kante, somit |E| = 4n2 .Daher läuft dieser Algorithmus in O(n2 ).. Die Frage ist, ob wir nicht einen Algorithus angeben können, der direkt auf der Matrix arbeitet. Wenigstens hätten wir uns dann die Überführung in einen Graphen gespart. Wir fangen bei der Zelle mir Inhalt S an, besuchen für jede Zelle A[i][j] jeden seiner vier horizontalen und vertikalen Nachbarn A[k][l] ∈ {A[i − 1][j], A[i + 1][j], A[i][j − 1], A[i][j + 1]} und tragen seine Distanz in eine eigene Matrix d[k][l], die wir mit ∞ initialisieren. Zusätzlich nehmen wir A[k][l] in einen Pfad auf, in dem wir ihn zum Nachfolger von A[i][j] machen. Dies bewerkstelligt eine weitere Matrix π ∈ N ×N und eine Anweisung π[k][l] := (i, j). Um nun wirklich breit zu suchen, stecken wir die maximal1 vier Nachbarn in eine Queue Q. In der nächsten Iteration werden wir jedes Element aus Q herausnehmen und obiges wiederholen, bis wir Z gefunden haben. Das Besuchen einer Zelle besorgt die Funktion Explore: int Explore(A,i,j,k,l) { if (d[k][l] == infinity) { if (A[k][l] == 0) return -1; if (A[k][l] == 1 || A[k][l] == Z) { d[k][l] = d[i][j]+1; pi[k][l] = (i,j); } if (A[k][l] == Z) return 1 else return 0; return -1; } } Die Indizes i, j stehen für die zuletzt besuchte Zelle, k, l ist eine ihrer vier Nachbarzellen. Mit Explore stellen wir also fest, ob ein Knoten bereits besucht wurde, nehmen ihn gleichzeitig in den Pfad mit auf und stecken ihn in die Queue. Außerdem prüfen wir, ob das Ziel Z bereits erreicht ist. All dies kann in konstanter Zeit bewerkstelligt werden, da wir auf die Matrix-Eintrage in O(1) zugreifen können und die Queue als doppelten Stack realisieren. Somit hat auch Explore Laufzeit O(1). Nun können wir in A eine Breitensuche durchführen, beginnend bei S, und weiter alle Nachbarn, deren Einträge 1 sind. Da es eine wirkliche Breitensuche ist, brauchen wir bereits besuchte Knoten nicht zu berücksichtigen, da sie eh bereits im kürzeren Pfad aufgenommen worden sind. Unsere Haupfunktion ist also wie folgt: int Search(A, start_x, start_y) { int i,j,k,l; Enqueue(Q,start_x, start_y); while (Q != empty} { (i,j) = Dequeue(Q); for each (k,l) element {(i-1,j), (i+1,j), (i,j-1), (i,j+1)} do { if (Explore(A,i,j,k,l) == 0) Enqueue(Q,k,l) 1 weil es nur Sinn macht, diejenigen Nachbarn A[k][l] weiter zu verfolgen, deren Inhalt 1 ist else if (Explore(A,i,j,k,l) == 1) return 1; \\ "Z gefunden" } } return 0; \\ "Es existiert kein Weg nach Z" } Korrektheit: Da wir lediglich eine Breitensuche durchführen, ist der erste gefundene (S, Z)-Pfad auch der kürzeste. Danach terminiert der Algorithmus. Existiert kein Pfad, läuft der Algorithmus, bis die Matrix keine unbesuchten, oder mit 1 gefüllten Zellen mehr hat und terminiert. Dies geschied in endlicher Zeit, da endlich viele Zellen da sind. Laufzeit: Wir besuchen jede mit 1 gefüllte Zelle. Für jede Zelle rufen wir 4 mal Explore für die vier Nachbarn auf und einmal Dequeue. Jeden mit 1 gefüllten Nachbarn schieben wir nur dann in die Queue wenn er noch nicht besucht worden ist. Mit 0 gefüllte Zellen berücksichtigen wir garnicht. Somit gibt es bei m mit 1 gefüllten Zellen m while-Iterationen und m DequeueOperationen. Wir haben maximal (n−2)2 1-Zellen, somit O(n2 ) while-Iterationen und ebensoviele Enqueue-Operationen. Eine Iteration kostet also O(1), macht gesamt O(n2 ). Aufgabe 3 Zunächst passen wir die Kantengewichte an unsere Anforderungen an. Sei ci,j der Umtauschfaktor von Währung i nach Währung j. Damit wir Gewinn erwirtschaften können muß es eine Folge F von Währungen geben, bei der das Produkt der Umtauschfaktoren > 1 ist, also Q cu,v > 1 (u,v)∈F Um Bellman-Ford anwenden zu können brauchen wird eine additive Regel. Q (u,v)∈F cu,v > 1 ⇔ P (u,v)∈F log cu,v > 0 ⇔ P − log cu,v < 0 (u,v)∈F Durch die Anwendung von − log auf die Umtauschfaktoren erhalten wir eine Kostenfunktion die unseren Anforderungen entspricht. Nun lassen wir Bellman-Ford auf unserem Graphen laufen (Knoten entsprechen Währungen und Kanten entsprechen möglichen Umtauschaktionen). Als Startpunkt nehmen wir unsere Landeswährung (spielt keine Rolle, da sich mögliche Zusatzkosten durch zusätzliche Zyklusdurchläufe ausgleichen lassen). initialize d[v]=infinity; p[v]=v; d[s]=0; do |V| - 1 times { foreach (u,v) in E relax (u,v) // d[v]>d[v]+c(u,v) => d[v]=d[v]+c(u,v); p[v]=u; } Wenn der Graph zykelfrei ist, dann haben wir jetzt für jede Fremdwährung den besten Umtauschweg von unserer Landeswährung bestimmt. Zum Test auf Zykel lassen wir jetzt noch einmal die innere Schleife von Bellman-Ford laufen. foreach (u,v) in E { if (d[v]>d[v]+c(u,v)) { p[v] = u; backtrack(v); } // Zykel gefunden Wenn sich der tentative Abstand d[v] von Knoten v zu s bei diesem Durchlauf nochmal ändert muß ein Zykel vorliegen, da ein zykelloser Pfad in einem Graph mit |V | Knoten maximal Länge |V | − 1 haben kann. Nach dem oberen Durchlauf haben wir also alle möglichen Zyklen der maximalen Länge |V | − 1 bereits ’entdeckt’ (Vorgänger sind richtig), aber noch nicht ’bemerkt’ (wir wissen noch nichts von diesem Zykel). Der untere Durchlauf ’bemerkt’ also entweder einen der bereits entdeckten Zykel (für selbige sind die Vorgänger schon korrekt als Zykel eingetragen), oder er bemerkt einen Zykel der Länge |V |, welcher also alle Knoten enthält und stellt mit der Zeile p[v] = u; die richtige Reihenfolge her. Um den Zykel jetzt explizit aufzuschreiben muß man nur von Knoten v aus, über die Vorgängerliste, alle Knoten ausgeben, bis man wieder bei v angekommen ist. Aufgabe 4 Das gestellte Problem ist bekannt unter den Namen Collatz, Ulams, 3x + 1 Problem und noch einigen mehr. Zu Beginn sei bemerkt, dass die Lösung des Problems noch nicht bekannt ist und eine Belohung von 1000 Pfund auf die Lösung ausgesetzt ist. Die herrschende Meinung ist, dass es nur eine Zusammenhangskomponente gibt. Diese Annahme wurde verifiziert für Zahlen bis 1015 . Wer sich die 1000 Pfund verdienen will oder sich weiter für das Problem interessiert, findet im Internet einige Paper zu dem Thema. Das aktuellste (leider nicht gerade hervoragend geschrieben) ist vom 17.06.01 und unter der URL http://www.occampress.com/intro.pdf zu finden.