Komplexität von Algorithmen und Problemen Klaus Becker 2010 2 Komplexität 3 Teil 1 Fallstudie - Sortieren Präzisierung von Berechnungskomplexität 4 Das Sortierproblem 5 Das Sortierproblem - formal betrachtet Gegeben ist eine Liste von Datensätzen [D0, D1, ..., Dn-2, Dn-1]. Die Datensätze sollen über eine Ordnungsrelation < verglichen werden können. Es soll also möglich sein, Vergleiche wie D0 < D1 eindeutig zu entscheiden. Mit der Ordnungsrelation ≤ erfassen wir die Möglichkeit, dass zwei Datensätze in dem zu vergleichenden Merkmal übereinstimmen können. Es soll D1 ≤ D0 gelten, wenn D0 < D1 nicht gilt. Gesucht ist eine neue Anordnung der Datensätze [Dp(0), Dp(1), ..., Dp(n-2), Dp(n-1)] mit der Eigenschaft, dass Dp(0) ≤ Dp(1) ≤ ... ≤ Dp(n-2) ≤ Dp(n-1) gilt. Die neue Anordnung der Datensätze wird hier durch eine bestimmte Anordnung (man sagt auch Permutation) p(0), p(1), ..., p(n-2), p(n-1) der Indexwerte 0, 1, ..., n-2, n-1 beschrieben. 6 Sortieralgorithmen - Selectionsort Sortieren durch Auswählen / Selectionsort 7 Sortieralgorithmen - Selectionsort # ALGORITHMUS selectionsort(L) unsortierter Bereich ist die gesamte Liste L SOLANGE der unsortierte Bereich mehr als ein Element hat: suche das kleinste Element im unsortierten Bereich tausche es mit dem ersten Element des unsortierten Bereichs verkleinere den unsortierten Bereich # Rückgabe: L 8 Sortieralgorithmen - Quicksort Sortieren durch Zerlegen / Quicksort Sortieralgorithmen - Quicksort 9 # ALGORITHMUS quicksort(L) wenn die Liste L mehr als ein Element hat: # zerlegen wähle als Pivotelement p das erste Element der Liste aus erzeuge Teillisten K und G aus der Restliste L ohne p mit - alle Elemente aus K sind kleiner als das Pivotelement p - alle Elemente aus G sind nicht kleiner als das Pivotelement p # Quicksort auf die verkleinerten Listen anwenden KSortiert = quicksort(K) GSortiert = quicksort(G) # zusammensetzen LSortiert = KSortiert + [p] + GSortiert # Rückgabe: LSortiert Sortieralgorithmen - Quicksort 10 # ALGORITHMUS quicksort(L) wenn die Liste L mehr als ein Element hat: # zerlegen wähle als Pivotelement p das erste Element der Liste aus tausche Elemente innerhalb L so, dass eine Zerlegung L = K + [p] + G entsteht mit - alle Elemente aus K sind kleiner als Pivotelement p - alle Elemente aus G sind nicht kleiner als das Pivotelement p # Quicksort auf die verkleinerten Listen anwenden quicksort(K) quicksort(G) # Rückgabe: L 11 Laufzeitmessungen - Selectionsort Anzahl der Listenelemente: 1000 Rechenzeit: 0.204296356101 *2 Anzahl der Listenelemente: 2000 Rechenzeit: 0.809496178984 Anzahl der Listenelemente: 3000 *2 Rechenzeit: 1.83842583345 Anzahl der Listenelemente: 4000 Rechenzeit: 3.23999810032 Anzahl der Listenelemente: 5000 Rechenzeit: 5.16765678319 Anzahl der Listenelemente: 6000 Rechenzeit: 7.2468548377 Anzahl der Listenelemente: 7000 Rechenzeit: 9.91592506869 Anzahl der Listenelemente: 8000 Rechenzeit: 12.9162480148 Anzahl der Listenelemente: 9000 Rechenzeit: 16.3896752241 ... *2 12 Laufzeitmessungen - Quicksort Anzahl der Listenelemente: 1000 Rechenzeit: 0.0178980848125 *2 # ALGORITHMUS quicksort(L) wenn die Liste L mehr als ein Element hat: Anzahl der Listenelemente: 2000 # zerlegen Rechenzeit: 0.0625291761942 wähle als Pivotelement p das erste Element d. Liste aus Anzahl der Listenelemente: 3000 *2 Rechenzeit: 0.133521718542 erzeuge Teillisten K und G aus "L ohne p" mit - alle Elemente aus K sind kleiner als das Pivotelement p - alle Elemente aus G sind nicht kleiner als p Anzahl der Listenelemente: 4000 # Quicksort auf die verkleinerten Listen anwenden Rechenzeit: 0.176368784301 KSortiert = quicksort(K) Anzahl der Listenelemente: 5000 GSortiert = quicksort(G) Rechenzeit: 0.351487409713 # zusammensetzen Anzahl der Listenelemente: 6000 Rechenzeit: 0.330103965727 Anzahl der Listenelemente: 7000 Rechenzeit: 0.58496680444 Anzahl der Listenelemente: 8000 Rechenzeit: 0.854964248249 Anzahl der Listenelemente: 9000 Rechenzeit: 0.942683218119 ... *2 LSortiert = KSortiert + [p] + GSortiert # Rückgabe: LSortiert 13 Laufzeitmessungen - Quicksort Anzahl der Listenelemente: 1000 Rechenzeit: 0.00662849607981 *2 # ALGORITHMUS quicksort(L) wenn die Liste L mehr als ein Element hat: Anzahl der Listenelemente: 2000 # zerlegen Rechenzeit: 0.0137794049244 wähle als Pivotelement p das erste Element d. Liste aus Anzahl der Listenelemente: 3000 *2 Rechenzeit: 0.0227299838387 tausche Elemente innerhalb L so, dass eine Zerlegung L = K + [p] + G entsteht mit - alle Elemente aus K sind kleiner als Pivotelement p Anzahl der Listenelemente: 4000 - alle Elemente aus G sind nicht kleiner als p Rechenzeit: 0.031230226188 # Quicksort auf die verkleinerten Listen anwenden Anzahl der Listenelemente: 5000 quicksort(K) Rechenzeit: 0.0419377323096 quicksort(G) Anzahl der Listenelemente: 6000 Rechenzeit: 0.0518194351517 Anzahl der Listenelemente: 7000 Rechenzeit: 0.0589655947893 Anzahl der Listenelemente: 8000 Rechenzeit: 0.069147894495 Anzahl der Listenelemente: 9000 Rechenzeit: 0.0784993623491 ... *2 # Rückgabe: L 14 Schwierigkeiten bei Laufzeitmessungen Start Stopp Rechenzeit: Start Stopp Rechenzeit: Start Stopp Rechenzeit: Start Stopp Rechenzeit: Start Stopp Rechenzeit: Start Stopp Rechenzeit: Start Stopp Rechenzeit: Start Stopp Rechenzeit: Start Stopp Rechenzeit: Start Stopp Rechenzeit: 3.27038296767 3.23336066455 Selectionsort wiederholte Durchführung einer Laufzeitmessung 3.25390210208 3.2653359575 3.24174441165 3.25976206473 3.2584529598 3.26073537279 3.23565201723 3.23315561056 Selectionsort wiederholte Durchführung einer Laufzeitmessung Start Stopp Rechenzeit: Start Stopp Rechenzeit: Start Stopp Rechenzeit: Start Stopp Rechenzeit: Start Stopp Rechenzeit: Start Stopp Rechenzeit: Start Stopp Rechenzeit: Start Stopp Rechenzeit: Start Stopp Rechenzeit: Start Stopp Rechenzeit: 3.2807468547 3.41092736647 3.4124093984 5.37245627587 6.69305316737 6.70120168904 6.67988976253 6.67656727321 6.70371150523 4.73779544607 15 Problematik von Laufzeitmessungen Laufzeitmessungen werden in der Praxis durchgeführt, um das Laufzeitverhalten eines Programms unter Realbedingungen zu ermitteln. Aus systematisch durchgeführten Laufzeitmessungen kann man oft Gesetzmäßigkeiten erschließen, wie die Laufzeit von den zu verarbeitenden Daten abhängt. Bei Laufzeitmessungen muss man aber sehr genau darauf achten, dass sie unter gleichen Bedingungen erfolgen. Ein Wechsel des Rechners führt in der Regel zu anderen Ergebnissen. Auch Änderungen in der Implementierung wirken sich in der Regel auf die Messergebnisse aus. Selbst bei ein und demselben Rechner und derselben Implementierung können sich die Bedingungen ändern, da oft mehrere Prozesse gleichzeitig ablaufen. Ergebnisse von Laufzeitmessungen sind also kaum auf andere Systeme (andere Rechner, andere Programmiersprachen) übertragbar. Um diese Schwierigkeit zu überwinden, soll im Folgenden ein anderer Weg zur Beschreibung der Berechnungskomplexität beschritten werden. 16 Kostenabschätzung def selectionsort(L): n = len(L) i = 0 while i < n-2: minpos = i m = i while m < n: if L[m] < L[minpos]: minpos = m m = m+1 h = L[i] L[i] = L[minpos] L[minpos] = h i = i+1 return L Bei der Ausführung des Algorithmus (bei vorgegebenen Problemgröße) spielt es eine Rolle, wie viele Operationen ausgeführt werden müssen. Operationen sind im vorliegenden Algorithmus u.a. das Ausführen eines Vergleichs und das Ausführen einer Zuweisung. Als Kostenmaß zur Beschreibung des zeitlichen Aufwands könnte man also die Gesamtanzahl der auszuführenden Vergleiche und Zuweisungen wählen. Bei der Festlegung eines Kostenmaßes müssen Annahmen über den Aufwand der verschiedenen auszuführenden Operationen gemacht werden. Zwei ganz unterschiedliche Wege kann man dabei bestreiten. Ein Weg besteht darin, unterschiedliche Aufwände von Operationen möglichst genau zu erfassen und im Kostenmaß zu berücksichtigen. Ein anderer Weg beschränkt sich darauf, dominante Operationen auszuwählen und die Kosten nur grob zuzuschätzen. Wir werden hier nur den zweiten Weg beschreiten. Kostenmodellierung 17 | 25 17 32 56 25 19 6 | 17 32 56 25 19 ^ 8 66 29 ^ 6 20 29 8 66 29 25 20 29 6 8 | 32 56 25 19 17 66 29 25 20 29 ^ 6 8 17 | 56 25 19 32 66 29 25 20 29 ^ 6 8 17 19 | 25 56 32 66 29 25 20 29 ^ 6 8 17 19 20 | 56 32 66 29 25 25 29 ^ 6 8 17 19 20 25 | 32 66 29 56 25 29 ^ 6 8 17 19 20 25 25 | 66 29 56 32 29 ^ 6 8 17 19 20 25 25 29 | 66 56 32 29 ^ 6 8 17 19 20 25 25 29 29 | 56 32 66 ^ ... Vergleiche: 11 Problemgröße: n Datensätze Kostenmaß: Anzahl der Vergleiche Kostenfunktion: n -> (n-1)+(n-2)+...+1 Selectionsort 18 Kostenmodellierung # ALGORITHMUS quicksort(L) # ALGORITHMUS quicksort(L) wenn die Liste L mehr als ein Element hat: wenn die Liste L mehr als ein Element hat: # zerlegen # zerlegen wähle als Pivotelement p das erste Element d. Liste aus wähle als Pivotelement p das erste Element d. Liste aus erzeuge Teillisten K und G aus "L ohne p" mit tausche Elemente innerhalb L so, dass eine Zerlegung L = K + [p] + G entsteht mit - alle Elemente aus K sind kleiner als das Pivotelement p - alle Elemente aus G sind nicht kleiner als p # Quicksort auf die verkleinerten Listen anwenden KSortiert = quicksort(K) GSortiert = quicksort(G) # zusammensetzen LSortiert = KSortiert + [p] + GSortiert - alle Elemente aus K sind kleiner als Pivotelement p - alle Elemente aus G sind nicht kleiner als p # Quicksort auf die verkleinerten Listen anwenden quicksort(K) quicksort(G) # Rückgabe: L # Rückgabe: LSortiert Problemgröße: n Datensätze Problemgröße: n Datensätze Kostenmaß: ??? Kostenmaß: Anzahl der Vergleiche Genauere Analysen zeigen, dass bei längeren Listen die Verwaltung und die Verarbeitung dieser Listen sehr aufwendig ist und bei der Kostenmodellierung nicht vernachlässigt werden können. 19 Fachkonzept Kostenfunktion Die Problemgröße ist eine präzise Beschreibung des Umfangs der zu verarbeitenden Daten, von dem das Zeitbzw. Speicherverhalten von Lösungalgorithmen maßgeblich beeinflusst wird. Ein Kostenmaß legt fest, in welchem Maße welche Operationen bei der Aufwandsbestimmung berücksichtigt werden. Eine Kostenfunktion ordnet der Problemgröße n die vom Algorithmus benötigten Gesamtkosten T(n) bzgl. des vorgegebenen Kostenmaßes zu. Bei der Beschreibung der Zeitkomplexität mit Hilfe einer Kostenfunktion werden in der Regel eine Reihe von Vereinfachungen vorgenommen sowie Annahmen gemacht. Die Festlegung einer Kostenfunktion kann somit als eine Form der Modellierung angesehen werden, weil hier ein Berechnungsmodell entwickelt werden muss, das den Berechnungsaufwand vereinfachend beschreibt. Wie bei jeder Modellierung kann das entwickelte Modell mehr oder auch weniger geeignet sein, um die zu beschreibende Wirklichkeit darzustellen. Bei der Modellierung der Zeitkomplexität kommt es darauf an, "richtige" Annahmen über den Aufwand bestimmter, im Algorithmus vorkommender Operationen zu machen. Dass das manchmal schwierig ist, zeigen die Implementierungen des QuicksortAlgorithmus. Kostenanalyse 20 25 ^ 17 32 56 17 ^ 19 8 6 8 ^ 6 | | | 25 19 8 66 20 | 25 | 32 56 | | ^ | | 6 | 17 | 19 20 | | 25 29 | | ^ | | ^ | | | | 8|| ||19 | 20 | ||25 | 29 || | | | || | ^ || | | | || ||29 | Quicksort - günstiger Fall 16 ^ 17 29 6 20 29 Vergleiche: 11 25 66 29 29 Vergleiche: 9 29 | 32 | 56 66 | | ^ | | 29 | ||56 | 66 | || | 29 | || | Vergleiche: 5 Vergleiche: 1 Tbest(1) = 0 und Tbest(n+1) ≤ 2*Tbest(n//2) + n 32 56 64 70 70 75 76 81 81 81 Vergleiche: 11 ||16 | 17 32 ^ || | || ||17 | 32 ... 56 64 70 70 75 76 81 81 81 Vergleiche: 56 64 70 70 75 76 81 81 81 Vergleiche: Quicksort - ungünstiger Fall Tworst(n) = (n-1) + (n-2) + ... + 1 21 Kostenanalyse best case (bester Fall): der Fall, in dem bei der Ausführung des Algorithmus die wenigsten Kosten anfallen worst case (schlechtester Fall): der Fall, in dem bei der Ausführung des Algorithmus die meisten Kosten anfallen Die Anzahl der Datensatzvergleiche bei der Ausführung eines Sortieralgorithmus hängt manchmal nicht nur von der Problemgröße n (d.h. der Anzahl der Listenelemente) ab, entscheidend ist auch, wie stark die Liste bereits vorsortiert ist. average case (durchschnittlicher Fall): eine Mittelung der Kosten über alle Fälle Selectionsort: Tbest(n) = (n-1) + (n-2) + ... + 1 Tworst(n) = (n-1) + (n-2) + ... + 1 Taverage(n) = (n-1) + (n-2) + ... + 1 Quicksort: Tbest(1) = 0 und Tbest(n+1) ≤ 2*Tbest(n//2) + n Tworst(n) = (n-1) + (n-2) + ... + 1 Taverage(n) = n*log2(n) -> Tbest(n) ≤ n*log2(n) 22 Vergleich von Kostenfunktionen Algorithmen sind in der Regel so konzipiert, dass sie eine Lösung für beliebige Problemgrößen liefern. Beim Vergleich zugehöriger Kostenfunktionen tritt die Schwierigkeit auf, dass globale Aussagen oft nicht möglich sind. Es kann vorkommen, dass in einem Bereich die eine Kostenfunktion günstiger ist, in einem anderen Bereich die andere Kostenfunktion. T1(n) = 0.01*n2 T2(n) = 100*n*log10(n) 23 Vergleich von Kostenfunktionen Oft ist der Problembereich, für den Algorithmen benötigt werden, nicht klar vorgegeben. Man benötigt dann ein Vergleichsverfahren für Kostenfunktionen, das auch mit Situationen wie in der Abbildung klarkommt. Eine Grundidee des in der Informatik gängigen Vergleichsverfahrens besteht darin, dass kleine Problemgrößen meist von geringerem Interesse sind als große Problemgrößen. Bei kleinen Problemgrößen unterscheiden sich Laufzeiten von Algorithmen z.B. nur im Millisekundenbereich, während die Unterschiede bei großen Problemgrößen im Sekunden-, Minuten-, Stunden-, Tage- oder gar Jahrebereich liegen können. Verbesserungen von Algorithmen zeigen sich in der Regel insbesondere bei großen Problemgrößen. Mathematisch präzisiert man diese Idee, indem man das Wachstumsverhalten von Kostenfunktionen vergleicht. Dabei idealisiert man, indem man das Grenzwertverhalten für gegen unendlich strebende Problemgrößen betrachtet. T1(n) = 0.01*n2 T2(n) = 100*n*log10(n) T2(n) / T1(n) -> 0 24 Vergleich von Kostenfunktionen Eine (Kosten-) Funktion f wächst schneller als eine (Kosten-) Funktion g, wenn der Quotient f(n)/g(n) mit wachsendem n gegen unendlich strebt. Eine (Kosten-) Funktion f wächst langsamer als eine (Kosten-) Funktion g, wenn der Quotient f(n)/g(n) mit wachsendem n gegen 0 strebt. Selectionsort: Tselectionsort(n) = (n-1) + (n-2) + ... + 1 = n*(n-1)/2 = n2/2 - n/2 Tquicksort(n) = n*log2(n) Eine (Kosten-) Funktion f wächst genauso schnell wie eine (Kosten-) Funktion g, wenn der Quotient f(n)/g(n) mit wachsendem n gegen eine Konstante c strebt. Beispiele: Tselectionsort(n) wächst genauso schnell wie T(n) = n2. Tselectionsort(n) wächst langsamer als Tquicksort(n). 25 Wachstumsprototypen Prototyp Grundeigenschaft f(n) = log(n) logarithmisches Wachstum: Wenn n sich verdoppelt, dann wächst f(n) um einen konstanten Betrag. f(n) = n lineares Wachstum: Wenn n sich verdoppelt, dann verdoppelt sich f(n) ebenfalls. f(n) = n*log(n) logarithmisch-lineares Wachstum f(n) = n2 quadratisches Wachstum: Wenn n sich verdoppelt, dann vervierfacht sich f(n). f(n) = n3 kubisches Wachstum: Wenn n sich verdoppelt, dann verachtfacht sich f(n). f(n) = nk polynomiales Wachstum Wenn n sich verdoppelt, dann vervielfacht sich f(n) mit 2k. f(n) = bn exponentielles Wachstum: Wenn n sich um 1 erhöht, dann vervielfacht sich f(n) mit b. 26 Wachstumsprototypen aus: P. Breuer: Praktische Grenzen der Berechenbarkeit. siehe: http://informatik.bildungrp.de/fileadmin/user_upload/informatik.bildungrp.de/Weiterbildung/pdf/WB-X-6-PraktischeGrenzen.pdf 27 Wachstumsklassen Eine (Kosten-) Funktion f wächst nicht schneller als eine (Kosten-) Funktion g, wenn f genauso schnell oder langsamer als g wächst. Die Klasse aller Funktionen, die nicht schneller wachsen als eine vorgegebene (Kosten-) Funktion f, wird mit O(f) bezeichnet. Man liest das so: "Groß O von f". Eine genaue Zuordnung zu einem der Wachstumsprototypen geling manchmal nicht. Dennoch ist ein Vergleich mit den Wachstumsprototypen sinnvoll. Beispiel: T2(n) = n2*(log2n)3 Die Klasse O(n3) umfasst alle (Kosten-) Funktionen, die nicht schneller wachsen als f(n) = n3. Zu dieser Klasse gehören also alle linearen, alle logarithmisch-linearen, alle quadratischen und alle kubischen Funktionen. Zu dieser Klasse gehört aber auch die Funktion T2(n) = n2*(log2n)3. 28 Wachstumsprototypen Algorithmen, deren Zeitkomplexität durch eine Kostenfunktion beschrieben wird, die exponentiell oder noch schneller wächst, gelten als praktisch nicht anwendbar. Wir nehmen hier an, dass zur Verarbeitung einer Kosteneinheit eine Millisekunde benötigt wird. aus: P. Breuer: Praktische Grenzen der Berechenbarkeit. 29 Komplexität von Problemen Die (Zeit-)Komplexität eines Problems beschreibt man durch Komplexitätsklassen, die untere Schranken für die Komplexität der Algorithmen, die das Problem lösen, bilden. Der Algorithmus selectionsort hat - im günstigsten wie auch ungünstigsten Fall - eine quadratische Zeitkomplexität. Die zur Beschreibung des Laufzeitverhalten gewählte Kostenfunktion gehört zur Klasse O(n2) der asymptotisch quadratisch wachsenden Funktionen. Der Algorithmus quicksort hat - im günstigsten (und auch durchschnittlichen) Fall - eine logischmisch-lineare Zeitkomplexität. Die zur Beschreibung des Laufzeitverhalten gewählte Kostenfunktion gehört hier zur Klasse O(n*log2(n)). Es gibt eine Vielzahl an weiteren Sortieralgorithmen. Die "schnelleren" dieser Algorithmen haben alle eine logischmisch-lineare Zeitkomplexität. Es stellt sich die Frage, ob es nicht noch schnelleren Sortieralgorithmen gibt - z.B. solche mit einer linearen Zeitkomplexität - und, ob es auch eine Art untere Schranke für die Zeitkomplexität gibt, die von keinem Sortieralgorithmus unterschritten werden kann. Diese Fragen betreffen die Komplexität des Sortierproblems. Zur Beschreibung der Komplexität eines Problems muss man folglich Aussagen über alle möglichen Algorithmen zur Lösung des Problems machen, indem man zeigt, dass ein bestimmter Ressourcenverbrauch bei all diesen Algorithmen erforderlich ist und von keinem Algorithmus unterschritten werden kann. Die Schwierigkeit beim Nachweis solcher Aussagen besteht darin, dass man den Nachweis über alle denkbaren - d.h. bis jetzt gefundenen und auch noch nicht bekannten - Algorithmen führen muss. 30 Problem - Sortieren {abcd,abdc,acbd,acdb,adbc,adcb,bacd,badc,bcad,bcda,bdac,bdca, cabd,cadb,cbad,cbda,cdab,cdba,dabc,dacb,dbac,dbca,dcab,dcba} [a, b, c, d] a < b? T: {abcd,abdc,acbd,acdb,adbc,adcb,cabd,cadb,cdab,dabc,dacb,dcab} [a, b, c, d] a < c? T: {abcd,abdc,acbd,acdb,adbc,adcb,dabc,dacb} [a, b, c, d] a < d? T: {abcd,abdc,acbd,acdb,adbc,adcb} [a, b, c, d] b < c? T: {abcd,abdc,adbc} [a, b, c, d] b < d? T: {abcd,abdc} [a, b, c, d] c < d? T: {abcd} [a, b, c, d] F: {abdc} [a, b, d, c] F: {adbc} [a, d, c, b] c < b? T: {} [a, d, c, b] F: {adbc} [a, d, b, c] F: {acbd,acdb,adcb} | 25 ^ 32 56 27 25 | 32 56 27 ^ 25 27 | 56 32 ^ 25 27 32 | 56 Selectionsort Problem - Sortieren 31 ... F: {dabc,dacb} [d, b, c, a] b < c? T: {dabc} [d, b, c, a] b < a? T: {} c < a? T: {} F: {} F: {dabc} [d, a, c, c < b? T: {} F: {dabc} [d, a, F: {dacb} [d, b, c, a] c < a? T: {} b < a? T: {} F: {} F: {dacb} [d, a, c, c < b? T: {dacb} [d, a, F: {} ... Selectionsort | 25 ^ 32 56 27 25 | 32 56 27 ^ 25 27 | 56 32 ^ 25 27 32 | 56 b] b, c] b] c, b] Wenn ein Sortieralgorithmus die Sortierung nur über Vergleiche von Listenelementen erzeugt, dann lässt sich die Ausführung des Algorithmus bei beliebigen Ausgangslisten über einen Entscheidungsbaum darstellen. Die "Tiefe des Baumes" (erkennbar an den Einrückungen) zeigt dabei, wie viele Entscheidungen im ungünstigsten Fall auszuführen sind. Im Fall des Algorithmus selectionsort beträgt die Tiefe des Baumes 6. Am Entscheidungsbaum zeigt sich also, wie gut oder schlecht ein Algorithmus ist. Wenn die Tiefe des Entscheidungsbaums groß bzw. klein ist, dann ist das worst-case-Verhalten des Algorithmus schlecht bzw. gut. 32 Problem - Sortieren {abcd,abdc,acbd,acdb,adbc,adcb,bacd,badc,bcad,bcda,bdac,bdca, cabd,cadb,cbad,cbda,cdab,cdba,dabc,dacb,dbac,dbca,dcab,dcba} a < b? T: {abcd,abdc,acbd,acdb,adbc,adcb,cabd,cadb,cdab,dabc,dacb,dcab} c < d? T: {abcd,acbd,acdb,cabd,cadb,cdab} a < c? T: {abcd,acbd,acdb} b < c? Am Entscheidungsbaum kann auch aufgezeigt T: {abcd} werden, wie gut das worst-case-Verhalten eines F: {acbd,acdb} Sortieralgorithmus überhaupt sein kann: Bei 24 b < d? T: {acbd} möglichen Anordnungen benötigt man F: {acdb} mindestens einen Entscheidungsbaum der Tiefe F: {cabd,cadb,cdab} 5, um alle Anordnungen durch Entscheidungen b < d? T: {cabd} erzeugen zu können. Das sieht man so ein: Durch F: {cadb,cdab} 1 Entscheidung erhält man eine Aufteilung der a < d? Anordnungen in 2 Teilmengen, durch 2, 3, 4, 5, T: {cadb} F: {cdab} ... Entscheidungen in 4, 8, 16, 32, ... Teilmengen. F: {abdc,adbc,adcb,dabc,dacb,dcab} Um alle 24 Anordnungen mittels Entscheidungen a < d? auseinander zu bekommen, sind daher T: {abdc,adbc,adcb} b < d? mindestens 5 Entscheidungen im T: {abdc} Entscheidungsbaum erforderlich. Das F: {adbc,adcb} Baumdiagramm zeigt, wie man mit 5 b < c? T: {abdc} geschachtelten Entscheidungen tatsächlich alle F: {adcb} Anordnungen erhält. F: ... 33 Komplexität des Sortierproblems Die Betrachtungen oben verdeutlichen, dass es zu jedem vergleichsbasierten Sortieralgorithmus (für jede Listenlänge) einen zugehörigen Entscheidungsbaum gibt. Die Tiefe des Entscheidungsbaum (in Abhängigkeit der Listenlänge) legt das worst-case-Verhalten des Algorithmus fest. Um eine untere Schranke für das worst-case-Verhalten von Sortieralgorithmen zu gewinnen, schauen wir uns die Tiefe von "optimalen Entscheidungsbäumen" (in Abhängigkeit der Listenlänge) an. Um k verschiedene Anordnungen nur mit Entscheidungen zu erzeugen, benötigt man einem Entscheidungsbaum mit einer Tiefe der Größenordnung log2(k). Wenn k = 2m eine Zweierpotenz ist, dann reicht die Tiefe m. Ansonsten benötigt man als Tiefe den Exponenten von der von k aus nächst größeren Zweierpotenz. Wenn beispielsweise k = 24, dann benötigt man eine Tiefe log2(32), also die Tiefe 5. Um eine Aussage über Listen beliebiger Länge treffen zu können, muss man wissen, wie viele Anordnungen jeweils möglich sind: Bei n Listenelementen gibt es n! = 1*2*...*n verschiedene mögliche Anordnungen der Listenelemente. Fasst man beide Ergebnisse zusammen, so erhält man folgende Aussage: Um eine Liste mit n Elementen zu sortieren, benötigt man einen Entscheidungsbaum, der eine Tiefe der Größenordnung log2(n!) hat. 34 Komplexität des Sortierproblems Jeder vergleichsbasierte Sortieralgorithmus hat demnach ein worst-case-Verhalten, das durch die Funktion K(n) = log2(n!) abgeschätzt werden kann. Mathematiker haben gezeigt, dass n! ≥ (n/2)(n/2) gilt. Mit dieser Abschätzung erhält man log2(n!) ≥ (n/2)*log2(n/2). Hieraus ergibt sich, dass jeder vergleichsbasierte Sortieralgorithmus ein worst-case-Verhalten hat, das nach unten durch die Funktion K(n) = (n/2)*log2(n/2) abgeschätzt werden kann. Betrachtet man - wie üblich - nur das asymptotische Verhalten, so zeigt dies, dass das worstcase-Verhalten eines beliebigen Sortieralgorithmus von der Ordnung n*log2(n) sein muss. Damit ist eine Schranke zur Beschreibung der Komplexität des Sortierproblems gefunden. Die Komplexität des Sortierproblems ist von der Ordnung n*log2(n) - sofern man ein vergleichsbasiertes Berechnungsmodell zu Grunde liegt. 35 Teil 2 Fallstudie - Das Affenpuzzle Praktische Anwendbarkeit von Algorithmen 36 Das Affenpuzzle Algorithmus zur Lösung d. Affenpuzzles 37 ALGORITHMUS affenpuzzle(n): erzeuge n*n Karten erzeuge die Startpermutation der Karten SOLANGE die letzte Permutation noch nicht erreicht und noch keine Lösung gefunden ist: erzeuge eine Startorientierung der Karten SOLANGE die letzte Orientierungen noch nicht erreicht ist: erzeuge das Kartenfeld zur Permutation und zur Orientierung überprüfe das Kartenfeld WENN ok: Lösung = Kartenfeld erzeuge die nächste Orientierung erzeuge die nächste Permutation Rückgabe: (Karten, Lösung) Idee: Man wählt eine bestimmte Kartenreihenfolge aus (eine sog. Permutation der Ausgangskartenreihe). Zudem wählt man eine bestimmte Orientierung der einzelnen Karten (jede Karte kann ja in 4 unterschiedlichen Ausrichtungen hingelegt werden). Wenn man jetzt die zur Permutation und Orientierung gehörende Kartenreihe in das 3x3-Kartenfeld legt (z.B. von links oben nach rechts unten), dann kann man anschließend überprüfen, ob die Kartenübergänge alle stimmen. 38 Aufgabe Benutze eine Implementierung des Algorithmus "affenpuzzle". Du must nicht alle Details der Implementierung verstehen. Wichtig für die Benutzung ist vor allem die Funktion affenpuzzle(n). Der Parameter n steht hier für die "Größe" des Puzzles. Bei einem 3x3-Puzzle beträgt die Größe n = 3. (a) Beim Testen ist die Größe n = 2 voreingestellt. Führe die Testanweisungen mehrere Male aus und versuche zu verstehen, wie die Karten im Implementierungsprogramm dargestellt werden. Beachte, dass es vorkommen kann, dass die erzeugten Karten keine Puzzle-Lösung haben. (b) Stelle beim Testen die Größe n = 3 ein. Wenn du jetzt die Ausführung startest, musst du erst einmal sehr lange warten. Wenn du ungeduldig wirst, dann versuche mit folgender Strategie die Wartezeit abzuschätzen: Schritt 1: Berechne, wie viele Konfigurationen erzeugt werden. Überlege dir, wie viele Permutationen der Ausgangskartenreihe möglich sind und wie viele Orientierungen jede Permutation zulässt. Zur Kontrolle: 95126814720 Schritt 2: In der Implementierung ist bereits eine Variable zaehler vorgesehen, die die erzeugten Konfigurationen mitzählt. Ergänze im Programm eine Ausgabeanweisung, die bei jeweils 1 Million überprüfter Konfigurationen eine Ausgabe auf dem Bildschirm macht. Stoppe dann die Zeit, die für 1 Million Konfigurationsüberprüfungen benötigt wird. Schritt 3: Mit den Ergebnissen aus Schritt 1 und Schritt 2 kannst du jetzt eine Hochrechnung machen. Problemgröße / Kostenfunktion 39 Problemgröße n: Anzahl der Karten, die eine Seite des Kartenfeldes bilden Kostenfunktion K(n): Problemgröße -> Anzahl der zu betrachtenden Konfigurationen (Permutation + Orientierung) ALGORITHMUS affenpuzzle(n): erzeuge n*n Karten erzeuge die Startpermutation der Karten SOLANGE die letzte Permutation noch nicht erreicht und noch keine Lösung gefunden ist: erzeuge eine Startorientierung der Karten SOLANGE die letzte Orientierungen noch nicht erreicht ist: erzeuge das Kartenfeld zur Permutation und zur Orientierung überprüfe das Kartenfeld WENN ok: Lösung = Kartenfeld erzeuge die nächste Orientierung erzeuge die nächste Permutation Rückgabe: (Karten, Lösung) 40 Kostenfunktion Die Herleitung einer Formel für K(n) soll hier für den Fall n = 3 durchgespielt werden. Im Fall n = 3 gibt es insgesamt 3*3 = 9 Karten. Wenn man jetzt eine Kartenreihenfolge bildet, dann gibt es 9 Möglichkeiten für die erste Karte, 8 Möglichkeiten für die zweite Karte, 7 Möglichkeiten für die dritte Karte, ... 2 Möglichkeiten für die achte Karte und schließlich 1 Möglichkeit für die neunte Karte. Ingesamt können bei 9 Karten also 9*8*7*6*5*4*3*2*1 verschiedene Kartenreihenfolgen (Permutationen) gebildet werden. Mathematiker schreiben auch 9! (gelesen: 9 Fakultät) für das Produkt 9*8*7*6*5*4*3*2*1. Wenn eine bestimmte Kartenreihenfolge vorliegt, dann können alle 9 Karten in jeweils 4 verschiedenen Weisen ausgerichtet werden. Zu jeder Kartenreihenfolge gibt es also 4*4*4*4*4*4*4*4*4 verschiedene Orientierungen der Karten. Mathematiker schreiben für das Produkt 4*4*4*4*4*4*4*4*4 kurz 49. Wenn man beide Ergebnisse zusammenfasst, dann ergeben sich im Fall n = 3 insgesamt 49*9! bzw. 4(3*3)*(3*3)! verschiedene Kartenkonfigurationen. Wenn man die Überlegungen verallgeinert, so erhält man die Formel: K(n) = 4(n*n)*(n*n)! 41 Aufgabe (a) Berechne (z.B. mit einem geeigneten Programm) Funktionswerte der Funktion K(n) = 4(n*n)*(n*n)! für n = 2, 3, 4, 5, 6, ... Was fällt auf? (b) Schätze ab, wie lange ein Rechner im ungünstigsten Fall zur Überprüfung eines 4x4-Puzzles (5x5-Puzzles) benötigt. Gehe hier zunächst auch davon aus, dass ein Rechner zur Erzeugung und Überprüfung von 1 Million Konfigurationen 1 Sekunde benötigt. (c) Die Erfahrung lehrt, dass künftige Rechnergenerationen viel schneller sind. Wie wirkt es sich aus, wenn Rechner 1000mal bzw. 1000000mal schneller sind als in der Hochrechnung oben angenommen wurde? (d) Warum kann man sagen, dass der Algorithmus praktisch nicht anwendbar ist? (e) Wie könnte man den Algorithmus verbessern? 42 Verbesserte Algorithmen Der oben gezeigte "naive" Algorithmus zur Bestimmung der Lösung eines Affenpuzzles ist in der Praxis nicht anwendbar. Nur für sehr kleine Problemgrößen erhält man in akzeptabler Zeit ein Ergebnis. Diese Eigenschaft spiegelt sich im Wachstumsverhalten der Kostenfunktion wider. Die Kostenfunktion K(n) = 4(n*n)*(n*n)! wächst schneller als jede Exponentialfunktion. Man kann jetzt versuchen, durch Überlegungen die Anzahl der zu überprüfenden Kartenkonfigurationen zu reduzieren (vgl. auch Affenpuzzle_GeroScholz.pdf). Verbesserte Algorithmen zeigen dann ein durchaus akzeptables Laufzeitverhalten für Problemgrößen bis n = 5. Mit wachsender Problemgröße zeigen sich aber dieselben Schwierigkeiten wie beim naiven Algorithmus: Die Kostenfunktion wächst so schnell, dass eine geringe Erhöhung der Problemgröße das Laufzeitverhalten explodieren lässt. Algorithmen mit einer exponentiellen oder noch ungünstigeren (Zeit-) Komplexität gelten als praktisch nicht anwendbar, da nur für kleine Problemgrößen akzeptable Laufzeiten - auch bei künftigen Rechnergenerationen - zu erwarten sind. Ob es Algorithmen zur Lösung des Affenpuzzles gibt, deren (Zeit-) Komplexität günstiger als exponentiell ist, ist nicht bekannt. 43 Teil 3 Fallstudie - Primfaktorzerlegung Praktische Anwendbarkeit von Algorithmen 44 Primfaktorzerlegung 260 2 * 2 * 5 * 13 Primzahlen sind natürliche Zahlen, die nur durch 1 und sich selbst ohne Rest teilbar sind. Beispiele: 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, ... Eine der wichtigsten Eigenschaften von Primzahlen ist, dass sie als Bausteine der natürlichen Zahlen angesehen werden können. Satz: Jede natürliche Zahl lässt sich als Produkt von Primzahlen schreiben. Diese Darstellung ist bis auf die Reihenfolge der Faktoren eindeutig. Beispiel: 260 = 2*2*5*13 = 22*5*13 Man nennt die Primzahlen, die in einer Produktdarstellung einer gegebenen Zahl vorkommen, auch Primfaktoren der Zahl. Das Faktorisierungsproblem besteht darin, eine vorgegebene Zahl in ein Produkt aus Primfaktoren zu zerlegen. 45 Aufgabe (a) Bei kleineren Zahlen kann man eine Primfaktorzerlegung oft direkt angeben. Bestimme eine Primfaktorzerlegung von n = 48 und n = 100. (b) Bei größeren Zahlen sollte man systematisch vorgehen, um die Primfaktoren zu bestimmen. Bestimme eine Primfaktorzerlegung von n = 221 und n = 585. (c) Entwickle zunächst einen Algorithmus zur Primfaktorzerlegung. Beschreibe in einem ersten Schritt in Worten das Verfahren, das du zur Primfaktorzerlegung von Zahlen benutzt. Beschreibe das Verfahren anschließend mit einem Struktogramm. Entwickle dann ein Programm zur Primfaktordarstellung. Hinweis: In Python bietet es sich an, eine Funktion primfaktoren(n) zu erstellen, die die Liste der Primfaktoren zurückgibt. 46 Ein einfaches Faktorisierungsverfahren ALGORITHMUS primfaktoren(n): # Übergabe: n = 51 initialisiere die Liste faktoren: faktoren = [] # Initialisierung initialisiere die Hilfsvariable z: z = n faktoren = [] {faktoren -> []} SOLANGE z > 1: z=n {z -> 51} bestimme den kleinsten Primfaktor p von z mit Probedivisionen # Probedivisionen füge p in die Liste faktoren ein z % 3 -> 0 z = z // p Rückgabe: faktoren z % 2 -> 1 # Aktualisierung p=z {p -> 3} faktoren = faktoren + [p] {faktoren -> [3]} z = z // p {z -> 17} # Probedivisionen Aufgabe: Bestimme mit (einer geeigneten Implementierung) der Funktion primfaktoren(n) die Primfaktorzerlegung der beiden Zahlen 484639526894037745950720 und 565765434324543216797351. Was stellst du fest? Stelle eine Vermutung auf, warum es hier zu einem unterschiedlichen Laufzeitverhalten kommt. z % 2 -> 1 z % 3 -> 2 z % 4 -> 1 z % 5 -> 2 # Aktualisierung p=z {p -> 17} faktoren = faktoren + [p] {faktoren -> [3, 17]} z = z // p # Rückgabe: [3, 17] {z -> 1} Laufzeitmessungen 47 from faktorisierung import primfaktoren testzahlen = [ from time import * 11, testzahlen = [...] 101, for z in testzahlen: 1009, t1 = clock() 10007, ergebnis = primfaktoren(z) 100003, t2 = clock() 1000003, t = t2 - t1 10000019, print("Zahl: ", z) 100000007, print("Primfaktoren:", ergebnis) 1000000007, print("Rechenzeit: ", t) 10000000019, print() 100000000003, Hinweis: Um Gesetzmäßigkeiten herauszufinden, sollte man systematisch vorgehen. Aufgabe: Führe die Messungen durch. Kannst du anhand der Zahlen erste Zusammenhänge erkennen? Kannst du Prognosen erstellen, wie lange man wohl bis zum nächsten Ergebnis warten muss? 1000000000039, 10000000000037, 100000000000031, 1000000000000037, 10000000000000061, 100000000000000003, 1000000000000000003, 10000000000000000051, 100000000000000000039, ...] 48 Zusammenhänge und Prognosen Gesetzmäßigkeit: Wenn man die Anzahl der Stellen der Ausgangszahl um 2 erhöht, dann erhöht sich die Laufzeit um den Faktor 10. Jede zusätzliche Stelle bei der Ausgangszahl führt also dazu, dass die Laufzeit mit dem Faktor √10 multipliziert wird. ... Es handelt sich hier um ein exponentielles Wachstumsverhalten, das man mathematisch mit einer Exponentialfunktion beschreiben kann: Wenn k die Anzahl der Stellen der Ausgangszahl angibt, dann erhält man eine Laufzeit vom Typ L(k) = c*(√10)k mit einer Konstanten c. Primfaktoren: [10000000000037] Prognose: Wenn die Zahl 100 Stellen haben soll, also 88 Stellen mehr als eine 12-stellige Zahl, so benötigt man nach der gefundenen Gesetzmäßigkeit 1044-mal so lange wie bei der 12-stelligen Zahl - also etwa 1044 Sekunden. Zahl: Zahl: 1000000000039 Primfaktoren: [1000000000039] Rechenzeit: 0.906267137304 Zahl: 10000000000037 Rechenzeit: 2.88270213114 Zahl: 100000000000031 Primfaktoren: [100000000000031] Rechenzeit: 9.1279123464 1000000000000037 Primfaktoren: [1000000000000037] Rechenzeit: 28.5701070946 Zahl: 10000000000000061 Primfaktoren: [10000000000000061] Rechenzeit: 91.2736900919 ... Problemgröße / Kostenfunktion 49 Problemgröße i: Anzahl der Stellen der Ausgangszahl n Kostenfunktion K(i): Problemgröße -> Anzahl der durchzuführenden Probedivisionen # Übergabe: n = 51 # Initialisierung faktoren = [] {faktoren -> []} z=n {z -> 51} # Probedivisionen z % 2 -> 1 z % 3 -> 0 # Aktualisierung p=z {p -> 3} faktoren = faktoren + [p] {faktoren -> [3]} z = z // p {z -> 17} # Probedivisionen ALGORITHMUS primfaktoren(n): z % 2 -> 1 initialisiere die Liste faktoren: faktoren = [] z % 3 -> 2 initialisiere die Hilfsvariable z: z = n z % 4 -> 1 SOLANGE z > 1: z % 5 -> 2 bestimme den kleinsten Primfaktor p von z mit Probedivisionen # Aktualisierung füge p in die Liste faktoren ein faktoren = faktoren + [p] {faktoren -> [3, 17]} z = z // p z = z // p Rückgabe: faktoren p=z # Rückgabe: [3, 17] {p -> 17} {z -> 1} 50 Kostenanalyse best case (bester Fall): der Fall, in dem bei der Ausführung des Algorithmus die wenigsten Kosten anfallen worst case (schlechtester Fall): der Fall, in dem bei der Ausführung des Algorithmus die meisten Kosten anfallen average case (durchschnittlicher Fall): eine Mittelung der Kosten über alle Fälle best case: n ist eine Zweierpotenz mit i Stellen worst case: n ist eine Primzahl mit i Stellen Beispiel: n = 29 = 512; i = 3 Beispiel: n = 983; i = 3 Beachte: 10 < 24; n < 10i < 24*i Beachte: 10i-1 < n < 10i Probedivisionen: Probedivisionen: z=n z=n z%2=0 z%2>0 p = z; faktoren = faktoren + [p]; z = z//2 z%3>0 z%2=0 z%4>0 p = z; faktoren = faktoren + [p]; z = z//2 ... ... z % 31 > 0 Anzahl der Probedivisionen: 9 < 4*3 -> z ist Primzahl K(i) <= 4*i Beachte: √983 = 31.35... Anzahl der Probedivisionen: 31 > √100 = (√10)2 K(i) >= √(10i-1) = (√10)i-1 51 Asymptotisches Wachstumsverhalten best case (bester Fall): der Fall, in dem bei der Ausführung des Algorithmus die wenigsten Kosten anfallen K(i) <= 4*i worst case (schlechtester Fall): der Fall, in dem bei der Ausführung des Algorithmus die meisten Kosten anfallen K(i) >= √(10i-1) = (√10)i-1 lineares Wachstum Wenn man die Problemgröße um 1 erhöht, dann wachsen die Kosten einen festen Betrag (hier maximal 4). exponentielles Wachstum Wenn man die Problemgröße um 1 erhöht, dann wachsen die Kosten mit dem Faktor √10. Wenn man die Problemgröße um 2 erhöht, dann wachsen die Kosten mit dem Faktor √10*√10, also mit den Faktor 10. 52 Anwendbarkeit des Algorithmus Algorithmen, deren Zeitkomplexität durch eine Kostenfunktion beschrieben wird, die exponentiell oder noch schneller wächst, gelten als praktisch nicht anwendbar. Angenommen, der Rechenaufwand beträgt bei 10 Stellen 1 Zeiteinheit. Dann beträgt der Rechenaufwand bei 100 Stellen 1045 Zeiteiheiten. Wenn sich die Rechnergeschwindigkeit um den Faktor 1000 verbessert, dann beträgt der rechenaufwand immer noch 1042 Zeiteiheiten. 53 Komplexität d. Faktorisierungsproblems Die Zeitkomplexität eines Problems wird durch eine untere Schranke für Kostenfunktionen von Algorithmen zur Lösung des Problems beschrieben. Bis jetzt sind solche unteren Schranken für das Faktorisierungsproblem nicht gefunden worden. Es wurde auch noch kein Algorithmus entwickelt, der das Faktorisierungsproblem mit mit nichtexponentieller Zeitkomplexität löst. Alle bisher entwickelten Algorithmen sind demnach - im Fall großer Ausgangszahlen - praktisch nicht anwendbar. Diesen Sachverhalt nutzt bei der Entwicklung kryptologische Verfahren aus (vgl. RSA-Verfahren). 54 Aufgabe Überlege dir Verbesserungen des Probedivisionsalgorithmus. Versuche auch, die Auswirkungen von Verbesserungen auf den Berechnungsaufwand herauszufinden. 55 Teil 4 Fallstudie - Das Rundreiseproblem Schwer lösbare Probleme 56 Rundreiseprobleme Das Königsberger Brückenproblem: Das Ikosaeder-Problem: Gibt es einen Rundgang durch Königsberg, der jede der sieben Brücken über den Fluss Pregel genau einmal benutzt? Gibt es eine Rundreise längs der Kanten eines Dodekaeders, so dass alle Ecken des Dodekaeders - außer der Start- und Zielecke - genau einmal besucht werden? 57 Lösung der Rundreiseprobleme Euler-Problem: Hamilton-Problem: Eine Rundreise, in der jede Kante (Brücke) genau einmal vorkommt, besucht einen Knoten (Stadtteil) über eine Kante (Brücke) und verlässt ihn über eine andere Kante (Brücke). Hieraus ergibt sich, dass die Anzahl der Kanten (Brücken), die von einem Knoten (Stadtteil) der Rundreise ausgehen, gerade sein muss, damit eine Rundreise der gewünschten Art zustande kommen kann. Diese notwendige Bedingung ist beim Königsberger Brückenproblem aber nicht erfüllt. Das Rundreiseproblem auf einem Dodekaeder hat eine Reihe von Lösungen. 58 Verallgemeinerte Rundreiseprobleme Euler-Problem: Hamilton-Problem: Gesucht ist eine Rundreise durch den Graphen, in der jede Kante genau einmal vorkommt. Eine solche Rundreise wird auch Eulerkreis genannt. Gegeben ist ein Graph mit seinen Knoten und Kanten. Gesucht ist eine Rundreise durch den Graphen, in der jeder Knoten genau einmal vorkommt - nur Start- und Zielknoten kommen genau zweimal vor. Eine solche Rundreise wird auch Hamiltonkreis genannt. Lösungsalgorithmen 59 ALGORITHMUS eulerkreis: Übergabe: Graph, dargestellt m. Nachbarschaftstabelle kreisExistiert = True FÜR alle Knoten des Graphen: bestimme die Anzahl der Nachbarn des Knoten WENN diese Anzahl ungerade ist: kreisExistiert = False Rückgabe: kreisExistiert Lösungsalgorithmen 60 ALGORITHMUS hamiltonkreis Übergabe: Graph, dargestellt mit Nachbarschaftslisten hamiltonkreisGefunden = False lege einen Startknoten fest erzeuge eine Ausgangsanordnung der zu besuchenden Knoten esGibtNochWeitereAnordnungen = True SOLANGE esGibtNochWeitereAnordnungen und nicht hamiltonkreisGefunden: WENN die Anordnung einen zulässigen Kreis beschreibt: hamiltonkreisGefunden = True erzeuge systematisch eine neue Anordnung der zu besuchenden Knoten WENN die neue Anordnung gleich der Ausgangsanordnung ist: esGibtNochWeitereAnordnungen = False Rückgabe: hamiltonkreisGefunden Kostenanalyse 61 ALGORITHMUS eulerkreis: Übergabe: Graph, dargestellt m. Nachbarschaftstabelle kreisExistiert = True FÜR alle Knoten des Graphen: bestimme die Anzahl der Nachbarn des Knoten WENN diese Anzahl ungerade ist: kreisExistiert = False Rückgabe: kreisExistiert Problemgröße n: Anzahl der Knoten des Graphen Kostenfunktion K(n): Anzahl der Verarbeitung von Nachbarknoten Kostenanalyse: Wenn der Graph n Knoten hat, so sind (im ungünstigsten Fall) höchstens n*n Verarbeitungen von Nachbarknoten zu erledigen. Die Kostenfunktion kann demnach durch eine quadratische Funktion abgeschätzt werden. Der Algorithmus hat also eine quadratische Komplexität. Lösungsalgorithmen 62 ALGORITHMUS hamiltonkreis Übergabe: Graph, dargestellt mit Nachbarschaftslisten hamiltonkreisGefunden = False lege einen Startknoten fest erzeuge eine Ausgangsanordnung der zu besuchenden Knoten esGibtNochWeitereAnordnungen = True SOLANGE esGibtNochWeitereAnordnungen und nicht hamiltonkreisGefunden: WENN die Anordnung einen zulässigen Kreis beschreibt: hamiltonkreisGefunden = True erzeuge systematisch eine neue Anordnung der zu besuchenden Knoten WENN die neue Anordnung gleich der Ausgangsanordnung ist: esGibtNochWeitereAnordnungen = False Rückgabe: hamiltonkreisGefunden Problemgröße n: Anzahl der Knoten des Graphen Kostenfunktion K(n): Anzahl der möglichen Knotenanordnungen zur Bildung eines Rundwegs Kostenanalyse: Wenn der Graph n Knoten hat, so gibt es (n-1)! Knotenanordnungen. Wegen n! >= (n/2)n/2 gilt: Der Algorithmus hat eine eponentielle Komplexität. Lösungsalgorithmen 63 ALGORITHMUS hamiltonkreis Übergabe: Graph, dargestellt mit Nachbarschaftslisten kreisExistiert = False lege einen startKnoten fest # erzeuge eine Liste wege, mit der die Anfangsteile möglicher Rundreisewege verwaltet werden wege = [[startKnoten]] SOLANGE wege nicht leer ist und nicht kreisExistiert: aktuellerWeg = erster Weg in wege entferne akteuellerWeg aus wege WENN aktuellerWeg alle Knoten des Graphen enthält: WENN es eine Kante vom letzten Knoten von aktuellerWeg zum startKnoten gibt: kreisExistiert = True SONST: letzterKnoten = letzter Knoten von aktuellerWeg FÜR alle nachbarKnoten von aktuellerWeg: WENN nachbarKnoten nicht bereits in aktuellerWeg vorkommt: erweiterterWeg = aktuellerWeg erweitert um nachbarKnoten nimm erweiterterWeg in der Liste wege auf Rückgabe: kreisExistiert Gibt es bessere Algorithmen zur Lösung des Hamilton-Problems? 64 Aufgabe Bearbeite Aufgabe 4 auf www.inf-schule.de Seite 1.20.4.3. 65 Die Klasse P P bezeichnet die Klasse der Probleme, die mit einem Algorithmus mit polynomialer Zeitkomplexität gelöst werden können. Ergebnisse: Das Euler-Problem gehört zur Klasse P. Anders verhält es sich beim Hamilton-Problem. Die hier vorgestellten Lösungsalgorithmen haben eine exponentielle Zeitkomplexität. Alle weiteren Lösungsalgorithmen, die bisher entwickelt worden sind, haben ebenfalls eine exponentielle Zeitkomplexität. Ob es Lösungsalgorithmen für das Hamilton-Problem gibt, die eine polynomiale Zeitkomplexität aufweisen, ist nicht bekannt. Es ist also noch offen, ob das Hamilton-Problem zur Klasse P gehört oder nicht. Man vermutet aufgrund der vielen negativen Ergebnisse, dass es wohl nicht zu P gehört. 66 Nichtdeterministische Algorithmen ALGORITHMUS hamiltonkreis_nichtdeterministisch Übergabe: Graph, dargestellt mit Nachbarschaftslisten # erzeuge nichtdeterministisch einen Rundreisekandidaten lege einen startKnoten fest weg = [startknoten] n = Anzahl der Knoten des Graphen restKnoten = Liste mit allen Knoten außer dem startKnoten WIEDERHOLE n-1 mal: naechsterKnoten = restKnoten[0] | restKnoten[1] | ... | restKnoten[n-2] weg = weg + [naechsterKnoten] weg = weg + [startKnoten] # überprüfe den Rundreisekandidaten hamiltonkreisExistiert = True FÜR i von 1 BIS n-1: WENN weg[i] kein Nachbar von weg[i-1] ist oder weg[i] bereits in [weg[0], ..., weg[i-1]] vorkommt: hamiltonkreisExistiert = False WENN weg[n] kein Nachbar von weg[n-1] ist: hamiltonkreisExistiert = False Rückgabe: hamiltonkreisExistiert polynomiale Zeitkomplexität nichtdeterministisch 67 Die Klasse NP NP bezeichnet die Klasse der Probleme, die mit einem nichtdeterministischen Algorithmus mit polynomialer Zeitkomplexität gelöst werden können. Ergebnisse: Das Euler-Problem gehört auch zur Klasse NP, da P eine Teilmenge von NP ist. Das Hamilton-Problem gehört zur Klasse NP. Problemreduktion 68 p: Gibt es einen Hamiltonkreis? ALGORITHMUS existiertRundreiseHamilton p': Gibt es eine Rundreise mit Gewicht <= 5? ALGORITHMUS existiertRundreiseHandlungsreisender Übergabe: ungewichteter Graph G Übergabe: gewichteter Graph, Gewichtsschranke # Umwandlung des Problems # ... erzeuge aus G den gewichteten Graphen G' (s.o.) Rückgabe: True / False gewichtsschranke = Anzahl der Knoten von G' (bzw. G) # Lösung des transformierten Problems ergebnis = existiertRundreiseHandlungsreisender (G', gewichtsschranke) # Lösung des Ausgangsproblems Rückgabe: ergebnis False False 69 Polynomiale Problemreduktion Bei einer polynomialen Problemreduktion muss der Aufwand zur Umwandlung des Problems (und zur Umwandlung der Lösung) polynomial sein. Wir schreiben p ≤ p', wenn das Problem p auf das Problem p' polynomial reduzierbar ist. p: Gibt es einen Hamiltonkreis? p': Gibt es eine Rundreise mit Gewicht <= 5? ALGORITHMUS existiertRundreiseHandlungsreisender ALGORITHMUS existiertRundreiseHamilton Übergabe: ungewichteter Graph G Übergabe: gewichteter Graph, Gewichtsschranke # Umwandlung des Problems # ... erzeuge aus G den gewichteten Graphen G' (s.o.) polynomial Rückgabe: True / False gewichtsschranke = Anzahl der Knoten von G' (bzw. G) # Lösung des transformierten Problems ergebnis = existiertRundreiseHandlungsreisender (G', gewichtsschranke) # Lösung des Ausgangsproblems False Hamilton-Problem ≤ Rückgabe: ergebnis False Problem des Handlungsreisenden Polynomiale Problemreduktion 70 Wenn p auf das Problem p' polynomial reduzierbar ist und wenn p' eine polynomiale Komplexität hat, dann hat auch das Problem p eine polynomiale Komplexität. kurz: aus p ≤ p' und p' in P folgt p in P polynomial p: ... p': ... ALGORITHMUS A Übergabe: ... erzeuge Übergabedaten von A' wende A' auf Die Daten an Rückgabe: ... ALGORITHMUS A' Übergabe: ... ... Rückgabe ... polynomial polynomial Argumentation: Aus einem Algorithmus A' mit polynomialer Komplexität zur Lösung des Problems p' lässt sich ein Algorithmus A mit polynomialer Komplexität zur Lösung des Problems p erzeugen. Man muss nur (wie im Beispiel oben) den Algorithmus A' mit den Umwandlungsalgorithmen kombinieren. Bei der Argumentation benutzt man zudem, dass das Hintereinanderausführen von Algorithmen mit polynomialer Komplexität zu einem Algorithmus mit polynomialer Komplexität führt. 71 NP-vollständige Probleme Ein Problem p* heißt NP-vollständig genau dann, wenn es in der Komplexitätsklasse NP liegt (d.h. mit einem nichtdeterministischen Algorithmus mit polynomialer Komplexität gelöst werden kann) und wenn jedes Problem p aus NP auf p* polynomial reduzierbar ist. p p p* NP p NP-vollständige Probleme spielen bei der Klärung der Frage P=NP? eine zentrale Rolle. Wenn es gelingt, ein NP-vollständiges Problem p* mit einem Algorithmus mit polynomialer Komplexität zu lösen, dann ist die Aussage P=NP bewiesen. Denn, NP-Vollständigkeit bedeutet ja, dass jedes Problem p aus NP auf p* polynomial reduzierbar ist. Aus einem polynomialen Algorithmus für p* lässt sich dann ein polynomialer Algorithmus für jedes p aus NP erzeugen. Zur Klärung der Frage P=NP? konzentriert man sich also auf das Lösen NP-vollständiger Probleme. 72 P = NP? Es gibt inzwischen eine Vielzahl von Problemen, die als NP-vollständig nachgewiesen sind. Zu diesen Problemen gehören auch das Hamilton-Problem und das Problem des Handlungsreisenden. Alle Versuche, ein NP-vollständiges Problem mit einem polynomialen Algorithmus zu lösen, sind bisher fehlgeschlagen. Die NP-vollständigen Probleme erweisen sich also als "harte Nüsse" und gelten als schwer lösbare Probleme. Aufgrund der vielen fehlgeschlagenen Versuche, einen polynomialen Lösungsalgorithmus für ein NP-vollständiges Problem zu finden, vermutet man, dass die Frage P=NP? negativ zu beantworten ist. 73 Teil 5 Fallstudie - Das Rucksackproblem Lösen schwieriger Probleme mit Näherungsverfahren 74 Rucksackproblem Franziska ist die Gewinnerin bei der neuen Fernsehshow "Knapsack". Als Gewinn erhält sie einen Rucksack, in den sie weitere Gegenstände aus einer vorgegebenen Auswahl einpacken kann. Es gibt allerdings einen kleinen Haken. Der Rucksack wird nach dem Einpacken der Gegenstände gewogen. Überschreitet das Gesamtgewicht die maximales Traglast von 15 kg, gibt es keinen Gewinn. Aufgabe: Wie soll Franziska den Rucksack packen? 75 Rucksackproblem Verallgemeinerung: Gegeben sind n Gegenstände mit ihren Gewichten und Werten. Gegeben ist zusätzlich ein Grenzgewicht, das die maximale Traglast des Rucksacks beschreibt. Gesucht ist eine Kombination von Gegenenständen, so dass das Grenzgewicht nicht überschritten wird und der Gesamtwert der Gegenstände möglichst hoch ist. Formalisierung: Gegeben sind n Zahlenpaare (g0, w0), ..., (gn-1, wn-1) (die Gewicht und Wert von n Gegenständen beschreiben). Gegeben ist zusätzlich eine Grenzzahl G (die die maximale Traglast des Rucksacks beschreibt). Alle diese Zahlen sind beliebige (positive) Dezimalzahlen. Gesucht ist eine 0-1-Folge x0, ..., xn-1 (die die Auswahl der Gegenstände beschreibt: 0 - nicht einpacken; 1 - einpacken), so dass x0*g0 + ... + xn-1*gn-1 <= G gilt und x0*g0 + ... + xn-1*gn-1 maximal ist. Ein Lösungsalgorithmus 76 Idee: Eine - wenig elegante - Lösung des Rucksackproblems besteht darin, alle möglichen Kombinationen von Gegenständen zu betrachten und die zugehörigen Gesamtgewichte und Gesamtwerte zu berechnen und aus all den ermittelten Zahlenwerten die gesuchte Kombination zu bestimmen. ALGORITHMUS optimaleLoesung: Übergabe: erzeuge eine erste kombination, z.B. '00000000' maxKombination = kombination maxWert = Gesamtwert von kombination SOLANGE noch nicht alle Kombinationen durchlaufen sind: erzeuge eine neue kombination WENN der Gesamtwert von kombination > maxWert und das Gesamtgewicht von kombination <= grenzgewichtRucksack: maxKombination = kombination maxWert = Gesamtwert von kombination Rückgabe: (maxKombination, maxWert, Gesamtgewicht von maxKombination) 77 Implementierung d. Lösungsalgorithmus Aufgabe: Ergänze die fehlenden Teile in der Implementierung. # Rucksackproblem gegenstaende = [(3.5, 375), (2.5, 300), (2.0, 100), (3.0, 225), (1.0, 50), (1.75, 125), (0.75, 75), (3.0, 275), (2.5, 150), (2.25, 50)] grenzgewichtRucksack = 15.0 # Funktionen zur Erzeugung einer Lösung def gesamtGewicht(kombination): # ... def gesamtWert(kombination): # ... def erzeugeKombinationAusZahl(zahl): kombination = bin(zahl)[2:] while len(kombination) < len(gegenstaende): kombination = '0' + kombination return kombination def optimaleLoesung(): # ... return (maxKombination, maxWert, gesamtGewicht(maxKombination)) # Test print(optimaleLoesung()) Komplexitätsbetrachtungen 78 ALGORITHMUS optimaleLoesung: Übergabe: erzeuge eine erste kombination, z.B. '00000000' maxKombination = kombination maxWert = Gesamtwert von kombination SOLANGE noch nicht alle Kombinationen durchlaufen sind: erzeuge eine neue kombination WENN der Gesamtwert von kombination > maxWert und das Gesamtgewicht von kombination <= grenzgewichtRucksack: maxKombination = kombination maxWert = Gesamtwert von kombination Rückgabe: (maxKombination, maxWert, Gesamtgewicht von maxKombination) Komplexität des Algorithmus: Problemgröße: Anzahl n der Gegenstände festgelegt. Kostenmaß: Anzahl der zu untersuchenden Kombinationen. Kostenfunktion: K(n) = 2n Diese Kostenfunktion ist eine Exponentialfunktion. Der Algorithmus hat demnach eine exponentielle Komplexität und ist (für große n) praktisch nicht anwendbar. 79 Komplexitätsbetrachtungen Komplexität des Problems Ob das Rucksackproblem selbst eine exponentielle Komplexität hat, ist hierdurch noch nicht erwiesen. Es könnte Algorithmen geben, die das Problem viel schneller - z.B. mit polynomialer Komplexität lösen. Tatsächlich gibt es einen Algorithmus, der das Rucksackproblem viel effizienter als der oben gezeigte Algorithmus bearbeiten. Er beruht auf der Idee, dass man das Problem, einen Rucksack bei n Gegenständen optimal zu packen, auf das Problem, einen Rucksack bei n-1 Gegenständen optimal zu packen, reduzieren kann. Analysiert man die Komplexität dieses Algorithmen, so erweist sich die oben vorgenommene "naive" Problembeschreibung als inadäquat. Man muss die Größe und Verarbeitung der insgesamt 2n+1 zu verarbeitenden Ausgangszahlen differenzierter betrachten. Insbesondere muss man berücksichtigen, dass sinnvollerweise die Rucksackkapazität mit wachsender Anzahl von Gegenständen auch wachsen sollte. Es erweist sich dann, dass bei diesem Algorithmus trotz großer Verbesserungen dennoch eine exponentielle Komplexität vorliegt. Alle bis heute entwickelten Algorithmen zur Lösung des Rucksackproblems haben eine exponentielle Komplexität. Es ist kein Algorithmus bekannt, der das Rucksackproblem mit polynomialem Zeitaufwand löst. Vieles spricht dafür, dass das Rucksackproblem nicht zur Klasse der Probleme mit polynomialer Komplexität gehört. Eine endgültige Klärung dieser Komplexitätsfrage ist noch nicht gelungen. 80 Evolution als Lösungsstrategie "Evolution ist die Veränderung der vererbbaren Merkmale einer Population von Lebewesen von Generation zu Generation. Diese Merkmale sind in Form von Genen kodiert, die bei der Fortpflanzung kopiert und an den Nachwuchs weitergegeben werden. Durch Mutationen entstehen unterschiedliche Varianten (Allele) dieser Gene, die veränderte oder neue Merkmale verursachen können. Diese Varianten sowie Rekombinationen führen zu erblich bedingten Unterschieden (Genetische Variabilität) zwischen Individuen. Evolution findet statt, wenn sich die Häufigkeit dieser Allele in einer Population (die Allelfrequenz) ändert, diese Merkmale in einer Population also seltener oder häufiger werden. Dies geschieht entweder durch Natürliche Selektion (unterschiedliche Überlebens- und Reproduktionsrate aufgrund dieser Merkmale) oder zufällig durch Gendrift." Quelle: Wikipedia Wie optimiert die Natur die Anpassung von Lebewesen an Umweltbedingungen? Evolution spielt in den Erklärungsmodellen der Biologen eine wesentliche Rolle. In der Informatik werden Elemente der Evolution übernommen und zu einer Strategie zum näherungsweisen Lösen von Optimierungsproblemen ausgebaut. Wir werden diese Strategie im Folgenden zur Lösung des Rucksackproblems erläutern. 81 Evolution als Lösungsstrategie 1111000000 Individuum 1110000110 Gene 1100000111 1011111100 Lösungskandidaten sind die Individuen der Population. Eine Population zum Rucksackproblem besteht demnach aus einer Menge von Lösungskandidaten. In der Regel gibt man die Größe der Population (d.h. die Anzahl der Individuen) fest vor. Evolution als Lösungsstrategie 82 1111000000 10.75 Individuum 10.75 1110000110 Gene 10.25 1100000111 Fitness 0 1011111100 Die Fitness von Lösungskandidaten beim Rucksackproblem wird so modelliert, das sie die Qualität der Lösung beschreibt. Die Qualität eines Lösungskandidaten wird durch die Summe der Werte der Gegenstände gegeben. Wenn diese Summe größer als die Kapazitätsgrenze des Rucksacks ist, dann soll die Fitness 0 betragen. 83 Evolution als Lösungsstrategie Die Fortpflanzung von Individiuen soll durch Kreuzung von zwei Lösungskandidaten realisiert werden. Hierzu wird zunächst eine zufällige Stelle im Gencode bestimmt (z.B. die Stelle 4). Die Genabschnitte der beiden Individuen werden jetzt (über Kreuz) neu kombiniert. Der 1. Abschnitt des ersten Individuums wird mit dem 2. Abschnitt des zweiten Individuums zusammengesetzt, ebenso der 2. Abschnitt des ersten Individuums mit dem 1. Abschnitt des zweiten Individuums. 1110|000110 10.75 1111|000000 10.75 1110000000 8.25 X 1111000110 13.25 84 Evolution als Lösungsstrategie Selektion führt dazu, dass eine Bevorzugung bestimmter Individuen bei der Fortpflanzung durch die Berücksichtigung der Fitness der Individuen stattfinden. Wir realisieren eine Fortpflanzung mit Selektion wie folgt: Aus der Population sollen zwei Eltern-Individuen ermittelt werden. Für jedes Elternteil werden zwei Kandidaten per Zufall aus der Population ausgewählt. Jetzt kommt die Fitness ins Spiel: Der fitere der beiden Kandidaten kommt zum Zug, der andere hat das Nachsehen. Aus den beiden fiteren Kandidaten werden jetzt durch Kreuzung die beiden Nachkommen erzeugt. Kandidaten für den Vater Selektion 1110|000110 1110000000 10.75 8.25 X Kandidaten für die Mutter 1111000110 Selektion 1111|000000 10.75 13.25 85 Evolution als Lösungsstrategie Mutation ist eine Veränderung des Gencodes eines Individuums. Sie kann positive, negative oder auch gar keine Auswirkungen auf die Eigenschaften des Individuum haben. Sie kommt spontan oder auch durch äußere Einflüsse zustande. Ohne Mutation würden die Individuen durch Kreuzung immer nur ihre Anfangs- und Endabschnitte austauschen. Auf diese Weise könnte es vorkommen, dass bestimmte gute Näherungslösungen gar nicht erzeugt werden können. Mutation bringt hier Bewegung ins Spiel. Indem einzelne Gene per Zufall abgeändert werden, können völlig neue Lösungskandidaten erzeugt werden. Das kann sich positiv, aber natürlich auch negativ auf die Qualität der Lösung auswirken. 111000|0|110 10.75 Mutation Wir realisieren Mutation, indem ein Gen per Zufall ausgewählt und abgeändert wird. Diesen Vorgang führen wir nur ab und zu (mit einer bestimmten Mutationswahrscheinlichkeit) bei einem neu generierten Individuum aus. 111000|1|110 11.75 86 Genetischer Algorithmus ALGORITHMUS loesungMitGenetischemAlgorithmus erzeuge eine initiale Population SOLANGE das Abbruchkriterium nicht erfüllt ist: lege eine neue Population an SOLANGE die Populationsgröße nicht erreicht ist: wähle durch Selektion 2 Individuen aus erzeuge 2 neue Individuen durch Kreuzung der Individuen verändere die Gensequenz der neuen Individuen durch Mutation nimm die neuen Individuen in die neue Population auf ersetze die alte durch die neue Population bestimme das Individuum mit maximaler Fitness Beachte, dass der Algorithmus keinen Bezug auf das Rucksackproblem nimmt. Er kann also auch bei anderen Optimierungsproblemen benutzt werden, sofern geeignete Realisierungen der Gen-Codierung, Selektion, Kreuzung und Mutation gefunden werden. Implementierung 87 ALGORITHMUS loesungMitGenetischemAlgorithmus erzeuge eine initiale Population SOLANGE das Abbruchkriterium nicht erfüllt ist: lege eine neue Population an SOLANGE die Populationsgröße nicht erreicht ist: wähle durch Selektion 2 Individuen aus erzeuge 2 neue Individuen durch Kreuzung der Individuen verändere die Gensequenz der neuen Individuen durch Mutation nimm die neuen Individuen in die neue Population auf ersetze die alte durch die neue Population bestimme das Individuum mit maximaler Fitness Aufgabe: Implementiere den genetischen Algorithmus zum Rucksackproblem (siehe inf-schule 1.20.5.5).