4.4 Klassen von Algorithmen Greedy Divide and Conquer Backtracking Dynamische Programmierung Genetische Algorithmen
Algorithmen sind „Handarbeit“ Die Ableitung eines optimalen Algorithmus aus der Anforderungsbeschrei-bung ist nicht automatisierbar. Algorithmenentwurf ist kreative Tätigkeit. Aber: Es gibt eine wertvolle Unterstützung durch Muster (Klassen von Algo-rithmen).
Wiederholung: schrittweise Verfeinerung (2) Gib Kaffeepulver in Tasse wird verfeinert zu (2.1) Öffne Kaffeeglas (2.2) Entnehme Löffel von Kaffee (2.3) Kippe Löffel in Tasse (2.4) Schließe Kaffeeglas
Einsatz von Algorithmenmustern Idee Anpassung von generischen Algorithmenmustern für bestimmte Problemklassen an eine konkrete Aufgabe Dokumentation des Lösungsverfahrens am Beispiel eines einfachen Vertreters der Problemklasse Bibliothek von Mustern („Design Pattern“) zur Ableitung eines abstrakten Pro-grammrahmens Programmiersprachenunterstützung durch parametrisierte Algorithmen und durch Vererbung
4.4.1 Greedy Greedy = „gierig“ Prinzip Versuche, mit jedem Teilschritt so viel wie möglich zu erreichen, einen möglichst großen Fortschritt zu erzielen Greedy-Algorithmen (gierige Algorithmen) zeichnen sich dadurch aus, dass sie immer denjenigen Folgezustand auswählen, der zum Zeitpunkt der Wahl den größten Ge-winn bzw. das beste Ergebnis verspricht.
Greedy-Algorithmen am Beispiel Herausgabe von Wechselgeld auf Beträge unter 1 Euro Verfügbare Münzen von 50, 10, 5, 2, 1 Cent Ziel: so wenig Münzen wie möglich herausgeben Beispiel 78 Cent = 50 + 10 + 10 + 5 + 2 + 1 Algorithmus 1. Nehme jeweils immer die größte Münze unterhalb des Zielwerts und ziehe ihren Wert dann von diesem ab. 2. Verfahre weiter so, bis Zielwert gleich null.
Problem: Lokales Optimum Greedy-Algorithmen berechnen in jedem Schritt ein lokales Optimum -> das globale Optimum kann verfehlt werden! Beispiel Münzen: 11, 5 und 1 Zielwert 15 Greedy: 11 + 1 + 1 + 1 + 1 Optimum: 5 + 5 + 5 aber: In vielen Fällen entsprechen lokale Optima den globalen, oder es reicht ein lokales Optimum aus. Denkaufgabe: Man finde ein Beispiel für unsere aktuellen Euro-Münzen!
Problemklasse für Greedy-Algorithmen Gegebene Menge von Eingabewerten Menge von Lösungen, die aus Eingabewerten aufgebaut sind Gesucht wird die / eine optimale Lösung. Lösungen lassen sich schrittweise aus partiellen Lösungen, beginnend bei der leeren Lösung, durch Hinzunahme von Eingabewerten aufbauen. Es gibt eine Bewertungsfunktion für partielle und vollständige Lösungen.
4.4.2 Divide and Conquer Auch: „Teile und Herrsche“ (divide et impera) Rekursive Rückführung auf ein identisches Problem mit kleinerer Eingabe-menge
Divide-and-Conquer-Algorithmen Grundidee Teile das gegebene Problem in mehrere getrennte Teilprobleme auf 1.1 löse diese einzeln 1.2 und setze die Lösungen des ursprünglichen Problems aus den Teillösungen zusammen (nicht immer trivial!) Wende dieselbe Technik rekursiv auf jedes der Teilprobleme an, dann auf deren Teilprobleme usw., bis die Teilprobleme so klein sind, dass man eine Lösung ex-plizit angeben kann. Strebe dabei an, dass jedes Teilproblem von derselben Art ist wie das ursprüng-liche Problem, so dass es mit demselben Algorithmus gelöst werden kann.
Beispiel: Binäre Suche in einer Liste Prinzip Wähle den mittleren Eintrag der Liste und prüfe, ob der gesuchter Wert in der ersten oder in der zweiten Hälfte der Liste ist. Fahre rekursiv mit der Hälfte fort, in der sich der Eintrag befindet. Wenn es nur noch einen Eintrag gibt, ist dieser der Gesuchte, oder er existiert nicht.
Binäre Suche: Beispiel (1) Suche nach der 33 in der sortierten Liste: 1 4 6 9 12 15 16 21 22 29 33 36 38 40 44 u m o Die 33 ist rechts von der 21: 1 4 6 9 12 15 16 21 22 29 33 36 38 40 44 u m o Die 33 ist links von der 36: 1 4 6 9 12 15 16 21 22 29 33 36 38 40 44 u m o
Binäre Suche: Beispiel (2) Die 33 ist rechts von der 29: 1 4 6 9 12 15 16 21 22 29 33 36 38 40 44 u m o 33 gefunden. Performance: In jedem Schritt wird die Größe des Intervalls halbiert. Das bedeutet: wir brauchen bei n Elementen log2n Schritte, bis wir den Wert gefunden haben.
Anderes Beispiel: MergeSort algorithm MergeSort (F) → FS Eingabe: eine zu sortierende Folge F Ausgabe: eine sortierte Folge FS if F einelementig then return F else Teile F in F1 und F2; F1 := MergeSort (F1); F2 := MergeSort (F2); return Merge (F1, F2) fi
4.4.3 Backtracking Das Prinzip von Versuch und Irrtum (trial and error) Versuche, eine erreichte Teillösung schrittweise zu einer Gesamtlösung auszubauen. Falls ein Schritt erkennbar nicht zu einer Lösung führen kann, nimm diesen Schritt zurück und probiere einen alternativen Schritt. Tue dies rekursiv, bis die beste Lösung gefunden ist oder erkennbar keine Lö-sung existiert.
Beispiel: Suche in einem Labyrinth Wie findet die Maus den Käse? (1,1) 1 2 3 1 M (1,2) 2 (2,2) (1,3) K 3 (2,1) (3,2) (2,3) (3,3) (3,1)
Backtracking-Muster procedure BACKTRACK (K: konfiguration) begin ... if [ K ist Lösung ] then [ gib K aus ] else for each [ jede direkte Erweiterung K’ von K ] do BACKTRACK(K’) od fi end
Typische Einsatzfelder des Backtracking Spielprogramme (Schach, Dame, Labyrinthsuche, ...) Erfüllbarkeit von komplexen logischen Aussagen (logische Programmiersprachen) Planungsprobleme, Konfigurationen logistische Fragestellungen kürzeste Wege, optimale Verteilungen Färben von Landkarten
Beispiel: Acht-Damen-Problem Gesucht: Alle Konfigurationen von 8 Damen auf einem 8x8-Schachbrett, so dass keine Dame eine andere bedroht. 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 D 1 D 2 2 3 3 4 4 5 5 6 6 7 7 8 8
Vereinfacht: Vier-Damen-Problem …… …… ……
Varianten des Backtracking Lösungen merken und bewerten; nach Lauf des Algorithmus wird die beste aus-gewählt Abbruch nach der ersten korrekten Lösung „Branch-and-Bound“: nur Zweige verfolgen, die eine Lösung prinzipiell zulassen (wie zum Beispiel beim 4-Damen-Problem) maximale Rekursionstiefe vorgeben (etwa bei Schachprogrammen!)
4.4.4 Dynamische Programmierung Vereint Ideen verschiedener Muster: Greedy: Wahl der optimalen Teillösung Divide-and-Conquer / Backtracking: Rekursion, Konfigurationsbaum Unterschiede: Divide-and-Conquer löst unabhängige Teilprobleme einzeln Dynamische Programmierung: Optimierung abhängiger Teilprobleme Die dynamische Programmierung ist eine „bottom-up“-Realisierung der Backtracking-Strategie. Anwendungsbereich: wie Greedy, jedoch insbesondere dort, wo Greedy nur sub-optimale Lösungen liefert.
Prinzip der dynamischen Programmierung Bei der dynamischen Programmierung werden kleinere Teilprobleme zuerst gelöst, um aus diesen größere Teillösungen zusammenzusetzen. Dabei Problemlösen „auf Vorrat“: Möglichst nur solche Teilprobleme lösen, die bei der Lösung des großen Pro-blems auch tatsächlich benötigt werden. Gewinn, falls identische Teilprobleme in mehreren Lösungszweigen betrachtet werden. Rekursives Problemlösen wird ersetzt durch Iteration und abgespeicherte Teilergeb-nisse.
Beispiel: Traveling Salesman Das Problem des Handelsreisenden (Traveling Salesman Problem, TSP) Gegeben: ein Graph mit n Knoten (Städten) und m Kanten (Straßen); Straßen sind mit Entfernungen versehen Gesucht: kürzeste Route, die alle Städte genau einmal besucht und an den Start-punkt zurückkehrt.
TSP im Detail Gegeben: n Städte in S Entfernungsmatrix M = (mi,j ) der Größe n × n mi ,j: Entfernung von Stadt i nach Stadt j Gesucht: Rundreise über alle Städte minimaler Länge Permutationen { 1, ..., n }, so dass die Summe der Entfernungen minimal ist.
TSP mit dynamischer Programmierung Betrachte g(i , S): Länge des kürzesten Weges von Stadt i über jede Stadt in S nach Stadt 1 Da Rundreise, kann Stadt 1 beliebig gewählt werden. Es gilt: g(i , S) = Idee: baue Rundreisen von hinten her schrittweise auf Lösung: g(1, { 2, ..., n } ) mi,1 falls S = { } minj∈S(mi,j + g(j, S - { j } )) sonst
TSP: Algorithmus Berechne g „bottom-up“ als Array: for i = 2 to n do g[i,{}] = mi,1 od; for k = 1 to n-2 for all S with | S | = k and 1 ∈ S do berechne g[i,S] nach Formel; od; berechne g[1,{2,...,n }] nach Formel;
TSP: Beispiel (1) Vier Städte, symmetrische Entfernungen: M = Initialisierung g[2,{ }] = 4 g[3,{ }] = 9 g[4,{ }] = 8 1 2 3 4 9 8 12 10
TSP: Beispiel (2) g[2, { 3 } ] = 12 + 9 = 21 g[2, { 4 } ] = 2 + 8 = 10 g[2, { 3, 4 } ] = min(m2,3 + g[3, { 4 } ], m2,4 + g[4, { 3 } ]) = 21 g[3, { 2, 4 } ] = min(m3,2 + g[2, { 4 } ], m3,4 + g[4, { 2 } ]) = 16 g[4, { 2, 3 } ] = min(m4,2 + g[2, { 3 } ], m4,3 + g[3, { 2 } ]) = 23 g[1, { 2, 3, 4 } ] = min(m1,2 + g[2, { 3, 4 } ], m1,3 + g[3, { 2, 4 } ], m1,4 + g[4, { 2, 3 } ]) = 25 Lösung: 1, 2, 4, 3, 1
4.4.5 Genetische Algorithmen Ein Prinzip der Informationsverarbeitung aus der Natur: Evolution: Mutation und natürliche Auslese, übertragen auf die Informations-verarbeitung Insbesondere einsetzbar für Optimierungsfragen. Ergibt oft gute Näherungslösungen. Genetische Algorithmen kodieren Lösungsstragien als Erbgut. Auf eine Population von „Problemlösern“ werden dann Evolutionsprinzipien (Ablauf von Generationen, Vererbung, ‘survival of the fittest´, Mutationen, Erbgutkombination, etc.) angewendet, um bessere Lösungen zu erhalten.
Prinzip evolutionärer Algorithmen Es gibt einen Pool von Phänotypen, das sind Problemlöser für ihre Problemklasse. Die Lösungsstrategie jedes Phänotyps ist im Erbgut kodiert, beispielsweise als Bit-folge fester Länge. Eine Generation von Individuen wird auf Probleme der Problemklasse angesetzt, und die Lösungen der Phänotypen werden bewertet (Fitness). Erreicht die „Fitness“ eines Phänotypen einen Schwellwert? Ja: Programm kann als Löser für die Probleme der Problemklasse eingesetzt werden andernfalls; neue Generation von Phänotypen berechnen: Fitness der bisherigen Phänotypen bestimmt, inwieweit diese als „Elternteil“ einbezogen werden. Das Erbgut eines Kindes kann durch Kombination des Erbguts zweier Eltern und / oder durch Mutation berechnet werden.
Evolutionäre Algorithmen Population Evaluation (Fitness) Recombination Mutation Selection Lösung
Acht-Damen-Problem Zwei Beispiellösungen: 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 1 D D 2 2 3 3 4 4 5 5 6 6 7 7 8 8
Kodierung der Lösungen Vektoren (Chromosom) mit n Elementen (Genen) für die Positionen der n zu platzierenden Damen: In jeder Zeile des Schachbretts steht genau eine Dame. Jedes Element des Vektors entspricht einer Zeile des Schachbretts. Jedes Vektorelement (Gen) gibt die Spaltenposition der Dame in der zugehörigen Zeile an. Jedes der n Gene hat einen Wert aus der Menge von n möglichen Werten, nämlich den Spaltennummern 1 bis n. Die Beispiele von der vorigen Seite entsprechen den Vektoren v1 = [3, 6, 8, 1, 4, 7, 5, 2] und v2 = [4, 6, 8, 2, 7, 1, 3, 5].
Fitness Die Fitness soll maximiert werden. Wir definieren Fitness als die Negation der Anzahl an Bedrohungen: je weniger Bedrohungen, desto fitter ist die Lösung. Obige Lösungen haben Fitness 0. Die beiden Vektoren v3 = [1, 2, 3, 4, 5, 6, 7, 8] und v4 = [1, 1, 1, 1, 1, 1, 1, 1] haben jeweils die sehr schlechte Bewertung von -28.
Genetischer Algorithmus für das 8-Damen-Problem Initialisierung: zufällige Vektoren Turniere bestimmen Fortpflanzungskandidaten: wähle zufällig k Phänotypen aus (Population) Der Gewinner des Turniers kommt in die nächste Generation. Anzahl Turniere: Größe der nächsten Generation. Mit bestimmter Wahrscheinlichkeit: Mutationen: zufällige Änderung eines Gens Kreuzungen: durchschneiden und neu kombinieren zweier Elternteile. Gen-Pool soll sich langsam in die „richtige Richtung“ entwickeln..
Genetischer Algorithmus Initialisiere erste Generation G; do BesteFitness = max { Fitness(x) | x in G }; if BesteFitness = 0 then break; Bestimme G´ aus G als Eltern der neuen Generation G; // etwa durch n Turniere G = {} ; for each x in G´ do with probability 0.7 do x = mutate(x) od; with probability 0.2 do wähle y aus G´ aus; x = crossover(x,y); od; G = G union { x }; return Kandidaten x aus G mit Fitness(x) = BesteFitness;
Bemerkungen zu genetischen Algorithmen Algorithmenmuster versus Berechnungsparadigma Kodierung im Erbgut entscheidend für das Funktionieren Grösse der Populationen, Wahrscheinlichkeiten der Mutation, Anzahl der Generationen relevant für Performanz und Qualität der Lösungen In der Regel gute, aber oft suboptimale Lösungen.
Zusammenfassung Wir haben Algorithmenmuster als „Best-Practice“-Lösungen für viele Probleme ken-nen gelernt. Konkret: Rekursion (in Kapitel 4.1) Greedy Divide-and-Conquer Backtracking dynamische Programmierung genetische Algorithmen. Der Programmierer sollte sich vor dem Codieren immer zuerst überlegen, welche Klasse von Algorithmen für die Lösung seines Problems am Geeignetsten ist.