Beim Lesen der Verwendung von std :: rand habe ich diesen Code auf cppreference.com gefunden
int x = 7;
while(x > 6)
x = 1 + std::rand()/((RAND_MAX + 1u)/6); // Note: 1+rand()%6 is biased
Was ist falsch an dem Ausdruck auf der rechten Seite? Versuchte es und es funktioniert perfekt.
std::uniform_int_distribution
für Würfel zu verwendenrand()
ist in typischen Implementierungen so schlecht, dass Sie genauso gut das xkcd RNG verwenden können . Also ist es falsch, weil es verwendetrand()
.uniform_int_distribution
.)Antworten:
Es gibt zwei Probleme mit
rand() % 6
(das1+
betrifft keines der beiden Probleme).Erstens ist, wie mehrere Antworten gezeigt haben
rand()
, das Ergebnis des Restoperators auch nicht einheitlich , wenn die niedrigen Bits von nicht angemessen einheitlich sind.Zweitens, wenn die Anzahl der von erzeugten unterschiedlichen Werte
rand()
kein Vielfaches von 6 ist, erzeugt der Rest mehr niedrige Werte als hohe Werte. Dies gilt auch dann, wennrand()
perfekt verteilte Werte zurückgegeben werden.Stellen Sie sich als extremes Beispiel vor, dass
rand()
gleichmäßig verteilte Werte im Bereich erzeugt werden[0..6]
. Wenn Sie sich die Reste für diese Werte ansehen undrand()
einen Wert im Bereich zurückgeben[0..5]
, führt der Rest zu gleichmäßig verteilten Ergebnissen im Bereich[0..5]
. Wennrand()
6 zurückgegeben wird, wirdrand() % 6
0 zurückgegeben, als hätte 0rand()
zurückgegeben. Sie erhalten also eine Verteilung mit doppelt so vielen Nullen wie jeder andere Wert.Das zweite ist das eigentliche Problem mit
rand() % 6
.Um dieses Problem zu vermeiden, müssen Werte verworfen werden, die zu ungleichmäßigen Duplikaten führen würden. Sie berechnen das größte Vielfache von 6, das kleiner oder gleich ist
RAND_MAX
, und wenn Sierand()
einen Wert zurückgeben, der größer oder gleich diesem Vielfachen ist, lehnen Sie es ab und rufen `rand () erneut auf, so oft dies erforderlich ist.So:
Dies ist eine andere Implementierung des fraglichen Codes, um klarer zu zeigen, was los ist.
quelle
Hier gibt es verborgene Tiefen:
Die Verwendung des kleinen
u
inRAND_MAX + 1u
.RAND_MAX
wird alsint
Typ definiert und ist oft der größtmöglicheint
. Das Verhalten vonRAND_MAX + 1
wäre in solchen Fällen undefiniert, in denen Sie einensigned
Typ überlaufen würden . Das Schreiben1u
erzwingt die Typkonvertierung vonRAND_MAX
inunsigned
, wodurch der Überlauf vermieden wird.Die Verwendung von
% 6
can (aber bei jeder Implementierung von can , diestd::rand
ich nicht gesehen habe ) führt zu zusätzlichen statistischen Verzerrungen, die über die vorgestellte Alternative hinausgehen. Solche Fälle, in denen dies% 6
gefährlich ist, sind Fälle, in denen der Zahlengenerator Korrelationsebenen in den Bits niedriger Ordnung aufweist, wie beispielsweise eine ziemlich berühmte IBM-Implementierung (in C) ausrand
den 1970er Jahren, in der die hohen und niedrigen Bits als "endgültig" umgedreht wurden blühen". Eine weitere Überlegung ist, dass 6 sehr klein ist, vgl.RAND_MAX
Es wird also einen minimalen Effekt geben, wennRAND_MAX
nicht ein Vielfaches von 6 ist, was wahrscheinlich nicht der Fall ist.Zusammenfassend würde ich heutzutage aufgrund seiner Traktierbarkeit verwenden
% 6
. Es ist unwahrscheinlich, dass statistische Anomalien auftreten, die über die vom Generator selbst eingeführten hinausgehen. Wenn Sie immer noch Zweifel haben, testen Sie Ihren Generator , um festzustellen, ob er die entsprechenden statistischen Eigenschaften für Ihren Anwendungsfall aufweist.quelle
% 6
erzeugt ein verzerrtes Ergebnis, wenn die Anzahl der durch erzeugten unterschiedlichen Werterand()
kein Vielfaches von 6 ist. Taubenlochprinzip. Zugegeben, die Vorspannung ist gering, wenn sieRAND_MAX
viel größer als 6 ist, aber sie ist da. Und für größere Zielbereiche ist der Effekt natürlich größer.x==7
. Grundsätzlich teilen Sie den Bereich[0, RAND_MAX]
in 7 Unterbereiche, 6 gleich große und einen kleineren Unterbereich am Ende. Ergebnisse aus dem letzten Unterbereich werden verworfen. Es ist ziemlich offensichtlich, dass Sie auf diese Weise am Ende nicht zwei kleinere Unterbereiche haben können.Dieser Beispielcode zeigt, dass
std::rand
es sich um einen alten Frachtkult-Balderdash handelt, bei dem Ihre Augenbrauen jedes Mal hochgezogen werden sollten, wenn Sie ihn sehen.Hier gibt es mehrere Probleme:
Die Vertragsleute gehen normalerweise davon aus - selbst die armen, unglücklichen Seelen, die es nicht besser wissen und nicht genau so denken -, dass
rand
Stichproben aus der gleichmäßigen Verteilung auf die ganzen Zahlen in 0, 1, 2,…RAND_MAX
,, und jeder Aufruf ergibt eine unabhängige Stichprobe.Das erste Problem besteht darin, dass der angenommene Vertrag, unabhängige einheitliche Zufallsstichproben bei jedem Aufruf, nicht den Angaben in der Dokumentation entspricht - und in der Praxis haben Implementierungen in der Vergangenheit nicht einmal das geringste Simulakrum der Unabhängigkeit geliefert. Zum Beispiel
rand
sagt C99 §7.20.2.1 'Die Funktion' ohne Ausarbeitung:Dies ist ein bedeutungsloser Satz, da Pseudozufälligkeit eine Eigenschaft einer Funktion (oder Funktionsfamilie ) ist, nicht einer ganzen Zahl, aber das hindert nicht einmal ISO-Bürokraten daran, die Sprache zu missbrauchen. Schließlich wissen die einzigen Leser, die sich darüber aufregen würden, besser, als die Dokumentation zu lesen,
rand
weil sie befürchten, dass ihre Gehirnzellen verfallen.Eine typische historische Implementierung in C funktioniert folgendermaßen:
Dies hat die unglückliche Eigenschaft, dass eine einzelne Stichprobe , obwohl sie unter einem einheitlichen zufälligen Startwert (der vom spezifischen Wert von abhängt) gleichmäßig verteilt sein kann,
RAND_MAX
bei aufeinanderfolgenden Aufrufen nachher zwischen geraden und ungeraden Ganzzahlen wechseltDer Ausdruck
(a & 1) ^ (b & 1)
ergibt 1 mit einer Wahrscheinlichkeit von 100%, was bei unabhängigen Zufallsstichproben für jede Verteilung, die auf geraden und ungeraden ganzen Zahlen unterstützt wird, nicht der Fall ist . So entstand ein Frachtkult, dass man die niederwertigen Teile wegwerfen sollte, um das schwer fassbare Tier der "besseren Zufälligkeit" zu jagen. (Spoiler-Alarm: Dies ist kein Fachbegriff. Dies ist ein Zeichen dafür, dass die Prosa, über die Sie lesen, entweder nicht weiß, wovon sie sprechen, oder dass Sie ahnungslos sind und sich herablassen müssen.)Das zweite Problem ist, dass selbst wenn jeder Aufruf unabhängig von einer gleichmäßigen Zufallsverteilung auf 0, 1, 2, ...
RAND_MAX
abgetastetrand() % 6
würde , das Ergebnis von nicht wie ein Würfel in 0, 1, 2, 3, 4, 5 gleichmäßig verteilt wäre roll, esRAND_MAX
sei denn, es ist kongruent zu -1 modulo 6. Einfaches Gegenbeispiel: WennRAND_MAX
= 6, dannrand()
haben alle Ergebnisse die gleiche Wahrscheinlichkeit 1/7, aber abrand() % 6
hat das Ergebnis 0 die Wahrscheinlichkeit 2/7, während alle anderen Ergebnisse die Wahrscheinlichkeit 1/7 haben .Der richtige Weg, dies zu tun, ist die Ablehnungsstichprobe: Ziehen Sie wiederholt eine unabhängige einheitliche Zufallsstichprobe
s
aus 0, 1, 2,…RAND_MAX
und lehnen Sie (zum Beispiel) die Ergebnisse 0, 1, 2,… ab -((RAND_MAX + 1) % 6) - 1
wenn Sie eine von erhalten diese fangen von vorne an; ansonsten ergebens % 6
.Auf diese Weise ist die Menge der Ergebnisse
rand()
, die wir akzeptieren, gleichmäßig durch 6 teilbar, und jedes mögliche Ergebnis vons % 6
wird durch die gleiche Anzahl akzeptierter Ergebnisse von erhaltenrand()
. Wennrand()
es also gleichmäßig verteilt ist, ist dies auch der Falls
. Die Anzahl der Versuche ist nicht begrenzt , aber die erwartete Anzahl beträgt weniger als 2, und die Erfolgswahrscheinlichkeit steigt exponentiell mit der Anzahl der Versuche.Die Wahl, welche Ergebnisse
rand()
Sie ablehnen, ist unerheblich, vorausgesetzt, Sie ordnen jeder Ganzzahl unter 6 eine gleiche Anzahl zu. Der Code auf cppreference.com trifft aufgrund des ersten oben genannten Problems eine andere Wahl: Es wird nichts über das garantiert Verteilung oder Unabhängigkeit der Ausgaben vonrand()
und in der Praxis zeigten die niederwertigen Bits Muster, die nicht zufällig genug aussehen (egal, dass die nächste Ausgabe eine deterministische Funktion der vorherigen ist).Übung für den Leser: Beweisen Sie, dass der Code auf cppreference.com eine gleichmäßige Verteilung auf Würfelrollen ergibt, wenn er
rand()
eine gleichmäßige Verteilung auf 0, 1, 2,… ,RAND_MAX
.Übung für den Leser: Warum möchten Sie vielleicht die eine oder andere Teilmenge ablehnen? Welche Berechnung ist in beiden Fällen für jeden Versuch erforderlich?
Ein drittes Problem ist, dass der Samenraum so klein ist, dass selbst wenn der Samen gleichmäßig verteilt ist, ein Gegner, der mit Kenntnis Ihres Programms und einem Ergebnis, aber nicht dem Samen ausgestattet ist, den Samen und die nachfolgenden Ergebnisse leicht vorhersagen kann, was sie nicht so erscheinen lässt Immerhin zufällig. Denken Sie also nicht einmal daran, dies für die Kryptografie zu verwenden.
std::uniform_int_distribution
Mit einem geeigneten Zufallsgerät und Ihrer bevorzugten Zufalls-Engine wie dem allseits beliebten Mersenne-Twister können Sie die ausgefallene überentwickelte Route und die Klasse von C ++ 11 gehenstd::mt19937
, um mit Ihrem vierjährigen Cousin Würfel zu spielen, aber selbst das wird nicht fit sein für kryptographischen Schlüssel Material-und die Mersenne Erzeugung Twister ein schrecklichen Raum Schwein mit einem Multi-Kilobyte Zustand verheerend auf Ihrem CPU-Cache mit einer obszönen Rüstzeit auch ist, so ist es schlecht , auch für ist, zB parallel Monte - Carlo - Simulationen mit reproduzierbare Bäume von Teilberechnungen; seine Popularität ergibt sich wahrscheinlich hauptsächlich aus seinem eingängigen Namen. Aber Sie können es für Spielzeugwürfel verwenden, die wie in diesem Beispiel rollen!Ein anderer Ansatz besteht darin, einen einfachen kryptografischen Pseudozufallszahlengenerator mit einem kleinen Status zu verwenden, z. B. ein einfaches PRNG zum schnellen Löschen von Schlüsseln , oder nur eine Stream-Verschlüsselung wie AES-CTR oder ChaCha20, wenn Sie sicher sind ( z. B. in einer Monte-Carlo-Simulation für naturwissenschaftliche Forschung), dass die Vorhersage früherer Ergebnisse keine nachteiligen Folgen hat, wenn der Staat jemals kompromittiert wird.
quelle
(RAND_MAX + 1 )% 6
Werten durchführt. Es spielt keine Rolle, wie Sie die möglichen Ergebnisse unterteilen. Sie können sie von überall im Bereich ablehnen[0, RAND_MAX)
, solange die Größe des zulässigen Bereichs ein Vielfaches von 6 Die Hölle ist, können Sie mit Hoch jede Ergebnis ablehnenx>6
, und Sie müssen nicht%6
mehr.Ich bin keineswegs ein erfahrener C ++ - Benutzer, war aber interessiert zu sehen, ob die anderen Antworten bezüglich
std::rand()/((RAND_MAX + 1u)/6)
weniger Voreingenommenheit als1+std::rand()%6
tatsächlich zutreffen. Also habe ich ein Testprogramm geschrieben, um die Ergebnisse für beide Methoden zu tabellieren (ich habe seit Jahren kein C ++ mehr geschrieben, bitte überprüfen Sie es). Einen Link zum Ausführen des Codes finden Sie hier . Es wird auch wie folgt reproduziert:Ich habe dann die Ausgabe davon genommen und die
chisq.test
Funktion in R verwendet, um einen Chi-Quadrat-Test durchzuführen, um festzustellen, ob die Ergebnisse signifikant von den erwarteten abweichen. Diese Stapelaustauschfrage geht detaillierter auf die Verwendung des Chi-Quadrat-Tests zum Testen der Würfelgerechtigkeit ein: Wie kann ich testen, ob ein Würfel fair ist? . Hier sind die Ergebnisse für einige Läufe:In den drei Läufen, die ich durchgeführt habe, war der p-Wert für beide Methoden immer größer als die typischen Alpha-Werte, die zum Testen der Signifikanz verwendet wurden (0,05). Dies bedeutet, dass wir keinen von beiden als voreingenommen betrachten würden. Interessanterweise weist die vermeintlich unvoreingenommene Methode durchweg niedrigere p-Werte auf, was darauf hinweist, dass sie möglicherweise tatsächlich voreingenommener ist. Die Einschränkung ist, dass ich nur 3 Läufe gemacht habe.
UPDATE: Während ich meine Antwort schrieb, hat Konrad Rudolph eine Antwort gepostet, die den gleichen Ansatz verfolgt, aber ein ganz anderes Ergebnis erzielt. Ich habe nicht den Ruf, seine Antwort zu kommentieren, deshalb werde ich sie hier ansprechen. Erstens ist die Hauptsache, dass der Code, den er verwendet, bei jeder Ausführung denselben Startwert für den Zufallszahlengenerator verwendet. Wenn Sie den Samen ändern, erhalten Sie tatsächlich eine Vielzahl von Ergebnissen. Zweitens erhalten Sie eine Vielzahl von Ergebnissen, wenn Sie den Startwert nicht ändern, aber die Anzahl der Versuche ändern. Versuchen Sie, um eine Größenordnung zu erhöhen oder zu verringern, um zu sehen, was ich meine. Drittens gibt es einige ganzzahlige Kürzungen oder Rundungen, bei denen die erwarteten Werte nicht ganz genau sind. Es ist wahrscheinlich nicht genug, um einen Unterschied zu machen, aber es ist da.
Zusammenfassend lässt sich sagen, dass er zufällig den richtigen Samen und die richtige Anzahl von Versuchen erhalten hat, um ein falsches Ergebnis zu erzielen.
quelle
rand()%6
mitrand()/(1+RAND_MAX)/6
. Vielmehr wird die einfache Entnahme des Restes mit der Ablehnungsstichprobe verglichen (eine Erklärung finden Sie in den anderen Antworten). Folglich ist Ihr zweiter Code falsch (diewhile
Schleife macht nichts). Ihre statistischen Tests weisen ebenfalls Probleme auf (Sie können nicht einfach Wiederholungen Ihres Tests auf Robustheit ausführen, Sie haben keine Korrektur durchgeführt,…).std::srand
(und ohne Verwendung von<random>
) auf standardkonforme Weise festzulegen, und ich wollte nicht, dass seine Kompliziertheit den verbleibenden Code beeinträchtigt. Für die Berechnung ist dies ebenfalls irrelevant: Das Wiederholen derselben Sequenz in einer Simulation ist völlig akzeptabel. Natürlich verschiedenen Samen werden zu unterschiedlichen Ergebnissen führen, und einige werden nicht von Bedeutung sein. Dies wird vollständig erwartet, basierend darauf, wie der p-Wert definiert ist.std::rand
ergibt meine Standardbibliotheksimplementierung bemerkenswert gute Münzwurfsimulationen für einen d6 über den Bereich zufälliger Samen.RAND_MAX
und die Effektgröße des Modulo Bias bestimmen . Die statistische Signifikanz ist die Wahrscheinlichkeit unter der Nullhypothese, dass Sie sie fälschlicherweise ablehnen. Was ist die statistische Aussagekraft - die Wahrscheinlichkeit unter einer alternativen Hypothese, dass Ihr Test die Nullhypothese korrekt ablehnt? Würden Sie dies erkennenrand() % 6
, wenn RAND_MAX = 2 ^ 31 - 1?Man kann sich einen Zufallszahlengenerator als Arbeit an einem Strom von Binärziffern vorstellen. Der Generator wandelt den Stream in Zahlen um, indem er ihn in Stücke schneidet. Wenn die
std:rand
Funktion mit einemRAND_MAX
von 32767 arbeitet, verwendet sie 15 Bits in jedem Slice.Wenn man die Module einer Zahl zwischen 0 und einschließlich 32767 nimmt, findet man, dass 5462 '0' und '1', aber nur 5461 '2', '3', '4' und '5'. Daher ist das Ergebnis voreingenommen. Je größer der RAND_MAX-Wert ist, desto geringer ist die Vorspannung, die jedoch unvermeidlich ist.
Was nicht voreingenommen ist, ist eine Zahl im Bereich [0 .. (2 ^ n) -1]. Sie können eine (theoretisch) bessere Zahl im Bereich 0..5 erzeugen, indem Sie 3 Bits extrahieren, diese in eine Ganzzahl im Bereich 0..7 konvertieren und 6 und 7 ablehnen.
Man hofft, dass jedes Bit im Bitstrom die gleiche Chance hat, eine '0' oder eine '1' zu sein, unabhängig davon, wo es sich im Strom befindet oder welche Werte andere Bits haben. Dies ist in der Praxis außerordentlich schwierig. Die vielen verschiedenen Implementierungen von Software-PRNGs bieten unterschiedliche Kompromisse zwischen Geschwindigkeit und Qualität. Ein linearer Kongruenzgenerator
std::rand
bietet beispielsweise die schnellste Geschwindigkeit bei niedrigster Qualität. Ein kryptografischer Generator bietet höchste Qualität bei niedrigster Geschwindigkeit.quelle