Perché la gente dice che c'è un errore di modulo quando si usa un generatore di numeri casuali?


277

Ho visto questa domanda molto posta ma non ho mai visto una vera risposta concreta ad essa. Quindi ne pubblicherò uno qui, che si spera aiuterà le persone a capire perché esattamente c'è "distorsione del modulo" quando si usa un generatore di numeri casuali, come rand()in C ++.

Risposte:


394

Quindi rand()è un generatore di numeri pseudo-casuale che sceglie un numero naturale tra 0 e RAND_MAX, che è una costante definita in cstdlib(vedi questo articolo per una panoramica generale su rand()).

Ora cosa succede se si desidera generare un numero casuale tra dire 0 e 2? Per motivi di spiegazione, supponiamo che RAND_MAXsia 10 e decido di generare un numero casuale compreso tra 0 e 2 chiamando rand()%3. Tuttavia, rand()%3non produce i numeri tra 0 e 2 con uguale probabilità!

Quando rand()restituisce 0, 3, 6 o 9 rand()%3 == 0 ,. Pertanto, P (0) = 4/11

Quando rand()restituisce 1, 4, 7 o 10 rand()%3 == 1 ,. Pertanto, P (1) = 4/11

Quando rand()restituisce 2, 5 o 8 rand()%3 == 2 ,. Pertanto, P (2) = 3/11

Questo non genera i numeri tra 0 e 2 con uguale probabilità. Naturalmente per piccoli intervalli questo potrebbe non essere il problema più grande, ma per un intervallo più ampio ciò potrebbe distorcere la distribuzione, influenzando i numeri più piccoli.

Quindi, quando rand()%nrestituisce un intervallo di numeri da 0 a n-1 con uguale probabilità? Quando RAND_MAX%n == n - 1. In questo caso, insieme alla nostra ipotesi precedente rand()restituisce un numero compreso tra 0 e RAND_MAXcon uguale probabilità, anche le classi modulo di n verrebbero equamente distribuite.

Quindi, come possiamo risolvere questo problema? Un modo grezzo è quello di continuare a generare numeri casuali fino a quando non ottieni un numero nell'intervallo desiderato:

int x; 
do {
    x = rand();
} while (x >= n);

ma questo è inefficiente per valori bassi di n, poiché hai solo la n/RAND_MAXpossibilità di ottenere un valore nel tuo intervallo, quindi dovrai eseguire RAND_MAX/nchiamate arand() in media.

Un approccio formula più efficiente sarebbe quello di prendere un ampio intervallo con una lunghezza divisibile per n, come RAND_MAX - RAND_MAX % n, continuare a generare numeri casuali fino a quando non ne ottieni uno che si trova nell'intervallo, quindi prendere il modulo:

int x;

do {
    x = rand();
} while (x >= (RAND_MAX - RAND_MAX % n));

x %= n;

Per piccoli valori di n, ciò richiederà raramente più di una chiamata a rand().


Opere citate e successive letture:



6
Un altro modo di pensare a RAND_MAX%n == n - 1_ _ è (RAND_MAX + 1) % n == 0. Quando leggo il codice, tendo a capire % something == 0come "uniformemente divisibile" più facilmente rispetto ad altri modi di calcolarlo. Naturalmente, se il tuo C ++ stdlib ha RAND_MAXlo stesso valore di INT_MAX, (RAND_MAX + 1)sicuramente non funzionerebbe; quindi il calcolo di Mark rimane l'implementazione più sicura.
Slipp D. Thompson,

risposta molto bella!
Sayali Sonawane,

Potrei essere pignolo, ma se l'obiettivo è ridurre i bit sprecati, potremmo migliorarlo leggermente per la condizione limite in cui RAND_MAX (RM) è solo 1 in meno rispetto a essere equamente divisibile per N. In questo scenario, nessun bit deve essere sprecato da facendo X> = (RM - RM% N)) che ha scarso valore per piccoli valori di N, ma diventa di valore maggiore per grandi valori di N. Come menzionato da Slipp D. Thompson, esiste una soluzione che funziona solo quando INT_MAX (IM)> RAND_MAX ma si interrompe quando sono uguali. Tuttavia, esiste una soluzione semplice per questo che possiamo modificare il calcolo X> = (RM - RM% N) come segue:
Ben Personick,

X> = RM - (((RM% N) + 1)% N)
Ben Personick,

Ho pubblicato una risposta aggiuntiva spiegando il problema in dettaglio e fornendo la soluzione del codice di esempio.
Ben Personick,

36

Continuare a selezionare un caso è un buon modo per rimuovere la distorsione.

Aggiornare

Potremmo velocizzare il codice se cerchiamo una x nell'intervallo divisibile per n.

// Assumptions
// rand() in [0, RAND_MAX]
// n in (0, RAND_MAX]

int x; 

// Keep searching for an x in a range divisible by n 
do {
    x = rand();
} while (x >= RAND_MAX - (RAND_MAX % n)) 

x %= n;

Il ciclo sopra dovrebbe essere molto veloce, diciamo 1 iterazione in media.


2
Accidenti :-P la conversione in doppio, quindi la moltiplicazione per MAX_UPPER_LIMIT / RAND_MAX è molto più pulita e offre prestazioni migliori.
Boycy,

22
@boycy: hai perso il punto. Se il numero di valori che rand()può restituire non è un multiplo di n, qualunque cosa tu faccia, otterrai inevitabilmente "distorsione del modulo", a meno che tu non scarti alcuni di quei valori. user1413793 lo spiega bene (anche se la soluzione proposta in quella risposta è davvero schifosa).
TonyK,

4
@TonyK mi scuso, mi sono perso il punto. Non ci ho pensato abbastanza e ho pensato che il bias si sarebbe applicato solo con metodi che utilizzavano un'operazione di modulo esplicita. Grazie per avermi
risolto

La precedenza dell'operatore fa RAND_MAX+1 - (RAND_MAX+1) % nfunzionare correttamente, ma penso ancora che dovrebbe essere scritto RAND_MAX+1 - ((RAND_MAX+1) % n)per chiarezza.
Linus Arver,

4
Questo non funzionerà se RAND_MAX == INT_MAX (come nella maggior parte dei sistemi) . Vedi il mio secondo commento a @ user1413793 sopra.
BlueRaja - Danny Pflughoeft il

19

@ user1413793 ha ragione sul problema. Non ne discuterò ulteriormente, tranne per fare un punto: sì, per valori piccoli di ne valori grandi diRAND_MAX , la distorsione del modulo può essere molto piccola. L'uso di un modello che induce la distorsione significa che è necessario considerare la distorsione ogni volta che si calcola un numero casuale e si scelgono schemi diversi per casi diversi. E se fai la scelta sbagliata, i bug che introduce sono impercettibili e quasi impossibili da testare. Rispetto al solo utilizzo dello strumento adeguato (come arc4random_uniform), questo è un lavoro extra, non meno lavoro. Fare più lavoro e ottenere una soluzione peggiore è una progettazione terribile, specialmente quando lo si fa sempre bene sulla maggior parte delle piattaforme.

Sfortunatamente, le implementazioni della soluzione sono tutte errate o meno efficienti di quanto dovrebbero essere. (Ogni soluzione ha vari commenti che spiegano i problemi, ma nessuna delle soluzioni è stata risolta per risolverli.) Ciò probabilmente confonderà il ricercatore di risposte casuali, quindi sto fornendo un'implementazione ben nota qui.

Ancora una volta, la soluzione migliore è solo quella di utilizzare arc4random_uniformsu piattaforme che lo forniscono, o una soluzione a distanza simile per la tua piattaforma (comeRandom.nextInt su Java). Farà la cosa giusta senza costi per te. Questa è quasi sempre la chiamata corretta da effettuare.

Se non lo hai arc4random_uniform, puoi usare la potenza di opensource per vedere esattamente come viene implementato in cima a un RNG a più ampio raggio (ar4random in questo caso, ma un approccio simile potrebbe funzionare anche su altri RNG).

Ecco l' implementazione di OpenBSD :

/*
 * Calculate a uniformly distributed random number less than upper_bound
 * avoiding "modulo bias".
 *
 * Uniformity is achieved by generating new random numbers until the one
 * returned is outside the range [0, 2**32 % upper_bound).  This
 * guarantees the selected random number will be inside
 * [2**32 % upper_bound, 2**32) which maps back to [0, upper_bound)
 * after reduction modulo upper_bound.
 */
u_int32_t
arc4random_uniform(u_int32_t upper_bound)
{
    u_int32_t r, min;

    if (upper_bound < 2)
        return 0;

    /* 2**32 % x == (2**32 - x) % x */
    min = -upper_bound % upper_bound;

    /*
     * This could theoretically loop forever but each retry has
     * p > 0.5 (worst case, usually far better) of selecting a
     * number inside the range we need, so it should rarely need
     * to re-roll.
     */
    for (;;) {
        r = arc4random();
        if (r >= min)
            break;
    }

    return r % upper_bound;
}

Vale la pena notare l'ultimo commento di commit su questo codice per coloro che devono implementare cose simili:

Cambia arc4random_uniform () per calcolare 2**32 % upper_boundcome -upper_bound % upper_bound . Semplifica il codice e lo rende uguale su entrambe le architetture ILP32 e LP64 e anche leggermente più veloce sulle architetture LP64 utilizzando un resto a 32 bit anziché un resto a 64 bit.

Segnalato da Jorden Verwer su tech @ ok deraadt; nessuna obiezione da parte di djm o otto

L'implementazione Java è anche facilmente reperibile (vedi link precedente):

public int nextInt(int n) {
   if (n <= 0)
     throw new IllegalArgumentException("n must be positive");

   if ((n & -n) == n)  // i.e., n is a power of 2
     return (int)((n * (long)next(31)) >> 31);

   int bits, val;
   do {
       bits = next(31);
       val = bits % n;
   } while (bits - val + (n-1) < 0);
   return val;
 }

Si noti che se arcfour_random() effettivamente utilizza il vero algoritmo RC4 nella sua implementazione, l'output avrà sicuramente dei bias. Spero che i tuoi autori di biblioteche siano passati a utilizzare un CSPRNG migliore dietro la stessa interfaccia. Ricordo che uno dei BSD ora utilizza effettivamente l'algoritmo ChaCha20 per implementare arcfour_random(). Maggiori informazioni sui pregiudizi dell'output RC4 che lo rendono inutile per la sicurezza o altre applicazioni critiche come il video poker: blog.cryptographyengineering.com/2013/03/…
rmalayter

2
@rmalayter Su iOS e OS X, arc4random legge da / dev / random che è l'entropia della massima qualità nel sistema. (L '"arc4" nel nome è storico e preservato per compatibilità.)
Rob Napier,

@Rob_Napier è buono a sapersi, ma /dev/randomin passato ha utilizzato RC4 su alcune piattaforme (Linux utilizza SHA-1 in modalità contatore). Sfortunatamente le pagine man che ho trovato tramite la ricerca indicano che RC4 è ancora in uso su varie piattaforme che offrono arc4random(anche se il codice effettivo potrebbe essere diverso).
rmalayter,

1
Non ho capito bene. No -upper_bound % upper_bound == 0??
Jon McClung,

1
@JonMcClung -upper_bound % upper_boundsarà effettivamente 0 se intè più largo di 32 bit. Dovrebbe essere (u_int32_t)-upper_bound % upper_bound)(supponendo che u_int32_tsia un ismo BSD per uint32_t).
Ian Abbott,

14

Definizione

La distorsione modulo è la distorsione intrinseca nell'uso dell'aritmetica modulo per ridurre un set di output a un sottoinsieme del set di input. In generale, esiste una distorsione ogni volta che la mappatura tra il set di input e output non è equamente distribuita, come nel caso dell'uso dell'aritmetica del modulo quando la dimensione del set di output non è un divisore della dimensione del set di input.

Questa distorsione è particolarmente difficile da evitare nel calcolo, in cui i numeri sono rappresentati come stringhe di bit: 0 e 1. Anche trovare fonti di casualità veramente casuali è estremamente difficile, ma va oltre lo scopo di questa discussione. Per il resto di questa risposta, supponiamo che esista una fonte illimitata di bit veramente casuali.

Esempio di problema

Consideriamo di simulare un tiro di dado (da 0 a 5) usando questi bit casuali. Ci sono 6 possibilità, quindi abbiamo bisogno di abbastanza bit per rappresentare il numero 6, che è 3 bit. Sfortunatamente, 3 bit casuali producono 8 possibili risultati:

000 = 0, 001 = 1, 010 = 2, 011 = 3
100 = 4, 101 = 5, 110 = 6, 111 = 7

Possiamo ridurre la dimensione del risultato impostato esattamente a 6 prendendo il valore modulo 6, tuttavia questo presenta il problema di polarizzazione del modulo : 110produce uno 0 e 111produce un 1. Questo dado viene caricato.

Soluzioni potenziali

Approccio 0:

Invece di fare affidamento su bit casuali, in teoria si potrebbe assumere un piccolo esercito per lanciare dadi tutto il giorno e registrare i risultati in un database, quindi utilizzare ogni risultato una sola volta. Ciò è tanto pratico quanto sembra, e molto probabilmente non produrrebbe comunque risultati veramente casuali (gioco di parole previsto).

Approccio 1:

Invece di utilizzare il modulo, una soluzione banale ma matematicamente corretto è risultato scarto che il rendimento 110e 111e semplicemente riprova con 3 nuovi bit. Sfortunatamente, questo significa che c'è una probabilità del 25% su ogni tiro che sarà richiesto un rilancio, incluso ciascuno dei rilanci stessi. Questo è chiaramente poco pratico per tutti tranne che per gli usi più banali.

Approccio 2:

Usa più bit: invece di 3 bit, usa 4. Questo produce 16 possibili risultati. Naturalmente, ripetere il rollback ogni volta che il risultato è maggiore di 5 peggiora le cose (10/16 = 62,5%) in modo che da solo non sia d'aiuto.

Si noti che 2 * 6 = 12 <16, in modo da poter prendere in sicurezza qualsiasi risultato inferiore a 12 e ridurre quel modulo 6 per distribuire uniformemente i risultati. Gli altri 4 risultati devono essere scartati e quindi ripetuti come nell'approccio precedente.

All'inizio suona bene, ma controlliamo la matematica:

4 discarded results / 16 possibilities = 25%

In questo caso, 1 bit in più non ha aiutato affatto!

Il risultato è sfortunato, ma riproviamo con 5 bit:

32 % 6 = 2 discarded results; and
2 discarded results / 32 possibilities = 6.25%

Un netto miglioramento, ma non abbastanza buono in molti casi pratici. La buona notizia è che l' aggiunta di più bit non aumenterà mai le possibilità di scartare e ripetere il tiro . Questo vale non solo per i dadi, ma in tutti i casi.

Come dimostrato , tuttavia, l'aggiunta di 1 bit in più potrebbe non cambiare nulla. Infatti se aumentiamo il nostro lancio a 6 bit, la probabilità rimane del 6,25%.

Ciò pone 2 domande aggiuntive:

  1. Se aggiungiamo abbastanza bit, c'è una garanzia che la probabilità di uno scarto diminuirà?
  2. Quanti bit sono sufficienti nel caso generale?

Soluzione generale

Per fortuna la risposta alla prima domanda è sì. Il problema con 6 è che 2 ^ x mod 6 si sposta tra 2 e 4 che per coincidenza sono un multiplo di 2 l'uno dall'altro, in modo che per un x> 1 pari,

[2^x mod 6] / 2^x == [2^(x+1) mod 6] / 2^(x+1)

Quindi 6 è un'eccezione piuttosto che la regola. È possibile trovare moduli più grandi che producono potenze consecutive di 2 allo stesso modo, ma alla fine questo deve avvolgere e la probabilità di uno scarto sarà ridotta.

Senza offrire ulteriori prove, in generale l'uso del doppio del numero di bit richiesto fornirà una possibilità minore, generalmente insignificante, di scartare.

Verifica teorica

Ecco un programma di esempio che utilizza il libcrypo di OpenSSL per fornire byte casuali. Durante la compilazione, assicurati di collegarti alla libreria con la -lcryptoquale quasi tutti dovrebbero avere a disposizione.

#include <iostream>
#include <assert.h>
#include <limits>
#include <openssl/rand.h>

volatile uint32_t dummy;
uint64_t discardCount;

uint32_t uniformRandomUint32(uint32_t upperBound)
{
    assert(RAND_status() == 1);
    uint64_t discard = (std::numeric_limits<uint64_t>::max() - upperBound) % upperBound;
    uint64_t randomPool = RAND_bytes((uint8_t*)(&randomPool), sizeof(randomPool));

    while(randomPool > (std::numeric_limits<uint64_t>::max() - discard)) {
        RAND_bytes((uint8_t*)(&randomPool), sizeof(randomPool));
        ++discardCount;
    }

    return randomPool % upperBound;
}

int main() {
    discardCount = 0;

    const uint32_t MODULUS = (1ul << 31)-1;
    const uint32_t ROLLS = 10000000;

    for(uint32_t i = 0; i < ROLLS; ++i) {
        dummy = uniformRandomUint32(MODULUS);
    }
    std::cout << "Discard count = " << discardCount << std::endl;
}

Incoraggio a giocare con i valori MODULUSe ROLLSper vedere quante ripetizioni avvengono effettivamente nella maggior parte delle condizioni. Una persona scettica potrebbe anche voler salvare i valori calcolati su file e verificare che la distribuzione appaia normale.


Spero davvero che nessuno abbia copiato ciecamente la tua uniforme implementazione casuale. La randomPool = RAND_bytes(...)riga risulterà sempre a randomPool == 1causa dell'asserzione. Ciò comporta sempre uno scarto e un rilancio. Penso che tu abbia voluto dichiarare su una linea separata. Di conseguenza, ciò ha causato il ritorno dell'RNG 1per ogni iterazione.
Qix - MONICA È STATA MISTREATA il

Per essere chiari, randomPoolvaluterà sempre 1secondo la documentazione diRAND_bytes() OpenSSL , poiché riuscirà sempre grazie RAND_status()all'asserzione.
Qix - MONICA È STATA MISTREATA il

9

Ci sono due soliti reclami con l'uso di modulo.

  • uno è valido per tutti i generatori. È più facile da vedere in un caso limite. Se il tuo generatore ha un RAND_MAX che è 2 (che non è conforme allo standard C) e vuoi solo 0 o 1 come valore, usando modulo genererà 0 due volte più spesso (quando il generatore genera 0 e 2) come farà genera 1 (quando il generatore genera 1). Si noti che questo è vero non appena non si rilasciano valori, qualunque sia la mappatura che si sta utilizzando dai valori del generatore a quello desiderato, uno si verificherà due volte più spesso dell'altro.

  • qualche tipo di generatore ha i suoi bit meno significativi meno casuali dell'altro, almeno per alcuni dei loro parametri, ma purtroppo quei parametri hanno altre caratteristiche interessanti (tale è stato in grado di avere RAND_MAX uno in meno di una potenza di 2). Il problema è ben noto e per molto tempo l'implementazione della libreria probabilmente evita il problema (ad esempio l'implementazione di esempio rand () nello standard C utilizza questo tipo di generatore, ma rilascia i 16 bit meno significativi), ma alcuni preferiscono lamentarsi quello e potresti avere sfortuna

Usando qualcosa di simile

int alea(int n){ 
 assert (0 < n && n <= RAND_MAX); 
 int partSize = 
      n == RAND_MAX ? 1 : 1 + (RAND_MAX-n)/(n+1); 
 int maxUsefull = partSize * n + (partSize-1); 
 int draw; 
 do { 
   draw = rand(); 
 } while (draw > maxUsefull); 
 return draw/partSize; 
}

generare un numero casuale compreso tra 0 e n eviterà entrambi i problemi (ed eviterà l'overflow con RAND_MAX == INT_MAX)

A proposito, C ++ 11 ha introdotto metodi standard per la riduzione e altro generatore di rand ().


n == RAND_MAX? 1: (RAND_MAX-1) / (n + 1): Capisco l'idea qui è prima di dividere RAND_MAX in uguali dimensioni di pagina N, quindi restituire la deviazione all'interno di N, ma non riesco a mappare il codice esattamente su questo.
tintinnio il

1
La versione ingenua dovrebbe essere (RAND_MAX + 1) / (n + 1) in quanto vi sono valori RAND_MAX + 1 da dividere in n + 1 bucket. Se l'ordine per evitare overflow durante il calcolo di RAND_MAX + 1, può essere trasformato in 1+ (RAND_MAX-n) / (n + 1). Al fine di evitare l'overflow durante il calcolo di n + 1, viene prima verificato il caso n == RAND_MAX.
AProgrammer,

+ inoltre, fare dividere sembra costare di più anche rispetto ai numeri rigenerati.
tintinnio il

4
Prendere il modulo e dividere hanno lo stesso costo. Alcuni ISA forniscono anche solo un'istruzione che fornisce sempre entrambi. Il costo della rigenerazione dei numeri dipenderà da n e RAND_MAX. Se n è piccolo rispetto a RAND_MAX, può costare molto. E ovviamente puoi decidere che i pregiudizi non sono importanti per la tua applicazione; Do solo un modo per evitarli.
AProgrammer,

9

Mark's Solution (La soluzione accettata) è quasi perfetta.

int x;

do {
    x = rand();
} while (x >= (RAND_MAX - RAND_MAX % n));

x %= n;

modificato il 25 marzo 16 alle 23:16

Mark Amery 39k21170211

Tuttavia, ha un avvertimento che scarta 1 serie valida di risultati in qualsiasi scenario in cui RAND_MAX( RM) è 1 in meno di un multiplo di N(Dove N= il numero di possibili risultati validi).

vale a dire, quando il "conteggio dei valori scartati" ( D) è uguale a N, allora sono in realtà un set valido ( V), non un set non valido ( I).

Ciò che provoca questo è che ad un certo punto Mark perde di vista la differenza tra Ne Rand_Max.

Nè un insieme i cui membri validi sono composti solo da numeri interi positivi, in quanto contiene un conteggio di risposte valide. (ad es .: Set N= {1, 2, 3, ... n })

Rand_max Tuttavia è un insieme che (come definito per i nostri scopi) include un numero qualsiasi di numeri interi non negativi.

Nella sua forma più generica, ciò che è definito qui Rand Maxè l'insieme di tutti i risultati validi, che potrebbe teoricamente includere numeri negativi o valori non numerici.

Pertanto Rand_Maxè meglio definito come l'insieme di "Risposte possibili".

però N opera contro il conteggio dei valori all'interno dell'insieme di risposte valide, quindi anche come definito nel nostro caso specifico, Rand_Maxsarà un valore uno in meno del numero totale che contiene.

Usando la soluzione di Mark, i valori vengono scartati quando: X => RM - RM% N

EG: 

Ran Max Value (RM) = 255
Valid Outcome (N) = 4

When X => 252, Discarded values for X are: 252, 253, 254, 255

So, if Random Value Selected (X) = {252, 253, 254, 255}

Number of discarded Values (I) = RM % N + 1 == N

 IE:

 I = RM % N + 1
 I = 255 % 4 + 1
 I = 3 + 1
 I = 4

   X => ( RM - RM % N )
 255 => (255 - 255 % 4) 
 255 => (255 - 3)
 255 => (252)

 Discard Returns $True

Come puoi vedere nell'esempio sopra, quando il valore di X (il numero casuale che otteniamo dalla funzione iniziale) è 252, 253, 254 o 255, lo scartiamo anche se questi quattro valori comprendono un insieme valido di valori restituiti .

IE: quando il conteggio dei valori scartati (I) = N (il numero di risultati validi), un set valido di valori restituiti verrà scartato dalla funzione originale.

Se descriviamo la differenza tra i valori N e RM come D, ovvero:

D = (RM - N)

Quindi, man mano che il valore di D diminuisce, la percentuale di ripetizioni non necessarie a causa di questo metodo aumenta ad ogni moltiplicativo naturale. (Quando RAND_MAX NON è uguale a un numero primo, questo è un problema valido)

PER ESEMPIO:

RM=255 , N=2 Then: D = 253, Lost percentage = 0.78125%

RM=255 , N=4 Then: D = 251, Lost percentage = 1.5625%
RM=255 , N=8 Then: D = 247, Lost percentage = 3.125%
RM=255 , N=16 Then: D = 239, Lost percentage = 6.25%
RM=255 , N=32 Then: D = 223, Lost percentage = 12.5%
RM=255 , N=64 Then: D = 191, Lost percentage = 25%
RM=255 , N= 128 Then D = 127, Lost percentage = 50%

Poiché la percentuale di rilanci necessari aumenta man mano che N si avvicina a RM, ciò può essere una valida preoccupazione per molti valori diversi a seconda dei vincoli del sistema che esegue il codice e dei valori ricercati.

Per negare questo possiamo fare un semplice emendamento Come mostrato qui:

 int x;

 do {
     x = rand();
 } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );

 x %= n;

Ciò fornisce una versione più generale della formula che tiene conto delle peculiarità aggiuntive dell'uso del modulo per definire i valori massimi.

Esempi di utilizzo di un valore piccolo per RAND_MAX che è un moltiplicativo di N.

Mark'original Version:

RAND_MAX = 3, n = 2, Values in RAND_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X >= (RAND_MAX - ( RAND_MAX % n ) )
When X >= 2 the value will be discarded, even though the set is valid.

Versione generalizzata 1:

RAND_MAX = 3, n = 2, Values in RAND_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X > (RAND_MAX - ( ( RAND_MAX % n  ) + 1 ) % n )
When X > 3 the value would be discarded, but this is not a vlue in the set RAND_MAX so there will be no discard.

Inoltre, nel caso in cui N dovrebbe essere il numero di valori in RAND_MAX; in questo caso, è possibile impostare N = RAND_MAX +1, a meno che RAND_MAX = INT_MAX.

Per quanto riguarda il ciclo, potresti semplicemente usare N = 1, e qualsiasi valore di X sarà comunque accettato e inserirà un'istruzione IF per il moltiplicatore finale. Ma forse hai un codice che potrebbe avere un motivo valido per restituire un 1 quando la funzione viene chiamata con n = 1 ...

Quindi potrebbe essere meglio usare 0, che normalmente fornirebbe un errore Div 0, quando si desidera avere n = RAND_MAX + 1

Versione generalizzata 2:

int x;

if n != 0 {
    do {
        x = rand();
    } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );

    x %= n;
} else {
    x = rand();
}

Entrambe queste soluzioni risolvono il problema con risultati validi inutilmente scartati che si verificano quando RM + 1 è un prodotto di n.

La seconda versione copre anche lo scenario del caso limite quando è necessario n per eguagliare l'insieme totale possibile di valori contenuti in RAND_MAX.

L'approccio modificato in entrambi è lo stesso e consente una soluzione più generale alla necessità di fornire numeri casuali validi e ridurre al minimo i valori scartati.

Reiterare:

La soluzione generale di base che estende l'esempio di mark:

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

 int x;

 do {
     x = rand();
 } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) ) );

 x %= n;

La soluzione generale estesa che consente uno scenario aggiuntivo di RAND_MAX + 1 = n:

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

int x;

if n != 0 {
    do {
        x = rand();
    } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) ) );

    x %= n;
} else {
    x = rand();
}

In alcune lingue (in particolare le lingue interpretate), fare i calcoli dell'operazione di confronto al di fuori della condizione while può portare a risultati più rapidi in quanto si tratta di un calcolo una tantum, indipendentemente dal numero di tentativi richiesti. YMMV!

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

int x; // Resulting random number
int y; // One-time calculation of the compare value for x

if n != 0 {
    y = RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) 
    do {
        x = rand();
    } while (x > y);

    x %= n;
} else {
    x = rand();
}

Non è sicuro affermare che il problema con la soluzione di Mark è che considera RAND_MAX et la stessa "unità di misura" quando in realtà significano due cose diverse? Mentre n rappresenta il "numero di possibilità" risultante, RAND_MAX rappresenta solo il valore massimo della possibilità originale, dove RAND_MAX + 1 sarebbe il numero originale di possibilità. Sono sorpreso che non sia arrivato alla tua conclusione poiché sembrava aver riconosciuto n e RAND_MAX non erano la stessa cosa con l'equazione:RAND_MAX%n = n - 1
Danilo Souza Morães,

@ DaniloSouzaMorães Grazie Danilo, hai posto la questione in modo molto succinto. Ho cercato di dimostrare cosa stava facendo insieme al perché e al come, ma non credo di essere mai stato in grado di affermare che cosa stava facendo di sbagliato in modo eloquente, dato che sono così coinvolto nei dettagli della logica su come e perché c'è un problema, che non sto affermando chiaramente ciò che è in questione. Ti dispiace se modifico la mia risposta per usare parte di ciò che hai scritto qui come mio riassunto al problema di cosa e dove sta facendo la soluzione accettata che deve essere affrontata in alto?
Ben Personick,

Sarebbe fantastico. Go for it
Danilo Souza Moraes

1

Con un RAND_MAXvalore di 3(in realtà dovrebbe essere molto più alto di quello ma il pregiudizio esisterebbe ancora) ha senso da questi calcoli che c'è un pregiudizio:

1 % 2 = 1 2 % 2 = 0 3 % 2 = 1 random_between(1, 3) % 2 = more likely a 1

In questo caso, % 2è quello che non dovresti fare quando vuoi un numero casuale tra 0e 1. Potresti ottenere un numero casuale tra 0e 2facendo % 3però, perché in questo caso: RAND_MAXè un multiplo di 3.

Un altro metodo

C'è molto più semplice ma da aggiungere ad altre risposte, ecco la mia soluzione per ottenere un numero casuale tra 0e n - 1, quindi n, possibilità diverse, senza distorsioni.

  • il numero di bit (non byte) necessari per codificare il numero di possibilità è il numero di bit di dati casuali necessari
  • codificare il numero da bit casuali
  • se questo numero è >= n, riavvia (nessun modulo).

Non è facile ottenere dati davvero casuali, quindi perché usare più bit del necessario.

Di seguito è riportato un esempio in Smalltalk, utilizzando una cache di bit da un generatore di numeri pseudo-casuale. Non sono un esperto di sicurezza, quindi utilizzare a proprio rischio.

next: n

    | bitSize r from to |
    n < 0 ifTrue: [^0 - (self next: 0 - n)].
    n = 0 ifTrue: [^nil].
    n = 1 ifTrue: [^0].
    cache isNil ifTrue: [cache := OrderedCollection new].
    cache size < (self randmax highBit) ifTrue: [
        Security.DSSRandom default next asByteArray do: [ :byte |
            (1 to: 8) do: [ :i |    cache add: (byte bitAt: i)]
        ]
    ].
    r := 0.
    bitSize := n highBit.
    to := cache size.
    from := to - bitSize + 1.
    (from to: to) do: [ :i |
        r := r bitAt: i - from + 1 put: (cache at: i)
    ].
    cache removeFrom: from to: to.
    r >= n ifTrue: [^self next: n].
    ^r

-1

Come indica la risposta accettata , "modulo bias" ha le sue radici nel valore basso di RAND_MAX. Usa un valore estremamente piccolo di RAND_MAX(10) per mostrare che se RAND_MAX fosse 10, allora si provasse a generare un numero compreso tra 0 e 2 usando%, si otterrebbero i seguenti risultati:

rand() % 3   // if RAND_MAX were only 10, gives
output of rand()   |   rand()%3
0                  |   0
1                  |   1
2                  |   2
3                  |   0
4                  |   1
5                  |   2
6                  |   0
7                  |   1
8                  |   2
9                  |   0

Quindi ci sono 4 output di 0 (possibilità 4/10) e solo 3 output di 1 e 2 (3/10 possibilità ciascuno).

Quindi è di parte. I numeri più bassi hanno maggiori possibilità di uscire.

Ma questo si presenta solo ovviamente quando RAND_MAXè piccolo . O più specificamente, quando il numero per cui stai modificando è grande rispetto aRAND_MAX.

Una soluzione molto migliore del looping (che è follemente inefficiente e non dovrebbe nemmeno essere suggerito) è quella di utilizzare un PRNG con un intervallo di output molto più ampio. L' algoritmo Mersenne Twister ha un output massimo di 4.294.967.295. Come taleMersenneTwister::genrand_int32() % 10 a tutti gli effetti, sarà equamente distribuito e l'effetto di distorsione del modulo scomparirà.


3
Il tuo è più efficiente ed è probabilmente vero che se RAND_MAX è significativamente più grande del numero che stai modificando, tuttavia il tuo sarà comunque distorto. Concesso che questi sono comunque tutti generatori di numeri pseudo casuali e che di per sé è un argomento diverso ma se si assume un generatore di numeri completamente casuale, il modo in cui orienta i valori più bassi.
user1413793

Poiché il valore più alto è dispari, MT::genrand_int32()%2seleziona 0 (50 + 2,3e-8)% del tempo e 1 (50 - 2,3e-8)% del tempo. A meno che tu non stia costruendo un RGN di un casinò (per il quale probabilmente useresti un RGN di gamma molto più ampia), nessun utente non noterà un ulteriore 2,3e-8% delle volte. Stai parlando di numeri troppo piccoli per essere importanti qui.
bobobobo,

7
Il looping è la soluzione migliore. Non è "follemente inefficiente"; che richiedono meno del doppio delle iterazioni nel caso medio peggiore. L'uso di un RAND_MAXvalore elevato riduce il bias del modulo, ma non lo elimina. Lo farà il ciclo.
Jared Nielsen,

5
Se RAND_MAXè sufficientemente più grande del numero che stai modificando, il numero di volte che devi rigenerare il numero casuale è evanescente e non influisce sull'efficienza. Dico di continuare a fare il ciclo, purché tu stia testando sul multiplo più grande npiuttosto che su nquello proposto dalla risposta accettata.
Mark Ransom,

-3

Ho appena scritto un codice per il metodo di gettoniera delle monete non distorte di Von Neumann, che dovrebbe teoricamente eliminare ogni pregiudizio nel processo di generazione di numeri casuali. Ulteriori informazioni sono disponibili all'indirizzo ( http://en.wikipedia.org/wiki/Fair_coin )

int unbiased_random_bit() {    
    int x1, x2, prev;
    prev = 2;
    x1 = rand() % 2;
    x2 = rand() % 2;

    for (;; x1 = rand() % 2, x2 = rand() % 2)
    {
        if (x1 ^ x2)      // 01 -> 1, or 10 -> 0.
        {
            return x2;        
        }
        else if (x1 & x2)
        {
            if (!prev)    // 0011
                return 1;
            else
                prev = 1; // 1111 -> continue, bias unresolved
        }
        else
        {
            if (prev == 1)// 1100
                return 0;
            else          // 0000 -> continue, bias unresolved
                prev = 0;
        }
    }
}

Questo non affronta la distorsione del modulo. Questo processo potrebbe essere utilizzato per eliminare la distorsione in un flusso di bit. Tuttavia, per passare da un flusso di bit a una distribuzione uniforme da 0 a n dove n non è uno in meno di una potenza di due, è necessario indirizzare il bias del modulo. Pertanto, questa soluzione non può eliminare alcun pregiudizio nel processo di generazione di numeri casuali.
Rick,

2
@Rick hmm. L'estensione logica del metodo di Von Neumann per eliminare la distorsione del modulo quando si genera un numero casuale tra, diciamo, 1 e 100, sarebbe: A) chiamare rand() % 100100 volte. B) se tutti i risultati sono diversi, prendi il primo. C) altrimenti, GOTO A. Funzionerà, ma con un numero previsto di iterazioni di circa 10 ^ 42, dovrai essere abbastanza paziente. E immortale.
Mark Amery,

@MarkAmery In effetti dovrebbe funzionare. Esaminando questo algoritmo sebbene non sia implementato correttamente. Il primo dovrebbe essere:else if(prev==2) prev= x1; else { if(prev!=x1) return prev; prev=2;}
Rick,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.