Nicht alle sind immer gleich
Zufallszahlen in C++ müssen nicht immer der Gleichverteilung folgen. Gerade für Simulationen sind oft andere Verteilungen wesentlich wichtiger, weswegen die Standardbibliothek auch eine Sammlung der häufigsten mitbringt.
Video
Code
#include <iostream>
#include <random>
#include <utility>
#include <vector>
#include <fstream>
using namespace std;
int main()
{
random_device dev;
auto seed = dev();
default_random_engine engine { seed };
auto normal = normal_distribution<double>(50, 12.5);
auto uniform = uniform_int_distribution<int>(0, 99);
const int total_runs = 1000000;
vector<pair<int, int>> result_bins(100);
for(unsigned int i = 0; i < total_runs; i++)
{
auto normal_value = normal(engine);
normal_value = static_cast<int>(min(max(normal_value, 0.0), 99.0));
auto uniform_value = uniform(engine);
++(result_bins[normal_value].first);
++(result_bins[uniform_value].second);
}
ofstream output_file {"output.csv" };
output_file << R"("","normal","uniform")";
output_file << endl;
int value = 0;
for (auto amount : result_bins)
{
output_file << value << "," << amount.first << "," << amount.second << endl;
++value;
}
output_file.close();
}
Erklärung
Auf dem eigentlichen Zufallszahlengenerator (hier in Form der
default_random_engine) sitzt eine Verteilung, die die erzeugten Zufallszahlen
in die vom Nutzer gewünschte Form bringt. Das kann eine einfache
Gleichverteilung in einem bestimmten Wertebereich sein, wie sie beispielsweise
uniform_int_distribution bereitstellt. Es sind aber auch komplexere
Verteilungen möglich, von denen die Standardbibliothek einige mitbringt.
Hier im Beispiel gezeigt ist die Normalverteilung mit ihrer bekannten
Gausskurve. Die Normalverteilung hat zwei Parameter: die Mittelwert µ und die
Standardabweichung σ. Ersterer gibt die Position der Glockenkurve der
Verteilungsdichtefunktion an, letztere deren Breite (mathematisch exakt
gesprochen ist σ; natürlich anders definiert). Beide Parameter kann man der
normal_distribution übergeben und erhält dann Zufallszahlen in der Häufigkeit,
wie sie die Dichtefunktion vorgibt. Hier im Beispiel bedeutet das zum Beispiel,
dass ~68% aller Zufallszahlen im Bereich [37,5 , 62,5] liegen (µ ± σ). Die
Zahlen weit entfernt vom Mittelwert kommen deutlich seltener vor.
Um die beiden Verteilungsdichtefunktionen zu visualisieren berechnet der Code im Beispiel ein Histogramm (letztlich bloß eine diskrete Variante der kontinuierlichen Verteilungsdichtefunktion). Dazu werden die generierten Zufallszahlen als Index in einen Vektor verwendet. Die Einträge des Vektor fungieren als Zähler, die die Häufigkeit ihres Indexes zählen (Zeilen 26 und 27 für jeweils die Normal- und die Gleichverteilung). Da die Normalverteilung eine unendliche Dichtefunktion hat, d.h. es können, wenn auch mit sehr geringer Wahrscheinlichkeit, beliebige Werte vom -∞ bis +∞ auftreten. Da wir unser Histogramm aber nur im Bereich von 0 bis 99 berechnen, müssen wir vorher noch auf diesen Bereich normieren. Dazu schneiden wie hier einfach den Wertebereich entsprechend ab und schlagen die entsprechenden Vorkommen außerhalb liegender Zahlen den Grenzen zu (Zeile 23). Für den Anwendungsfall hier reicht das. In realen Anwendungsfällen sollte man damit allerdings vorsichtig sein. Wie im Video zu sehen ist, verschieben wir dadurch die Wahrscheinlichkeiten. Die Grenzwerte kommen plötzlich mit zu hoher Wahrscheinlichkeit vor. Dieses Problem entsteht generell bei der Begrenzung von Verteilungen mit unendlicher Dichtefunktion auf einen endlichen Bereich (so zum Beispiel auch bei der Exponientialfunktion).
Die Zeilen 30 bis 39 schreiben schließlich die gezählten Häufigkeiten noch in eine CSV-Datei, die dann in einer handelsüblichen Tabellenkalkulation geöffnet und visualisiert werden kann. Im Ergebnis sehen wie die erwarteten Dichtefunktionen: eine waagerechte Linie für die Gleichverteilung und die bekannte Gausskurve der Normalverteilung.