Lösung Ü7A1 - TU Ilmenau

Werbung
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
Herunterladen