Perché rand () + rand () produce numeri negativi?


304

Ho osservato che la rand()funzione di libreria quando viene chiamata una sola volta all'interno di un ciclo, produce quasi sempre numeri positivi.

for (i = 0; i < 100; i++) {
    printf("%d\n", rand());
}

Ma quando aggiungo due rand()chiamate, i numeri generati ora hanno più numeri negativi.

for (i = 0; i < 100; i++) {
    printf("%d = %d\n", rand(), (rand() + rand()));
}

Qualcuno può spiegare perché nel secondo caso visualizzo numeri negativi?

PS: inizializzo il seme prima del ciclo come srand(time(NULL)).


11
rand()non può essere negativo ...
twentylemon,

293
rand () + rand () can owerflow
maskacovnik

13
Cos'è RAND_MAXper il tuo compilatore? Di solito puoi trovarlo in stdlib.h. (Divertente: controllo man 3 rand, porta la descrizione di una riga "generatore di numeri casuali
errato

6
fai quello che farebbe ogni programmatore sano di mente abs(rand()+rand()). Preferirei avere un UB positivo piuttosto che uno negativo! ;)
Vinicius Kamakura,

11
@hexa: questo non è un sotution per l'UB, poiché si verifica già per l'aggiunta. Non puoi fare in modo che UB diventi un comportamento definito . Un programmatore sano dovrebbe evitare UB come l'inferno.
troppo onesto per questo sito il

Risposte:


542

rand()è definito per restituire un numero intero compreso tra 0e RAND_MAX.

rand() + rand()

potrebbe traboccare. Ciò che si osserva è probabilmente il risultato di un comportamento indefinito causato da un overflow di numeri interi.


4
@JakubArnold: In che modo il comportamento di overflow è specificato in modo diverso da ogni lingua? Python per esempio non ne ha (beh, fino alla memoria disponibile), poiché int cresce.
troppo onesto per questo sito il

2
@Olaf Dipende da come una lingua decide di rappresentare numeri interi con segno. Java non aveva alcun meccanismo per rilevare l'overflow di numeri interi (fino a java 8) e lo definiva da avvolgere e Go utilizza solo la rappresentazione del complemento di 2 e lo definisce legale per gli overflow di numeri interi firmati. C ovviamente supporta più del complemento di 2.
PP,

2
@EvanCarslake No, non è un comportamento universale. Quello che dici riguarda la rappresentazione del complemento di 2. Ma il linguaggio C consente anche altre rappresentazioni. La specifica del linguaggio C afferma che l'overflow di numeri interi con segno non è definito . Quindi, in generale, nessun programma dovrebbe fare affidamento su tale comportamento e deve codificare attentamente per non causare overflow di numeri interi con segno. Ma questo non è applicabile per numeri interi senza segno poiché si "avvolgono" in un modo ben definito (riduzione modulo 2). [continua] ...
PP,

12
Questa è la citazione dallo standard C relativa all'overflow di numeri interi con segno: se durante la valutazione di un'espressione si verifica una condizione eccezionale (ovvero, se il risultato non è definito matematicamente o non è compreso nell'intervallo di valori rappresentabili per il suo tipo), il comportamento non è definito.
PP,

3
@EvanCarslake si allontana un po 'dalla domanda che i compilatori C usano lo standard e per gli interi con segno possono supporre che a + b > ase lo sanno b > 0. Possono anche presumere che se in seguito viene eseguita un'istruzione, a + 5allora il valore corrente è inferiore INT_MAX - 5. Quindi anche sul processore / interprete del complemento di 2 senza programma trappole potrebbe non comportarsi come se intfosse un complemento di 2 senza trappole.
Maciej Piechotka,

90

Il problema è l'aggiunta. rand()restituisce un intvalore di 0...RAND_MAX. Quindi, se ne aggiungi due, ti alzi RAND_MAX * 2. Se questo supera INT_MAX, il risultato dell'aggiunta trabocca nell'intervallo valido che intpuò essere mantenuto. L'overflow dei valori firmati è un comportamento indefinito e può portare la tua tastiera a parlarti in lingue straniere.

Dato che qui non c'è alcun vantaggio nell'aggiungere due risultati casuali, l'idea semplice è semplicemente di non farlo. In alternativa è possibile eseguire il cast di ogni risultato unsigned intprima dell'aggiunta se è possibile contenere la somma. Oppure usa un tipo più grande. Nota che longnon è necessariamente più ampio di int, lo stesso vale per long longse intè almeno 64 bit!

Conclusione: basta evitare l'aggiunta. Non fornisce più "casualità". Se sono necessari più bit, è possibile concatenare i valori sum = a + b * (RAND_MAX + 1), ma probabilmente è necessario un tipo di dati più grande di int.

Poiché il motivo dichiarato è quello di evitare un risultato zero: ciò non può essere evitato aggiungendo i risultati di due rand()chiamate, poiché entrambi possono essere zero. Invece, puoi semplicemente incrementare. Se RAND_MAX == INT_MAXnon è possibile farlo int. Tuttavia, (unsigned int)rand() + 1lo farà molto, molto probabilmente. Probabile (non definitivamente), perché lo richiede UINT_MAX > INT_MAX, il che è vero su tutte le implementazioni di cui sono a conoscenza (che copre alcune architetture embedded, DSP e tutte le piattaforme desktop, mobili e server degli ultimi 30 anni).

Avvertimento:

Sebbene sia già cosparso nei commenti qui, tieni presente che l'aggiunta di due valori casuali non ottiene una distribuzione uniforme, ma una distribuzione triangolare come il lancio di due dadi: per ottenere 12(due dadi) entrambi i dadi devono mostrare 6. perché 11ci sono già due possibili varianti: 6 + 5o 5 + 6, ecc.

Quindi, anche l'aggiunta è negativa da questo aspetto.

Si noti inoltre che i risultati rand()generati non sono indipendenti l'uno dall'altro, poiché sono generati da un generatore di numeri pseudocasuali . Si noti inoltre che lo standard non specifica la qualità o la distribuzione uniforme dei valori calcolati.


14
@badmad: E se entrambe le chiamate restituissero 0?
troppo onesto per questo sito il

3
@badmad: mi chiedo solo se UINT_MAX > INT_MAX != falseè garantito dallo standard. (Sembra probabile, ma non sono sicuro se necessario). Se è così, puoi semplicemente lanciare un singolo risultato e incremento (in questo ordine!).
troppo onesto per questo sito il

3
C'è un guadagno nell'aggiungere più numeri casuali quando si desidera una distribuzione non uniforme: stackoverflow.com/questions/30492259/…
Cœur

6
per evitare 0, un semplice "mentre il risultato è 0, ripetere il rollback"?
Olivier Dulac il

2
Non solo li aggiunge un modo negativo per evitare 0, ma si traduce anche in una distribuzione non uniforme. Ottieni una distribuzione come i risultati del lancio di dadi: 7 è 6 volte più probabile di 2 o 12.
Barmar

36

Questa è una risposta a un chiarimento della domanda fatta in commento a questa risposta ,

il motivo che stavo aggiungendo era di evitare "0" come numero casuale nel mio codice. rand () + rand () è stata la rapida soluzione sporca che mi è venuta subito in mente.

Il problema era evitare 0. Esistono (almeno) due problemi con la soluzione proposta. Uno è, come indicano le altre risposte, che rand()+rand()può invocare un comportamento indefinito. Il miglior consiglio è di non invocare mai comportamenti indefiniti. Un altro problema è che non esiste alcuna garanzia che rand()non produrrà 0 due volte di seguito.

Quanto segue rifiuta zero, evita comportamenti indefiniti e nella stragrande maggioranza dei casi sarà più veloce di due chiamate a rand():

int rnum;
for (rnum = rand(); rnum == 0; rnum = rand()) {}
// or do rnum = rand(); while (rnum == 0);

9
Che dire rand() + 1?
askvictor,

3
@askvictor Potrebbe traboccare (anche se è improbabile).
Gerrit,

3
@gerrit - dipende da MAX_INT e RAND_MAX
askvictor

3
@gerrit, sarei sorpreso se non fossero gli stessi, ma suppongo che questo sia un posto per i pedanti :)
askvictor

10
Se RAND_MAX == MAX_INT, rand () + 1 traboccerà con esattamente la stessa probabilità del valore di rand () essendo 0, il che rende questa soluzione completamente inutile. Se sei disposto a rischiare e ignorare la possibilità di un overflow, puoi anche usare rand () così com'è e ignorare la possibilità che restituisca 0.
Emil Jeřábek

3

Fondamentalmente rand()produce numeri tra 0e RAND_MAX, e 2 RAND_MAX > INT_MAXnel tuo caso.

È possibile modulare con il valore massimo del tipo di dati per evitare l'overflow. Questo corso interromperà la distribuzione dei numeri casuali, ma randè solo un modo per ottenere numeri casuali rapidi.

#include <stdio.h>
#include <limits.h>

int main(void)
{
    int i=0;

    for (i=0; i<100; i++)
        printf(" %d : %d \n", rand(), ((rand() % (INT_MAX/2))+(rand() % (INT_MAX/2))));

    for (i=0; i<100; i++)
        printf(" %d : %ld \n", rand(), ((rand() % (LONG_MAX/2))+(rand() % (LONG_MAX/2))));

    return 0;
}

2

Forse potresti provare un approccio piuttosto difficile assicurandoti che il valore restituito dalla somma di 2 rand () non superi mai il valore di RAND_MAX. Un possibile approccio potrebbe essere sum = rand () / 2 + rand () / 2; Ciò garantirebbe che per un compilatore a 16 bit con valore RAND_MAX di 32767 anche se entrambi i rand restituiscono 32767, anche allora (32767/2 = 16383) 16383 + 16383 = 32766, non comporterebbe quindi una somma negativa.


1
L'OP voleva escludere 0 dai risultati. Inoltre, l'aggiunta non fornisce una distribuzione uniforme di valori casuali.
troppo onesto per questo sito il

@Olaf: non vi è alcuna garanzia che due chiamate consecutive rand()non generino entrambe zero, quindi il desiderio di evitare zero non è un buon motivo per aggiungere due valori. D'altra parte, il desiderio di avere una distribuzione non uniforme sarebbe una buona ragione per aggiungere due valori casuali se uno assicura che non si verifichi un overflow.
supercat

1

il motivo che stavo aggiungendo era di evitare "0" come numero casuale nel mio codice. rand () + rand () è stata la rapida soluzione sporca che mi è venuta subito in mente.

Una soluzione semplice (va bene, chiamala "Hack") che non produce mai un risultato zero e non trabocca mai:

x=(rand()/2)+1    // using divide  -or-
x=(rand()>>1)+1   // using shift which may be faster
                  // compiler optimization may use shift in both cases

Questo limiterà il tuo valore massimo, ma se non ti interessa, allora dovrebbe funzionare bene per te.


1
Sidenote: attenti ai giusti spostamenti delle variabili firmate. È ben definito solo per i valori non negativi, per i negativi, è definito dall'implementazione. (Fortunatamente, rand()restituisce sempre un valore non negativo). Tuttavia, lascerei l'ottimizzazione al compilatore qui.
troppo onesto per questo sito il

@Olaf: in generale, la divisione firmata per due sarà meno efficiente di un turno. A meno che uno scrittore compilatore ha investito sforzi nel dire al compilatore che randsarà non negativo, lo spostamento sarà più efficiente di divisione per un intero con segno 2. Divisione per 2upotrebbe funzionare, ma se xè un intpuò comportare avvertimenti circa conversione implicita da non firmato firmato.
supercat

@supercat: Per favore, leggi di nuovo il mio commento car3efully. Dovresti ben sapere che qualsiasi compilatore ragionevole utilizzerà / 2comunque un turno (l'ho visto anche per qualcosa del genere -O0, cioè senza ottimizzazioni esplicitamente richieste). È forse l'ottimizzazione più banale e più consolidata del codice C. Il punto è che la divisione è ben definita dallo standard per l'intero intervallo intero, non solo valori non negativi. Ancora: lascia le ottimizzazioni al compilatore, scrivi prima il codice corretto e chiaro. Questo è ancora più importante per i principianti.
troppo onesto per questo sito il

@Olaf: ogni compilatore che ho testato genera codice più efficiente quando si sposta a rand()destra per uno o si divide per 2urispetto a quando si divide per 2, anche quando si utilizza -O3. Si potrebbe ragionevolmente affermare che tale ottimizzazione è improbabile, ma dire "lasciare tali ottimizzazioni al compilatore" implicherebbe che i compilatori potrebbero eseguirle. Conosci qualche compilatore che effettivamente lo farà?
supercat

@supercat: allora dovresti usare compilatori più moderni. gcc ha appena generato il codice fine l'ultima volta che ho controllato l'Assembler generato. Tuttavia, per quanto mi appresti ad avere un brontolone, preferirei non essere molestato nella misura in cui ti presenti l'ultima volta. Questi post hanno anni, i miei commenti sono perfettamente validi. Grazie.
troppo onesto per questo sito il

1

Per evitare 0, prova questo:

int rnumb = rand()%(INT_MAX-1)+1;

È necessario includere limits.h.


4
Che raddoppierà la probabilità di ottenere 1. E 'fondamentalmente la stessa (ma possiblly più lento) come condizionalmente l'aggiunta di 1, se rand()i rendimenti 0.
troppo onesto per questo sito

Sì, hai ragione Olaf. Se rand () = 0 o INT_MAX -1 il rnumb sarà 1.
Doni

Ancora peggio, quando arrivo a pensarci. In realtà raddoppierà la propensione per 1e 2(tutti ipotizzati RAND_MAX == INT_MAX). Mi sono dimenticato del - 1.
troppo onesto per questo sito il

1
Il -1qui non ha valore. rand()%INT_MAX+1; genererebbe comunque solo valori nell'intervallo [1 ... INT_MAX].
chux - Ripristina Monica il

-2

Mentre ciò che tutti gli altri hanno detto sul probabile overflow potrebbe benissimo essere la causa del negativo, anche quando si usano numeri interi senza segno. Il vero problema è in realtà l'utilizzo della funzionalità data / ora come seme. Se hai acquisito familiarità con questa funzionalità, saprai esattamente perché lo dico. Ciò che realmente fa è fornire una distanza (tempo trascorso) da una data / ora specifica. Mentre l'uso della funzionalità data / ora come seme di un rand () è una pratica molto comune, in realtà non è l'opzione migliore. Dovresti cercare alternative migliori, in quanto ci sono molte teorie sull'argomento e non potrei probabilmente approfondirle tutte. Aggiungete a questa equazione la possibilità di overflow e questo approccio è stato condannato dall'inizio.

Quelli che hanno pubblicato il rand () + 1 stanno usando la soluzione che più usano per garantire che non ottengano un numero negativo. Ma anche questo approccio non è il modo migliore.

La cosa migliore che puoi fare è prenderti il ​​tempo extra per scrivere e usare la corretta gestione delle eccezioni, e aggiungere solo al numero rand () se e / o quando finisci con un risultato zero. E, per gestire correttamente i numeri negativi. La funzionalità rand () non è perfetta e pertanto deve essere utilizzata insieme alla gestione delle eccezioni per garantire che si ottenga il risultato desiderato.

Prendersi il tempo e gli sforzi extra per indagare, studiare e implementare correttamente la funzionalità rand () vale la pena dedicare tempo e fatica. Solo i miei due centesimi. Buona fortuna per il tuo impegno...


2
rand()non specifica quale seme utilizzare. Lo standard lo specifica per utilizzare un generatore pseudocasuale, non una relazione con nessun momento. Inoltre, non indica la qualità del generatore. Il vero problema è chiaramente l'overflow. Si noti che rand()+1viene utilizzato per evitare 0; rand()non restituisce un valore negativo. Mi dispiace, ma ti sei perso il punto qui. Non si tratta della qualità del PRNG. ...
troppo onesto per questo sito il

... Le buone pratiche sotto GNU / Linux derivano /dev/randome usano un buon PRNG in seguito (non sono sicuro della qualità di rand()da glibc) o continuano a usare il dispositivo - rischiando l'applicazione di bloccare se non c'è abbastanza entropia disponibile. Cercare di ottenere l'entropia nell'applicazione potrebbe benissimo essere una vulnerabilità in quanto è probabilmente più facile da attaccare. E ora si tratta di indurimento - non qui
troppo onesto per questo sito il
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.