Algorithmen und Datenstrukturen Werner Struckmann Wintersemester 2005/06
9. Entwurf von Algorithmen 9.1 Einführung 9.2 Teile-und-Beherrsche-Algorithmen 9.3 Gierige Algorithmen 9.4 Backtracking-Algorithmen 9.5 Dynamische Programmierung
Entwurf von Algorithmen In diesem Kapitel stellen wir anhand von Beispielen einige typische Prinzipien für den Entwurf von Algorithmen vor. Die folgenden Techniken haben wir (implizit oder explizit) bereits kennen gelernt. Schrittweise Verfeinerung des Problems Reduzierung der Problemgröße durch Rekursion Einsatz von Algorithmenmustern 9.1 Einführung 9-1
Schrittweise Verfeinerung des Problems Die erste Formulierung des Problems erfolgt in einem sehr abstrakten Pseudocode. Die schrittweise Verfeinerung basiert auf dem Ersetzen von Pseudocode durch verfeinerten Pseudocode und letztlich durch konkrete Algorithmenschritte. 9.1 Einführung 9-2
Problemreduzierung durch Rekursion Diese Technik kann angewendet werden, wenn das Problem auf ein gleichartiges, aber kleineres Problem zurückgeführt werden kann. Die Rekursion muss schließlich auf ein oder mehrere kleine Probleme führen, die sich direkt lösen lassen. Rekursion bietet sich an, wenn die Problemstruktur rekursiv aufgebaut ist. Beispiele: Listen, Bäume. Zu rekursiven Lösungen gibt es iterative Entsprechungen (zum Beispiel durch Einsatz eines Kellers, s. Aufgabe 23). Bei der Auswahl zwischen iterativer und rekursiver Lösung ist die Effizienz der Realisierung zu berücksichtigen. 9.1 Einführung 9-3
Einsatz von Algorithmenmustern Beispiele für Algorithmenmuster: Inkrementelle Vorgehensweise Teile-und-Beherrsche-Algorithmen Gierige Algorithmen (Greedy Algorithmen) Backtracking-Algorithmen Dynamische Programmierung Die Zuordnung eines Musters zu einem Algorithmus ist nicht immer eindeutig und manchmal sogar unmöglich. Beispielsweise kann der Algorithmus von Kruskal als inkrementeller und als gieriger Algorithmus gesehen werden. Es gibt weitere Algorithmenmuster. 9.1 Einführung 9-4
Inkrementelle Vorgehensweise Beispiel: Sortieren durch Einfügen benutzt eine inkrementelle Herangehensweise. Nachdem das Teilfeld a[1..j 1] sortiert wurde, wird das Element a[j] an der richtigen Stelle eingefügt, woraus sich das sortierte Teilfeld a[1..j] ergibt. Weitere Beispiele: Algorithmus von Kruskal Algorithmus von Prim Beide Algorithmen bauen schrittweise einen minimalen Spannbaum auf. 9.1 Einführung 9-5
Teile-und-Beherrsche-Algorithmen Teile das Problem in eine Anzahl von Teilproblemen auf. Beherrsche die Teilprobleme durch rekusives Lösen. Wenn die Teilprobleme hinreichend klein sind, dann löse sie auf direktem Wege. Verbinde die Lösungen der Teilprobleme zur Lösung des Ausgangsproblems. 9.2 Teile-und-Beherrsche-Algorithmen 9-6
Beispiel: Sortieren durch Mischen Sortieren durch Mischen (Mergesort, vgl. Abschnitt 3.2) arbeitet rekursiv nach folgendem Schema: 1. Teile die Folge in zwei Teilfolgen auf. 2. Sortiere die beiden Teilfolgen. 3. Mische die sortierten Teilfolgen. 4 2 9 5 8 2 1 6 4 2 9 5 8 2 1 6 2 4 5 9 1 2 6 8 1 2 2 4 5 6 8 9 9.2 Teile-und-Beherrsche-Algorithmen 9-7
Beispiel: Sortieren durch Mischen Alternativ könnte man die Liste auch in mehr als zwei Listen aufteilen, hätte dann aber in der Mischphase größeren Aufwand. Allgemein: Die Rekursionstiefe kann durch stärkere Spaltung verringert werden. Dies bedingt allerdings einen größeren Aufwand in der Teile- und der Zusammenführungsphase. 9.2 Teile-und-Beherrsche-Algorithmen 9-8
Beispiel: Türme von Hanoi n Scheiben verschiedener Größe sind aufeinandergestapelt. Es liegen stets nur kleinere Scheiben auf größeren. Der gesamte Stapel soll Scheibe für Scheibe umgestapelt werden. Ein dritter Stapel darf zur Zwischenlagerung benutzt werden, ansonsten dürfen die Scheiben nirgendwo anders abgelegt werden. Auch in jedem Zwischenzustand dürfen nur kleinere Scheiben auf größeren liegen. 9.2 Teile-und-Beherrsche-Algorithmen 9-9
Beispiel: Türme von Hanoi Gesucht ist ein Algorithmus, der dieses Problem löst. Dazu muss der Algorithmus angeben, in welcher Reihenfolge die Scheiben zu bewegen sind. 1. Bringe die obersten n 1 Scheiben von Turm 1 zu Turm 3. 2. Bewege die unterste Scheibe von Turm 1 zu Turm 2. 3. Bringe die n 1 Scheiben von Turm 3 zu Turm 2. 2 1 3 9.2 Teile-und-Beherrsche-Algorithmen 9-10
Beispiel: Türme von Hanoi 1. Bringe die obersten n 1 Scheiben von Turm 1 zu Turm 3. 2. Bewege die unterste Scheibe von Turm 1 zu Turm 2. 3. Bringe die n 1 Scheiben von Turm 3 zu Turm 2. proc hanoi(n:int; t1, t2, t3: Turm) begin if n > 1 then hanoi(n-1, t1, t3, t2); fi; <bewege die Scheibevon t1 nach t2>; if n > 1 then hanoi(n-1, t3, t2, t1); fi; end 9.2 Teile-und-Beherrsche-Algorithmen 9-11
Beispiel: Türme von Hanoi Diese Lösung besteht aus einer rekursiven Prozedur hanoi(n, t1, t2, t3). Die Problemgröße ist n. Der Aufrufhanoi(n, t1, t2, t3) bewegt den Stapel von t1 nacht2 und verwendett3 als Hilfsstapel. Für die Anzahl T(n) der notwendigen Schritte gilt T(n) = Explizit ergibt sich T(n) = 2 n 1. 1 für n = 1, 2T(n 1)+1 für n>1. 9.2 Teile-und-Beherrsche-Algorithmen 9-12
Komplexität von Teile-und-Beherrsche-Algorithmen Die Problemgröße sei durch eine natürliche Zahl n gegeben. Die Berechnung der Komplexität führt häufig auf Rekurrenzgleichungen der Form T(n) = Θ(1), falls n klein ist, at(n/b) + f(n), falls n groß genug ist, oder T(n) = f(t(n 1),...,T(n k)) mit gegebenen Anfangswerten T(0),...,T(k 1). Beispiel: Sortieren durch Mischen, Türme von Hanoi. Wir wiederholen jetzt das Mastertheorem und das Verfahren zur Lösung linearer Rekurrenzgleichungen mit konstanten Koeffizienten. 9.2 Teile-und-Beherrsche-Algorithmen 9-13
Beispiel: Die Multiplikation nach Karatsuba Wie groß ist die Komplexität des klassischen Verfahrens zur Multiplikation natürlicher Zahlen? Wenn der erste Faktor n-stellig und der zweite m-stellig ist, dann müssen zuerst n m Einzelmultiplikationen durchgeführt werden. Anschließend sind m Zahlen der Maximallänge n+m zu addieren. Das Ergebnis ist im Allgemeinen eine (n + m)-stellige Zahl. Den größten Anteil trägt offenbar das Produkt n m bei. Die Komplexität des Verfahrens liegt daher in Θ(n m) bzw. in Θ(n 2 ), wenn beide Zahlen die Länge n besitzen. 9.2 Teile-und-Beherrsche-Algorithmen 9-14
Beispiel: Die Multiplikation nach Karatsuba Im Jahre 1962 stellte A. Karatsuba ein schnelleres Verfahren zur Multiplikation vor. Die Idee besteht darin, die Zahlen x und y der Länge nin Stücke der Länge n/2 aufzuteilen, sodass x y = a 10 n/2 + b = c 10 n/2 + d gilt. Beispiel: n = 4, x = 3141 = 31 10 2 + 41 9.2 Teile-und-Beherrsche-Algorithmen 9-15
Beispiel: Die Multiplikation nach Karatsuba Wir erhalten: x y = (a 10 n/2 + b)(c 10 n/2 + d) = ac 10 n +(ad + bc) 10 n/2 + bd = ac 10 n +((a + b)(c + d) ac bd) 10 n/2 + bd Die Berechnung des Produkts zweier Zahlen x und y der Länge nwird zurückgeführt auf die Berechnung der drei Produkte ac, bd und (a + b)(c + d) der Länge n/2. Dann wird dasselbe Verfahren rekursiv auf diese drei Produkte angewendet. 9.2 Teile-und-Beherrsche-Algorithmen 9-16
Beispiel: Die Multiplikation nach Karatsuba Beispiel: x = 3141, y = 5927 x y = 3141 5927 = 31 59 10 4 + ((31+41)(59+27) 31 59 41 27) 10 2 + 41 27 = 31 59 10 4 + (72 86 31 59 41 27) 10 2 + 41 27 = 1829 10 4 +(6192 1829 1107) 10 2 + 1107 = 1829 10 4 + 3256 10 2 + 1107 = 18616707 9.2 Teile-und-Beherrsche-Algorithmen 9-17
Beispiel: Die Multiplikation nach Karatsuba Für die Komplexität T(n) des Verfahrens gilt: T(n) = k, n = 1 ( n 3 T 2) + kn, n>1 Diese Rekurrenzgleichung besitzt die Lösung T(n) = 3kn log 2(3) 2kn = Θ ( n log 2(3) ) = Θ ( n 1,585). Das ist deutlich günstiger als Θ ( n 2). Allerdings wirken sich die Verbesserungen erst bei großen Zahlen aus. 9.2 Teile-und-Beherrsche-Algorithmen 9-18
Beispiel: Die Multiplikation nach Karatsuba Wir haben oben die Faktoren x und y in je zwei Teile zerlegt. Durch Aufspalten in noch mehr Teile können wir die Laufzeit weiter verbessern: Für jedesε>0gibt es ein Multiplikationsverfahren, das höchstens c(ε)n 1+ε Schritte benötigt. Die Konstante c(ε) hängt nicht von n ab. In den 1970er Jahren wurde diese Schranke auf verbessert. O (n log(n) log(log(n))) 9.2 Teile-und-Beherrsche-Algorithmen 9-19
Gierige Algorithmen Annahmen: Es gibt eine endliche Menge von Eingabewerten. Es gibt eine Menge von Teillösungen, die aus den Eingabewerten berechnet werden können. Es gibt eine Bewertungsfunktion für Teillösungen. Die Lösungen lassen sich schrittweise aus Teillösungen, beginnend bei der leeren Lösung, durch Hinzunahme von Eingabewerten ermitteln. Gesucht wird eine/die optimale Lösung. Vorgehensweise: Nimm (gierig) immer das am besten bewertete Stück. 9.3 Gierige Algorithmen 9-20
Beispiel: Algorithmus zum Geldwechseln Münzwerte: 1, 2, 5, 10, 20, 50 Cent und 1, 2 Euro. Wechselgeld soll mit möglichst wenig Münzen ausgezahlt werden. 1,42 : 1 +20 Cent + 20 Cent + 2 Cent Allgemein: Wähle im nächsten Schritt die größtmögliche Münze. In unserem Münzsystem gibt diese Vorgehensweise immer die optimale Lösung. Im Allgemeinen gilt dies nicht. Angenommen, es stünden 1, 5 und 11 Cent Münzen zur Verfügung. Um 15 Cent herauszugeben, ergäbe sich: gierig: 11 + 1 + 1 + 1 + 1, optimal: 5 + 5 + 5. 9.3 Gierige Algorithmen 9-21
Gierige Algorithmen func greedy(e: Eingabemenge): Ergebnis begin var L: Ergebnismenge; var x: Element; E.sort(); while! E.empty() do x E.first(); E.remove(x); if valid(l {x}) then L.add(x); fi; od; return L; end 9.3 Gierige Algorithmen 9-22
Beispiel: Bedienreihenfolge im Supermarkt n Kunden warten vor einer Kasse. Der Bezahlvorgang von Kunde i dauere c i Zeiteinheiten. Welche Reihenfolge der Bedienung der Kunden führt zur Minimierung der mittleren Verweilzeit (über alle Kunden)? Die Gesamtbedienzeit T ges = n i=1 c i ist konstant. Die mittlere Verweilzeit ist T = 1 n (c 1 +(c 1 + c 2 )+ +(c 1 + +c n )) = 1 n (nc 1 +(n 1)c 2 +(n 2)c 3 + +2c n 1 + c n ) = 1 n n (n k + 1)c k k=1 9.3 Gierige Algorithmen 9-23
Beispiel: Bedienreihenfolge im Supermarkt Die mittlere Verweilzeit pro Kunde steigt, wenn Kunden mit langer Bedienzeit vorgezogen werden. sinkt, wenn Kunden mit kurzer Bedienzeit zuerst bedient werden. wird minimal, wenn die Kunden nach c i aufsteigend sortiert werden. Konsequenzen: Greedy-Algorithmus ist geeignet. Die Funktion zur Bestimmung des nächsten Kandidaten wählt den Kunden mit minimaler Bedienzeit. Frage: Ist dies eine geeignete Strategie für die Prozessorvergabe? 9.3 Gierige Algorithmen 9-24
Beispiel: Algorithmus von Kruskal Selektiere fortwährend eine verbleibende Kante mit geringstem Gewicht, die keinen Zyklus erzeugt, bis alle Knoten verbunden sind (Kruskal, 1956). Eine eindeutige Lösung ist immer dann vorhanden, wenn alle Gewichte verschieden sind. 4 3 3 2 6 3 5 8 6 4 5 7 2 6 Nach Wahl der Kanten 2, 2, 3 und 3 darf die verbleibende 3 nicht gewählt werden, da sonst ein Zyklus entstünde. 9.3 Gierige Algorithmen 9-25
Matroide Greedy-Algorithmen liefern nicht immer eine optimale Lösung. Mithilfe der Theorie der gewichteten Matroide kann bestimmt werden, wann ein Greedy-Algorithmus eine optimale Lösung liefert. Die Theorie der bewerteten Matroide deckt nicht alle Fälle ab. 9.3 Gierige Algorithmen 9-26
Matroide Ein Matroid ist ein Paar M = (S,I) mit folgenden Eigenschaften: S ist eine endliche Menge. I ist eine nichtleere Familie von Teilmengen von S. Vererbungseigenschaft: Sind A B und B I, so ist A I. Austauscheigenschaft: Sind A, B I und A < B, so gibt es ein x (B\ A) mit A {x} I. Die Mengen iniheißen unabhängig. Eine unabhängige Menge A Iheißt maximal, wenn es keine Erweiterung x mit A {x} Igibt. 9.3 Gierige Algorithmen 9-27
Matroide Ein Matroid M = (S,I) heißt gewichtet, wenn es eine Gewichtsfunktion w : S R + gibt. Die Gewichtsfunktion lässt sich auf Teilmengen A S durch w(a) = w(a) erweitern. x A Eine Menge A Imit maximalem Gewicht heißt optimal. 9.3 Gierige Algorithmen 9-28
Matroide Satz: Es sei M = (S,I) ein gewichtetes Matroid mit der Gewichtsfunktion w : S R +. Der folgende gierige Algorithmus gibt eine optimale Teilmenge zurück. func greedy(m, w): I begin A ; sortiere S in monoton fallender Reihenfolge nach dem Gewicht w; foreach x S do if A {x} I then A A {x}; return A; end 9.3 Gierige Algorithmen 9-29
Matroide Satz: Die Komplexität des gierigen Algorithmus liegt in O(n log n + n f(n)), wobei n log n der Aufwand für das Sortieren und f(n) der Aufwand für den Test A {x} ist. n ist die Anzahl der Elemente von S, d. h. S = n. 9.3 Gierige Algorithmen 9-30
Matroide Beispiel: Es sei G = (V, E) ein ungerichteter Graph. Dann ist M G = (S G,I G ) ein Matroid, dabei gilt: S G = E, A E: A I G A azyklisch. Eine Menge A von Kanten ist genau dann unabhängig, wenn der Graph G A = (V, A) einen Wald bildet. Der Algorithmus von Kruskal ist ein Beispiel für das obige allgemeine Verfahren. 9.3 Gierige Algorithmen 9-31
Backtracking-Algorithmen Das Backtracking realisiert eine systematische Suchtechnik, die die Menge aller möglichen Lösungen eines Problems vollständig durchsucht. Führt die Lösung auf einem Weg nicht zum Ziel, wird zur letzten Entscheidung zurückgegangen und dort eine Alternative untersucht. Da alle möglichen Lösungen untersucht werden, wird eine Lösung wenn sie existiert stets gefunden. 9.4 Backtracking-Algorithmen 9-32
Beispiel: Wegsuche in einem Labyrinth Gesucht ist ein Weg in einem Labyrinth von einem Start- zu einem Zielpunkt. Gehe zur ersten Kreuzung und schlage dort einen Weg ein. Markiere an jeder Kreuzung den eingeschlagenen Weg. Falls eine Sackgasse gefunden wird, gehe zurück zur letzten Kreuzung, die einen noch nicht untersuchten Weg aufweist und gehe diesen Weg. 9.4 Backtracking-Algorithmen 9-33
Beispiel: Wegsuche in einem Labyrinth Die besuchten Kreuzungspunkte werden als Knoten eines Baumes aufgefasst. Der Startpunkt bildet die Wurzel dieses Baumes. Blätter sind die Sackgassen und der Zielpunkt. Der Baum wird beginnend mit der Wurzel systematisch aufgebaut. Wegpunkte sind Koordinatentupel (x, y). Im Beispiel ist (1, 1) der Startpunkt und (1, 3) der Zielpunkt. (1,1) (2,1) (2,2) (3,1) (1,2) (2,3) (3,2) (3,3) (1,3) 9.4 Backtracking-Algorithmen 9-34
Backtracking-Algorithmen Es gibt eine endliche Menge K von Konfigurationen. K ist hierarchisch strukturiert: Es gibt eine Ausgangskonfiguration k 0 K. Zu jeder Konfiguration k x K gibt es eine Menge k x 1,..., k x nx von direkt erreichbaren Folgekonfigurationen. Für jede Konfiguration ist entscheidbar, ob sie eine Lösung ist. Gesucht werden Lösungen, die von k 0 aus erreichbar sind. 9.4 Backtracking-Algorithmen 9-35
Backtracking-Algorithmen proc backtrack(k: Konfiguration) Konfiguration begin if k ist Lösung then print(k); fi; foreach Folgekonfiguration k von k do backtrack(k ); od; end Dieses Schema terminiert nur, wenn der Lösungsraum endlich und wiederholte Bearbeitung einer bereits getesteten Konfiguration ausgeschlossen ist (keine Zyklen). Kritisch ist ggf. der Aufwand. Er ist häufig exponentiell. 9.4 Backtracking-Algorithmen 9-36
Backtracking-Algorithmen (Varianten) Lösungen werden bewertet. Zuletzt wird die beste ausgewählt. Das angegebene Schema findet alle Lösungen. Oft genügt es, den Algorithmus nach der ersten Lösung zu beenden. Aus Komplexitätsgründen wird eine maximale Rekursionstiefe vorgegeben. Als Lösung dient dann beispielsweise die am besten bewertete Lösung aller bisher gefundenen. Branch-and-Bound-Algorithmen. 9.4 Backtracking-Algorithmen 9-37
Branch-and-Bound-Algorithmen Das angegebene Schema untersucht jeden Konfigurationsteilbaum. Oft kann man schon im Voraus entscheiden, dass es sich nicht lohnt, einen bestimmten Teilbaum zu besuchen. Dies ist zum Beispiel bei einer Sackgasse der Fall oder wenn man weiß, dass die zu erwartende Lösung auf jeden Fall schlechter sein wird, als eine bisher gefundene. In diesem Fall kann auf die Bearbeitung des Teilbaums verzichtet werden. 9.4 Backtracking-Algorithmen 9-38
Branch-and-Bound-Algorithmen Beispiele: Spiele (insbesondere rundenbasierte Strategiespiele), zum Beispiel Schach. Konfigurationen entsprechen den Stellungen, Nachfolgekonfigurationen sind durch die möglichen Spielzüge bestimmt. Nachweisbar schlechte Züge müssen nicht untersucht werden. Erfüllbarkeitstests von logischen Aussagen. Planungsprobleme. Optimierungsprobleme. 9.4 Backtracking-Algorithmen 9-39
Beispiel: Das N-Damen-Problem Es sollen alle Stellungen von n Damen auf einem n n-schachbrett ermittelt werden, bei denen keine Dame eine andere bedroht. Es dürfen also nicht zwei Damen in der gleichen Zeile, Spalte oder Diagonale stehen. 8 7 6 5 4 3 2 1 1 2 3 4 5 6 7 8 8 7 6 5 4 3 2 1 1 2 3 4 5 6 7 8 9.4 Backtracking-Algorithmen 9-40
Beispiel: Das N-Damen-Problem K sei die Menge aller Stellungen mit einer Dame in jeder der ersten m Zeilen, 0 m n, sodass je zwei Damen sich nicht bedrohen. K enthält alle Lösungen. Nicht jede Stellung lässt sich allerdings zu einer Lösung erweitern. So ist zum Beispiel unten jedes Feld in der 7. Zeile bereits bedroht, sodass dort keine Dame mehr gesetzt werden kann. Durch Ausnutzung von Symmetrien lässt sich der Aufwand verringern. 8 7 6 5 4 3 2 1 1 2 3 4 5 6 7 8 9.4 Backtracking-Algorithmen 9-41
Beispiel: Das N-Damen-Problem proc platziere(zeile: int) begin var i: int; for i 1 to n do if <feld (zeile, i) nicht bedroht> then <setze Dame auf (zeile, i)>; if zeile = n then <gib Konfiguration aus>; else platziere(zeile + 1); fi; fi; od; end 9.4 Backtracking-Algorithmen 9-42
Beispiel: Das N-Damen-Problem 4 3 2 1 1 2 3 4 4 3 2 1 4 3 2 1 1 2 3 4 4 3 2 1 4 3 2 1 4 3 2 1 1 2 3 4 1 2 3 4 Sackgasse 4 3 2 1 4 3 2 1 1 2 3 4 Sackgasse 4 3 2 1 Lösung 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 9.4 Backtracking-Algorithmen 9-43
Beispiel: Das N-Damen-Problem Das N-Damen-Problem ist für n 4lösbar. Wenn die erste Dame nicht richtig gesetzt ist, werden allerdings bis zu (n 1)! Schritte benötigt, um dies herauszufinden. Nach der stirlingschen Formel ist der Aufwand also exponentiell. n! n n e n 2πn, Für jedes n 4ist ein Verfahren bekannt (Ahrens, 1912), das in linearer Zeit eine Lösung findet (nur eine, nicht alle). Es basiert auf der Beobachtung, dass in Lösungsmustern häufig Rösselsprung-Sequenzen auftreten. Im Jahre 1990 ist ein schneller probabilistischer Algorithmus veröffentlicht worden, dessen Laufzeit in O(n 3 ) liegt. 9.4 Backtracking-Algorithmen 9-44
Beispiel: Problem des Handlungsreisenden Gegeben seien n durch Straßen verbundene Städte mit Reisekosten c(i, j) zwischen je zwei Städten i und j, 1 i, j n. Gesucht ist die billigste Rundreise, die jede Stadt genau einmal besucht (Traveling Salesman Problem, TSP). Eine solche Kantenfolge heißt hamiltonscher Zyklus. Dieser Graph ist vollständig. Augsburg Frankfurt 2 4 9 9 9 3 Braunschweig 1 8 3 2 2 Erfurt 8 6 9 3 Celle Darmstadt Die billigste Rundreise kostet 13 Einheiten. 9.4 Backtracking-Algorithmen 9-45
Beispiel: Problem des Handlungsreisenden Ein naiver Algorithmus beginnt bei einem Startknoten und sucht dann alle Wege ab. Die Komplexität dieses Verfahrens liegt in O(n!). Das folgende Verfahren führt zu einer Näherungslösung: 1. Die Kanten werden nach ihren Kosten sortiert. 2. Man wählt die billigste Kante unter den beiden folgenden Nebenbedingungen: Es darf kein Zyklus entstehen (außer am Ende der Rundreise). Kein Knoten darf zu mehr als 2 Kanten adjazent sein. 9.4 Backtracking-Algorithmen 9-46
Beispiel: Problem des Handlungsreisenden Die Laufzeit dieses gierigen Algorithmus liegt in O(n 2 log n 2 ). Das Verfahren führt nicht immer zu einer optimalen Lösung. Trotzdem wird es in der Praxis erfolgreich eingesetzt. Branch-and-Bound: Wenn man weiß, dass eine Lösung mit Kosten k existiert (zum Beispiel durch obigen Algorithmus), dann kann ein Backtrack-Algorithmus alle Teillösungen abschneiden, die bereits teurer als k sind. 9.4 Backtracking-Algorithmen 9-47
Dynamische Programmierung Rekursive Problemstruktur: 1. Aufteilung in abhängige Teilprobleme. 2. Berechnen und Zwischenspeichern wiederbenötigter Teillösungen. 3. Bestimmung des Gesamtergebnisses unter Verwendung der Teillösungen. Die dynamische Programmierung ist mit der Teile-und-Beherrsche-Methode verwandt. Die Teilprobleme sind aber abhängig. Einmal berechnete Teillösungen werden wiederverwendet. Die dynamische Programmierung wird häufig bei Optimierungsproblemen angewendet. 9.5 Dynamische Programmierung 9-48
Beispiel: Fibonacci-Zahlen fib(n) = 0 n = 0 1 n = 1 fib(n 1)+fib(n 2) n 2 0, 1, 1, 2, 3, 5, 8, 13, 21, 34,... func fib(n: int): int begin if n < 2 then return n; fi; return fib(n-1) + fib(n-2); end 9.5 Dynamische Programmierung 9-49
Beispiel: Fibonacci-Zahlen Berechnung vonfib(5): fib5 fib4 fib3 fib3 fib2 fib2 fib1 fib2 fib1 fib1 fib0 fib1 fib0 1 fib1 fib0 1 1 0 1 0 1 0 9.5 Dynamische Programmierung 9-50
Beispiel: Fibonacci-Zahlen Die Berechnung vonfib(5) führt zweimal auf die Berechnung von fib(3). Die zugehörigen Teilbäume werden zweimal ausgewertet. Aufwandsabschätzung: T(n) = Anzahl der Funktionsaufrufe 1 n = 0, n = 1 = 1+T(n 1)+T(n 2) n 2 T(n) wächst exponentiell. Wir haben in der Übung gezeigt: T(n) = ( 1+ 1 5 1,45 1,62 n. 5 ) 1+ 5 2 n + ( 1 1 5 5 ) 1 5 2 n 1 9.5 Dynamische Programmierung 9-51
Beispiel: Fibonacci-Zahlen Iterative dynamische Lösung (vgl. Abschnitt 2.1): func fibdyn(n: int): int begin var i, result, minus1, minus2: int; if n < 2 then return n; fi; minus2 0; minus1 1; for i 2 to n do result minus1 + minus2; minus2 minus1; minus1 result; od; return result; end 9.5 Dynamische Programmierung 9-52
Beispiel: Das Rucksackproblem Das Rucksackproblem (knapsack problem): Ein Wanderer findet einen Schatz aus Edelsteinen. Jeder Edelstein hat ein bestimmtes Gewicht und einen bestimmten Wert. Er hat nur einen Rucksack, dessen Kapazität durch ein maximales Gewicht begrenzt ist. Gesucht ist ein Algorithmus, der diejenige Befüllung des Rucksacks ermittelt, die einen maximalen Wert hat, ohne die Gewichtsbeschränkung zu verletzen. 9.5 Dynamische Programmierung 9-53
Beispiel: Das Rucksackproblem Gegeben: Kapazität c N, Menge O mit n NObjekten o 1,...,o n, Gewichtsfunktion g : O Nmit j O g(j)>c, Bewertungsfunktion w : O N. Gesucht ist eine Menge O O mit g(j) c und w(j) maximal. j O j O Da Gegenstände nur vollständig oder gar nicht eingepackt werden, spricht man auch vom 0-1-Rucksackproblem. Beim fraktionalen Rucksackproblem können auch Teile eines Gegenstands ausgewählt werden. 9.5 Dynamische Programmierung 9-54
Beispiel: Das Rucksackproblem Der gierige Algorithmus führt nicht zur Lösung: O ={o 1, o 2, o 3 }, Kapazität c = 5. Gewichte: g(o 1 ) = 1, g(o 2 ) = 2, g(o 3 ) = 3. Werte: w(o 1 ) = 6, w(o 2 ) = 10, w(o 3 ) = 12. Ein gieriger Algorithmus wählt das Objekt mit dem größten relativen Wert. r(o) = w(o) g(o), r(o 1) = 6, r(o 2 ) = 5, r(o 3 ) = 4. O ={o 1, o 2 }, g(j) = 3 5=c, w(j) = 16. j O j O Die optimale Lösung ist O ={o 2, o 3 }, j O g(j) = 5 = c, j O w(j) = 22. 9.5 Dynamische Programmierung 9-55
Beispiel: Das Rucksackproblem Backtracking liefert die korrekte Lösung, ist aber ineffizient. O ={o 1, o 2, o 3, o 4 }, c = 10. Gewichte: g(o 1 ) = 2, g(o 2 ) = 2, g(o 3 ) = 6, g(o 4 ) = 5. Werte: w(o 1 ) = 6, w(o 2 ) = 3, w(o 3 ) = 5, w(o 4 ) = 4. 10 nein ja 10 8 10 8 8 6 10 4 8 2 8 2 6 0 10 5 4 8 3 2 8 3 2 6 1 0 zur Disposition o : (g,w) o 1 : (2,6) o 2 : (2,3) o 3 : (6,5) o 4 : (5,4) (0/0),(5/4),(6/5),(2/3),(7/7),(8/8),(2/6),(7/10),(8/11),(4/9),(9/13),(10/14), jeweils (Gewicht/Wert). Es ist O ={o 1, o 2, o 3 } mit g(o ) = 10 und w(o ) = 14. 9.5 Dynamische Programmierung 9-56
Beispiel: Das Rucksackproblem Rückgabewert ist der Wert der optimalen Füllung. Aufruf: btknapsack(1,c). func btknapsack(i, rest: int): int begin if i = n then if g(i) > rest then return 0; else return w(i); fi; else if g(i) > rest then return btknapsack(i+1,rest); else return max(btknapsack(i+1,rest), btknapsack(i+1,rest-g(i))+w(i)); fi; fi; end Das Optimierungspotential durch Vermeidung wiederkehrender Berechnungen wird nicht genutzt. 9.5 Dynamische Programmierung 9-57
Beispiel: Das Rucksackproblem Dynamische Programmierung: O ={o 1,...,o 5 }, c = 10. Gewichte: g(o 1 ) = 2, g(o 2 ) = 2, g(o 3 ) = 6, g(o 4 ) = 5, g(o 5 ) = 4. Werte: w(o 1 ) = 6, w(o 2 ) = 3, w(o 3 ) = 5, w(o 4 ) = 4, w(o 5 ) = 6. Es wird ein zweidimensionalen Feld f[i, r] berechnet: r i 0 1 2 3 4 5 6 7 8 9 10 5 0 0 0 0 6 6 6 6 6 6 6 4 0 0 0 0 6 6 6 6 6 10 10 3 0 0 0 0 6 6 6 6 6 10 11 2 0 0 3 3 6 6 9 9 9 10 11 f[4, 9] = 10: Wenn o 4 und o 5 bei der Restkapazität 9 zur Disposition stehen, beträgt der Wert der zusätzlichen Füllung 10. 9.5 Dynamische Programmierung 9-58
Beispiel: Das Rucksackproblem Die zentrale Anweisung in btknapsack: return max(btknapsack(i+1,rest), btknapsack(i+1,rest-g(i))+w(i)); r i 0 1 2 3 4 5 6 7 8 9 10 5 0 0 0 0 6 6 6 6 6 6 6 4 0 0 0 0 6 6 6 6 6 10 10 3 0 0 0 0 6 6 6 6 6 10 11 2 0 0 3 3 6 6 9 9 9 10 11 f[3, 8] = max(f[4, 8], f[4, 2]+5) = max(6, 0+5) = 6 9.5 Dynamische Programmierung 9-59
Beispiel: Das Rucksackproblem Der Algorithmus arbeitet folgendermaßen: 1. Berechne zunächst die Werte f[n, 0],...,f[n, c]. 2. Berechne anschließend f[i, 0],...,f[i, c] für i = n 1 bis i = 2 unter Rückgriff auf die bereits berechneten Werte der Zeile i + 1. 3. Das Gesamtergebnis f[1, c] ergibt sich dann aus f[1, 10] = max(f[2, 10], f[2, 8]+6) = max(11, 9+6) = 15. 9.5 Dynamische Programmierung 9-60
Beispiel: Das Rucksackproblem func dynknapsack(n, c: int): int begin var f[2..n,0..c]:int; var i, r: int; for r 0 to c do if g(n)>rthen f[n,r] 0; else f[n,r] w(n);fi; for i n - 1 downto2do for r 0 to c do if g(i) > r then f[i,r] f[i+1,r]; else f[i, r] max(f[i+1,r],f[i+1,r-g(i)]+w(i)); fi; od; od; if g(1) > c then returnf[2, c]; else return max(f[2,c],f[2,c-g(1)]+w(1)); end 9.5 Dynamische Programmierung 9-61
Beispiel: Suche in einem Text Im Folgenden werden Zeichenketten als Felder behandelt. Gegeben: Feld t[1..n] von Zeichen, der Text, Feld p[1..m] von Zeichen, das Muster (pattern). Es sei m n. In der Regel ist sogar m<< n. Gesucht: Vorkommen von p in t, d. h. Indices s, 0 s n m, mit t[s + 1..s + m] = p[1..m]. 9.5 Dynamische Programmierung 9-62
Beispiel: Suche in einem Text Naive Lösung: Vergleiche für alle s = 0..n m und für alle i = 0..m die Zeichen p[i] = t[s + i]. proc naiv(t, p): begin var i,s: int; for s 0 to n - m do i 1; while i m && p[i] = t[s+i] do i i+1; od; if i = m+1 then print(s);fi; od; end 9.5 Dynamische Programmierung 9-63
Beispiel: Suche in einem Text Als Maß für die Laufzeit nehmen wir die Anzahl der ausgeführten Tests der inneren Schleife. Der schlimmste Fall tritt ein, wenn für jeden Wert s die Zeichenkette p bis zum letzten Zeichen mit t verglichen werden muss. Beispiel: t = "aaaaaaaaaaaaaaaaab", p = "aaab". Die Laufzeit liegt in O((n m)m) = O(nm). Gesucht ist ein effizienterer Algorithmus. 9.5 Dynamische Programmierung 9-64
Beispiel: Suche in einem Text s = 4, q = 5: s = s + 2: t: bacbababaabcbab p: ababaca t: bacbababaabcbab p: ababaca Die nächste möglicherweise erfolgreiche Verschiebung ist s = s +(q d[q]). 9.5 Dynamische Programmierung 9-65
Beispiel: Suche in einem Text Es seien die Musterzeichen p[1..q] gegeben, die mit den Textzeichen t[s + 1..s + q] übereinstimmen. Wie groß ist die geringste Verschiebung s > s für die mit s + k = s + q gilt? p[1..k] = t[s + 1..s + k] 9.5 Dynamische Programmierung 9-66
Beispiel: Suche in einem Text Die Präfixfunktion für das Musterababababca: i 1 2 3 4 5 6 7 8 9 10 p[i] a b a b a b a b c a d[i] 0 0 1 2 3 4 5 6 0 1 9.5 Dynamische Programmierung 9-67
Beispiel: Suche in einem Text proc Berechnung der Präfixfunktion d begin d(1) 0; k 0; for q 2 to m do while k > 0 p[k+1] p[q]; do k d[k]; od; if p[k+1] = p[q] then k k+1; fi; d[q] k; od; return d; end 9.5 Dynamische Programmierung 9-68
Beispiel: Suche in einem Text Der Knuth-Morris-Pratt-Algorithmus: proc kmp(t, p): begin berechne d; q 0; for i 1 to n do while q > 0 p[q+1] t[i] do q d[q]; od; if p[q+1] = t[i] then q q+1; fi; if q = m then print(i-m); q d[q]; fi; od; end Die Laufzeit dieses Algorithmus liegt in O(n + m). 9.5 Dynamische Programmierung 9-69
Weitere Algorithmenmuster Zufallsgesteuerte Algorithmen Verteilte und parallele Algorithmen Lokale Suche Amortisierte Analyse Approximative Algorithmen Genetische Algorithmen Schwarmbasierte Algorithmen und Koloniealgorithmen... 9.5 Dynamische Programmierung 9-70
Weitere Gebiete Mathematische Algorithmen Geometrische Algorithmen Algorithmen für Texte Lineare Programmierung... Große Bedeutung für die Theorie der Algorithmen besitzt die Komplexitätstheorie. Hierzu zählen zum Beispiel die Untersuchung von Komplexitätsklassen wie P und NP und die so genannte NP-Vollständigkeit. Die Komplexitätstheorie wird in der Vorlesung Theoretische Informatik II behandelt. 9.5 Dynamische Programmierung 9-71