PDF

Werbung
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.
Herunterladen