Java.util.Random è davvero così casuale? Come posso generare 52! (fattoriali) possibili sequenze?


203

Ho usato Random (java.util.Random)per mescolare un mazzo di 52 carte. Ce ne sono 52! (8.0658175e + 67) possibilità. Tuttavia, ho scoperto che il seme per java.util.Randomè a long, che è molto più piccolo a 2 ^ 64 (1.8446744e + 19).

Da qui, ho il sospetto che java.util.Random sia davvero così casuale ; è effettivamente in grado di generare tutti i 52! possibilità?

In caso contrario, come posso generare in modo affidabile una sequenza casuale migliore in grado di produrre tutti i 52! possibilità?


21
"come posso sicuramente generare un numero casuale reale superiore a 52!" I numeri da Randomnon sono mai numeri casuali reali . È un PRNG, dove P sta per "pseudo". Per numeri casuali reali , hai bisogno di una fonte di casualità (come random.org).
TJ Crowder,

7
@JimGarrison Questo non è ciò che l'OP cerca. Sta parlando di 10 ^ 68 possibili sequenze. Poiché ogni sequenza pseudo-casuale è identificata dal suo seme, OP dice che potrebbero esserci al massimo 2 ^ 64 sequenze diverse.
dasblinkenlight,

6
Penso che sia una domanda interessante, e vale la pena pensarci. Ma non posso fare a meno di chiedermi quale sia il contesto del tuo problema: cosa sta portando esattamente al requisito di poter generare tutti i 52! permutazioni? Ad esempio, nel bridge del mondo reale possiamo mescolare il mazzo e distribuire una carta alla volta, ma ci sono solo ~ 6e11 mani diverse poiché molte permutazioni diverse danno come risultato la stessa mano. Pensando nella direzione opposta, hai bisogno di una soluzione specifica per 52! Oppure hai bisogno di una che generalizzi, per esempio, due mazzi mescolati insieme (104! / (2 ** 52) possibilità, o ~ 2e150)?
NPE

9
@NPE - Prendi ad esempio il solitario (Klondike), 52! è esattamente il numero di mani possibili ..
Serj Ardovic

3
Penso che questa sia una lettura interessante: superuser.com/a/712583
Dennis_E

Risposte:


153

La selezione di una permutazione casuale richiede simultaneamente una casualità sempre maggiore rispetto a quanto implica la tua domanda. Lasciatemi spiegare.

Le cattive notizie: servono più casualità.

Il difetto fondamentale nel tuo approccio è che sta cercando di scegliere tra ~ 2 226 possibilità usando 64 bit di entropia (il seme casuale). Per scegliere equamente tra ~ 2 226 possibilità dovrai trovare un modo per generare 226 bit di entropia anziché 64.

Esistono diversi modi per generare bit casuali: hardware dedicato , istruzioni CPU , interfacce del sistema operativo , servizi online . C'è già un presupposto implicito nella tua domanda che puoi in qualche modo generare 64 bit, quindi fai tutto ciò che avresti fatto, solo quattro volte e donare i pezzi in eccesso in beneficenza. :)

La buona notizia: serve meno casualità.

Una volta che hai quei 226 bit casuali, il resto può essere fatto in modo deterministico e quindi le proprietà di java.util.Randompossono essere rese irrilevanti . Ecco come.

Diciamo che generiamo tutti e 52! permutazioni (orso con me) e ordinarle lessicograficamente.

Per scegliere una delle permutazioni ci serve solo un intero intero casuale tra 0e 52!-1. Quel numero intero è il nostro 226 bit di entropia. Lo useremo come indice nel nostro elenco ordinato di permutazioni. Se l'indice casuale è distribuito uniformemente, non solo ti viene garantito che tutte le permutazioni possono essere scelte, ma verranno scelte in modo equiprobabile (che è una garanzia più forte di quello che la domanda si pone).

Ora, in realtà non è necessario generare tutte quelle permutazioni. Puoi produrne uno direttamente, data la sua posizione scelta casualmente nel nostro ipotetico elenco ordinato. Questo può essere fatto in O (n 2 ) tempo usando il codice Lehmer [1] (vedi anche permutazioni di numerazione e sistema numerico factoriadico ). La n qui è la dimensione del tuo mazzo, cioè 52.

C'è un'implementazione C in questa risposta StackOverflow . Ci sono diverse variabili intere lì che traboccerebbero per n = 52, ma fortunatamente in Java puoi usare java.math.BigInteger. Il resto dei calcoli può essere trascritto quasi com'è:

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] Da non confondere con Lehrer . :)


7
Eh, ed ero sicuro che il link alla fine sarebbe stato New Math . :-)
TJ Crowder

5
@TJCrowder: lo era quasi! Erano le varietà infinitamente differenziabili di Riemannian a farlo oscillare. :-)
NPE

2
Bello vedere le persone apprezzare i classici. :-)
TJ Crowder

3
Dove prendi i 226 bit casuali in Java ? Siamo spiacenti, il tuo codice non risponde a questo.
Thorsten S.

5
Non capisco cosa intendi, Java Random () non fornirà 64 bit di entropia. L'OP implica una fonte non specificata che può produrre 64 bit per il seeding del PRNG. Ha senso supporre che si possa chiedere alla stessa fonte per 226 bit.
Smetti di fare del male a Monica l'

60

La tua analisi è corretta: il seeding di un generatore di numeri pseudo-casuale con qualsiasi seme specifico deve produrre la stessa sequenza dopo uno shuffle, limitando il numero di permutazioni che potresti ottenere a 2 64 . Questa affermazione è facile da verificare sperimentalmente chiamando Collection.shuffledue volte, passando un Randomoggetto inizializzato con lo stesso seme e osservando che i due shuffle casuali sono identici.

Una soluzione a questo, quindi, è usare un generatore di numeri casuali che consenta un seme più grande. Java fornisce una SecureRandomclasse che potrebbe essere inizializzata con byte[]array di dimensioni praticamente illimitate. È quindi possibile passare un'istanza di SecureRandomto Collections.shuffleper completare l'attività:

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

8
Sicuramente, un seme grande non è una garanzia che tutti e 52! le possibilità sarebbero prodotte (qual è la questione specifica di questa domanda)? Come esperimento mentale, considera un PRNG patologico che prende un seme arbitrariamente grande e genera una serie infinitamente lunga di zero. Sembra abbastanza chiaro che il PRNG deve soddisfare più requisiti rispetto a prendere un seme abbastanza grande.
NPE

2
@SerjArdovic Sì, qualsiasi materiale seed trasferito a un oggetto SecureRandom deve essere imprevedibile, come da documentazione Java.
dasblinkenlight,

10
@NPE Hai ragione, anche se un seme troppo piccolo è una garanzia del limite superiore, un seme abbastanza grande non è garantito sul limite inferiore. Tutto ciò che fa è la rimozione di un limite superiore teorico, consentendo all'RNG di generare tutti i 52! combinazioni.
dasblinkenlight,

5
@SerjArdovic Il numero minimo di byte richiesto per questo è 29 (sono necessari 226 bit per rappresentare 52! Possibili combinazioni di bit, che è 28,25 byte, quindi dobbiamo arrotondare per eccesso). Si noti che l'utilizzo di 29 byte di materiale seme rimuove il limite teorico superiore del numero di shuffle che è possibile ottenere, senza stabilire il limite inferiore (vedere il commento di NPE su un RNG schifoso che prende un seme molto grande e genera una sequenza di tutti gli zeri).
dasblinkenlight,

8
L' SecureRandomimplementazione utilizzerà quasi sicuramente un PRNG sottostante. E dipende dal periodo di quel PRNG (e, in misura minore, dalla lunghezza dello stato) se è in grado di scegliere tra 52 permutazioni fattoriali. (Si noti che la documentazione afferma che l' SecureRandomimplementazione "è minimamente conforme a" alcuni test statistici e genera output che "devono essere crittograficamente forti", ma non pone limiti espliciti inferiori sulla lunghezza dello stato del PRNG sottostante o sul suo periodo.)
Peter O.

26

In generale, un generatore di numeri pseudocasuali (PRNG) non può scegliere tra tutte le permutazioni di un elenco di 52 elementi se la sua lunghezza di stato è inferiore a 226 bit.

java.util.Randomimplementa un algoritmo con un modulo di 2 48 ; quindi la sua lunghezza dello stato è di soli 48 bit, molto meno dei 226 bit a cui mi riferivo. Dovrai utilizzare un altro PRNG con una lunghezza dello stato maggiore, in particolare uno con un periodo di 52 fattoriale o superiore.

Vedi anche "Mischiare" nel mio articolo sui generatori di numeri casuali .

Questa considerazione è indipendente dalla natura del PRNG; si applica ugualmente ai PRNG crittografici e non crittografici (ovviamente, i PRNG non crittografici sono inappropriati ogni volta che si tratta di sicurezza delle informazioni).


Sebbene java.security.SecureRandomconsenta il passaggio di seed di lunghezza illimitata, l' SecureRandomimplementazione potrebbe utilizzare un PRNG sottostante (ad esempio, "SHA1PRNG" o "DRBG"). E dipende dal periodo di quel PRNG (e, in misura minore, dalla lunghezza dello stato) se è in grado di scegliere tra 52 permutazioni fattoriali. (Nota che definisco "lunghezza dello stato" come "la dimensione massima del seme che un PRNG può prendere per inizializzare il suo stato senza accorciare o comprimere quel seme ").


18

Vorrei scusarmi in anticipo, perché è un po 'difficile da capire ...

Prima di tutto, sai già che java.util.Randomnon è del tutto casuale. Genera sequenze in modo perfettamente prevedibile dal seme. Hai perfettamente ragione, dal momento che il seme è lungo solo 64 bit, può generare solo 2 ^ 64 sequenze diverse. Se dovessi in qualche modo generare 64 bit casuali reali e usarli per selezionare un seme, non potresti usare quel seme per scegliere casualmente tra tutti i 52! possibili sequenze con uguale probabilità.

Tuttavia, questo fatto non ha alcuna conseguenza finché non si generano più di 2 ^ 64 sequenze, purché non vi sia nulla di "speciale" o "notevolmente speciale" sulle sequenze 2 ^ 64 che può generare .

Diciamo che hai avuto un PRNG molto migliore che utilizzava semi a 1000 bit. Immagina di avere due modi per inizializzarlo: un modo lo inizializzerebbe usando l'intero seme e un modo lo avrebbe ridotto a 64 bit prima di inizializzarlo.

Se non sapessi quale inizializzatore fosse quale, potresti scrivere qualsiasi tipo di test per distinguerli? A meno che tu non sia stato (non) abbastanza fortunato da finire per inizializzare il cattivo con gli stessi 64 bit due volte, allora la risposta è no. Non è possibile distinguere tra i due inizializzatori senza una conoscenza dettagliata di alcuni punti deboli nell'implementazione specifica del PRNG.

In alternativa, immagina che la Randomclasse avesse un array di 2 ^ 64 sequenze che sono state selezionate completamente e casualmente in qualche momento in un lontano passato e che il seme era solo un indice in questo array.

Quindi il fatto che Randomusi solo 64 bit per il suo seme è in realtà non è necessariamente un problema statistico, fintanto che non v'è alcuna possibilità significativa che verrà utilizzato lo stesso seme per due volte.

Naturalmente, per scopi crittografici , un seme a 64 bit non è abbastanza, perché ottenere un sistema per usare lo stesso seme due volte è fattibile dal punto di vista computazionale.

MODIFICARE:

Dovrei aggiungere che, anche se tutto quanto sopra è corretto, che l'implementazione effettiva di java.util.Randomnon è eccezionale. Se stai scrivendo un gioco di carte, potresti usare l' MessageDigestAPI per generare l'hash SHA-256 "MyGameName"+System.currentTimeMillis()e usare quei bit per mescolare il mazzo. Con l'argomento sopra, fintanto che i tuoi utenti non stanno davvero giocando d'azzardo, non devi preoccuparti che currentTimeMillisritorni a lungo. Se i tuoi utenti stanno davvero giocando d'azzardo, usa SecureRandomsenza seed.


6
@ThorstenS, come potresti scrivere qualsiasi tipo di test che potrebbe determinare che ci sono combinazioni di carte che non possono mai venire fuori?
Matt Timmermans,

2
Esistono diverse suite di test di numeri casuali come Diehard di George Marsaglia o TestU01 di Pierre L'Ecuyer / Richard Simard che trovano facilmente anomalie statistiche nell'output casuale. Per il controllo delle carte puoi usare due quadrati. Determinare l'ordine delle carte. Il primo quadrato mostra la posizione delle prime due carte come coppia xy: la prima carta come xe la posizione della differenza (!) (-26-25) della seconda carta come y. Il secondo quadrato mostra la terza e la quarta carta con (-25-25) rispetto alla seconda / terza. Questo mostrerà immediatamente lacune e cluster nella tua distribuzione se lo esegui per un certo periodo.
Thorsten S.

4
Bene, questo non è il test che hai detto di poter scrivere, ma non si applica. Perché supponi che ci siano lacune e cluster nella distribuzione che tali test avrebbero scoperto? Ciò implicherebbe una "debolezza specifica nell'attuazione del PRNG", come ho già detto, e non ha assolutamente nulla a che fare con il numero di semi possibili. Tali test non richiedono nemmeno il ridimensionamento del generatore. All'inizio ho avvertito che era difficile da capire.
Matt Timmermans,

3
@ThorstenS. Quelle suite di test non determineranno assolutamente se la tua sorgente è un PRNG crittograficamente sicuro con seeding a 64 bit o un vero RNG. (Dopo tutto, testare i PRNG è quello a cui servono quelle suite.) Anche se si conosceva l'algoritmo in uso, un buon PRNG rende impossibile determinare lo stato senza una ricerca della forza bruta dello spazio degli stati.
Sneftel,

1
@ThorstenS .: In un vero mazzo di carte, la stragrande maggioranza delle combinazioni non verrà mai fuori. Semplicemente non sai quali sono quelli. Per un PRNG mezzo decente è lo stesso: se riesci a verificare se una determinata sequenza di output è così lunga nella sua immagine, questo è un difetto nel PRNG. Stato / periodo ridicolmente enormi come 52! non è necessario; 128 bit dovrebbe essere sufficiente.
R .. GitHub FERMA AIUTANDO ICE

10

Ho intenzione di prendere un po 'di virata diversa su questo. Hai ragione sui tuoi presupposti: il tuo PRNG non sarà in grado di colpire tutti e 52! possibilità.

La domanda è: qual è la scala del tuo gioco di carte?

Se stai realizzando un semplice gioco in stile Klondike? Quindi sicuramente non hai bisogno di tutti i 52! possibilità. Invece, guarda in questo modo: un giocatore avrà 18 quintilioni di giochi distinti. Anche tenendo conto del "problema del compleanno", dovrebbero giocare miliardi di mani prima di imbattersi nel primo gioco duplicato.

Se stai realizzando una simulazione monte-carlo? Allora probabilmente stai bene. Potrebbe essere necessario gestire gli artefatti a causa della 'P' nel PRNG, ma probabilmente non si verificheranno problemi semplicemente a causa di uno spazio di semi ridotto (di nuovo, si stanno esaminando quintilioni di possibilità uniche). rovescio della medaglia, se stai lavorando con un conteggio di iterazioni di grandi dimensioni, quindi, sì, il tuo spazio seme basso potrebbe essere un affare.

Se stai realizzando un gioco di carte multiplayer, in particolare se ci sono soldi sulla linea? Quindi dovrai cercare su come i siti di poker online gestiscono lo stesso problema di cui ti stai chiedendo. Perché mentre il problema di spazio di seeding basso non è evidente per il giocatore medio, è sfruttabile se vale la pena investire tempo. (Tutti i siti di poker hanno attraversato una fase in cui i loro PRNG sono stati "hackerati", permettendo a qualcuno di vedere le carte coperte di tutti gli altri giocatori, semplicemente deducendo il seme dalle carte scoperte.) Se questa è la situazione in cui ti trovi, don 't semplicemente trovare un migliore PRNG - è necessario trattarlo come serio come un problema Crypto.


9

Soluzione breve che è essenzialmente la stessa di dasblinkenlight:

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

Collections.shuffle(deck, random);

Non devi preoccuparti dello stato interno. Spiegazione lunga del perché:

Quando si crea un SecureRandom un'istanza in questo modo, accede a un generatore di numeri casuali veri specifici del sistema operativo. Questo è un pool di entropia in cui si accede a valori che contengono bit casuali (ad esempio per un timer a nanosecondi la precisione dei nanosecondi è essenzialmente casuale) o un generatore di numeri hardware interno.

Questo input (!) Che può ancora contenere tracce spurie viene immesso in un hash crittograficamente forte che rimuove tali tracce. Questo è il motivo per cui vengono utilizzati quei CSPRNG, non per creare quei numeri stessi! Il SecureRandomha un contatore che traccia quanti bit sono stati utilizzati ( getBytes(), getLong()etc.) e ricariche la SecureRandomcon bit entropia quando necessario .

In breve: dimentica semplicemente le obiezioni e usalo SecureRandomcome vero generatore di numeri casuali.


4

Se consideri il numero solo come un array di bit (o byte), forse potresti utilizzare le Random.nextBytessoluzioni (sicure) suggerite in questa domanda Stack Overflow e quindi mappare l'array in a new BigInteger(byte[]).


3

Un algoritmo molto semplice è applicare SHA-256 a una sequenza di numeri interi che va da 0 in su. (Se si desidera "ottenere una sequenza diversa", è possibile aggiungere un salt.) Se assumiamo che l'output di SHA-256 sia "buono come" interi distribuiti uniformemente tra 0 e 2 256 - 1, allora abbiamo abbastanza entropia per il compito.

Per ottenere una permutazione dall'output di SHA256 (quando espresso come numero intero) è sufficiente ridurlo modulo 52, 51, 50 ... come in questo pseudocodice:

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]
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.