Fakultäten, Binomialkoeffizienten und Herr Legendre

Die Fakultät ist in der Mathematik eine Funktion, die einer natürlichen Zahl n das Produkt aller natürlichen Zahlen kleiner und gleich dieser Zahl zuordnet: n! = 1 * 2 * 3 * … * n. In der Kombinatorik spielt die Fakultät ebenfalls eine große Rolle, weil n! die Anzahl der Möglichkeiten ist, unterschiedliche Gegenstände der Reihe nach anzuordnen. Bei diesen Überlegungen kommen auch Binomialkoeffizienten ins Spiel. Sie geben an, auf wie viele verschiedene Arten man k Objekte aus einer Menge von n verschiedenen Objekten auswählen kann.

Sowohl Fakultäten als auch Binomialkoeffizienten weisen eine unangenehme Eigenschaft auf: Ihre Werte können sehr schnell sehr groß werden – und sind damit in Variablen des Typs long oder size_t nicht mehr darstellbar. Ein Satz von Legendre verschafft hier Abhilfe: Er befasst sich mit der Frage, wie oft ein Primfaktor in der Primfaktorzerlegung von n! für ein n ∈ N vorkommt – um damit eine alternative Darstellung für Fakultäten und Binomialkoeffizienten zu ermöglichen, die das Problem des Überlaufs umgehen kann.

Wir betrachten in dieser Fallstudie die Berechnung von Fakultäten und Binomialkoeffizienten sowohl in ihrer klassischen Definition als auch mit Hilfe des Satzes von Legendre als Produkt von Primfaktorzerlegungen.

Lernziele

  • Klasse std::unordered_map
  • Structured Binding
  • Bereichsbasierte for-Wiederholungsschleife (Range-based for-Loop)

Einführung

Notiert wird die Fakultätsfunktion durch ein dem Argument nachgestelltes Ausrufezeichen, einer Schreibweise, die dem elsässischen Mathematiker Christian Kramp (1760 – 1826) zugeordnet wird. Betrachten wir zur Veranschaulichung dieser Funktion beispielsweise die drei Farben Rot, Grün und Blau. Wie viele Möglichkeiten gibt es, sie anzuordnen? Für die erste Position kommen alle drei Farben in Betracht. Ist die erste Farbe gesetzt, können nur noch zwei Farben um die zweite Position konkurrieren. Für die Belegung der Positionen 1 und 2 ergeben sich bei drei Farben also 3 * 2 Möglichkeiten. Für die letzte Position bleibt nur noch eine Farbe übrig, letztlich gilt für die Anzahl der Anordnungsmöglichkeiten somit 3 * 2 * 1 = 3! = 6.

Auch für Binomialkoeffizienten wollen wir ein Beispiel geben. Wie groß ist also die Wahrscheinlichkeit, 6 Richtige aus 49 Möglichkeiten zu raten? Also in anderen Worten: Wie groß ist die Wahrscheinlichkeit eines Lottogewinns? Zur Antwort dieser Frage benötigen wir die Anzahl der verschiedenen Möglichkeiten, 6 Zahlen aus 49 möglichen zu wählen. Dazu müssen wir den Binomialkoeffizienten “49 über 6” berechnen, das Ergebnis ist 13.983.816.

Klasse Factorial

Die Fakultätsfunktion lässt sich sehr einfach iterativ wie auch rekursiv programmieren. Erstellen Sie eine Klasse Factorial mit zwei Methoden factorialRecursive und factorialIterative. Die folgenden Code-Fragmente sollten mit Hilfe Ihrer Implementierung ausführbar sein:

Factorial f(5);
f.factorialIterative();
std::cout << f.get() << "! = " << f.value() << std::endl;

f.set(10);
f.factorialRecursive();
std::cout << f.get() << "! = " << f.value() << std::endl;

Ausgabe:

5! = 120
10! = 3628800

Kontrollfrage: Ab welchem größeren n erhalten Sie mit Ihrem Testprogramm – offensichtlich – falsche Resultate? Welche Erklärung haben Sie dafür?

Nachschlagewerke

Zur Vorbereitung der nachfolgenden Teilaufgaben betrachten wir nun Nachschlagewerke in der Programmierung. Darunter verstehen wir eine Datenstruktur, die zu einer bestimmten Information (auch Schlüssel genannt) einen Wert verwaltet. Ein Telefonbuch ist ein solches Beispiel: Als Schlüssel fungiert ein Name, die Telefonnummer ist ihm als Wert zugeordnet. Fremdsprachenlexika sind ein weiteres Beispiel.

Wozu benötigen wir in dieser Fallstudie Nachschlagewerke? Im Folgenden zerlegen wir bestimmte Zahlen in ihre Primfaktoren. Als Schlüssel fungiert die Primzahl, der zugeordnete Wert ist der Exponent, der angibt, wie oft die Primzahl in der zu zerlegenden Zahl vorkommt. Auf die Primfaktorenzerlegung gehen wir später ein, zunächst halten wir fest, dass wir eine Klasse PrimeDictionary entwickeln, deren Schlüssel und Werte jeweils vom Typ size_t sind. Weitere Hinweise entnehmen Sie bitte Tabelle 1:

Element Beschreibung
Konstruktor PrimeDictionary();
Standardkonstruktor für ein PrimeDictionary-Objekt.
Methode get size_t get(size_t key) const;
Liefert Wert zum Schlüssel key zurück.
Methode set void set(size_t key, size_t value);
Fügt einen neuen Eintrag (Schlüssel key mit Wert value) in das PrimeDictionary-Objekt ein.
Ausgabe std::ostream& operator<< (std::ostream&, const PrimeDictionary&);
Es ist der Standardausgabeoperator zu implementieren. Eine Ausgabe könnte so aussehen:
(2,4)(3,2)(5,1)(7,1)

Tabelle 1: Element der Klasse PrimeDictionary.

Hinweis: Zur Realisierung der Klasse PrimeDictionary sollten Sie einen geeigneten Container aus der STL einsetzen. Welche Klasse bietet sich hier an?

Folgendes Code-Fragment sollte mit Hilfe Ihrer Realisierung ausführbar sein:

PrimeDictionary dict{};
dict.set(2, 4);  // 2 hoch 4
dict.set(3, 2);  // 3 hoch 2
dict.set(5, 0);  // 5 hoch 0
dict.set(7, 1);  // 7 hoch 1
std::cout << dict << std::endl;

size_t key = 3;
size_t value = dict.get(key);
std::cout << "Value at " << key << ": " << value << std::endl;
dict.set(key, 22);
value = dict.get(key);
std::cout << "Value at " << key << ": " << value << std::endl;

Ausgabe:

(2,4)(3,2)(5,0)(7,1)
Value at 3: 2
Value at 3: 22

Der Satz von Legendre

Die bislang vorgestellten Möglichkeiten zur Berechung der Fakultätsfunktion lösen nicht das Problem, dass die Werte sehr schnell wachsen und in Variablen des Typs size_t ab einer bestimmten Größe nicht mehr darstellbar sind. Am Beispiel von

100! =
93.326.215.443.944.152.681.699.238.856.266.700.490.715.968.264.381.621.
468.592.963.895.217.599.993.229.915.608.941.463.976.156.518.286.253.697.
920.827.223.758.251.185.210.916.864.000.000.000.000.000.000.000.000

können Sie das Problem eindrucksvoll erkennen.

Wir kommen nun zu einer anderen Alternative, große Zahlen darzustellen. Speziell für Fakultäten ist dies auf eine Entdeckung des franz. Mathematikers Adrien-Marie Legendre (1752 bis 1833) zurückzuführen. Anstatt die Zahl im Zehnersystem hinzuschreiben, stellen wir sie in ihrer Primfaktorzerlegung dar. Um die Primfaktoren einer Zahl zu berechnen, müssten wir die Zahl aber selbst kennen, werden Sie nun vermutlich argumentieren – wir können sie auf Grund ihrer Größe aber gar nicht softwaretechnisch darstellen. An dieser Stelle kommt nun der Satz von Legendre ins Spiel, er lautet:

Satz von Legendre: In der Primfaktorzerlegung von n! gilt für den Exponenten einer jeder Primzahl, die n! teilt:

/img/legendre/SatzVonLegendre.png

Abbildung 1: Der Satz von Legendre mit Gaußklammern.

Hierbei repräsentieren in Abbildung 1 die senkrechten Klammern so genannte “Gaußklammern”: Sie stehen für “die größte ganze Zahl kleiner gleich n/p” am Beispiel des Bruchs n/p. Ferner beachte man: Von den Summanden |n/pi| sind nur endlich viele ungleich 0, nämlich die für alle i mit pin.

Beispiel: Wir probieren die Formel am Beispiel von 10! aus. Die 2 hat in der Primfaktorzerlegung von 10! den Exponenten

e(2) = |10/2| + |10/4| + |10/8| = 5 + 2 + 1 = 8.

Für die 3 ergibt sich folgender Exponent:

e(3) = |10/3| + |10/9| = 3 + 1 = 4.

Für 5 gilt:

e(5) = |10/5| = 2.

Damit bleibt nur noch die Primzahl 7 übrig, alle weiteren Primzahlen sind größer als 10:

e(7) = |10/7| = 1.

Wir können uns nun von der Korrektheit des Satzes von Legendre überzeugen. Für die Zahl 10! schreiben wir in der dezimalen Darstellung 3.628.800. Unter Verwendung der Primfaktoren 2, 3, 5, und 7 gilt

28 * 34 * 52 * 71 = 256 * 81 * 25 * 7 = 3.628.800.

Wir sehen, dass bei dieser Darstellung der Wert von n! nicht benötigt wird, die einzelnen Primfaktoren lassen sich nur unter Zuhilfenahme von n berechnen! Da die Hochstellung von Zahlen in einer Konsolenanwendung nicht ganz so einfach ist, modifizieren wir diese Darstellung softwaregerechter ab in

10! = (2,8) (3,4) (5,2) (7,1)

Erweitern Sie nun die Klasse Factorial um eine Methode factorialLegendre, um die Berechnung der Fakultät einer natürlichen Zahl in Partialbruchzerlegung durchzuführen:

PrimeDictionary factorialLegendre ();

Einen Pseudo-Code für den Satz von Legendre finden Sie in Abbildung 2 vor:

/img/legendre/PseudoCodeLegendre.png

Abbildung 2: Satz von Legendre in Pseudo-Code Darstellung.

Folgendes Code-Fragment sollte damit ausführbar sein:

Factorial f(10);
PrimeDictionary primes = f.factorialLegendre();
std::cout << primes << std::endl;

Ausgabe:

(2,8)(3,4)(5,2)(7,1)

Mit Hilfe des Satzes von Legendre lassen sich weitere Fragen aus der Zahlentheorie beantworten. Ein Beispiel gefällig? Auf wie viele Nullen endet die Zahl 2022! ?

Hinweis: Eine Null am Ende bedeutet, dass die Zahl durch 10 teilbar ist. Wie oft ist 2022! durch 10 teilbar? Sie ist so oft durch 10 teilbar, wie in der Primfaktorzerlegung genügend Zweien und Fünfen vorkommen, um den Faktor 10 zu bilden.

Da der Faktor 2 alle geraden Zahlen teilt und somit häufiger vorhanden ist, lässt sich festhalten, dass die Antwort allein durch die Anzahl der Fünfen bestimmt ist.

Binomialkoeffizienten

Eine weitere Funktion aus dem Themenkreis der Kombinatorik ist der Binomialkoeffizient. Er gibt an, auf wie viele verschiedene Arten man k Objekte aus einer Menge von n verschiedenen Objekten auswählen kann – ohne Zurücklegen und ohne Beachtung der Reihenfolge. Die mathematische Formel zur Berechnung des Binomialkoeffizienten für 0 ≤ n und 0 ≤ kn lautet wie folgt (Abbildung 3):

/img/legendre/BinomialCoefficient.png

Abbildung 3: Definition eines Binomialkoeffizienten.

Auf der linken Seite steht die Kurzschreibweise des Binomialkoeffizienten, gesprochen “n über k”. Auf der rechten Seite steht die Berechnung des Koeffizienten als Bruch mit drei Fakultäten. Wir verdeutlichen dies an zwei Beispielen in Abbildung 4:

/img/legendre/BinomialCoefficient_TwoSamples.png

Abbildung 4: Zwei Beispiele für Binomialkoeffizienten.

Der Binomialkoeffizient “49 über 6” entspricht damit beispielsweise der Anzahl der möglichen Ziehungen beim Lotto – nicht zu verwechseln mit der Treffer-Wahrscheinlichkeit “6 Richtige” beim Lotto, die durch eine hypergeometrische Verteilung ermittelbar ist.

Erstellen Sie eine Klasse BinomialCoefficient mit einer Methode calculate zur Berechnung des Binomialkoeffizienten. Die folgenden Code-Fragmente sollten mit Hilfe Ihrer Klassenimplementierung ausführbar sein:

BinomialCoefficient coeff;

for (size_t i{ 1 }; i != 10; ++i)
{
    coeff.setUpper(2 * i);
    coeff.setLower(i);
    coeff.calculate();

    std::cout
        << "Binomial "
        << coeff
        << " = "
        << coeff.value() << std::endl;
}

Ausgabe:

Binomial (2, 1) = 2
Binomial (4, 2) = 6
Binomial (6, 3) = 20
Binomial (8, 4) = 70
Binomial (10, 5) = 252
Binomial (12, 6) = 924
Binomial (14, 7) = 3432
Binomial (16, 8) = 12870
Binomial (18, 9) = 48620

Berechnen Sie der Reihe nach die Binomialkoeffizienten “2 über 1”, “4 über 2”, “8 über 4”, usw. Ab welchem Koeffizienten machen Sie auch hier die Beobachtung, dass die sehr schnell wachsenden Werte zu falschen Resultaten führen.

Methode reduce

Um dieses Problem zu lösen, kommen wir wieder auf den Trick mit der Partialbruchzerlegung zurück. Um den Koeffizienten “n über k” auszurechnen, berechnen wir die drei Werte n!, k! und (nk)! als PrimeDictionary-Objekte und vereinfachen diese anschließend. Dazu werden wir eine Methode reduce einführen, die zwei PrimeDictionary-Objekte als Parameter hat und die Vereinfachung des ersten PrimeDictionary-Objekts um das zweite PrimeDictionary-Objekt durchführt. Dazu betrachten wir am besten ein Beispiel in Abbildung 5:

/img/legendre/legendre_reduce_example.svg

Abbildung 5: Vereinfachung zweier PrimeDictionary-Objekte.

Ergänzen Sie nun Klasse PrimeDictionary um eine Methode reduce:

void reduce (const PrimeDictionary&);

Die Reduktion eines PrimeDictionary-Objekts durch ein zweites erfolgt dadurch, indem man das zweite PrimeDictionary-Objekt Eintrag für Eintrag traversiert und die Potenzen der Primzahlen von denen des ersten PrimeDictionary-Objekts subtrahiert. Verwenden Sie zum Testen Ihrer Implementierung das Beispiel aus Abbildung 5.

Darstellung von Binomialkoeffizienten in Partialbruchdarstellung

Mit Hilfe der reduce-Methode können wir nun sehr schnell der Klasse BinomialCoefficient eine Methode calculateLegendre hinzufügen:

PrimeDictionary calculateLegendre () const;

Zur Berechnung eines Binomialkoeffizienten zu n und k sind gemäß Abbildung 3 die drei Fakultäten n!, k! und (nk)! zu berechnen. Wenn Sie zuerst das PrimeDictionary-Objekt zu n! um das PrimeDictionary-Objekt bzgl. k! reduzieren und das resultierende PrimeDictionary-Objekt noch einmal um das PrimeDictionary-Objekt bzgl. (nk)! reduzieren, erhalten Sie den Binomialkoeffizienten – auch für sehr große Werte – in Partialbruchdarstellung.

Überprüfen Sie Ihre Implementierung an folgendem Codefragment:

BinomialCoefficient bin;

for (size_t i{ 1 }; i != 25; i++)
{
    bin.setUpper(2 * i);
    bin.setLower(i);
    PrimeDictionary result = bin.calculateLegendre();

    std::cout
        << "Binomial ("
        << std::setw(2) << bin.getUpper()
        << ", "
        << std::setw(2) << bin.getLower()
        << ") = "
        << result << std::endl;
}

Ausgabe:

Binomial ( 2,  1) = (2,1)
Binomial ( 4,  2) = (2,1)(3,1)
Binomial ( 6,  3) = (2,2)(3,0)(5,1)
Binomial ( 8,  4) = (2,1)(3,0)(5,1)(7,1)
Binomial (10,  5) = (2,2)(3,2)(5,0)(7,1)
Binomial (12,  6) = (2,2)(11,1)(3,1)(5,0)(7,1)
Binomial (14,  7) = (2,3)(11,1)(3,1)(13,1)(5,0)(7,0)
Binomial (16,  8) = (2,1)(11,1)(3,2)(13,1)(5,1)(7,0)
Binomial (18,  9) = (2,2)(11,1)(3,0)(13,1)(5,1)(7,0)(17,1)
Binomial (20, 10) = (2,2)(19,1)(11,1)(3,0)(13,1)(5,0)(7,0)(17,1)
Binomial (22, 11) = (2,3)(19,1)(11,0)(3,1)(13,1)(5,0)(7,1)(17,1)
Binomial (24, 12) = (2,2)(19,1)(11,0)(3,0)(13,1)(5,0)(7,1)(17,1)(23,1)
Binomial (26, 13) = (2,3)(19,1)(11,0)(3,0)(13,0)(5,2)(7,1)(17,1)(23,1)
Binomial (28, 14) = (2,3)(19,1)(11,0)(3,3)(13,0)(5,2)(7,0)(17,1)(23,1)
Binomial (30, 15) = (2,4)(19,1)(11,0)(3,2)(13,0)(5,1)(7,0)(17,1)(23,1)(29,1)
Binomial (32, 16) = (2,1)(19,1)(11,0)(3,2)(13,0)(5,1)(7,0)(17,1)(23,1)(29,1)(31,1)
Binomial (34, 17) = (2,2)(19,1)(11,1)(3,3)(13,0)(5,1)(7,0)(17,0)(23,1)(29,1)(31,1)
Binomial (36, 18) = (2,2)(19,1)(11,1)(3,1)(13,0)(5,2)(7,1)(17,0)(23,1)(29,1)(31,1)
Binomial (38, 19) = (2,3)(19,0)(11,1)(3,1)(13,0)(5,2)(7,1)(17,0)(23,1)(29,1)(31,1)(37,1)
Binomial (40, 20) = (2,2)(19,0)(11,1)(3,2)(13,1)(5,1)(7,1)(17,0)(23,1)(29,1)(31,1)(37,1)
Binomial (42, 21) = (2,3)(19,0)(11,1)(3,1)(13,1)(5,1)(7,0)(17,0)(23,1)(29,1)(31,1)(37,1)(41,1)
Binomial (44, 22) = (2,3)(19,0)(11,0)(3,1)(13,1)(5,1)(7,0)(17,0)(23,1)(29,1)(31,1)(37,1)(41,1)(43,1)
Binomial (46, 23) = (2,4)(19,0)(11,0)(3,3)(13,1)(5,2)(7,0)(17,0)(23,0)(29,1)(31,1)(37,1)(41,1)(43,1)
Binomial (48, 24) = (2,2)(19,0)(11,0)(3,2)(13,1)(5,2)(7,0)(17,0)(23,0)(29,1)(31,1)(37,1)(41,1)(43,1)(47,1)

Lösung

Quellcode: Siehe auch Github.

Auf Grund der recht umfangreichen Spezifikation aller beteiligten Klassen können wir gleich die Implementierung betrachten, die Klasse PrimeDictionary macht den Anfang (Listing 1 und Listing 2):

01: class PrimeDictionary
02: {
03: private:
04: 	std::unordered_map<size_t, size_t> m_map;
05: 
06: public:
07: 	// getter
08: 	size_t get(size_t key) const;
09: 	void set(size_t key, size_t value);
10: 
11: 	// public interface
12: 	void reduce (const PrimeDictionary&);
13: 
14: 	// output
15: 	friend std::ostream& operator<< (std::ostream&, const PrimeDictionary&);
16: };

Listing 1: Klasse PrimeDictionary: Definition.

01: size_t PrimeDictionary::get(size_t key) const
02: {
03:     return m_map.at(key);
04: }
05: 
06: void PrimeDictionary::set(size_t key, size_t value)
07: {
08:     m_map[key] = value;
09: }
10: 
11: void PrimeDictionary::reduce (const PrimeDictionary& dict)
12: {
13:     for (const auto& [key, value] : dict.m_map) {
14: 
15:         size_t newCoeff = get(key) - value;
16:         set(key, newCoeff);
17:     }
18: }
19: 
20: std::ostream& operator<< (std::ostream& os, const PrimeDictionary& dict)
21: {
22:     // iterate and print keys and values using structured binding
23:     for (int k = 0; const auto & [key, value] : dict.m_map) {
24:         os << '(' << key << ',' << value << ')';
25:         ++k;
26:         if (k % 16 == 0)
27:             os << '\n';
28:     }
29: 
30:     return os;
31: }

Listing 2: Klasse PrimeDictionary: Implementierung.

Es folgt Klasse Factorial in Listing 3 und Listing 4:

01: class Factorial
02: {
03: private:
04:     size_t m_n;
05:     size_t m_value;
06: 
07: public:
08:     // c'tors
09:     Factorial();
10:     Factorial(size_t n);
11: 
12:     // getter / setter
13:     size_t get() const;
14:     void set(size_t);
15:     size_t value() const;
16: 
17:     // public interface
18:     void factorialRecursive();
19:     void factorialIterative();
20:     PrimeDictionary factorialLegendre();
21: 
22: private:
23:     // private helper
24:     static size_t factorialRecursive(size_t n);
25:     static bool isPrime(size_t);
26: };

Listing 3: Klasse Factorial: Definition.

01: size_t Factorial::get() const
02: {
03:     return m_n;
04: }
05: 
06: void Factorial::set(size_t n)
07: {
08:     m_n = n;
09: }
10: 
11: size_t Factorial::value() const
12: {
13:     return m_value;
14: }
15: 
16: void Factorial::factorialIterative()
17: {
18:     m_value = 1;
19: 
20:     if (m_n <= 1) {
21:         return;
22:     }
23:     else {
24:         for (int i{ 2 }; i <= m_n; ++i)
25:             m_value *= i;
26:     }
27: }
28: 
29: void Factorial::factorialRecursive()
30: {
31:     m_value = factorialRecursive(m_n);
32: }
33: 
34: PrimeDictionary Factorial::factorialLegendre()
35: {
36:     // algorithm of Legendre
37:     PrimeDictionary result{};
38:     size_t prime{ 2 };
39:     while (prime <= m_n) {
40: 
41:         if (size_t quo, exp; isPrime(prime)) {
42:             quo = m_n / prime;
43:             exp = 0;
44: 
45:             while (quo != 0) {
46:                 exp += quo;
47:                 quo /= prime;
48:             }
49: 
50:             result.set(prime, exp);
51:         }
52: 
53:         prime++;
54:     }
55: 
56:     return result;
57: }
58: 
59: size_t Factorial::factorialRecursive(size_t n)
60: {
61:     return (n <= 1) ? 1 : n * factorialRecursive(n - 1);
62: }
63: 
64: bool Factorial::isPrime(size_t number)
65: {
66:     // the smallest prime number is 2
67:     if (number <= 2) {
68:         return number == 2;
69:     }
70: 
71:     if (number % 2 == 0) {
72:         return false;
73:     }
74: 
75:     // check odd divisors from 3 to the square root of the number
76:     size_t end{ static_cast<size_t>(std::ceil(std::sqrt(number))) };
77:     for (size_t i{ 3 }; i <= end; i += 2) {
78:         if (number % i == 0) {
79:             return false;
80:         }
81:     }
82: 
83:     return true;
84: }

Listing 4: Klasse Factorial: Implementierung.

Es fehlt noch eine Klasse für Binomialkoeffizienten (Listing 5 und Listing 6):

01: class BinomialCoefficient
02: {
03: private:
04: 	size_t m_n;
05: 	size_t m_k;
06: 	size_t m_value;
07: 
08: public:
09: 	// c'tors
10: 	BinomialCoefficient ();
11: 	BinomialCoefficient (size_t, size_t);
12: 
13: 	// getter / setter
14: 	size_t getUpper() const;
15: 	size_t getLower() const;
16: 	void setUpper(size_t n);
17: 	void setLower(size_t n);
18: 	size_t value() const;
19: 
20: 	// public interface
21: 	void calculate ();
22: 	PrimeDictionary calculateLegendre () const;
23: };
24: 
25: // output
26: std::ostream& operator<< (std::ostream&, const BinomialCoefficient&);

Listing 5: Klasse BinomialCoefficient: Definition.

01: BinomialCoefficient::BinomialCoefficient() 
02:     : m_n{ 1 }, m_k{ 1 }, m_value{ 1 } {}
03: 
04: BinomialCoefficient::BinomialCoefficient (size_t n, size_t k) 
05:     : m_n{ n }, m_k{ k }, m_value{ 0 } {}
06: 
07: size_t BinomialCoefficient::getUpper() const
08: {
09: 	return m_n;
10: }
11: 
12: size_t BinomialCoefficient::getLower() const
13: {
14: 	return m_k;
15: }
16: 
17: void BinomialCoefficient::setUpper(size_t n)
18: {
19:     m_n = n;
20: }
21: 
22: void BinomialCoefficient::setLower(size_t k)
23: {
24:     m_k = k;
25: }
26: 
27: size_t BinomialCoefficient::value() const
28: {
29: 	return m_value;
30: }
31: 
32: void BinomialCoefficient::calculate ()
33: {
34:     if (m_k == 0 || m_k == m_n) {
35:         m_value = 1;
36:     }
37:     else {
38:         size_t a{ 1 }, b{ 1 };
39:         for (size_t i{ m_n - m_k + 1 }; i <= m_n; ++i) {
40:             a *= i;
41:         }
42:         for (int j{ 1 }; j <= m_k; ++j) {
43:             b *= j;
44:         }
45:         m_value = a / b;
46:     }
47: }
48: 
49: PrimeDictionary BinomialCoefficient::calculateLegendre () const
50: {
51:     Factorial facUpper{ m_n };
52:     PrimeDictionary dictUpper{ facUpper.factorialLegendre() };
53: 
54: 	Factorial facLower{ m_k };
55: 	PrimeDictionary dictLower{ facLower.factorialLegendre() };
56: 
57: 	dictUpper.reduce(dictLower);
58: 
59: 	Factorial facUpperMinusLower{ m_n - m_k };
60: 	PrimeDictionary dictUpperMinusLower{ facUpperMinusLower.factorialLegendre() };
61: 
62: 	dictUpper.reduce(dictUpperMinusLower);
63: 
64: 	return dictUpper;
65: }
66: 
67: std::ostream& operator<< (std::ostream& os, const BinomialCoefficient& coeff)
68: {
69:     os 
70:         << "("
71:         << coeff.getUpper()
72:         << ", "
73:         << coeff.getLower()
74:         << ")";
75: 
76:     return os;
77: }

Listing 6: Klasse BinomialCoefficient: Implementierung.

Eine Frage wollen wir im Lösungsabschnitt noch betrachten. Erinnern Sie sich: “Auf wie viele Nullen endet die Zahl 2022! ?” Mit folgendem Code-Snippet ist die Frage gelöst:

Factorial f(2022);
PrimeDictionary primes = f.factorialLegendre();
size_t numFactorsOf5 = primes.get(5);
std::cout << "Factors of 5: " << numFactorsOf5 << "." << std::endl;

Ausgabe:

Factors of 5: 503.

There‘s more

Die drei Klassen PrimeDictionary, Factorial und BinomialCoefficient sind bzgl. ihrer Instanzvariablen mit dem Datentyp size_t realisiert. Diese harte Festlegung könnte man mit Klassentemplates vermeiden. Es bietet sich an, diese Klassen als Templates zu realisieren.


Literatur

Einige Beispiele und Anregungen zu dem Satz von Legende stammen aus dem Aufsatz “Die Sätze von Legendre und Tchebychef” (abgerufen am 27. März 2022).


See also