Stichpunktezettel fürs Tutorium Moritz und Dorian 13. Januar 2010 1 Komplexitätsanalyse 1.1 Einführendes Beispiel Gegeben ist der folgende Algorithmus, der eine Liste mit Zahlen sortiert: void sort(list_t* list) { 3 int count = length(list); 1 2 4 while (--count > 0) { for (int i = 0; i < count; ++i) { if (at(list, i) > at(list, i+1)) swap(list, i, i+1); } } 5 6 7 8 9 10 11 12 13 } Ist der Algorithmus besser oder schlechter als der folgende Algorithmus, der ebenfalls diese Aufgabe erfüllt? 1 void _sort(list_t* list, int left, int right) 2 { 3 while (left < right) 4 { 5 int pivot = left, 6 first = left; 1 7 do { 8 9 if (at(list, first) < at(list, right)) { swap(list, pivot, first); ++pivot; } 10 11 12 13 14 15 ++first; } while (first < right); 16 17 18 19 swap(pivot, right); 20 21 _sort(list, left, pivot-1); left = pivot+1; 22 23 } 24 25 } 26 void sort(list_t* list) 28 { 29 _sort(list, 0, length(list) - 1); 30 } 27 Die Theorie der Komplexität von Algorithmen ist ein Ansatz, Algorithmen untereinander vergleichbar zu machen. Eine weitere Anwendung findet sie bei der Beantwortung der Frage, wie schwer lösbar Probleme in Wirklichkeit sind. Die Komplexitätsklasse bietet dort einen Anhaltspunkt für den Vergleich unterschiedlicher Problemarten. Wir wollen uns in diesem Tutorium mit unterschiedlichen Problemen der Informatik beschäftigen, ihre Komplexität einschätzen, und lernen, die Güte einfacher Algorithmen zu analysieren. 2 1.2 Landau-Notation Die Landau-Symbole werden in der Informatik verwendet, um das asymptotische Verhalten von Algorithmen zu beschreiben. Sie abstrahieren von einer konkreten Rechengeschwindigkeit oder einem konkreten Instruktionsset eines Prozessors bestimmter Bauart und betrachten lediglich das Wachstum der Anzahl der Schritte, die ein Algorithmus bis zu seiner Terminierung benötigt, in Abhängigkeit von der Eingabe. Andere Anwendungen beziehen sich auf den benötigten Speicherplatz oder weitere, beschränkte Ressourcen eines Computersystems, doch diese sollen heute nicht betrachtet werden. Die folgende Tabelle listet die einzelnen Symbole, ihre Definitionen sowie die intuitiven Bedeutungen auf: Symbol f ∈ O(g) f ∈ o(g) f ∈ Ω(g) f ∈ ω(g) f ∈ Θ(g) Definition ∃c > 0 ∃x0 ∀x > x0 : |f (x)| ≤ c · |g(x)| f wächst nicht wesentlich schneller als g ∀c > 0 ∃x0 ∀x > x0 : |f (x)| < c · |g(x)| f wächst langsamer als g ∃c > 0 ∃x0 ∀x > x0 : |f (x)| ≥ c · |g(x)| f wächst nicht wesentlich langsamer als g ∀c > 0 ∃x0 ∀x > x0 : |f (x)| > c · |g(x)| f wächst schneller als g ∃c0 > 0 ∃c1 > 0 ∃x0 ∀x > x0 : c0 · |g(x)| ≤ |f (x)| ≤ c1 · |g(x)| f wächst genauso schnell wie g Konstante Faktoren oder Terme geringeren Wachstums werden in der Landau-Notation ignoriert. Wie leicht zu erkennen ist, beschreibt jedes Symbol eine Menge von Funktionen, trotzdem wird gewöhnlicherweise das Gleichheitszeichen für die Zugehörigkeit zur entsprechenden Menge verwendet (also beispielsweise f (n) = O(n) anstatt f (n) ∈ O(n)). 1.3 Bubblesort Der erste Sortieralgorithmus weiter oben ist sogenanntes Bubblesort, dessen Komplexität bestimmt werden soll, wobei die Annahme gilt, dass at() und swap() konstante Laufzeit haben und length() höchstens linear viel Zeit benötigt. Wir berechnen die Komplexität in Abhängigkeit von der Länge n der übergebenen Liste und betrachten dabei nur die Anzahl der Vergleiche, die der Algorithmus benötigt, weil diese Operation im Zweifelsfall teuer ist. Die Schleife zwischen Zeile 5 und 12 wird genau n mal durchlaufen. Innerhalb dieser Schleife befindet sich eine weitere, die bei jeder Iteration ein Mal weniger durchlaufen wird. Bei der ersten Iteration werden noch P n Vergleiche durchgeführt, danach n − 1, n − 2, usw. Insgesamt brauchen wir also ni=1 i viele Vergleiche. Nach der Gauß’schen Summenformel gilt: 3 n X i= i=1 n · (n + 1) n2 + n = 2 2 Da konstante Faktoren und Terme geringerer Ordnung in der Landau-Notation verschwinden, folgt n2 + n ∈ O(n2 ) 2 Das bedeutet, der Algorithmus hat quadratische Komplexität. Bemerkung. Die Laufzeit von length() musste nicht betrachtet werden, da sie außerhalb der Schleifen steht und höchstens O(n) viele Schritte braucht. Die Analyse der Laufzeit des zweiten Algorithmus (Quicksort) gestaltet sich etwas schwieriger, weshalb wir diese Analyse euren Dozenten überlassen wollen. Die erwartete Laufzeit liegt bei diesem Algorithmus bei O(n · log n), allerdings kann sie auch O(n2 ) betragen (im worst case). Damit liegt sie nie über der Laufzeit von Bubblesort, weshalb dieser Algorithmus besser geeignet ist eine Liste von Zahlen zu sortieren als der erste. 1.4 Inkrementieren einer Zahl um eins Das nächste Problem, das wir analysieren wollen, ist das Problem eine binär kodierte Zahl um eins zu erhöhen. Dafür betrachten wir die folgende Turingmaschine: M := (Q, Σ, Γ, δ, s, , {t}) mit Q := {s, m, t} Σ := {0, 1} Γ := {0, 1, } δ := Zustand s s s m m Symbol 0 1 0 → → → → → neues Symbol 1 0 1 0 neuer Zustand m s m m t Bewegungsrichtung R L R R L Die Turingmaschine erwartet als Eingabe eine binär kodierte Zahl, die von links nach rechts auf das Band geschrieben wurde, wobei der Schreib-/Lesekopf am rechten Ende der Eingabe startet. Sie fährt das Band von rechts nach links ab und ändert dabei alle Einsen zu Nullen, bis sie die erste null oder sieht. Das entsprechende Feld wird auf eins gesetzt und 4 der Schreib-/Lesekopf wieder in die Ausgangsposition gebracht. Danach kann sie erneut gestartet werden. Im ungünstigsten Fall stehen auf dem Band n Einsen und die Turingmaschine muss 2 · n Felder abfahren, um die Zahl zu erhöhen. In der Landau-Notation bedeutet das: der worstcase hat eine Komplexität von O(n). 1.5 Amortisierte Laufzeitanalyse Der folgende Abschnitt kommt praktisch direkt aus dem Skript zur Vorlesung „Graphen und Algorithmen 1“ von Prof. Hougardy. Ich hätte es nicht besser formulieren können, daher habe ich es erst gar nicht versucht. „Bei der amortisierten Laufzeitanalyse geht es darum, die Laufzeit einer in einem Algorithmus auftretenden Folge von Operationen zu berechnen. Gegeben sei ein Algorithmus A, der ausgehend von einer Ausgangskonfiguration D0 eine Folge von m Operationen op1 , op2 , . . . , opm durchführt, wobei Operation i die Konfiguration Di−1 nach Di überführt. Die Laufzeit für die Operation opi betrage ti . Gesucht ist nun die Gesamtlaufzeit der m Operationen, d. h. der Wert der Summe m X ti . i=1 Falls es eine nicht von m abhängende Funktion a gibt, so dass für jede im Algorithmus mögliche Folge von m Operationen gilt, dass m X ti ≤ m · a, i=1 so sagt man, dass die amortisierte Laufzeit einer jeden Operation höchstens a ist. Die amortisierte Laufzeit einer Operation ist also die mittlere Laufzeit einer einzelnen Operation in einer Folge von Operationen. Offensichtlich gilt m X ti ≤ m · max ti , 1≤i≤m i=1 d. h. die maximale Laufzeit einer einzelnen Operation ist sicherlich eine obere Schranke für die amortisierte Laufzeit. In vielen Fällen lassen sich aber wesentlich bessere Schranken angeben.“ Wir betrachten nochmal das Beispielproblem eine Zahl um eins zu erhöhen. 5 Zahl geänderte Bits 0000 0001 0010 0011 0100 0101 0110 0111 1000 ... 1 2 1 3 1 2 1 4 Die naive Analyse von oben liefert eine Gesamtlaufzeit von O(m · log m), da die größte auftretende Zahl nach m Inkrementierungsschritten log m Bits hat. Damit ist also log m eine obere Schranke für die amortisierte Laufzeit eines Inkrementierungsschrittes. Diese Grenze lässt sich allerdings deutlich verbessern. Wenn die Bits unabhängig voneinander betrachtet werden, sieht man, dass das erste Bit jedes mal geändert wird, das zweite jedes zweite mal, das dritte jedes vierte mal und allgemein, das n’te jedes 2n ’te mal. Bei m Anwendungen des Algorithmus wird das erste Bit also m mal invertiert, das zweite m/2 mal, das dritte m/4 mal und das n’te m/2n mal. Das ergibt für die gesamte Zahl veränderter Bits blog mc j X i=0 ∞ X 1 mk < m · = 2m 2i 2i i=0 Die amortisierte Laufzeitanalyse zeigt, dass für m Inkrementierungsschritte 2m Operationen benötigt werden. Die amortisierten Kosten pro Schritt sind damit O(1). 2 Komplexitätsquiz Welche der folgenden Probleme sind (a) in der Klasse P (effizient lösbar) (b) in der Klasse NP (schwer lösbar) (c) unentscheidbar (gar nicht lösbar) 1. Eine Zahl um eins erhöhen. 2. Den kürzesten Weg in einem Netzwerk finden. 6 3. Sortieren einer Liste von Zahlen. 4. Testen, ob eine Zahl prim ist. 5. Testen, ob eine ALU richtig rechnet. (vgl. Pentium-FDIV-Bug) 6. Entscheiden, ob sich ein Graph überschneidungsfrei in die Ebene zeichnen lässt. 7. Entscheiden, ob sich ein Graph überschneidungsfrei auf die Oberfläche eines beliebig komplexen Körpers zeichnen lässt. 8. Automatisierter Beweis, dass ein gegebenes Programm erwünschte Eigenschaften aufweist. 9. Berechnen einer Zahl aus ihren Primfaktoren. 10. Berechnen der Primfaktoren einer Zahl. 3 Doppelt verkettete Liste mit nur einem Zeiger Normalerweise würde eine doppelt verkettete Liste in jedem Element previous und next Zeiger enthalten und die aktuelle Position würde durch einen einzigen Zeiger markiert werden. Wenn stattdessen zwei Zeiger für die aktuelle Position benutzt werden, einer zum ausgewählten Element, der zweite zum Vorgänger, ist es möglich nur einen einzigen Zeiger pro Knoten zu verwenden. Als Wert dieses Zeigers wird die Adresse des Vorgängers xor dem Wert des Zeigers zum nächsten Element verwendet. Um in der Liste vorwärts zu laufen, wird der Vorgängerzeiger der aktuellen Position mit dem gespeicherten Wert des aktuellen Knotens via xor verknüpft und ergibt dann den Zeiger zum nächsten Element. In die umgekehrte Richtung kommt man, wenn der Zeiger auf die aktuelle Position mit dem gespeicherten Wert des Zeigers der Vorgängerposition mittels xor verknüpft wird. Lösungen des Komplexitätsquiz 1. Ist in P wie oben gezeigt. 2. Ist in P. Der beste Algorithmus benötigt O(n·log n) viele Schritte (was der Komplexität von Sortieren entspricht). 3. Ist in P. Mindestens O(n · log n) viele Schritte werden gebraucht. 4. Ist in P. Der Agrawal-Kayal-Saxena-Primzahlentest hat die Komplexität O((log n)6+ ), besser ist allerdings der Miller-Rabin-Test (aber Monte-Carlo). 7 5. Ist in NP. Gatter in Chips lassen sich durch aussagenlogische Formeln darstellen; ein unerwünschter Zustand ist dann ein Erfüllbarkeitsproblem. 6. Ist in P. Der Satz von Kuratowski zeigt ein einfaches Unterscheidungskriterium. 7. Ist in P wenn der Körper nicht Teil der Eingabe ist, nach einer Serie von Beweisen von Robertson und Seymour. 8. Ist unentscheidbar nach dem Satz von Rice. 9. Ist in P. Einfache Multiplikation der Primfaktoren genügt. 10. Ist in NP. Der Algorithmus von Shor braucht O((log n)3 ) viele Schritte – auf einem Quantenrechner. 8