web-dev-qa-db-fra.com

Java.util.Random est-il vraiment aussi aléatoire? Comment puis-je générer 52! (factorielle) des séquences possibles?

J'utilise Random (Java.util.Random) pour mélanger un jeu de 52 cartes. Il y en a 52! (8.0658175e + 67) possibilités. Pourtant, j'ai découvert que la graine de Java.util.Random est une long, qui est beaucoup plus petite à 2 ^ 64 (1.8446744e + 19).

A partir de là, je me demande si Java.util.Randomest-ce vraiment aléatoire; est-il réellement capable de générer tous les 52! possibilités?

Sinon, comment puis-je générer de manière fiable une meilleure séquence aléatoire pouvant produire les 52! possibilités?

200
Serj Ardovic

La sélection d'une permutation aléatoire nécessite simultanément plus et moins de caractère aléatoire que ce que votre question implique. Laissez-moi expliquer.

La mauvaise nouvelle: il faut plus de hasard.

Le défaut fondamental de votre approche est qu’il essaie de choisir entre ~ 2226 possibilités utilisant 64 bits d'entropie (la graine aléatoire). Pour choisir équitablement entre ~ 2226 possibilités que vous allez devoir trouver un moyen de générer 226 bits d'entropie au lieu de 64.

Il existe plusieurs façons de générer des bits aléatoires: matériel dédié , instructions de la CP , interfaces du système d'exploitation , services en ligne . Votre question repose déjà sur une hypothèse implicite selon laquelle vous pouvez en quelque sorte générer 64 bits. Il vous suffit donc de faire ce que vous alliez faire, seulement quatre fois, et de faire don des bits excédentaires à une association caritative. :)

La bonne nouvelle: il faut moins de hasard.

Une fois que vous avez ces 226 bits aléatoires, le reste peut être effectué de manière déterministe et ainsi les propriétés de Java.util.Random peuvent être rendues non pertinentes . Voici comment.

Disons que nous générons tous les 52! permutations (ours avec moi) et les trier lexicographiquement.

Pour choisir l'une des permutations, il suffit d'un seul entier aléatoire entre 0 et 52!-1. Cet entier est notre 226 bits d'entropie. Nous l'utilisons comme index dans notre liste triée de permutations. Si l'index aléatoire est uniformément distribué, non seulement vous êtes assuré que toutes les permutations peuvent être choisies, elles le seront également de manière égale (ce qui est une garantie plus forte que celle posée par la question) .

Maintenant, vous n'avez pas réellement besoin de générer toutes ces permutations. Vous pouvez en produire un directement, étant donné sa position choisie au hasard dans notre liste hypothétique triée. Cela peut être fait dans O (n2) temps en utilisant le Lehmer[1] code (voir aussi permutations de numérotation et système de numérotation factoriadique ). Le n ici est la taille de votre deck, c’est-à-dire 52.

Il y a une implémentation C dans ceci réponse StackOverflow . Plusieurs variables entières pourraient déborder pour n = 52, mais heureusement, dans Java, vous pouvez utiliser Java.math.BigInteger. Le reste des calculs peut être transcrit presque tel quel:

public static int[] shuffle(int n, BigInteger random_index) {
    int[] perm = new int[n];
    BigInteger[] fact = new BigInteger[n];
    fact[0] = BigInteger.ONE;
    for (int k = 1; k < n; ++k) {
        fact[k] = fact[k - 1].multiply(BigInteger.valueOf(k));
    }

    // compute factorial code
    for (int k = 0; k < n; ++k) {
        BigInteger[] divmod = random_index.divideAndRemainder(fact[n - 1 - k]);
        perm[k] = divmod[0].intValue();
        random_index = divmod[1];
    }

    // readjust values to obtain the permutation
    // start from the end and check if preceding values are lower
    for (int k = n - 1; k > 0; --k) {
        for (int j = k - 1; j >= 0; --j) {
            if (perm[j] <= perm[k]) {
                perm[k]++;
            }
        }
    }

    return perm;
}

public static void main (String[] args) {
    System.out.printf("%s\n", Arrays.toString(
        shuffle(52, new BigInteger(
            "7890123456789012345678901234567890123456789012345678901234567890"))));
}

[1] Ne pas confondre avec Lehrer . :)

151
NPE

Votre analyse est correcte: ensemencer un générateur de nombres pseudo-aléatoires avec une graine spécifique doit donner la même séquence après un brassage, en limitant le nombre de permutations que vous pourriez obtenir à 264. Cette assertion est facile à vérifier expérimentalement en appelant Collection.shuffle à deux reprises, en passant un objet Random initialisé avec le même germe et en observant que les deux combinaisons aléatoires sont identiques.

Une solution à ce problème consiste alors à utiliser un générateur de nombres aléatoires permettant une graine plus volumineuse. Java fournit SecureRandom classe pouvant être initialisée avec le tableau byte[] de taille pratiquement illimitée. Vous pouvez ensuite passer une instance de SecureRandom à Collections.shuffle pour terminer la tâche:

byte seed[] = new byte[...];
Random rnd = new SecureRandom(seed);
Collections.shuffle(deck, rnd);
61
dasblinkenlight

En général, un générateur de nombres pseudo-aléatoires (PRNG) ne peut pas choisir parmi toutes les permutations d'une liste de 52 éléments si sa longueur d'état est inférieure à 226 bits.

Java.util.Random implémente un algorithme avec un module de 248; ainsi, sa longueur d’état n’est que de 48 bits, soit bien moins que les 226 bits que j’ai mentionnés. Vous devrez utiliser un autre PRNG avec une longueur d'état plus grande, plus précisément avec une période de 52 facteurs ou plus.

Voir aussi "Mélanger" dans mon article sur les générateurs de nombres aléatoires .

Cette considération est indépendante de la nature du PRNG; cela s'applique également aux PRNG cryptographiques et non cryptographiques (bien entendu, les PRNG non cryptographiques sont inappropriés lorsque la sécurité de l'information est impliquée).


Bien que Java.security.SecureRandom autorise la transmission de germes de longueur illimitée, l'implémentation SecureRandom peut utiliser un PRNG sous-jacent (par exemple, "SHA1PRNG" ou "DRBG"). Et cela dépend de la période (et, dans une moindre mesure, de la longueur) du PRNG, s'il est capable de choisir parmi 52 permutations factorielles. (Notez que je définis "longueur d'état" comme "taille maximale de la graine qu'un PRNG peut prendre pour initialiser son état sans raccourcir ou compresser cette graine ").

26
Peter O.

Permettez-moi de m'excuser par avance, car c'est un peu difficile à comprendre ...

Tout d’abord, vous savez déjà que Java.util.Random n’est pas complètement aléatoire. Il génère des séquences de manière parfaitement prévisible à partir de la graine. Vous avez tout à fait raison, étant donné que la graine ne fait que 64 bits de long, elle ne peut générer que 2 ^ 64 séquences différentes. Si vous deviez en quelque sorte générer 64 bits aléatoires réels et les utiliser pour sélectionner une graine, vous ne pourriez pas utiliser cette graine pour choisir au hasard entre tous sur les 52! séquences possibles avec probabilité égale.

Cependant, ce fait est sans conséquence tant que vous n'allez réellement générer plus de 2 ^ 64 séquences, tant qu'il n'y a rien de "spécial" ou de "remarquablement spécial" dans la 2 ^ 64 séquences qu’il peut générer.

Disons que vous avez un bien meilleur PRNG qui utilise des semences de 1000 bits. Imaginez que vous disposiez de deux manières pour l'initialiser: une manière de l'initialiser en utilisant la graine entière et une autre pour la hacher en 64 bits avant de l'initialiser.

Si vous ne saviez pas quel initialiseur était lequel, pourriez-vous écrire un test pour les distinguer? À moins que vous n'ayez (pas) eu la chance d'initialiser le mauvais avec le identique 64 bits deux fois, la réponse est non. Vous ne pouvez pas faire la distinction entre les deux initialiseurs sans une connaissance détaillée de certaines faiblesses de la mise en œuvre spécifique de PRNG.

Vous pouvez également imaginer que la classe Random possède un tableau de 2 ^ 64 séquences sélectionnées de manière complète et aléatoire à un moment donné, et que la graine n'est qu'un index dans ce tableau.

Donc, le fait que Random utilise seulement 64 bits pour sa graine est en fait pas nécessairement un problème statistique, tant qu’il n’ya aucune chance significative que vous utilisiez la même graine deux fois.

Bien sûr, à des fins cryptographiques, un germe 64 bits n’est tout simplement pas suffisant, car il est réalisable sur un système d’utiliser deux fois le même germe.

MODIFIER:

Je devrais ajouter que, même si tout ce qui précède est correct, la mise en œuvre réelle de Java.util.Random n’est pas impressionnante. Si vous écrivez un jeu de cartes, utilisez peut-être l'API MessageDigest pour générer le hachage SHA-256 de "MyGameName"+System.currentTimeMillis(), et utilisez ces bits pour mélanger le paquet. Par l’argument ci-dessus, tant que vos utilisateurs ne jouent pas vraiment, vous n’aurez pas à vous inquiéter que currentTimeMillis retourne longtemps. Si vos utilisateurs sont vraiment jouer, utilisez SecureRandom sans aucune graine.

18
Matt Timmermans

Je vais prendre un point de vue différent sur ce point. Vous avez raison sur vos hypothèses - votre PRNG ne pourra pas atteindre les 52! possibilités.

La question est: quelle est l'ampleur de votre jeu de cartes?

Si vous faites un simple jeu de style klondike? Alors vous n'avez absolument pas besoin tous les 52! possibilités. Au lieu de cela, regardez comme ceci: un joueur aura 18 quintillion parties distinctes. Même en tenant compte du "problème d'anniversaire", ils devraient jouer des milliards de mains avant de rencontrer le premier jeu dupliqué.

Si vous faites une simulation de monte-carlo? Alors vous êtes probablement ​​d'accord. Vous devrez peut-être faire face à des artefacts dus au "P" dans PRNG, mais vous ne rencontrerez probablement pas de problèmes simplement en raison d'un espace de départ réduit (encore une fois, vous envisagez des milliards de possibilités uniques.) D'un autre côté, si vous travaillez avec un nombre d'itérations élevé, alors, oui, votre espace de stockage bas peut être un facteur décisif.

Si vous créez un jeu de cartes multijoueur, en particulier s'il y a de l'argent en jeu? Ensuite, vous devrez faire des recherches sur Google pour savoir comment les sites de poker en ligne ont traité le même problème que vous leur demandez. sur. Parce que bien que le problème de l’espace de stockage minimal ne soit pas perceptible pour le joueur moyen, c’est exploitable s’il vaut l’investissement de temps. (Les sites de poker sont tous passés par une phase où leurs PRNG ont été "piratés", laissant quelqu'un voir les cartes cachées de tous les autres joueurs, simplement en déduisant la graine des cartes exposées.) Si telle est la situation dans laquelle vous vous trouvez, - ne pas simplement trouver un meilleur PRNG - vous devrez le traiter aussi sérieusement qu'un problème de cryptographie.

10
Kevin

Solution courte qui est essentiellement la même chose que dasblinkenlight:

// Java 7
SecureRandom random = new SecureRandom();
// Java 8
SecureRandom random = SecureRandom.getInstanceStrong();

Collections.shuffle(deck, random);

Vous n'avez pas besoin de vous soucier de l'état interne. Explication longue pourquoi:

Lorsque vous créez de cette manière une instance SecureRandom, elle accède à un générateur de nombre aléatoire réel spécifique au système d'exploitation. Il s'agit soit d'un pool d'entropie où l'on accède à des valeurs contenant des bits aléatoires (par exemple, pour une minuterie en nanosecondes, la précision en nanosecondes est essentiellement aléatoire), ou d'un générateur de numéro de matériel interne.

Cette entrée (!) Qui peut encore contenir des traces parasites est introduite dans un hachage cryptographiquement fort qui supprime ces traces. C'est la raison pour laquelle ces CSPRNG sont utilisés, pas pour créer ces numéros eux-mêmes! La SecureRandom a un compteur qui indique combien de bits ont été utilisés (getBytes(), getLong() etc.) et remplit la SecureRandom avec des bits d'entropie si nécessaire =.

En bref: oubliez simplement les objections et utilisez SecureRandom comme véritable générateur de nombres aléatoires.

9
Thorsten S.

Si vous considérez le nombre comme un simple tableau de bits (ou d'octets), vous pourriez peut-être utiliser les solutions (sécurisées) Random.nextBytes suggérées dans cette question dépassement de pile , puis mapper le tableau dans _new BigInteger(byte[]).

4
IvanK

Un algorithme très simple consiste à appliquer SHA-256 à une séquence d'entiers incrémentant à partir de 0. (Un sel peut être ajouté si désiré pour "obtenir une séquence différente".) Si nous supposons que la sortie de SHA-256 est "aussi bonne que" des entiers uniformément répartis entre 0 et 2256 - 1 alors nous avons assez d'entropie pour la tâche.

Pour obtenir une permutation de la sortie de SHA256 (lorsqu'elle est exprimée sous la forme d'un entier), il suffit simplement de la réduire modulo 52, 51, 50 ... comme dans ce pseudocode:

deck = [0..52]
shuffled = []
r = SHA256(i)

while deck.size > 0:
    pick = r % deck.size
    r = floor(r / deck.size)

    shuffled.append(deck[pick])
    delete deck[pick]
3
Artelius