Zettel 03
Aufgabe 2
2.1
Folgendes Program loesst das problem (auch im zip als potenz.cc
enthalten)
#include "fcpp.hh"
int quadrat (int x)
{
return x*x;
}
int potenz(int x, int n)
{
return cond(n == 0,
1,
(n % 2 == 0,
cond(potenz(x, n / 2)),
quadrat* potenz(x, n - 1)));
x }
int main(int argc, char** argv)
{
return print(potenz(
(argc, argv, 1),
readarg_int(argc, argv, 2)));
readarg_int}
Argumente muessen in der Konsole eingegeben werden, z.B.:
$ ./potenz 4 3
81
Aufgabe 3
3.1
Folgendes Program realisiert die rekursive Berechnung der Binomialkoeffizienten (auch im Zip als binomial.cc
enthalten):
#include "fcpp.hh"
int binomial(int n, int k)
{
return cond(k == 0 || k == n,
1,
(n - 1, k - 1) + binomial(n - 1, k));
binomial}
int main(int argc, char** argv)
{
return print(binomial(
(argc, argv, 1),
readarg_int(argc, argv, 2)));
readarg_int}
Wir haben das Program auf verschieden Werte \(n\) und \(k\) getestet (zusammen mit der time
Funktion fuer Messung der Laufzeit) und die Ergebnisse in der folgenden Tabelle zusammengefasst:
Specs des Systems auf der wir getestet haben:
- PC: ThinkCentre M700
- Processors: 4 × Intel® Core™ i5-6400 CPU @ 2.70GHz
- Memory: 15,5 GiB of RAM
Befehl | Ergebniss | Laufzeit (real) |
time ./binomial 1 0 |
1 | real 0m0,004s |
time ./binomial 1 1 |
1 | real 0m0,002s |
time ./binomial 3 2 |
3 | real 0m0,002s |
time ./binomial 10 4 |
210 | real 0m0,004s |
time ./binomial 20 13 |
77520 | real 0m0,007s |
time ./binomial 32 15 |
565722720 | real 0m3,519s |
time ./binomial 36 13 |
-1984177696 | real 0m13,791s |
Wie aus der Tabelle zu sehen ist, ist die Laufzaut \(>10s\) fuer die Berechnung time ./binomial 36 13
jedoch mit dem falschen Ergebniss -1984177696 statt das richtige \(\binom{36}{13} = 2310789600\). Wir erklaeren dieses Phaenomen in der folgenden Teilaufgabe.
3.2
Fuer \(n = 34\), \(k = 18\) liefert das Program
$ ./binomial 34 18
-2091005866
im Gegensatz zu dem erwarteten mathematischen Ergebniss \(\binom{34}{18} = 2203961430\).
Dieser ‘Fehler’ liegt an der 32 bit 2er Komplement Darstellung des Datentyps int
auf dem Computer. Darunter koennen eine endliche Anzahl von int
Zahlen dargestellt werden, die im Bereich \([-2^{31}, 2^{31} - 1] = [-2147483648, 2147483648]\) liegen. Das mathematische Ergebnis liegt also ausserhalb des darstellbaren Bereiches mit \(2203961430 > 2147483648\).
Da, unter der 2er Komplement Darstellung der MSB (Most Significant Bit) den Bereich der Negativen Zahlen representiert kann die Addition zweier groessen Zahlen, deren Ergebniss ausserhalb des darstellbaren Bereiches liegt wieder bei dem negativen Bereich landen, aehnlich wie Modulorechnung. Das wird als overflow bezeichnet.
3.3
Sei \(A_{n, n} = \alpha = A_{n, 0}\) und \(\beta := \text{Die konstanten Kosten der Addition}\). Dann gilt:
\[\begin{align*} &A_{n, k} = A_{n-1, k-1} + A_{n-1, k} + \beta \tag{Rekursive Beziehung des Rechenaufwands} \\ &A_{n, 0} = \alpha = A_{n, n} \end{align*}\]
Da die rekursive Beziehungs des Rechenaufwands eine aehnliche Beziehung wie die Binomialkoeffizienten erfuellen koennen diese in einem paskalschen Dreieck wie folgt eingetragen werden (Siehe Figure 1)
Betrachten wir nur die Koeffizienten von \(\alpha\) und \(\beta\) seperat so erhalten wir folgende paskalschen Dreiecke (Siehe Figure 2)
Von diesen Figuren ist es leicht zu sehen, dass \(\alpha_{n, k} = B_{n, k}\) und \(\beta_{n, k} = B_{n, k} - 1\), wobei \(\alpha_{n, k}, \beta_{n, k}\) die Koeffizienten von \(\alpha\) bzw. \(\beta\) sind bzgl der Rechenaufwands \(A_{n, k}\).
Somit erhalten wir:
\[A_{n, k} = B_{n, k}\alpha + (B_{n,k} - 1)\beta\]
Formaler Beweis:
Fuehre die Variablentransformation \(\tilde{A}_{n, k} := \frac{A_{n, k} + \beta}{\alpha + \beta}\). Dann erhalten wir die folgende rekursive Gleichung:
\[\begin{align*} &\tilde{A}_{n, k} = \tilde{A}_{n-1, k-1} + \tilde{A}_{n-1, k} \\ &\tilde{A}_{n, 0} = 1 = \tilde{A}_{n, n} \end{align*}\]
Das ist genau die Definition des Binomialkoeffizientes \(B_{n, k}\). Somit gilt:
\[\begin{align*} \tilde{A}_{n, k} &= B_{n, k} \\ \Rightarrow A_{n, k} &= (\alpha + \beta)\tilde{A}_{n, k} - \beta \\ &= (\alpha + \beta)B_{n, k} - \beta \quad \blacksquare \end{align*}\]
3.4
Folgend geben wir die iterative Implementierung der Binomialkoeffizienten anhand einer tail-rekursiven Implemenierung der Fakultaet-funktion an (auch im Zip als binomial_fast.cc
erhalten):
#include "fcpp.hh"
//iterative Implementierung der Fakultaetsfunktion durch Tail-recursion
// mit fakit und fak
int fakit(int res, int n)
{
return cond(
> 1,
n (res * n, n - 1),
fakit
res);
}
int fak(int n)
{
return fakit(1, n);
}
int binomial_fast(int n, int k)
{
return fak(n) / (fak(k) * fak(n - k));
}
int main(int argc, char** argv)
{
return print(binomial_fast(
(argc, argv, 1),
readarg_int(argc, argv, 2)));
readarg_int}
Da, die Implementierung von Fakultaet fak
\(\mathcal{O}(n)\) ist , binomial_fast
nur drei mal fak
aufruft und nur zwei weitere Basisoperationen verwendet - eine Multiplikation und eine Division - hat diese Implementierung eine Laufzeitkomplexitaet von \(\mathcal{O}(n)\).
Wir haben binomial
und binomial_fast
auf verschiedene Eingaben hinsichtlich Ausgaben und Geschwindigkeit getestet und die Ergebnisse in der folgenden Tabelle zusammengefasst (Table 1):
Befehl | Ergebniss | Laufzeit (real) |
|
|
|
|
|
real 0m0,005s real 0m0,005s |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Da binomial_fast
eine lineare Laufzeitkomplezitaet hat bleibt die Laufzeit c. .004s fuer alle Eingaben.
Im Gegensatz waechst die Laufzeit von binomial
proportional zu dem mathematischen Wert \(B_{n, k}\) fuer Eingaben \(n\) und \(k\). Somit ist die Laufzeit sehr hoch fuer grosse Werte von \(B_{n, k}\).
Wie aus der Tabelle zu sehen ist, gibt es bereits ab \(n = 27, k = 15\) einen Unterschied und fuer hoehere Werte wie \(n = 34, k = 19\) oder \(n = 36, k = 13\) unterscheiden sich die Laufzeiten um mehr als 10s. Fuer \(n = 39, k = 23\) ist die Laufzeit von binomial
sogar fast 4 minuten, wobei binomial_fast
immer noch bei ~0.004s bleibt.
3.5
Fuer diese Teilaufgabe verwenden wir wieder die obige Tabelle Table 1. Fuer die ersten zwei Zeilen, also fuer \(n = 10, k = 4\) und \(n = 11, k = 6\) liefern beide Programme die richtige mathematische Ergebnisse \(\binom{10}{4} = 210\), bzw. \(\binom{11}{6} = 462\). Jedoch liefert binomial_fast
bereits ab der dritten Zeile, also fuer \(n = 13, k = 10\) ein falsches Ergebnis, wobei binomial
bis der 7en Zeile, d.h fuer \(n = 34, k = 19\) richtige Ergebnisse liefert.
Wie in der Teilaufgabe 3.3 erklaert wurde, gibt es das sogenannte Phaenomen “overflow” fuer den Datentyp int
. Die Fakultaetsfunktion waechst sehr schnell und hat bereits fuer die Eingabe 13 den Wert \(13! = 6 227 020 800 > 2 147 483 647 = 2^{31} - 1\). Somit fuehrt bereits fak(13)
zu einem overflow. Da, die Implementierung vonbinomial_fast
die Funktion fak
benutzt, liefert dieses Program fuer Eingaben \(n \geq 13\) ein falsches Ergebnis.