WS 13/14 Algorithmen und Programmierung Dr. Sascha Grau, Michael Pfeiffer Lösungsvorschlag Aufgabe 1 (Übungsblatt 7) Aufgabe 1 Der nebenstehende Algorithmus erhält eine natürliche Zahl n als Eingabe und liefert ein Feld von Wahrheitswerten zurück. boolean[] s(n) { boolean p := new boolean[n+1]; p[0] := false; p[1] := false}; for(int i := 2; i <= n; i++){ p[i] := true; } int i:=2; /* A */ while(i <= n){ if(p[i]){ int j:= i * i; while(j <= n){ p[j] := false; j:=j+i; } } i := i+1; } /* B */ return p; } a) Geben Sie in asymptotischer Notation eine möglichst genaue Schätzung der Laufzeitkomplexität des Algorithmus an. b) Welche Positionen von p wären am Ende mit true belegt, wenn das Programm mit Parameter n= 15 aufgerufen wird? Welche gemeinsame Eigenschaft besitzen diese Zahlen? c) Finden Sie für die durch /* A */ . . . /* B */ eingefasste Schleife eine sinnvolle (und nichttriviale) Schleifeninvariante, welche abhängig von Laufvariable i Aussagen über die in p mit true belegten Positionen erlaubt. Beweisen Sie die Gültigkeit ihrer Invariante. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Lösungsvorschlag Aufgabe 1 a) Aufwand der Initialisierung (Zeilen 2-71 ): O(n) Aufwand der verschachtelten Schleife (Zeilen 9-18): • Äußere Schleife: Faktor O(n) • Innere Schleife: Ausführung nur falls p[i] = true. Wir nehmen an, das wäre immer 2 = ni − i = O(n), denn die Schleife läuft von i2 (Zeile der Fall. Dann gilt: Aufwand n−i i 11) bis n (Zeile 9) in Schritten von i (Zeile 14). Der Aufwand im Schleifenrumpf ist O(1). ⇒ Aufwand des gesamten Algorithmus: O(n) + O(n2 ) = O(n2 ) Genauere Abschätzung (nicht verlangt): Aufwand der zweiten Schleife: 1 n n n n n + + + + + ··· 2 3 5 7 11 1 1 1 1 1 1 + + + + + ··· + 1 2 3 4 5 n | {z } ·c≤c·n· Harmonische Zahl Hn <ln n+1 Alle Zeilenangaben in diesem Dokument beziehen sich immer auf den Algorithmus. 1 = O(n · log n) Die innere Schleife wird nur ausgeführt, wenn i eine Primzahl ist (Zeile 10), also bei 2, 3, 5 . . . und steigt dann in Schritten von i, also führt sie für i = 2 n2 -mal den Schleifenrumpf aus, für i = 3 n3 -mal usw. Der Schleifenrumpf selbst hat jeweils konstanten Aufwand (c). Durch Hinzunahme zusätzlicher Summanden und Ausklammern von n kann der Term zur nten Partialsumme der Harmonischen Reihe2 (auch n-te Harmonische Zahl Hn ) ergänzt werden. Wegen „≤“ können problemlos Summanden hinzugenommen werden (die Gesamtsumme wird nur größer, nicht kleiner). Hn ist durch ln n + 1 nach oben beschränkt, und so ergibt sich O(n · log n) als obere Schranke für den Aufwand des Algorithmus. Ganz genau (natürlich auch nicht verlangt, ohne „Beweis“): Es gilt: n X n i=1 i prim i = O(n · log log n) Der Ansatz ist analog zur vorherigen Abschätzung. Interessanterweise ist die Schranke wesentlich kleiner als unsere erste Abschätzung (größerer Unterschied als beispielsweise zwischen Insertion-Sort und Quicksort). b) p[i] = true an den Positionen i ∈ {2, 3, 5, 7, 11, 13} (Primzahlen kleiner gleich 15). Bei dem Algorithmus handelt es sich um das Sieb des Eratosthenes 3 . c) Schleifeninvariante4 : n P = p[j] = true ⇐⇒ @k ∈ {2, . . . , i − 1} : k | j ∧ k 6= j o für alle j ∈ {2, . . . n} „Erläuterung“ der Invariante: Angenommen, wir befinden uns „mitten im Programmablauf“, i wurde durch die äußere Schleife schon etwas „hochgezählt“. Dann muss für jeden (!) Eintrag des Arrays (ausgedrückt über j) gelten: Der Wahrheitswert ist genau dann true, wenn der entsprechende Index keinen Teiler zwischen 2 und i − 1 hat, mit Ausnahme des Index selbst. Anders ausgedrückt: Wenn meine äußere Schleife bis zu Position i gelaufen ist, so habe ich für jeden „Eintrag“ des Arrays schon mit den Zahlen zwischen 2 und i − 1 ausprobiert, ob der Index des Eintrags eine Primzahl sein könnte, und falls nicht, den Eintrag auf false gesetzt. Noch ein Hinweis zu Schleifeninvarianten: Für Schleifeninvarianten gibt es leider kein Kochrezept. Man muss nach einem logischen Ausdruck suchen, der, wenn die Abbruchbedingung der Schleife nicht erfüllt ist, nach Abarbeitung des Schleifenkörpers weiterhin gilt und zudem nach Eintreten der Abbruchbedingung der Schleife auf die Nachbedingung und damit die korrekte Funktionsweise des Programms schließen lässt. Das ist in der Praxis leider mindestens so kompliziert, wie den letzten Satz zu verstehen und wird in dieser Aufgabe durch die zwei geschachtelten Schleifen erschwert. Gültigkeit der Schleifeninvariante vor dem Schleifeneintritt Vor der Schleife gilt ∀j ∈ {2, . . . , n} : p[j] = true ∧ i = 2 (Zeilen 4-6 bzw. 7). Somit gilt auch P, denn {2, . . . , i − 1} = {2, . . . , 1} = ∅, denn jede Nichtexistenzaussage über der leeren Menge ist wahr (Einfach ausgedrückt: In der leeren Menge findet sich auf jeden Fall kein k, das irgendein j teilt). 2 2 http://de.wikipedia.org/wiki/Harmonische_Zahl http://de.wikipedia.org/wiki/Sieb_des_Eratosthenes 4 „a | b“ := „a teilt b“ 3 2 Gültigkeit in der Schleife ({P ∧ B}β{P }) n P ∧ B = (∀j ∈ {2, . . . n} : p[j] = true ⇐⇒ @k ∈ {2, . . . , i − 1} : k | j ∧ k 6= j) ∧ i ≤ n Wir unterscheide zwei Fälle: • 1. Fall: p[i] = false Dann existiert ein k mit 2 ≤ k < i und k | i (folgt aus P). Für alle q ∈ {2, . . . , n} gilt dann i | q → k | q. Durch P gilt auch bereits k | q ∧ q 6= k → p[q] = false. Also gilt: n p’[j] = true ⇐⇒ @k ∈ {2, . . . , i0 − 1} : k | j ∧ k 6= j | {z ={2,...,i} o für alle j ∈ {2, . . . n} } Anders ausgedrückt: Wenn der i-te Eintrag des Arrays auf false steht, so gab es bereits eine kleinere Zahl, die i geteilt hat, sonst wäre der Eintrag nicht auf false gesetzt worden. Diese Zahl haben wir k genannt. Also werden alle Zahlen, die durch i geteilt werden, auch durch k geteilt. Also sind alle Einträge, die jetzt auf false geändert werden müssten, bereits früher auf false gesetzt worden. Das heißt, die Invariante gilt nicht nur für alle Zahlen k von 2 bis i − 1, sondern eben auch für i, und damit für alle Zahlen zwischen 2 und i. Da i = i0 −1, gilt die Schleifeninvariante für alle Zahlen zwischen 2 und i0 − 1. 2 • 2. Fall: p[i] = true Wir definieren die drei Mengen C, F und T , um diesen Fall zu beweisen. Diese Mengen definieren „wir“ selbst, um den Beweis durchführen zu können, sie fallen keinesfalls vom Himmel! C: Menge der Indizes der Arrayeinträge, die wir beim aktuellen Schleifendurchlauf auf false ändern. Das sind die, die durch i teilbar sind. n C := j ∈ {2, . . . , n} j ≥ i2 ∧ | {z } vgl. Zeile 11 j = k · i für ein k ∈ N | {z vgl. Zeilen 14 und 15 o } F : Menge der Indizes der Arrayeinträge, die vor dem aktuellen Schleifendurchlauf bereits false waren, also die einen Teiler zwischen 2 und i − 1 haben. n F := j ∈ {2, . . . , n}∃k ∈ {2, . . . , i − 1} : k | j ∧ k 6= j o T : Menge der Indizes der Arrayeinträge, die nach dem aktuellen Schleifendurchlauf true sind. Anders ausgedrückt, sind das alle Indizes, die weder vor dem Schleifendurchlauf schon false waren (Menge F ) noch im aktuellen Schleifendurchlauf als false markiert worden sind (Menge C)5 . T := {2, . . . , n} \ F \ C Einsetzen der Definitionen der Mengen F und C in die Gleichung von T . Da es sich um Differenzen (von Mengen) handelt, müssen wir die Bedingungen jeweils negieren. n o T = j ∈ {2, . . . , n}(@k < i : k | j ∧ k 6= j) ∧ (j < i2 ∨ i - j) 5 „\“ ist das Symbol für die Differenz der Mengen. 3 o Falls j < i2 ∧ i | j, so folgt j = i · r für ein r ∈ N mit r < i → j ∈ F . Anders ausgedrückt: Alle Indizes j, die kleiner sind als i2 und durch i teilbar sind, haben auch einen Teiler kleiner i, den wir r nennen (das folgt aus der einfachen Überlegung j < i2 ∧ j = i · r → i · r < i2 → r < i). Da j also den Teiler r hat, der kleiner als i ist, ist j bereits in der Menge F enthalten. Wenn man also diese j bereits mit der Menge F von {2, . . . , n} subtrahiert, so braucht man das nicht noch einmal zu tun. Die Bedingung j < i2 ist also gewissermaßen „überflüssig“. Man kann sich auch am Algorithmus leicht klar machen, dass es genügt, bei i2 „einzusteigen“. Damit gilt: n T = j ∈ {2, . . . , n}(@k < i : k | j ∧ k 6= j) ∧ i - j o Die beiden Bedingungen lassen sich nun Zusammenfassen: n T = j ∈ {2, . . . , n}@k ≤ i : k | j ∧ k 6= j o Das Ganze noch ein kleines bisschen umformulieren: n T = j ∈ {2, . . . , n}@k ∈ {2, . . . , i} : k | j ∧ k 6= j o Wenn man sich nun klar macht, das T die Menge der Indizes war, die nach dem Schleifendurchlauf true waren, und dass i = i0 − 1 ist, so folgt direkt die Schleifeninvariante. n p’[j] = true ⇐⇒ @k ∈ {2, . . . , i0 − 1} : k | j ∧ k 6= j o für alle j ∈ {2, . . . n} 2 Gültigkeit nach der Schleife Nun sind nur noch zwei einfache Umformungen zu machen: n o P ∧ ¬B = (∀j ∈ {2, . . . n} : p[j] = true ⇐⇒ @k ∈ {2, . . . , i − 1} : k | j ∧ k 6= j) ∧ i > n = n o = ∀j ∈ {2, . . . n} : p[j] = true ⇐⇒ @k ∈ {2, . . . , n} : k | j ∧ k 6= j = n = ∀j ∈ {2, . . . n} : p[j] = true ⇐⇒ j prim o 2 Und damit ist gezeigt, dass der angegebene Algorithmus auch wirklich Primzahlen generiert und zudem dieser ziemlich lange Beweis abgeschlossen :-) 4