web-dev-qa-db-fra.com

Comment générer un nombre entier aléatoire dans une plage

C’est le suivi d’une question précédemment postée:

Comment générer un nombre aléatoire en C?

Je souhaite être en mesure de générer un nombre aléatoire à partir d'une plage particulière, telle que 1 à 6 pour imiter les côtés d'un dé.

Comment pourrais-je m'y prendre?

98
Jamie Keeling

Jusqu'à présent, toutes les réponses sont mathématiquement fausses. Le renvoi de Rand() % N ne donne pas uniformément un nombre compris entre [0, N) sauf si N divise la longueur de l'intervalle dans lequel Rand() renvoie (c'est-à-dire une puissance de 2). De plus, on ne sait pas si les modules de Rand() sont indépendants: il est possible qu’ils passent au 0, 1, 2, ..., qui est uniforme mais pas très aléatoire. La seule hypothèse qu'il semble raisonnable de faire est que Rand() génère une distribution de Poisson: deux sous-intervalles non chevauchants de même taille sont également probables et indépendants. Pour un ensemble fini de valeurs, cela implique une distribution uniforme et garantit également que les valeurs de Rand() sont bien dispersées.

Cela signifie que la seule façon correcte de modifier la plage de Rand() consiste à la diviser en zones; Par exemple, si Rand_MAX == 11 et que vous souhaitez une plage de 1..6, vous devez affecter {0,1} à 1, {2,3} à 2, etc. Ce sont des intervalles disjoints, de taille égale et donc uniformément et indépendamment répartis.

La suggestion d'utiliser la division en virgule flottante est mathématiquement plausible, mais elle pose des problèmes d'arrondi en principe. double est peut-être suffisamment précis pour le faire fonctionner; peut-être pas. Je ne sais pas et je ne veux pas avoir à le comprendre; dans tous les cas, la réponse dépend du système.

La bonne façon consiste à utiliser l'arithmétique entière. C'est-à-dire que vous voulez quelque chose comme ce qui suit:

#include <stdlib.h> // For random(), Rand_MAX

// Assumes 0 <= max <= Rand_MAX
// Returns in the closed interval [0, max]
long random_at_most(long max) {
  unsigned long
    // max <= Rand_MAX < ULONG_MAX, so this is okay.
    num_bins = (unsigned long) max + 1,
    num_Rand = (unsigned long) Rand_MAX + 1,
    bin_size = num_Rand / num_bins,
    defect   = num_Rand % num_bins;

  long x;
  do {
   x = random();
  }
  // This is carefully written not to overflow
  while (num_Rand - defect <= (unsigned long)x);

  // Truncated division is intentional
  return x/bin_size;
}

La boucle est nécessaire pour obtenir une distribution parfaitement uniforme. Par exemple, si on vous donne des nombres aléatoires de 0 à 2 et que vous voulez seulement des nombres de 0 à 1, vous continuez à tirer jusqu'à ce que vous n'obteniez pas un 2; il n'est pas difficile de vérifier que cela donne 0 ou 1 avec une probabilité égale. Cette méthode est également décrite dans le lien que nous avons donné dans leur réponse, bien que codé différemment. J'utilise random() plutôt que Rand() car sa distribution est meilleure (comme l'indique la page de manuel relative à Rand()).

Si vous voulez obtenir des valeurs aléatoires en dehors de la plage par défaut [0, Rand_MAX], vous devez faire quelque chose de délicat. Le plus pratique est peut-être de définir une fonction random_extended() qui extrait n bits (en utilisant random_at_most()) et la renvoie dans [0, 2**n), puis applique random_at_most() avec random_extended() à la place de random() (et 2**n - 1 à la place de Rand_MAX) pour extraire une valeur aléatoire inférieure à 2**n, en supposant que vous avez un type numérique qui peut contenir une telle valeur. Enfin, bien sûr, vous pouvez obtenir des valeurs dans [min, max] en utilisant min + random_at_most(max - min), y compris des valeurs négatives.

159
Ryan Reich

Suite à la réponse de @Ryan Reich, j'ai pensé proposer ma version nettoyée. La vérification de la première limite n'est pas nécessaire étant donné la vérification de la deuxième limite, et je l'ai rendue itérative plutôt que récursive. Il renvoie des valeurs comprises dans la plage [min, max], où max >= min et 1+max-min < Rand_MAX.

unsigned int Rand_interval(unsigned int min, unsigned int max)
{
    int r;
    const unsigned int range = 1 + max - min;
    const unsigned int buckets = Rand_MAX / range;
    const unsigned int limit = buckets * range;

    /* Create equal size buckets all in a row, then fire randomly towards
     * the buckets until you land in one of them. All buckets are equally
     * likely. If you land off the end of the line of buckets, try again. */
    do
    {
        r = Rand();
    } while (r >= limit);

    return min + (r / buckets);
}
32
theJPster

Voici une formule si vous connaissez les valeurs maximales et minimales d'une plage et souhaitez générer des nombres compris entre ces plages:

r = (Rand() % (max + 1 - min)) + min
18
Sattar
unsigned int
randr(unsigned int min, unsigned int max)
{
       double scaled = (double)Rand()/Rand_MAX;

       return (max - min +1)*scaled + min;
}

Voir ici pour d'autres options.

17
nos

Ne vous contentez-vous pas de faire:

srand(time(NULL));
int r = ( Rand() % 6 ) + 1;

% est l'opérateur de module. Essentiellement, il suffit de diviser par 6 et de restituer le reste ... de 0 à 5

11
Armstrongest

Pour ceux qui comprennent le problème de biais mais ne supportent pas le temps d'exécution imprévisible des méthodes basées sur le rejet, cette série produit un entier aléatoire de moins en moins biaisé dans l'intervalle [0, n-1]:

r = n / 2;
r = (Rand() * n + r) / (Rand_MAX + 1);
r = (Rand() * n + r) / (Rand_MAX + 1);
r = (Rand() * n + r) / (Rand_MAX + 1);
...

Pour ce faire, il synthétise un nombre aléatoire de bits i * log_2(Rand_MAX + 1) de haute précision à virgule fixe (où i est le nombre d'itérations) et effectue une multiplication longue de n.

Lorsque le nombre de bits est suffisamment grand par rapport à n, le biais devient extrêmement petit.

Peu importe si Rand_MAX + 1 est inférieur à n (comme dans cette question ), ou si ce n'est pas une puissance de deux, mais il faut veiller à éviter le dépassement d'entier si Rand_MAX * n est grand.

8
sh1

Afin d'éviter le biais modulo (suggéré dans d'autres réponses), vous pouvez toujours utiliser:

arc4random_uniform(MAX-MIN)+MIN

Où "MAX" est la limite supérieure et "MIN" est la limite inférieure. Par exemple, pour les nombres entre 10 et 20:

arc4random_uniform(20-10)+10

arc4random_uniform(10)+10

Solution simple et meilleure que d'utiliser "Rand ()% N".

4
magamig

Voici un algorithme légèrement plus simple que la solution de Ryan Reich:

/// Begin and end are *inclusive*; => [begin, end]
uint32_t getRandInterval(uint32_t begin, uint32_t end) {
    uint32_t range = (end - begin) + 1;
    uint32_t limit = ((uint64_t)Rand_MAX + 1) - (((uint64_t)Rand_MAX + 1) % range);

    /* Imagine range-sized buckets all in a row, then fire randomly towards
     * the buckets until you land in one of them. All buckets are equally
     * likely. If you land off the end of the line of buckets, try again. */
    uint32_t randVal = Rand();
    while (randVal >= limit) randVal = Rand();

    /// Return the position you hit in the bucket + begin as random number
    return (randVal % range) + begin;
}

Example (Rand_MAX := 16, begin := 2, end := 7)
    => range := 6  (1 + end - begin)
    => limit := 12 (Rand_MAX + 1) - ((Rand_MAX + 1) % range)

The limit is always a multiple of the range,
so we can split it into range-sized buckets:
    Possible-Rand-output: 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16
    Buckets:             [0, 1, 2, 3, 4, 5][0, 1, 2, 3, 4, 5][X, X, X, X, X]
    Buckets + begin:     [2, 3, 4, 5, 6, 7][2, 3, 4, 5, 6, 7][X, X, X, X, X]

1st call to Rand() => 13
    → 13 is not in the bucket-range anymore (>= limit), while-condition is true
        → retry...
2nd call to Rand() => 7
    → 7 is in the bucket-range (< limit), while-condition is false
        → Get the corresponding bucket-value 1 (randVal % range) and add begin
    => 3
4
K. Biermann

Bien que Ryan ait raison, la solution peut être beaucoup plus simple en fonction de ce que l’on sait de la source de l’aléatoire. Pour reformuler le problème: 

  • Il existe une source d’aléatoire, générant des nombres entiers dans la plage [0, MAX) avec une distribution uniforme. 
  • Le but est de produire des nombres entiers aléatoires uniformément répartis dans la plage [rmin, rmax]0 <= rmin < rmax < MAX.

D'après mon expérience, si le nombre de cases (ou de "cases") est nettement inférieur à la plage des numéros d'origine, et la source d'origine est cryptographiquement puissante - il n'est pas nécessaire de passer par tout ce rigamarole. une simple division modulo suffirait (comme output = rnd.next() % (rmax+1), si rmin == 0) et produirait des nombres aléatoires répartis uniformément "assez" et sans perte de vitesse. Le facteur clé est la source d’aléatoire (c’est-à-dire les enfants, n’essayez pas cela à la maison avec Rand()).

Voici un exemple/une preuve de son fonctionnement pratique. Je voulais générer des nombres aléatoires de 1 à 22, avec une source cryptographiquement puissante qui produisait des octets aléatoires (basé sur Intel RDRAND). Les résultats sont:

Rnd distribution test (22 boxes, numbers of entries in each box):     
 1: 409443    4.55%
 2: 408736    4.54%
 3: 408557    4.54%
 4: 409125    4.55%
 5: 408812    4.54%
 6: 409418    4.55%
 7: 408365    4.54%
 8: 407992    4.53%
 9: 409262    4.55%
10: 408112    4.53%
11: 409995    4.56%
12: 409810    4.55%
13: 409638    4.55%
14: 408905    4.54%
15: 408484    4.54%
16: 408211    4.54%
17: 409773    4.55%
18: 409597    4.55%
19: 409727    4.55%
20: 409062    4.55%
21: 409634    4.55%
22: 409342    4.55%   
total: 100.00%

C’est aussi proche de l’uniforme que j’ai besoin pour mon but (lancer de dés équitables, générer des livres de code puissants pour les machines de chiffrement de la Seconde Guerre mondiale telles que http://users.telenet.be/d.rijmenants/en/kl-7sim.htm , etc). La sortie ne montre aucun biais appréciable.

Voici la source du générateur de nombres aléatoires (vrais) fort sur le plan cryptographique: Générateur de nombres aléatoires Intel Digital Et un exemple de code produisant des nombres aléatoires de 64 bits (non signés).

int rdrand64_step(unsigned long long int *therand)
{
  unsigned long long int foo;
  int cf_error_status;

  asm("rdrand %%rax; \
        mov $1,%%edx; \
        cmovae %%rax,%%rdx; \
        mov %%edx,%1; \
        mov %%rax, %0;":"=r"(foo),"=r"(cf_error_status)::"%rax","%rdx");
        *therand = foo;
  return cf_error_status;
}

Je l'ai compilé sur Mac OS X avec clang-6.0.1 (straight) et avec gcc-4.8.3 avec l'indicateur "-Wa, q" (car GAS ne prend pas en charge ces nouvelles instructions).

2
Mouse

Comme dit précédemment, modulo n'est pas suffisant car il biaise la distribution. Voici mon code qui masque les bits et les utilise pour s'assurer que la distribution n'est pas faussée.

static uint32_t randomInRange(uint32_t a,uint32_t b) {
    uint32_t v;
    uint32_t range;
    uint32_t upper;
    uint32_t lower;
    uint32_t mask;

    if(a == b) {
        return a;
    }

    if(a > b) {
        upper = a;
        lower = b;
    } else {
        upper = b;
        lower = a; 
    }

    range = upper - lower;

    mask = 0;
    //XXX calculate range with log and mask? nah, too lazy :).
    while(1) {
        if(mask >= range) {
            break;
        }
        mask = (mask << 1) | 1;
    }


    while(1) {
        v = Rand() & mask;
        if(v <= range) {
            return lower + v;
        }
    }

}

Le code simple suivant vous permet de regarder la distribution:

int main() {

    unsigned long long int i;


    unsigned int n = 10;
    unsigned int numbers[n];


    for (i = 0; i < n; i++) {
        numbers[i] = 0;
    }

    for (i = 0 ; i < 10000000 ; i++){
        uint32_t Rand = random_in_range(0,n - 1);
        if(Rand >= n){
            printf("bug: Rand out of range %u\n",(unsigned int)Rand);
            return 1;
        }
        numbers[Rand] += 1;
    }

    for(i = 0; i < n; i++) {
        printf("%u: %u\n",i,numbers[i]);
    }

}
1
Andrew Chambers

Renverra un nombre à virgule flottante compris dans la plage [0,1]:

#define Rand01() (((double)random())/((double)(Rand_MAX)))
0
Geremia