À la découverte de la méthode
L'une des méthodes les plus utilisées actuellement est
la méthode des congruences linéaires.
Là encore, des grands mots pour quelque chose de pas si compliqué que ça ! Décortiquons ensemble cette expression barbare.
Congruence : dans notre cas, cela se réfère à ce qui a rapport avec le reste de la division entière (vous savez, le signe '%' qui ne sert à rien ?).
Linéaire : en simplifiant, cela revient à dire que l'on ne fait que des opérations de type addition et multiplication. Le type d'opération "mise au carré" et "logarithme" n'existe pas (ce n'est pas très grave si vous ne comprenez pas cette phrase, c'est juste pour la culture).
De façon plus concrète, voici une formule permettant de calculer une suite de nombres aléatoires :
Code : Autre1
| nouveau_nombre= (a*ancien_nombre + b) % m |
Petit rappel important pour la suite, "a%n" signifie le reste de la division de a par n. Par exemple 11%3=2 : 11 divisé par 3 égal 3, il reste 2 ; et 12%3=0 : 12 divisé par 3 égal 4, il reste 0.
Pour traduire cela avec des mots, ça donne : pour trouver le nouveau nombre aléatoire, il faut :
1. Prendre celui que l'on vient de calculer, le multiplier par a puis ajouter b ;
2. Si le nombre obtenu est plus grand que m, lui enlever m jusqu'à ce qu'il soit plus petit.
Rien ne vous choque ? Pas un petit problème de variable non définie ? Ah si quand même ! La formule dit qu'il faut prendre le nombre que l'on vient de calculer, mais si on n'a encore rien calculé, on prend quoi ? C'est là un sujet épineux dont on pourrait débattre longtemps, mais nous n'allons pas nous étendre sur ce sujet. En général, comme première valeur, on prend le
timestamp ; cette première valeur s'appelle
la graine (
seed en anglais).
Cela vous explique pourquoi, en C, vous devez initialiser rand par srand (qui doit vouloir dire seedrand), en passant en argument le timestamp actuel ! Si vous ne le faites pas, la suite de nombres générée est toujours la même... c'est fâcheux pour un jeu de hasard...
Dans des langages plus évolués, comme Java ou Python, on n'a pas à faire cette initialisation, le langage s'en charge lui-même !
Un premier code
Maintenant que vous avez la théorie, je vous propose de coder votre propre générateur aléatoire. L'exercice consiste à écrire un programme (tout dans le main, on s'embêtera plus tard avec des jolies structures) qui affiche 15 nombres aléatoires.
Vous devez réfléchir comment coder la formule que j'ai donnée au-dessus (pour les valeurs de a, b et m, mettez un peu au hasard pour l'instant, en bidouillant pour que ça marche mieux si besoin, mais gardez-les relativement petits).
Attention, prêts ? C'est parti !
Secret (cliquez pour afficher)
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12 | #include <iostream>
#include <ctime>
using namespace std;
main(){
unsigned int a=3,b=3,m=5; // Définition des valeurs
unsigned int nombre = time(NULL); // Définition de la graine
for(int i=0;i<15;i++){
nombre = (a*nombre+ b) % m; // Calcul effectif
cout<<nombre<<" "; // Affichage
}
}
|
Voilà, je pense que ça ne présentait pas de grandes difficultés ; si oui, déroulez le programme sur papier (par exemple 1
e boucle : nombre=10, nombre*a=30..., 2
e boucle : nombre =..., ) en ayant la "formule" donnée plus haut sous les yeux. Pour ceux qui ont utilisé un tableau pour stocker toutes les valeurs, ce n'était pas utile, et je dirais même une erreur. Là, ça va, il n'y avait que 15 nombres à générer. Mais imaginez deux secondes que vous produisiez des nombres pour une application qui demande des milliards de nombres aléatoires ? La mémoire ne sera jamais assez grande pour conserver tous ces nombres !
Maximisons la période !
Maintenant, analysons la sortie de ce programme :
Code : Console | 4 0 3 2 4 0 3 2 4 0 3 2 4 0 3 |
J'avais pris m = 5, donc logiquement, toutes mes valeurs sont entre 0 et 4 (inclus).
On peut déjà constater 2 choses :
- les nombres suivent une certaine séquence qui se répète (ici 4,0,3,2). Quelles que soient les valeurs que l'on prend pour a, b et m, nous aurons toujours ce comportement ; cela peut paraître gênant, vu que le générateur n'est du coup plus du tout aléatoire. En pratique m est très grand, donc la séquence est également très longue ;
- il manque un nombre dans la séquence : 1 ; le générateur est donc clairement faussé ! En effet, pour un générateur produisant des nombres entre 0 et 4, on doit s'attendre à une moyenne de 2 ; ici, si on fait le calcul, on a (0+2+3+4)/4 = 2.25 comme moyenne !
Cela m'amène à vous parler de la
période. La période correspond à
la longueur de la séquence générée; dans notre exemple, la période est de 4. Evidemment, plus cette période est grande, meilleur est le générateur.
Dans le cas de cette méthode, on dit que la
période est maximale si elle est égale à m. Vous remarquerez facilement qu'avec cette méthode, chaque nombre n'est "visité" qu'une fois
au plus ; si le calcul "repasse" par le même nombre, cela signifie qu'on a fait le tour de la séquence.
Avoir une période maximale signifie donc que
tous les nombres entre 0 et m-1 (inclus) sont "visités"
une seule fois. Si la période est maximale, la moyenne des nombres sortis sera de (m-1)/2 (je ne ferai pas le calcul précis ici, mais on peut voir "intuitivement" que ça marche), on a donc un bon argument en faveur de la validité du générateur.
Ceci est un des aspects fondamentaux de la génération algorithmique de nombres aléatoires. Si vous devez retenir quelque chose, c'est ça ! Les formules que je donne, vous pourrez les utiliser sans rien comprendre à tout ça, mais dans ce cas-là, rien ne sert de lire ce tuto. Les fonctions rand et compagnie feront très bien l'affaire.
Au passage, vous remarquerez que si la première valeur générée est 1, toutes les valeurs suivantes seront les mêmes (= 1) ! On voit donc clairement ici une limite de ce générateur.
Ok ! Donc pour avoir une période maximale, il suffit que je teste des valeurs a, b et m et que je regarde si tous les chiffres sont "visités" ?
On pourrait faire comme ça... Mais ça serait trèèèèèèèès long si m est très grand (ce qui est le cas en pratique). Heureusement des gens très intelligents ont réfléchi au problème et nous ont donné un
critère pour que la période soit maximale. Ce critère s'écrit sous la forme de trois conditions.
La période est maximale si et seulement si :
- b et m sont premiers entre eux ;
- si m est un multiple de 4, alors a%4 = 1 ;
- pour tous les nombres premiers p diviseurs de m, on a a%p =1 .
Mais la 2e condition, elle ne sert à rien ! Ce n'est qu'un cas particulier de la 3e !
Erreur ! La troisième nous parle des diviseurs
premiers de m. La deuxième est effective si 4 est un diviseur de m. Mais 4 n'est pas premier (eh oui 4=2*2).
Si vous n'avez absolument jamais entendu parler de nombres premiers, ne vous attardez pas dessus, retenez juste qu'il suffit de vérifier certaines conditions sur a, b et m pour avoir une période maximale.
Pour les autres qui en savent un peu plus, essayez vraiment de voir à quoi ces conditions correspondent, et tentez de montrer que les valeurs que l'on a prises au-dessus ne donnent pas une période maximale.
Pour les très forts en arithmétique, vous pouvez essayer de démontrer ce résultat (sans aller voir la démo sur Wikipédia ou équivalent, cela va sans dire...).
Si vous voulez voir comment vérifier effectivement ce critère, jetez un coup d'oeil au QCM, la correction est assez complète.
Maintenant, nous pouvons former un générateur à période maximale. Par exemple les valeurs :
a=5, b=3, m=8 vérifient les 3 conditions ci-dessus et produisent la sortie suivante (c'est exactement le même programme, il suffit de changer les valeurs dans la première ligne du main) :
Code : Console | 0 3 2 5 4 7 6 1 0 3 2 5 4 7 6 1 |
Tous les chiffres sont bien parcourus, la période est maximale, YOUHOU !!! Nous avons notre premier générateur aléatoire efficace !!!
Mais attendez, ce n'est pas fini ! Les nombres que nous utilisons là sont très petits. Quand on veut utiliser des nombres plus grands, d'autres problèmes apparaissent.
Minimisons la corrélation !
Encore un mot compliqué, mais comme d'hab, une signification pas bien sorcière : la corrélation entre deux nombres aléatoires peut être vue comme la capacité, connaissant un nombre, de déterminer le second.
Un exemple pour comprendre : on prend
a=230 +1,
b=3 et
m=231. La sortie donne un truc du genre :
Code : Console | 1073741832 1073741835 14 17 1073741844 1073741847 26 29 1073741856 1073741859 38 41 |
Si on fait attention, on remarque qu'on passe d'une valeur à l'autre en ajoutant 3 (écrivez à la main ce que ça donne, vous verrez). Les nombres sont donc fortement corrélés.
Il va donc falloir encore une fois trouver des critères qui nous permettent d'avoir une faible corrélation. Et encore une fois, on dit merci aux matheux :
- m doit être le plus grand possible (en général, on veut profiter de tous les entiers disponibles, donc on prend 232-1 ou 231). C'est un truc qui nous arrange bien car cela permet d'avoir une période très longue ;
- b doit être "petit" par rapport à a et m ;
- a doit être proche de la racine carrée de m.
Dans l'exemple que j'ai pris juste au-dessus (
a=230 +1,
b=3 et
m=231),
a est très éloigné de la racine de
m ; grâce à ce critère, nous aurions pu voir immédiatement que ce générateur était pourri !
Si on prend m=232-1, il n'est pas nécessaire d'effectuer le modulo explicitement car ce nombre correspond au plus grand entier non signé en C (sur une machine 32 bits). Le modulo se fera donc de lui-même ! Cependant, 232-1 n'est pas forcément très pratique à utiliser, et on préfère des fois prendre 231 qui n'a qu'un seul diviseur (2) ce qui simplifie l'étude du générateur. Nous garderons donc le %m ; de plus la définition des entiers varie d'une plate-forme à l'autre, on ne peut pas supposer froidement que les entiers sont codés sur 32 bits !
Quelques bons générateurs
Voilà, maintenant, vous savez pratiquement tout sur les générateurs congruentiels linéaires.
Donc pour résumer, pour avoir un bon générateur, il faut avoir une période maximale
et il faut que les coefficients respectent quelques règles. Si vous oubliez une des conditions, le générateur sera médiocre.
Je vais terminer cette partie en vous invitant à aller voir quelques "bons" générateurs utilisés par certaines librairies (j'ai choisi l'article anglais car l'article français est beaucoup moins bien fait) :
article anglais sur les générateurs congruentiels linéaires sur Wikipédia (le coefficient que j'ai appelé 'b' est appelé 'c' dans cet article).