1.6- Génération de nombres aléatoires

publicité
1.6- Génération de nombres aléatoires
1- Le générateur aléatoire disponible en C++
2 – Création d'un générateur aléatoire uniforme sur un intervalle
3- Génération de valeurs aléatoires selon une loi normale
4- Génération de valeurs aléatoire selon une distribution ad hoc
5- Génération d'un ordre aléatoire
On introduit des fonctions aléatoires classiques pour lesquelles on fournit ensuite un fichier qui les implémente.
1 Le générateur aléatoire disponible en C++
Génération de valeurs aléatoires
En toute rigueur, dans ce qui suit, on devrait parler de génération de nombres pseudo-aléatoires.
Les fonctions nécessaires à la génération aléatoire sont contenues dans la librairie <cstdlib>. La génération de nombres
aléatoires implique d'abord d'initialiser la racine (seed) par l'instruction :
srand(time(0));
time est une fonction de la librairie <ctime> : sans entrer dans les détails, time(0) est la date du jour exprimée en nombre de
secondes depuis le 1er janvier 1970. Le chiffre va permettre d'initialiser une séquence de nombres pseudo-aléatoires. Tous les
programmes qui comprennent la génération de nombres aléatoires contiendront cette instruction ou des instructions
d'initialisation de la racine des nombres aléatoires, celle qui est présentée ici étant la plus utilisée.
Par suite, on utilise la fonction rand() qui renvoie (selon un tirage uniforme) un nombre entier entre 0 et un entier maximal
susceptible de varier en fonction des implémentations. Un premier exemple de génération de nombre aléatoire :
#include <cstdlib>
#include <ctime>
#include <iostream>
int main(){
srand(time(0));
for(int i=0;i<5;i++)
std::cout << "Nombre aleatoire : " << rand() << "\n";
}
Les nombres qui sont tirés sont entre 0 et RAND_MAX (inclus) qui est une constante entière définie dans <cstdlib>.
Tirage de valeurs aléatoires selon une loi uniforme sur [0:1]
Pour tirer des nombres aléatoires compris dans [0:1] :
#include <cstdlib>
#include <ctime>
#include <iostream>
int main(){
srand(time(0));
for(int i=0;i<5;i++)
std::cout << "Nombre aleatoire : " << rand()/(double)RAND_MAX << "\n";
}
On peut donc créer une fonction de génération de nombres aléatoires selon une loi uniforme sur [0:1] :
#include <cstdlib>
#include <ctime>
#include <iostream>
double randomUniform()
{
return
}
int main(){
rand()/(double)RAND_MAX;
srand(time(0));
for(int i=0;i<5;i++)
std::cout << "Nombre aleatoire : " << randomUniform() << "\n";
}
2 Création d'un générateur aléatoire uniforme sur un intervalle
Tirage uniforme de réels sur un intervalle quelconque
A partir de la fonction randomUniform() qui a été définie plus haut, on peut définir une fonction plus complexe qui tire des
valeurs aléatoires dans un intervalle [a:b] de réels. Pour se faire, on surcharge la fonction randomUniform en passant des
paramètres d'intervalle :
#include <cstdlib>
#include <ctime>
double randomUniform()
{
return
rand()/(double)RAND_MAX;
}
double randomUniform(double a,double b)
{
return (b-a)*randomUniform()+a;
}
Cette fonction fait bien ce qui a été annoncé :
•tirageAleatoireUniforme01() est tiré dans [0:1]
•Donc (b-a)*tirageAleatoireUniforme01() est dans [0;b-a]
•Donc (b-a)*tirageAleatoireUniforme01()+a est dans [a;b]
Note : la fonction randomUniform(double a,double b) s'exécutera quand même, même si a>b. On pourrait envisager une
fonction plus complexe qui ne s'exécuterait qu'à la condition que a<=b.
Tirage uniforme d'entiers sur un intervalle quelconque
Pour tirer des entiers dans [0;n-1], on pourrait produire une fonction assez rapidement :
int randomInteger(int n)
{
return rand()%n;
}
Certes, il est évident que la fonction renvoie des entiers dans [0:n-1]. Néanmoins, cette fonction est biaisée. En effet,
supposons que RAND_MAX soit 10 et que l'on veuille faire un tirage sur [0:2], ie avec n=3. Comme RAND_MAX est 10, cela
signifie que les valeurs que rand() peut renvoyer sont 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 et 10.
•Lorsque rand() renvoie 0, 3, 6, 9, randomInteger() renvoie 0.
•Lorsque rand() renvoie 1, 4, 7, 10, randomInteger() renvoie 1.
•Lorsque rand() renvoie 2, 5, 8, randomInteger() renvoie 2.
Donc la fonction renvoie 0 avec une probabilité 4/11, 1 avec une probabilité 4/11 et 2avec une probabilité 3/11. Donc la fonction
ne tire pas les entiers de manière uniforme sur [0:2], sauf dans le cas où RAND_MAX est un entier premier (à vérifier).
Pour tirer des entiers de manière uniforme, on préférera une fonction de la forme randomInteger ci dessous :
#include <cstdlib>
#include <ctime>
#include <iostream>
double randomUniform()
{
return
rand()/(double)RAND_MAX;
}
double randomUniform(double a,double b)
{
return (b-a)*randomUniform()+a;
}
long arrondir(double x)//arrondi à l'entier le plus proche
{
long a=(long)x;
if(a+0.5<=x)
return a+1;
else
return a;
}
long randomInteger(long a,long b)
{
double x=randomUniform(a-0.5,b+0.5);
return arrondir(x);
}
int main(){
srand(time(0));
long a=20;
long b=40;
unsigned long nombreTests=1000000;
unsigned long compte[b+1];
for(int i=0;i<=b;i++)
{
compte[i]=0;
}
for(int i=0;i<nombreTests;i++)
{
compte[randomInteger(a,b)]++;
}
double somme=0;
for(int i=0;i<=b;i++)
{
double x=compte[i]/(double)nombreTests;
somme+=x;
std::cout << i << " " << x << "\n";
}
std::cout << "La somme : " << somme << "\n";
}
3 Génération de valeurs aléatoires selon une loi normale
Plusieurs méthodes sont possibles pour tirer des normes selon une loi normale à partir d'un générateur uniforme
pseudo-aléatoire. La méthode la plus connue est celle de Box-Muller. On renvoie à l'exposé Wikipedia sur cette méthode pour
son fonctionnement. On donne une version optimisée de cette fonction :
void polarMarsaglia(double *x1,double *x2)
{
double W,V1,V2;
do
{
double U1=randomUniform();
double U2=randomUniform();
V1=2*U1-1;
V2=2*U2-1;
W=V1*V1+V2*V2;
}while(W>1);
/*Apres cette étape, V1 et V2 ont été tirés de manière uniforme sur [-1;1] / V2²+V1²<=1*/
double W_function=sqrt(-2*log(W)/W);
*x1=V1*W_function;
*x2=V2*W_function;
}
Cette fonction va affecter à x1 et x2 des valeurs pseudo-aléatoires tirés selon une loi gaussienne. Il est possible de tester cette
fonction en regardant quelle distribution des données elle produit :
#include <cstdlib>
#include <cmath>
#include <iostream>
#include <fstream>
const double PI=3.14159265358979323846;
using namespace std;
double randomUniform()
{
return
rand()/(double)RAND_MAX;
}
void polarMarsaglia(double *x1,double *x2)
{
double W,V1,V2;
do
{
double U1=randomUniform();
double U2=randomUniform();
V1=2*U1-1;
V2=2*U2-1;
W=V1*V1+V2*V2;
}while(W>1);
/*Apres cette étape, V1 et V2 ont été tirés de manière uniforme sur [-1;1] / V2²+V1²<=1*/
double W_function=sqrt(-2*log(W)/W);
*x1=V1*W_function;
*x2=V2*W_function;
}
int main(){
/*INITIALISATION DES STRUCTURES DE DONNEES*/
srand(time(0));
int taille=1200;
double nombreTests=1000000;
long compteur1[taille];
for(int i=0;i<taille;i++)
compteur1[i]=0;
/*TIRAGE DE VALEUR ALEATOIRE*/
for(int i=0;i<nombreTests;i++)
{
double x1,x2;
polarMarsaglia(&x1,&x2);
compteur1[(int)(x1*100+600)]++;
compteur1[(int)(x2*100+600)]++;
}
/*AFFICHAGE DE LA DISTRIBUTION*/
ofstream f("c:\\CPP\\testDistribution.txt");
for(int i=0;i<1200;i+=1)
{
double valeur=(i-600)/(double)100;
if(compteur1[i]>0)
f << valeur << " " << compteur1[i]/nombreTests << "\n";
}
f.close();
}
Ici, l'instruction ofstream f("c:\\CPP\\testDistribution.txt") dépasse le cadre des cours qui ont été rédigés précédemment. Il s'agit
de créer un objet de flux vers fichier. Lors de la création de cet objet, un fichier testDistribution.txt est crée dans le répertoire
c:\CPP (il faut donc adapter le chemin en fonction de la machine). L'instruction :
f << valeur << " " << compteur1[i]/nombreTests << "\n";
demande l'écriture de ce qui suit sur le fichier, à l'instar de l'utilisation de std::cout qui demande l'écriture sur l'invite de
commande.
Enfin, l'instruction f.close() permet de fermer le fichier sur lequel on aura écrit la distribution.
La distribution que l'on obtient à partir de la fonction proposée et du fichier proposé :
D'autres algortihmes sont possibles, dont l'algorithme de Ziggurat, ils ne sont pas évoqués ici cependant.
4 Génération de valeurs aléatoires selon une distribution ad hoc
A partir d'un tableau passé en paramètre et qui représente une distribution de probabilité, on veut effectuer des tirages
aléatoires. Soit l'exemple d'untel tableau : double t[ ]={0.5,0.03,0.05,0.3,0.01,0.01,0.1} : ce tableau est une distribution de
probabilité : la somme des t[i] est égale à 1 et chaque t[i] est compris dans [0:1]. On peut vouloir faire des tirages, de sorte que
0 soit tiré avec une probabilité 0.5, que 1 soit tiré avec une probabilité 0.03 etc...
On peut utiliser la fonction tirageSelonDistribution proposée dans le code qui suit, en notant qu'on ne vérifie pas, pour
cette fonction, que la somme des probabilités soit égale à 1 : on ne vérifie pas qu'on ait une distribution. La fonction main()
propose un test de cette fonction.
#include <cstdlib>
#include <ctime>
#include <iostream>
double randomUniform()
{
return
rand()/(double)RAND_MAX;
}
int tirageSelonDistribution(double distrib[],int nbElts)
{
double x=randomUniform();
double borneMin=0,borneMax=distrib[0];
int indiceRenvoye=-1;
if(borneMin<=x && x<=borneMax)
indiceRenvoye=0;
for(int rg=1;rg<nbElts && indiceRenvoye==-1;rg++)
{
borneMin+=distrib[rg-1];
borneMax+=distrib[rg];
if(borneMin<=x && x<=borneMax)
indiceRenvoye=rg;
}
return indiceRenvoye;
}
int main(){
srand(time(0));
double distrib[]={0.5,0.03,0.05,0.3,0.01,0.01,0.1};
unsigned long nombreTests=1000000;
unsigned long compte[7];
for(int i=0;i<7;i++)
{
compte[i]=0;
}
for(int i=0;i<nombreTests;i++)
{
compte[tirageSelonDistribution(distrib,7)]++;
}
double somme=0;
for(int i=0;i<7;i++)
{
double x=compte[i]/(double)nombreTests;
somme+=x;
std::cout << i << " " << x << "\n";
}
std::cout << "La somme : " << somme << "\n";
}
A noter qu'à partir de la fonction tirageSelonDistribution, on peut répondre à une série de problèmes : tirer des noms au hasard
avec pour chaque nom une probabilité qui lui est associée...
5 Génération d'un ordre aléatoire
Le problème est ici le suivant : on dispose d'une collection de valeurs ou d'objets, on veut modifier la collection de
manière à ce que les valeurs ou objets soient mélangés dans un ordre aléatoire. A noter que ce problème se ramène à un
problème plus simple. On note N la taille de la collection, vouloir mélanger les éléments de la collection est équivalent à
produire une séquence aléatoire où chaque nombre entier de 0 à N-1 apparaît une et une seule fois. Il existe N! séquences
possibles. La fonction ordreAleatoire(t[N]) remplit le tableau t avec une séquence aléatoire.
int[] ordreAleatoire(int N){
int tableauDesEntiers[N];
double valeursAleatoires[N];
for(int i=0;i<N;i++)
{
tableauDesEntiers[i]=i;
valeursAleatoires[i]=randomUniform();
}
bool b=true;
while(b)
{
b=false;
for(int rang=1;rang<N;rang++)
{
if(valeursAleatoires[i-1]>valeursAleatoires[i])
{
int inter=tableauDesEntiers[i];
tableauDesEntiers[i]=tableauDesEntiers[i-1];
tableauDesEntiers[i-1]=inter;
double inter2=valeursAleatoires[i];
valeursAleatoires[i]=valeursAleatoires[i-1];
valeursAleatoires[i-1]=inter2;
b=true;
}
}
}
}
Attention ici, la procédure de tri pourrait être améliorée : on a en effet choisi d'adopter une méthode de tri à bulle pour simplifier
la présentation de la fonction quand un algorithme de tri-fusion aurait beaucoup amélioré la fonction en rendant sa lecture plus
confuse cependant.
Conclusions
Un
fichier
contient
toutes
les
fonctions
qui
viennent
d'être
présentées
:
http://rdorat.free.fr/Enseignement/CPP/PI/utilitaireAlea.cpp. On peut utiliser les fonctions de ce fichier dans un autre fichier, à
partir du moment où
•
dans cet autre fichier, on met include "utilitaireAlea.cpp"
•
on met le fichier utilitaireAlea.cpp dans le même répertoire que le nouveau fichier.
C'est un premier exemple de répartition du code en plusieurs fichiers.
Les fonctions présentées dans le présent cours et dans le fichier utilitaireAlea.cpp permettent de résoudre un assez
grand nombre des problèmes de générations aléatoire, les exercices liés à ce cours illustrent quelques utilisations.
Téléchargement