Karlsruher Institut für Technologie Institut für Theoretische Informatik Prof. Dr. Peter Sanders G.V. Batz, C. Schulz, J. Speck 1. Übungsblatt zu Algorithmen I im SS 2010 http://algo2.iti.kit.edu/AlgorithmenI.php {sanders,batz,christian.schulz,speck}@kit.edu Musterlösungen Aufgabe 1 (8 Punkte) Beweisen oder widerlegen Sie: √ a) n = O( n), n2 = O n3 , b) 2n = O(3n ), 2n+1 = O(2n ), c) log2 n = Θ(log10 n), 2loga (n) n3 = O n2 . (n + 1)! = O(n!). = Θ 2logb (n) für a 6= b d) f (n)+g(n) = O(min(f (n), g(n))), f (n)+g(n) = O(max(f (n), g(n))), f (n)g(n) = O(f (n)g(n)) e) f (n) = O(n) ⇒ 2f (n) = O(2n ), f (n) = O(n) ⇒ f (n)2 = O n2 f) Auf der Menge der asymptotisch positiven Funktionen ist f ∼Θ g gdw. f ∈ Θ(g) eine Äquivalenzrelation. Musterlösung: a) b) √ • Die Behauptuung n = O( n) gilt nicht. Beweis durch Widerspruch. √ √ Angenommen, es gilt n = O( n). Dann ∃c > 0 : ∃n0 ∈ N : ∀n ≥ n0 : n ≤ c · n. Also gilt √ √ nach teilen durch n: n ≤ c ∀n ≥ n0 • Die Behauptung n2 = O n3 gilt: wähle c = 1, n0 = 1 3 = O n2 gilt nicht. Beweis durch Widerspruch. Angenommen, es gilt • Die Behauptuung n n3 = O n2 . Dann ∃c > 0 : ∃n0 ∈ N : ∀n ≥ n0 : n3 ≤ c · n2 . Also gilt nach teilen durch n2 : n ≤ c ∀n ≥ n0 • Die Behauptung 2n = O(3n ) gilt: 2 ≤ 3 ⇒ 2n ≤ 3n ∀n ∈ N. Wähle also c = 1, n0 = 1. • Die Behauptung 2n+1 = O(2n ) gilt: 2n+1 = 2 · 2n , wähle also n0 = 1, c = 2 • Die Behauptung (n + 1)! = O(n!) gilt nicht. Beweis durch Widerspruch. Angenommen, es gilt (n + 1)! = O(n!). Dann ∃c > 0 : ∃n0 ∈ N : ∀n ≥ n0 : (n + 1)! ≤ c · n!. Also gilt nach teilen durch n!: n + 1 ≤ c ∀n ≥ n0 c) d) 10 n • Die Behauptung log2 n = Θ(log10 n) gilt, da log2 n = log log10 2 • Die Behauptung 2loga n = Θ 2logb n für a 6= b gilt nicht: wähle a = 2, b = 10. Dann ist 2loga n = n und 2logb n = 2log10 n = 2log2 n∗log10 2 = n0.30... . Analog zu a) oder b) zeigt man n 6∈ Θ nlog10 2 • Die Behauptung f (n) + g(n) = O(min(f (n), g(n))) gilt nicht. Gegenbeispiel: f (n) := n, √ g(n) := n. • Die Behauptung f (n)+g(n) = O(max(f (n), g(n))) gilt, da f (n)+g(n) ≤ 2 max(f (n), g(n)). 1 • Die Behauptung f (n) · g(n) = O(f (n) · g(n)) gilt: wähle c = 1, n0 = 1 . e) • Die Behauptung f (n) = O(n) ⇒ 2f (n) = O(2n ) gilt nicht. Gegenbeispiel: f (n) = 2n. Dann f (n) ∈ O(n), aber 4n 6∈ O(2n ) • Die Behauptung f (n) = O(n) ⇒ f (n)2 ∈ O n2 gilt. Beweis: f (n) = O(n) ⇒ ∃c > 0∃n0 > 0 : f (n) ≤ cn ∀n ≥ n0 . Quadrieren liefert: ∃c > 0∃n0 > 0 : f (n)2 ≤ c2 n2 ∀n ≥ n0 . Wähle also n˜0 = n0 und c̃ = c2 und erhalte mit diesen Konstanten die Behauptung. f) Behauptung gilt. Beweis: • Reflexivität: zu zeigen f ∼Θ f . Beweis: c1 f (n) ≤ f (n) ≤ c2 f (n) ist trivialerweise erfüllt für c1 = c2 = 1, und n0 beliebig. • Symmetrie: zu zeigen f ∼Θ g gdw. g ∼Θ f . Beweis: Es sei f ∼Θ g mit den Konstanten c1 , c2 , n0 . D.h. es gilt c1 g(n) ≤ f (n) ≤ c2 g(n). Umstellen liefert uns: c12 f (n) ≤ g(n) ≤ c11 f (n). Also ist g(n) = Θ(f (n)) mit den Konstanten 1 1 c2 , c1 , n0 . Also g ∼Θ f • Transitivität: zu zeigen f ∼Θ g und g ∼Θ h impliziert f ∼Θ h Beweis: Es sei f ∼Θ g und g ∼Θ h. Also gilt c1 g(n) ≤ f (n) ≤ c2 g(n) und d1 h(n) ≤ g(n) ≤ d2 h(n) für entsprechende Konstanten und n0 . Daraus folgt: c1 d1 h(n) ≤ f (n) ≤ c2 d2 h(n) ∀n ≥ n0 , was zu zeigen war. Aufgabe 2 (Zeitaufwands von Algorithmen, 2 + 1 + 1 + 2 Punkte) Gehen Sie davon aus, dass Sie mit einem Rechner arbeiten, der nur die Rechenoperationen + und −, sowie die Vergleiche <, ≤, =, >, ≥ in Zeit Θ(1) ausführen kann. Zuweisungen an Variablen oder an Einträge von Arrays sind ebenfalls möglich und benötigen ebenfalls Θ(1) Zeit; ebenso auch das Auslesen von Variablen und Array-Einträgen. Ansonsten unterstützt der Rechner nur die übliche Ablaufsteuerung (Schleifen, Fallunterscheidung, Unterprogrammaufruf) und keine weiteren Operationen. Der einzige verfügbare Basisdatentyp ist zudem die Menge N≥0 . Im folgenden müssen Sie nun einige Algorithmen für diesen Rechner angeben. Dabei spielt nur die Laufzeit eine Rolle. Ihr Algorithmus könnte z.B. immer den gleichen Wert als Ergebnis zurückgeben. Wichtig ist nur, dass er dabei das gewünschte Laufzeitverhalten aufweist. a) Geben Sie einen Algorithmus an, der als Eingabe eine Zahl n ∈ N≥0 hat und Θ(log n) Zeit benötigt. b) Geben Sie einen Algorithmus an, der als Eingabe eine Zahl n ∈ N≥0 hat und Θ(n4 log n) Zeit benötigt. c) Geben Sie einen Algorithmus an, der als Eingabe eine Zahl n ∈ N≥0 hat und Θ(2n ) Zeit benötigt. d) Geben Sie einen Algorithmus an, der als Eingabe eine Zahl n ∈ N≥0 hat und Θ(nn ) Zeit benötigt. Zeigen Sie jeweils, dass Ihr Algorithmus tatsächlich die geforderte Laufzeit hat. Hinweis: Soweit sinnvoll können Sie dabei die Erkenntnisse aus Aufgabe 1 verwenden. Musterlösung: a) Ein möglicher Algorithmus: 1: procedure A(n: N≥0 ) 2: a := 1 : N≥0 3: while a < n do a := a + a 4: return 2 In der While-Schleife hat a nach i Schritten den Wert 2i . Also gilt nach i ≥ log n Schritten a = 2i ≥ 2log n = n, was zum Abbruch der While-Schleife führt. Insgesamt werden also Θ(logn) Additionen, Zuweisungen und Vergleiche ausgeführt. Die Gesamtlaufzeit ist also in Θ(log n) b) Ein möglicher Algorithmus: 1: procedure B (n: N≥0 ) 2: for i1 := 1, . . . , n do 3: for i2 := 1, . . . , n do 4: for i3 := 1, . . . , n do 5: for i4 := 1, . . . , n do 6: a := 1 : N≥0 7: while a < n do a := a + a 8: end for 9: end for 10: end for 11: end for 12: return Die Schachtelung von vier For-Schleife bewirkt, dass die innere While-Schleife n4 -mal ausgeführt wird. Die For-Schleifen selbst verursachen dabei einen Zeitaufwand von Θ(n4 ) Die Zeitkomplexität der While-Schleife liegt, wie bereits in a) gezeigt, in Θ(log n). Insgesamt ergibt sich die gewünschte Laufzeit von Θ(n4 log n). c) Ein möglicher Algorithmus: 1: procedure C (n: N≥0 ) 2: if n = 0 then return 3: C(n − 1) 4: C(n − 1) 5: return Ein Aufruf von C verursacht einen gewissen eigenen Aufwand zuzuzüglich dem Aufwand der Kindaufrufe, also insgesamt den Zeitaufwand Tgesamt (0) = Teigen , Tgesamt (n) = Teigen + 2Tgesamt (n − 1) Der Eigenaufwand ist aber offenbar in Θ(1), da er nicht von n abhängt. D.h. es gibt eine Konstante c > 0 mit Teigen ≤ c. Dann gilt Tgesamt (n) ≤ c + 2Tgesamt (n − 1) ≤ c + 2(c + 2Tgesamt (n − 2)) = c + 2c + 4Tgesamt (n − 2)) ≤ · · · ≤ c + 2c + . . . 2n c n X =c 2i = c2n+1 − c i=0 Also gilt Tgesamt (n) = O 2n+1 = O(2n ). Analog zeigt man Tgesamt (n) = Ω(2n ), in diesem Fall mit einer Abschätzung nach unten. Allerdings bekommt man dabei 0 Tgesamt (n) ≥ c n X 2i = c0 2n+1 − c0 i=0 für ein geeignetes c0 > 0, was aber nicht ganz zur definition des Ω-Symbols passt. Allerdings gibt es eine Konstante c00 > 0 mit c0 2n+1 − c0 ≥ c00 2n+1 , was dieses Problem löst. Ein Aufruf von C(n) hat also einen Zeitaufwand von Θ(2n ). 3 d) Ein möglicher Algorithmus (rekursiver Algorithmus mit Wrapper“-Prozedur, wird benutzt weil ” die Rekursion eine hässliche“ Parameterliste erfordert): ” 1: procedure D(n: N≥0 ) 2: D rec(n, n) 3: return Hilfsprozedur, in der die Rekursion stattfindet: 1: procedure D rec(n, k: N≥0 ) 2: if n ≤ 1 then return 3: if k = 0 then return 4: for i := 1, . . . , n do D rec(n, k − 1) 5: return Abgesehen von dem Aufwand, der durch rekursive Aufrufe verursacht wird, entsteht in D rec höchstens konstanter Zeitaufwand. Sei n > 1, k > 0. Dann gilt für den Zeitaufwand T (n, k) eines Aufrufes D rec(n, k) also T (n, k) ≤ c + nT (n, k − 1) und T (n, 0) ≤ c für ein geeignetes c > 0. Damit gilt T (n, n) ≤ c + nT (n, n − 1) ≤ c + n c + nT (n, n − 2) = c + nc + n2 T (n, n − 2) ≤ · · · ≤ c + nc + n2 c + · · · + nn−1 c + nn c n X nn+1 − 1 nn+1 − 1 ≤ 2c ≤ 2cnn =c ni = c n−1 n i=0 für eine Konstante c > 0 und ausreichend große n. Analog zeigt man T (n, n) ≥ c0 nn+1 − 1 n−1 für eine Konstante c0 > 0 und schätzt ab T (n, n) ≥ c0 nn+1 − 1 nn+1 − 1 1 ≥ c0 = c0 nn − c0 ≥ c0 nn − c0 . n−1 n n Es gibt aber eine weitere Konstante c00 > 0 mit c0 nn − c0 > c00 n. Also ist insgesamt T (n, n) = Θ(nn ), was der Zeitaufwand ist, den ein Aufruf von D(n) verursacht. Aufgabe 3 (Finden einer fehlenden ganzen Zahl, 4 + 1 + 1 Punkte) Ein Feld A[0..n − 2] enthält alle ganzen Zahlen von 0 bis n − 1 außer einer. In der vorliegenden Problemstellung können wir auf die vollständige ganze Zahl nicht mit einer einzigen Operation zugreifen. Die Elemente von A sind binär dargestellt und die einzige Operation, die wir benutzen können, um auf sie zuzugreifen, ist ”rufe das j-te Bit von A[i] auf”. Dies benötigt konstante Zeit. Gehen Sie dabei davon aus, dass n = 2k für ein k ∈ N und die Codierung pro Zahl k Bits verwendet, also führende Nullen vorhanden sind. Weiter können Sie davon vereinfachend davon ausgehen, dass die Zahlen aufsteigend sortiert sind. a) Geben Sie einen rekursiven Algorithmus an, der die fehlende ganze Zahl immer noch in Zeit O(n) findet. b) Geben Sie für den Algorithmus eine Rekursion für die Laufzeitabschätzung an. c) Lösen Sie die Rekursion und beweisen Sie damit, dass ihr Algorithmus in Zeit O(n) läuft. 4 Musterlösung: a) Man startet mit dem k-ten Bit und zählt für alle Wörter wie oft dieses Bit 0 bzw. 1 ist. Da eine Zahl fehlt, muss entweder die 0 oder die 1 häufiger als k-tes Bit vorkommen. Die fehlende Zahl ist unter den Zahlen enthalten, dessen k-tes Bit weniger häufig war. Damit muss die fehlende Zahl mit diesem Bit beginnen. Nun macht man das ganze rekursiv auf den Zahlen, dessen k-tes Bit weniger häufig war (≤ n/2 Elemente) und zwar mit dem k −1-ten Bit und so weiter. So bekommt man Stück für Stück die Bits der fehlenden Zahl, während die Anzahl der zu untersuchenden Elemente sich immer halbiert. 1: procedure search(A : Array [0..n − 2] of N≥0 ) 2: left := 0; right := n − 2, bit = k 3: call findMissingNumber(A, left, right, bit) 4: return 1: procedure findMissingNumber (A : Array [0..n − 2] of N≥0 , left, right, bit : N≥0 ) 2: zeros := 0, ones := 0; 3: for i :=left to right do 4: if Bit bit of A[i] is 0 then zeros++ else ones++ 5: if zeros > ones then 6: print 1, left := b left+right c+1 2 7: else 8: print 0, right := b left+right c−1 2 9: if left ! = right then call findMissingNumber(A, left, right, bit−−) 10: return b) Aus der Beschreibung des Algorithmus ergibt sich sofort die folgende Abschätzung für die Laufzeit T (n) ≤ T ( n2 ) + n. c) Das Mastertheorem liefert uns mit d = 1, b = 2, c = 1 eine Gesamtlaufzeit von Θ(n). Alternativ kann man die Rekursion auch lösen: T (n) ≤ T ( n2 ) + n = T ( n4 ) + n2 + n = P∞folgendermaßen n n n 1 T ( 8 ) + 4 + 2 + n = · · · ≤ n i=1 2i = 2n, wobei die letzte Reihe gerade die geometrische Reihe ist. Aufgabe 4 (Karatsuba-Ofman Multiplikation, 4 Punkte) Multiplizieren Sie 1242 mit 3163 (das sind Dezimalzahlen!) nach dem Algorithmus von Karatsuba und Ofman. Hinweis: Führen Sie genau die einstelligen Operationen direkt aus. Musterlösung: Der Algorithmus: 1: Function recMult(a, b) 2: assert a und b haben n = 2k Ziffern 3: if n = 1 then return a · b 4: Schreibe a als a1 · B k + a0 5: Schreibe b als b1 · B k + b0 6: c11 := recMult(a1 , b1 ) 7: c00 := recMult(a0 , b0 ) 8: c0110 := recMult((a1 + a0 ), (b1 + b0 )) 9: e:= c11 · B 2k + (c0110 − c11 − c00 )B k + c00 10: return e Schema zum Aufschrieb eines Multiplikationsaufrufs: a·b a1 · b1 = recMult(a1 , b1 ) a0 · b0 = recMult(a0 , b0 ) e (a1 + a0 )·(b1 + b0 )=recMult((a1 + a0 ), (b1 + b0 )) 5 1242·3163 3928446 12·31 372 12·31= 372 42·63=2646 54·94=5076 s HHH HH HH HH HH HH s s Hs 1·3= 3 2·1= 2 3·4=12 42·63 4·6=24 2·3= 6 6·9=54 2646 54·94 5076 5· 9 = 45 4· 4 = 16 9·13=117 s s 9·13 117 Abbildung 1: Berechnungsbaum Berechnung: Aufbau als Rekursionsbaum. 6 0·1= 0 9·3=27 9·4=36