1. ¨Ubungsblatt zu Algorithmen II im WS 2010/2011

Werbung
Karlsruher Institut für Technologie
Institut für Theoretische Informatik
Prof. Dr. Peter Sanders
Dennis Luxen, Dr. Johannes Singler
1. Übungsblatt zu Algorithmen II im WS 2010/2011
http://algo2.iti.kit.edu/AlgorithmenII.php
{luxen,sanders,singler}@kit.edu
Musterlösungen
Aufgabe 1
(Laufzeitschranken für Prioritätswarteschlangen)
a) Beweisen Sie allgemein für adressierbare Prioritätswarteschlangen die untere Laufzeit-Schranke
von Ω(log n) für deleteMin, gegeben dass insert konstante Laufzeit hat.
b) Warum gilt die Laufzeitschranke nicht, wenn es keine Laufzeitbeschränkung für insert gibt?
Musterlösung:
a) Mit einer Laufzeit von O(f (n)) für deleteMin ließe sich in Zeit O(nf (n)) + n · O(1) vergleichsbasiert sortieren, indem man zuerst alle Elemente einfügt und dann eines nach dem anderen in
aufsteigender Reihenfolge entnimmt. Für deleteMin in sublogarithmischer Zeit wäre das einen
Widerspruch zur bekannten unteren Schranke für vergleichsbasiertes Sortieren von Ω(n log n).
b) insert könnte nach jedem Aufruf eine aufsteigend sortierte Liste aller Elemente hinterlassen, mit
deren Hilfe sich alle folgenden min- und deleteMin-Operationen in konstanter Zeit beantworten
ließen.
Aufgabe 2
(Entwurfsaufgabe: Auftragsmanagement)
Ihr Firma bezieht ein Produkt von einem von mehreren möglichen Zulieferbetrieben. Sie wollen nun
die Art und Weise, wie Aufträge vergeben werden, neu gestalten. Die Entscheidung, wie die Aufträge
an die Zulieferer verteilt werden, geschieht nach folgender Regel. Derjenige Zulieferer, der bisher im
laufenden Jahr das kleinste Auftragsvolumen erhalten hat, bekommt den nächsten Auftrag.
a) Entwerfen Sie eine Algorithmus für diese Entscheidung, dessen Laufzeit sublinear ist.
Im darauffolgenden Jahr wird die Auftragsvergabe anhand einer neuen Regel entschieden. Der Zulieferer mit der kleinsten Zahl noch nicht fertiggestellter Aufträge erhält den nächsten neuen Auftrag.
Sie müssen also festhalten, wie viele noch nicht fertiggestellte Aufträge ein Zulieferer hat.
b) Ergänzen Sie Ihre Lösung aus der obigen Teilaufgabe für die Entscheidungsfindung. Unterscheidet sich die Laufzeit asymptotisch von der vorherigen Lösung?
Musterlösung:
Jeder Zulieferer wird anhand einer ID identifiziert.
a) Hauptdatenstruktur ist eine Prioritätswarteschlange. Der Key der Prioritätswarteschlange ist
die Zahl der vergebenen Aufträge, also initial 0. Jeder Zulieferer wird eingefügt und der nächste
Zulieferer mit deleteMin herausgefunden. Der neu beauftragte Zulieferer wird mit aktualisiertem
Auftragsvolumen per insert danach wieder eingefügt. Mit einem Binären Heap lässt sich dies
in Zeit O(log n) pro Auftrag erledigen.
1
b) Hauptdatenstruktur ist eine adressierbare Prioritätswarteschlange. Anstatt des Gesamtauftragsvolumen entspricht der Key in der Prioritätswarteschlange jetzt dem noch nicht abgearbeiteten
Auftragsvolumen. Die Adressen für jeden Zulieferer müssen nach jedem insert natürlich gespeichert werden, um später drauf zu greifen zu können. Zuweisung von Aufträgen erfolgt wieder
über deleteMin und insert, wie schon in Teilaufgabe a). Wird ein Teil eines Auftrags oder ein
ganzer Auftrag fertig gemeldet, so wird per decreaseKey die Priorität des Zulieferers angepasst.
Die Laufzeit ist identisch zur Teilaufgabe a), da deleteMin die Laufzeit dominiert.
Aufgabe 3
(Spezialfälle von Prioritätswarteschlangen)
Sie erarbeiten in dieser Aufgabe, dass die Datenstrukturen Stack und Queue Spezialfälle einer Prioritätswarteschlange sind. Machen Sie sich gegebenenfalls erneut mit den grundlegenden Operationen
dieser beiden Datenstrukturen vertraut.
a) Welchen Schlüssel muss man bei einem push auf einem Stack einfügen, damit die Prioritätswarteschlange zum Stack wird?
b) Welchen Schlüssel muss man bei einem push auf einer Queue einfügen, damit die Prioritätswarteschlange zur Queue wird?
c) Angenommen, Stack und Queue halten nie mehr als O(n) Elemente. Kommt man mit O(n)
vielen verschiedenen Schlüsseln aus?
Musterlösung:
Als Keys verwenden wir natürliche Zahlen.
a) Sei j der bisher kleinste Key in der Prioritätswarteschlange und k der aktuell einzufügende.
Dann muss immer gelten k < j. Als erster Schlüssel wird 0 gewählt, danach immer k := j − 1.
b) Als erster Schlüssel wird s := 0 gewählt, und s wird bei jedem push inkrementiert.
c) Im Fall des Stacks reichen O(n) viele Schlüssel, denn der zuletzt entnommene Schlüssel kann
jeweils neu benutzt werden (impliziert durch k := j − 1).
Für den Fall der Queue gilt das nicht. Die Elemente werden in FIFO-Reihenfolge eingefügt und
entnommen. Das heißt, dass die Differenz zwischen erstem und letztem Key immer mindestens
Anzahl Elemente in der Queue minus 1 ist. Daraus folgt, dass die Schlüsselfolge monoton wachsend ist, sofern die Queue nie leer wird. Nur in letzterem Fall kann s zurückgesetzt werden. Im
schlimmsten Fall benötigt man also O(i) Schlüssel, wobei i die Anzahl inserts ist.
Aufgabe 4
(Asymptotische Laufzeit-Unterschiede zwischen Fibonacci-Heaps und Suchbäumen)
a) Bei welchen Operationen hat ein Fibonacci-Heap asymptotische Laufzeit-Vorteile gegenüber
Suchbäumen?
b) Was ist der Unterschied zwischen Concatenation beim Suchbäumen und Merge bei FibonacciHeaps (Funktionalität und Laufzeit)?
Musterlösung:
a) Bei decreaseKey und insert benötigt ein Fibonacci-Heap nur O(1) statt O(log n) Zeit. Mergen
von zwei Fibonacci-Heaps benötigt nur konstante Zeit, während dies bei Suchbäumen im Allgemeinen lineare Zeit braucht (auslesen der sortierten Listen, mischen, Aufbau eines neuen Baums
aus der sortierten Liste).
2
left sibling
or parent
B1
data
B2
B3
right sibling
one child
Abbildung 1: Ein Heap-Item für eine Drei-Zeiger-Implementierung eines Pairing Heaps (links) sowie
die Bäume B1 , B2 und B3 .
b) Concatenation bei (a, b)-Bäumen vereinigt wie Mergen zwei Suchbäume, allerdings ist Voraussetzung, dass die Wertebereiche der beiden Suchbäume disjunkt sind, d. h. das kleinste Element
des einen größer gleich dem größten des anderen ist, oder umgekehrt. Dafür läuft das dann auch
in logarithmischer Zeit in der Größe des größeren der beiden Suchbäume.
Aufgabe 5
(Implementierung von Pairing Heaps)
Betrachten Sie eine Implementierung von Pairing Heaps, die — wie in der Vorlesung vorgestellt — mit
Hilfe von drei Pointern pro Heap-Item realisiert wurde. Abbildung 1 zeigt die schematische Darstellung
eines einzelnen Heap-Items für diese Implementierung. Man darf aber nicht vergessen, dass auch
die Menge der Wurzelknoten ( Root-Set“) irgendwie dargestellt werden muss. Wie in der Vorlesung
”
angegeben, soll dies in Form einer doppelt verketteten Liste geschehen.
a) Wie stellt die Datenstruktur einen Baum der Form B3 (siehe Abbildung 1) im Detail dar?
Zeichnen Sie diesen Fall. Null-Zeiger sollen durch leere Kästchen symbolisiert werden. Das RootSet soll uns in dieser Teilaufgabe aber noch nicht interessieren, ignorieren Sie es also.
b) Wie kann das Root-Set als doppelt verkettete Liste im Detail realisiert werden? Verwenden Sie
nur die Heap-Items und keine zusätzlichen Datentypen. Wie stellt Ihre Realisierung einen Pairing
Heap, der zwei Bäume der Form B1 bzw. B2 enthält, im Detail dar? Zeichnen Sie, diesmal mit
Darstellung des Root-Set.
c) Geben Sie den Pseudo-Code für die Operationen cut(h : Handle) und decreaseKey(h : Handle, k :
Key) an. Spendieren Sie dazu den Heap-Items aber eine Markierung, um deren Mitgliedschaft
im Root-Set anzuzeigen.
d) Angenommen, Sie statten die Heap-Items nicht mit einer Markierung für die Wurzeleigenschaft
aus. Wie wirkt sich das auf den Pseudo-Code von decreaseKey bzw. cut aus?
Musterlösung:
a) Der Einfachheit halber lassen wir in der folgenden Zeichnung das Daten-Feld der Heap-Items weg.
Das Root-Set war ja nach Aufgabenstellung in dieser Teilaufgabe ebenfalls nicht zu betrachten.
3
b) Nun wird auch das Root-Set dargestellt, und zwar als doppelt verkettete Root-Liste“. Um auch
”
den leeren Pairing Heap darstellen zu können (der ja nur aus einem leeren Root-Set besteht),
muss es möglich sein, die leere Root-Liste darzustellen. Zu diesem Zweck gibt es in der RootListe ein speziell ausgezeichnetes Dummy-Item (grau). Wäre die Liste leer, würde sie nur aus
dem Dummy-Item bestehen.
Root−Liste
B1
B2
c) Zunächst geben wir eine detaillierte Spezifikation des Datentyps für die Heap-Items an. Dabei
spendieren wir den Heap-Items wie in der Aufgabenstellung gefordert eine Markierung für die
Wurzeleigenschaft:
1:
2:
3:
4:
5:
6:
7:
class Heap Item
key : Key
parent or left : Handle
right : Handle
child : Handle
is root : Boolean
end class
Das Feld parent or left zeigt entweder auf auf den linken Nachbar oder — wenn es keinen gibt
— auf das Elternelement. Nun können wir den Pseudo-Code für die Operation cut angeben.
Letztlich handelt es sich dabei nur um eine Abfolge von Zeiger-Umbiegereien“. Dabei bezeichne
”
root list das speziell ausgezeichnete Dummy-Item, das die Root-Liste repräsentiert.
Zunächst entfernen wir h (und damit auch seinen Unterbaum) aus dem Unterbaum des Elternelementes von h (Zeilen 6 bis 15). Dann hängen wir h (und damit auch seinen Unterbaum) in
4
die Root-Liste ein (Zeilen 17 bis 22). Dabei sehen wir, dass cut einer Splice-Operation ähnelt
(siehe doppelt verkettete Listen).
1:
procedure cut(h : Pointer toItem)
2:
3:
4:
// Wenn h schon Wurzel ist, macht die Anwendung von cut keinen Sinn
assert not h.is root
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
// Entferne h samt Unterbaum aus dem Unterbaum des Elternelementes von h
if h.parent or left.child = h then
// erstes Kind des Elternknotens
h.parent or left.child := h.right
h.right.parent or left := h.parent
else
// weiteres Kind des Elternknotens
h.parent or left.right := h.right
h.right.parent or left := h.parent or left
end if
16:
17:
18:
19:
20:
21:
22:
// Füge h samt Unterbaum an das Ende der Root-Liste an
last := root list.parent or left //lokale Variable
root list.parent or left := h
h.right := root list
h.parent or left := last
last.right := h
23:
// h ist jetzt eine Wurzel
h.is root := true
26: return
24:
25:
Unter Nutzung des Attributes is root des Datentyps Heap Item können wir auch den PseudoCode für decreaseKey formulieren:
procedure decreaseKey(h : Handle, k : Key)
assert k ≤ h.key
3: h.key := k
4: if not h.is root then cut(h)
5: return
1:
2:
Natürlich kann man (so wie in der Vorlesung) die Zeilen 17 bis 25 von cut durch einen Aufruf
an newTree ersetzen.
d) Es gibt nun außer dem Ablaufen der Root-Liste keine Möglichkeit mehr die Wurzeleigenschaft
festzustellen. In cut muss nun Zeile 4 (das ist die assert-Anweisung) ersetzt werden durch
assert h bezeichnet keine Wurzel
da das Attribut is root ja nicht mehr existiert. Für die Zeile 4 von decreaseKey gibt es u.a. diese
beiden Möglichkeiten:
1. Ersetze
if not h.is root then cut(h)
durch ein bloßes cut(h).
2. Statt das Attribut is root abzufragen, überprüfe die Wurzeleigenschaft durch Ablaufen der
Root-Liste.
5
In beiden Fällen bleibt das äußere Verhalten des Pairing-Heap korrekt — wenn man das Zeitverhalten außer acht lässt. Im ersten Fall wird bei jedem Aufruf von decreaseKey(h, k) das Item von
h samt seinem Unterbaum an das Ende der Liste verschoben — auch dann, wenn es sich bereits
um ein Wurzelelement handelt. Wie sich dies aber auf das (amortisierte) Laufzeitverhalten des
Pairing-Heap auswirkt, ist fraglich. Im zweiten Fall wird bei jedem Aufruf von decreaseKey(h, k)
die Root-Liste abgelaufen. Dies dauert natürlich seine Zeit, was das (amortisierte) Zeitverhalten
ebenfalls verändern könnte. An dieser Stelle muss aber darauf hingewiesen werden, dass das
amortisierte Laufzeitverhalten von Pairing-Heaps sowieso noch nicht völlig verstanden ist.
Aufgabe 6
(Rechenaufgabe Fibonacci-Heaps)
Gegeben sei ein leerer Fibonacci-Heap M . Nun werde folgende Operationsfolge ausgeführt (M.insert(n)
liefere dabei einen Zeiger auf ein neu eingefügtes Element mit Schlüssel n ∈ N zurück):
M.insert(3); M.insert(8); M.insert(2);
x := M.insert(5); M.insert(10); M.deleteMin();
M.insert(6); M.insert(7); M.decreaseKey(x, 2); M.deleteMin()
a) Welche Zustände durchläuft der Fibonacci-Heap im Verlauf der Ausführung? Zeichnen Sie abstrakt (d. h. die einzelnen Zeiger müssen nicht gezeichnet werden).
b) Nehmen Sie nun an, dass der Fibonacci-Heap mit Hilfe von vier Pointern pro Heap-Item realisiert
wird (vgl. Buch von Mehlhorn und Sanders, Abbildung 6.8). Zeichnen Sie den Zustand nach
Ausführung der letzten Operation detailliert.
Musterlösung:
a) Nach ausführen der ersten 5 Operationen hat der Fibonacci-Heap folgenden Zustand (die gestrichelte Linie symbolisiert das Root-Set):
min
3
8
2
5
10
Durch das anschließende deleteMin() wird dann die 2 entfernt, was eine Konsolidierung“ der
”
Datenstruktur nach sich zieht. Dabei wird das folgende Verfahren angewendet: Solange im RootSet noch Knoten gleichen Ranges vorkommen werden diese verbunden. Der Rang eines Knotens
ist dabei die Anzahl seiner Kinder. Das verbinden zweier Wurzelknoten bedeutet, dass der eine
als Kind an den anderen angehängt wird (der Rang des Knotens, der im Root-Set verbleibt steigt
dabei um 1). Beim verbinden zweier Knoten ist jedoch zu beachten, dass die Heap-Bedingung
erhalten werden muss (diese Besagt, dass ein Elternknoten nie einen größeren Schlüssel enthalten
darf als einer seiner Kindknoten). Welcher Knoten an welchen angehängt wird, hängt also davon
ab, welcher Knoten den kleineren Schlüssel hat (bei gleichen Schlüsseln kann frei entschieden
werden). Das Verfahren terminiert erst, wenn keine Knoten gleichen Ranges mehr vorhanden
sind.
Wir führen nun die Konsolidierung für unser Beispiel durch:
6
min
3
8
5
10
3
5
8
10
3
8
5
10
Als nächstes werden zwei weitere Elemente eingefügt (mit Schlüsseln 6 und 7), und dann der
Schlüssel 5 des Elements x auf 2 gesenkt (decreaseKey(x, 2)). Normalerweise hätten wir keinen
Zugang zu dem entsprechenden Element, da das Interface einer Priority Queue keinen wahlfreien
Zugriff bietet. Allerdings hatten wir uns ja einen Zeiger x auf dieses Element gemerkt. Die
nächsten Zustände sind also die folgenden:
min
min
3
8
6
3
7
6
7
2
8
5
10
10
Nun wird noch einmal deleteMin() ausgeführt, was das Element mit der 2 entfernt und zu einer
erneuten Konsolidierung der Datenstruktur führt:
min
3
8
6
7
10
3
6
8
7
10
3
8
10
6
7
Bemerkung: Die Technik der kaskadierenden Schnitte muss in diesem Beispiel nicht angewendet werden, da der einzige Aufruf von decreaseKey nur ein Kind einer Wurzel betrifft. Wurzeln
werden bei den kaskadierenden Schnitten aber nicht markiert.
b) Wie in der Lösung zu Aufgabe 5(b) wird das Root-Set wieder als doppelt verkettete, zyklische
List mit Dummy-Item (grau) realisiert.
7
3
10
6
8
7
Achtung: In der Übung haben wir den Eltern-Zeiger des Knotens, der den Schlüssel 6 hat,
leider vergessen. Aber in einem Fibonacci-Heap mit 4-Pointer-Realisierung soll jeder Knoten
auf seinen Elternknoten zeigen.
Aufgabe 7
(Rechenaufgabe Fibonacci-Heaps)
Abbildung 2 zeigt den inneren Zustand eines Fibonacci-Heap. Das angekreuzte Heap-Item sei dabei
besonders markiert (wie es im Rahmen der Cascading-Cuts-Technik üblich ist).
a) Gegeben sei ein Fibonacci-Heap mit einem inneren Zustand wie in Abbildung 2. Wie in der
Abbildung dargestellt seien außerdem drei Zeiger x, y und z auf Heap-Items gegeben. Nun
werde folgende Operationenfolge ausgeführt:
decreaseKey(x, 2); decreaseKey(y, 7); decreaseKey(z, 4); deleteMin()
Welche inneren Zustände durchläuft der Fibonacci-Heap? Zeichnen Sie abstrakt.
b) Sind folgende Aussagen richtig oder falsch? Begründen Sie jeweils kurz.
1. In einem Pairing-Heap ist jeder Baum von der Form Bi für ein jeweils geeignetes i ∈ N0 .
2. Die Cascading-Cuts-Technik sorgt dafür, dass die Anzahl der Wurzelknoten in einem Fibonacci-Heap höchstens logarithmisch in n ist (n sei die Anzahl der Elemente im Heap).
3. Für Pairing-Heaps hat die Operation decreaseKey einen amortisierten Zeitaufwand von
O(log n), wobei n die Anzahl der Elemente im Heap sein soll.
Musterlösung:
a) Als erstes wird die Operation decreaseKey(x, 2) ausgeführt. Dies hat zur Folge, dass die 12 zur 2
wird und das durch x bezeichnete Item zu einer Wurzel wird. Letzteres bedeutet aber, dass ein
cut ausgeführt wird. Gemäß der Cascading-Cuts-Strategie muss also das Elternitem von ∗x (im
Beispiel hat es den Schlüssel 9) markiert werden, falls es noch nicht markiert ist. Natürlich muss
8
min
3
6
11
z
x
9
12
14
17
y
Abbildung 2: Innerer Zustand eines Fibonacci-Heaps.
auch der Zeiger auf das Item mit dem minimalen Schlüssel neu gesetzt werden. Entsprechend
sieht der neue innere Zustand des Fibonacci-Heaps folgendermaßen aus (die kurzen doppelten
Linien bezeichnen die Stelle, wo als nächstes abgeschnitten werden muss):
min
min
3
3
6
6
11
z
x
11
9
12
9
14
14
17
2
y
17
Nun wird die Operation decreaseKey(y, 7) ausgeführt. Das Item ∗y ist nach wie vor das, welches
den Schlüssel 17 trägt. Wie schon bei der vorigen Operation muss das Item abgeschnitten und
zur Wurzel gemacht werden. Wiederum muss auch das zugehörige Elternitem (hat den Schlüssel
14) markiert werden, diesmal ist das Elternitem aber schon markiert. In solchen Fällen schreibt
die Cascading-Cuts-Strategie vor, dass das Elternitem ebenfalls abgeschnitten werden muss und
wiederum dessen Elternitem markiert werden soll, usw... Dieser Vorgang darf erst abgebrochen
werden, wenn ein nicht markiertes Elternitem erreicht wird (dabei beachte man das Wurzeln
nie markiert sind). Übrigens: Wenn ein markiertes Element abgeschnitten und somit zur Wurzel
gemacht wird, dann wird eine etwaige Markierung entfernt.
Für unser Beispiel sehen die durchlaufenen inneren Zustände wie folgt aus (die doppelten Linien
bezeichnen wieder, wo als nächstes abgeschnitten werden muss):
min
3
2
min
7
3
6
11
2
6
9
11
14
9
9
7
14
min
3
2
14
7
9
6
11
Als nächstes wird die Operation decreaseKey(z, 4) ausgeführt. Das Item ∗z ist immer noch das,
welches den Schlüssel 11 trägt. Sein Elternitem ist markiert, so dass es ebenfalls abgeschnitten
werden muss:
min
3
7
14
9
4
7
14
9
4
2
6
min
3
2
6
Als letztes wird schließlich die Operation deleteMin() ausgeführt, was zu einer Konsolidierung
des Fibonacci-Heap führt. Da das entsprechende Verfahren auf dem vorherigen Übungsblatt ja
ausführlich behandelt worden ist, wird hier nur noch der Zustand nach Ende der Konsolidierung
dargestellt:
min
3
7
4
9
6
14
b) Wir betrachten die Aussagen 1 bis 3 aus der Aufgabenstellung der Reihe nach:
Aussage 1: Falsch.
Der B2 , wie direkt unten abgebildet, bezeichnet eine mögliche Form für einen Baum in
einem Pairing-Heap (überlegen!):
10
min
3
7
9
14
x
Nun werde decreaseKey(x, 4) ausgeführt. Dies hat zur Folge, dass das Item ∗x abgeschnitten
wird und ein Baum entsteht, dessen Form sich nicht mehr durch Bi darstellen lässt (für ein
i ∈ N0 ).
Aussage 2: Falsch.
Die Cascading-Cuts-Technik sorgt dafür, dass der Rang aller Knoten (d. h. Heap-Items) im
Fibonacci-Heaps höchstens logarithmisch in n ist. Der Rang eines Heap-Items ist dabei die
Anzahl der Kinder dieses Items.
Aussage 3: Richtig.
Der amortisierte Zeitaufwand von decreaseKey ist für Pairing-Heaps zwar noch nicht völlig
verstanden, auf jeden Fall liegt er aber in O(log n). Möglicherweise ist er aber sogar besser.
Aufgabe 8
(Starke Zusammenhangskomponenten)
Gegeben sei ein gerichteter Graph G = (V, E). Wie schon in der Vorlesung definieren wir
∗
v ←→ w
es gibt Pfade hv, . . . , wi und hw, . . . , vi in G
:⇐⇒
∗
für v, w ∈ V . Zeigen Sie nun, dass ←→ eine Äquivalenzrelation ist.
Musterlösung: Um nachzuweisen, dass es sich um eine Äquivalenzrelation handelt, müssen Reflexivität, Symmetrie und Transitivität der Relation gezeigt werden.
Reflexivität: Alle Pfade hvi mit v ∈ V existieren per Definition.
∗
Symmetrie: Es seien v, w ∈ V mit v ←→ w. Dann gibt es Pfade hv, . . . , wi und hw, . . . , vi in G. Damit
∗
gilt aber auch w ←→ v.
∗
∗
Transitivität: Es seien u, v, w ∈ V mit u ←→ v und v ←→ w. Dann gibt es also Pfade hu, . . . , vi und
hv, . . . , wi in G und somit auch einen Pfad hu, . . . , v, . . . , wi. Analog zeigt man, dass es einen Pfad
∗
∗
hw, . . . , ui in G gibt. Damit folgt u ←→ w. Also ist ←→ transitiv.
11
Herunterladen