Apakah java.util.Random benar-benar acak? Bagaimana saya bisa menghasilkan 52! (faktorial) kemungkinan urutan?

203

Saya telah menggunakan Random (java.util.Random)untuk mengocok setumpuk 52 kartu. Ada 52! (8.0658175e + 67) kemungkinan. Namun, saya telah menemukan bahwa seed untuk java.util.Randoma long, yang jauh lebih kecil pada 2 ^ 64 (1,8446744e + 19).

Dari sini, saya curiga apakah java.util.Random ini benar-benar acak ; apakah ini benar-benar mampu menghasilkan semua 52! kemungkinan?

Jika tidak, bagaimana saya bisa menghasilkan urutan acak yang lebih baik yang dapat menghasilkan semua 52! kemungkinan?

Serj Ardovic
sumber
21
"Bagaimana aku bisa menghasilkan bilangan acak nyata di atas 52!" Angka-angka dari Randomtidak pernah angka acak nyata . Ini adalah PRNG, di mana P berarti "semu". Untuk bilangan acak nyata , Anda memerlukan sumber keacakan (seperti random.org).
TJ Crowder
7
@ JimGarrison Bukan itu yang diinginkan OP. Dia berbicara tentang 10 ^ 68 kemungkinan urutan. Karena setiap urutan pseudo-acak diidentifikasi oleh benihnya, OP mengatakan mungkin ada paling banyak 2 ^ 64 urutan yang berbeda.
dasblinkenlight
6
Saya pikir ini pertanyaan yang menarik, dan patut dipikirkan. Tetapi saya tidak dapat berhenti bertanya-tanya tentang konteks masalah Anda: apa sebenarnya yang mengarah pada persyaratan untuk dapat menghasilkan semua 52! permutasi? Sebagai contoh, di jembatan dunia nyata kita dapat mengocok deck dan memberikan satu kartu pada satu waktu, namun hanya ada ~ 6e11 tangan yang berbeda karena banyak permutasi yang berbeda menghasilkan tangan yang sama. Berpikir ke arah lain, apakah Anda memerlukan solusi khusus untuk 52 !, atau Anda perlu yang menggeneralisasi, katakanlah, dua deck dikocok bersama kemungkinan (104! / (2 ** 52), atau ~ 2e150)?
NPE
9
@NPE - Ambil Solitaire (Klondike) misalnya, 52! persis jumlah tangan yang mungkin ..
Serj Ardovic
3
Saya pikir ini adalah bacaan yang menarik: superuser.com/a/712583
Dennis_E

Jawaban:

153

Memilih permutasi acak membutuhkan lebih banyak dan lebih sedikit keacakan secara serentak daripada yang disiratkan oleh pertanyaan Anda. Biarkan saya jelaskan.

Berita buruknya: perlu lebih banyak keacakan.

Kelemahan mendasar dalam pendekatan Anda adalah bahwa ia mencoba untuk memilih antara ~ 2 226 kemungkinan menggunakan 64 bit entropi (benih acak). Untuk secara adil memilih antara ~ 2 226 kemungkinan Anda harus menemukan cara untuk menghasilkan 226 bit entropi alih-alih 64.

Ada beberapa cara untuk menghasilkan bit acak: perangkat keras khusus , instruksi CPU , antarmuka OS , layanan online . Sudah ada asumsi implisit dalam pertanyaan Anda bahwa Anda entah bagaimana dapat menghasilkan 64 bit, jadi lakukan saja apa yang akan Anda lakukan, hanya empat kali, dan sumbangkan kelebihan bit tersebut untuk amal. :)

Berita baiknya: perlu sedikit keacakan.

Setelah Anda memiliki 226 bit acak tersebut, sisanya dapat dilakukan secara deterministik dan sehingga sifat-sifatnya java.util.Randomdapat menjadi tidak relevan . Begini caranya.

Katakanlah kita menghasilkan semua 52! permutasi (tahan dengan saya) dan mengurutkannya secara leksikografis.

Untuk memilih salah satu permutasi yang kita butuhkan adalah bilangan bulat acak tunggal antara 0dan 52!-1. Bilangan bulat itu adalah 226 bit entropi kami. Kami akan menggunakannya sebagai indeks ke daftar permutasi yang kami sortir. Jika indeks acak terdistribusi secara seragam, Anda tidak hanya dijamin bahwa semua permutasi dapat dipilih, mereka akan dipilih secara dapat disetel (yang merupakan jaminan yang lebih kuat dari apa yang ditanyakan oleh pertanyaan).

Sekarang, Anda sebenarnya tidak perlu membuat semua permutasi itu. Anda dapat menghasilkan satu secara langsung, mengingat posisi yang dipilih secara acak di daftar hipotetis kami yang diurutkan. Hal ini dapat dilakukan di O (n 2 ) waktu menggunakan Lehmer [1] kode (juga lihat penomoran permutasi dan sistem nomor factoriadic ). N di sini adalah ukuran dek Anda, yaitu 52.

Ada implementasi C dalam jawaban StackOverflow ini . Ada beberapa variabel integer di sana yang akan melimpah untuk n = 52, tetapi untungnya di Jawa Anda dapat menggunakan java.math.BigInteger. Sisa perhitungan dapat ditranskripsikan seperti apa adanya:

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] Jangan bingung dengan Lehrer . :)

NPE
sumber
7
Heh, dan saya yakin tautan pada akhirnya adalah New Math . :-)
TJ Crowder
5
@TJCrowder: Hampir saja! Itu adalah manifold Riemannian yang sangat berbeda yang mengayunkannya. :-)
NPE
2
Senang melihat orang menghargai klasik. :-)
TJ Crowder
3
Di mana Anda mendapatkan 226 bit acak di Jawa ? Maaf, kode Anda tidak menjawabnya.
Thorsten S.
5
Saya tidak mengerti apa yang Anda maksud, Java Random () tidak akan memberikan 64 bit entropi juga. OP menyiratkan sumber yang tidak ditentukan yang dapat menghasilkan 64 bit untuk menaburkan PRNG. Masuk akal untuk berasumsi bahwa Anda dapat meminta sumber yang sama untuk 226 bit.
Berhentilah merugikan Monica
60

Analisis Anda benar: menaburkan generator nomor pseudo-acak dengan seed spesifik apa pun harus menghasilkan urutan yang sama setelah shuffle, membatasi jumlah permutasi yang bisa Anda peroleh hingga 64 . Pernyataan ini mudah diverifikasi secara eksperimental dengan menelepon Collection.shuffledua kali, melewati aRandom objek yang diinisialisasi dengan seed yang sama, dan mengamati bahwa kedua acak acak identik.

Maka, solusi untuk ini adalah dengan menggunakan generator angka acak yang memungkinkan benih yang lebih besar. Java menyediakan SecureRandomkelas yang dapat diinisialisasi dengan byte[]berbagai ukuran yang hampir tidak terbatas. Anda kemudian bisa melewati contoh SecureRandomuntuk Collections.shuffleuntuk menyelesaikan tugas:

byte seed[] = new byte[...];
Random rnd = new SecureRandom(seed);
Collections.shuffle(deck, rnd);
dasblinkenlight
sumber
8
Tentunya, benih besar bukan jaminan bahwa semua 52! kemungkinan akan dihasilkan (yang merupakan pertanyaan khusus tentang ini)? Sebagai percobaan pemikiran, pertimbangkan PRNG patologis yang mengambil benih besar secara sewenang-wenang dan menghasilkan serangkaian nol yang sangat panjang. Tampaknya cukup jelas bahwa PRNG perlu memenuhi lebih banyak persyaratan daripada hanya mengambil benih yang cukup besar.
NPE
2
@SerjArdovic Ya, semua materi unggulan yang diteruskan ke objek SecureRandom harus tidak dapat diprediksi, seperti dokumentasi Java.
dasblinkenlight
10
@NPE Anda benar, meskipun benih yang terlalu kecil adalah jaminan batas atas, benih yang cukup besar tidak dijamin pada batas bawah. Semua ini dilakukan adalah menghilangkan batas atas teoritis, sehingga memungkinkan bagi RNG untuk menghasilkan semua 52! kombinasi.
dasblinkenlight
5
@SerjArdovic Jumlah byte terkecil yang diperlukan untuk itu adalah 29 (Anda membutuhkan 226 bit untuk mewakili 52! Kombinasi bit yang mungkin, yaitu 28,25 byte, jadi kita harus mengumpulkannya). Perhatikan bahwa menggunakan 29 byte bahan benih menghilangkan batas teoritis atas jumlah pengocokan yang Anda dapatkan, tanpa menetapkan batas bawah (lihat komentar NPE tentang RNG jelek yang membutuhkan benih sangat besar dan menghasilkan urutan semua nol).
dasblinkenlight
8
The SecureRandompelaksanaan hampir pasti akan menggunakan PRNG yang mendasari. Dan itu tergantung pada periode PRNG itu (dan pada tingkat lebih rendah, panjang menyatakan) apakah itu mampu memilih dari antara 52 permutasi faktorial. (Perhatikan bahwa dokumentasi mengatakan SecureRandomimplementasi "minimal sesuai dengan" tes statistik tertentu dan menghasilkan keluaran yang "harus kuat secara kriptografi", tetapi tidak menempatkan batas bawah eksplisit pada panjang status PRNG yang mendasari atau pada periode.)
Peter O.
26

Secara umum, generator nomor pseudorandom (PRNG) tidak dapat memilih di antara semua permutasi dari daftar 52-item jika panjang statusnya kurang dari 226 bit.

java.util.Randommengimplementasikan algoritma dengan modulus 2 48 ; jadi panjang statusnya hanya 48 bit, jauh lebih sedikit dari 226 bit yang saya maksud. Anda perlu menggunakan PRNG lain dengan panjang status yang lebih besar - khususnya, yang memiliki periode 52 faktorial atau lebih besar.

Lihat juga "Mengacak" di artikel saya tentang generator angka acak .

Pertimbangan ini tidak tergantung pada sifat PRNG; itu berlaku sama untuk PRNGs kriptografi dan nonkriptografi (tentu saja, PRNG nonkriptografi tidak tepat ketika keamanan informasi terlibat).


Meskipun java.security.SecureRandommemungkinkan benih dengan panjang tak terbatas dilewatkan, SecureRandomimplementasinya dapat menggunakan PRNG yang mendasarinya (misalnya, "SHA1PRNG" atau "DRBG"). Dan itu tergantung pada periode PRNG itu (dan pada tingkat lebih rendah, panjang menyatakan) apakah itu mampu memilih dari antara 52 permutasi faktorial. (Perhatikan bahwa saya mendefinisikan "panjang negara" sebagai "ukuran maksimum benih yang dapat diambil oleh PRNG untuk menginisialisasi keadaannya tanpa memperpendek atau mengompresi benih itu ").

Peter O.
sumber
18

Biarkan saya minta maaf sebelumnya, karena ini agak sulit untuk dipahami ...

Pertama-tama, Anda sudah tahu bahwa java.util.Randomitu tidak sepenuhnya acak sama sekali. Ini menghasilkan urutan dengan cara yang dapat diprediksi dengan sempurna dari seed. Anda sepenuhnya benar bahwa, karena benih hanya panjang 64 bit, hanya dapat menghasilkan 2 ^ 64 urutan yang berbeda. Jika Anda entah bagaimana menghasilkan 64 bit acak nyata dan menggunakannya untuk memilih sebuah seed, Anda tidak dapat menggunakan seed itu untuk secara acak memilih di antara semua 52! kemungkinan urutan dengan probabilitas yang sama.

Namun, fakta ini tidak ada konsekuensi selama Anda tidak benar-benar akan menghasilkan lebih dari 2 ^ 64 sekuens, selama tidak ada yang 'istimewa' atau 'terasa istimewa' tentang sekuens 2 ^ 64 yang dapat dihasilkan dihasilkannya .

Katakanlah Anda memiliki PRNG yang jauh lebih baik yang menggunakan biji 1000-bit. Bayangkan Anda memiliki dua cara untuk menginisialisasi - satu cara akan menginisialisasi menggunakan seluruh benih, dan satu cara akan memotong benih menjadi 64 bit sebelum menginisialisasi itu.

Jika Anda tidak tahu inisialisasi mana yang mana, bisakah Anda menulis segala jenis tes untuk membedakannya? Kecuali jika Anda (tidak) cukup beruntung untuk akhirnya menginisialisasi yang buruk dengan yang sama 64 bit yang dua kali, maka jawabannya adalah tidak. Anda tidak dapat membedakan antara kedua inisialisasi tanpa sedikit pengetahuan rinci tentang beberapa kelemahan dalam implementasi PRNG tertentu.

Atau, bayangkan bahwa Random kelas memiliki array 2 ^ 64 urutan yang dipilih sepenuhnya dan acak pada suatu waktu di masa lalu, dan bahwa benih itu hanya indeks ke dalam array ini.

Jadi fakta yang Randomhanya menggunakan 64 bit untuk bijinya sebenarnya tidak selalu menjadi masalah secara statistik, selama tidak ada peluang signifikan bahwa Anda akan menggunakan seed yang sama dua kali.

Tentu saja, untuk keperluan kriptografi , seed 64 bit tidak cukup, karena mendapatkan sistem untuk menggunakan seed yang sama dua kali layak secara komputasi.

EDIT:

Saya harus menambahkan bahwa, meskipun semua hal di atas benar, bahwa implementasi sebenarnya java.util.Randomtidak mengagumkan. Jika Anda menulis permainan kartu, mungkin menggunakan MessageDigestAPI untuk menghasilkan hash SHA-256 "MyGameName"+System.currentTimeMillis(), dan menggunakan bit-bit itu untuk mengocok deck. Dengan argumen di atas, selama pengguna Anda tidak benar-benar berjudi, Anda tidak perlu khawatir itu akan currentTimeMilliskembali lama. Jika pengguna Anda benar - benar berjudi, maka gunakan SecureRandomtanpa benih.

Matt Timmermans
sumber
6
@ ThorstenS, bagaimana Anda bisa menulis segala jenis tes yang dapat menentukan bahwa ada kombinasi kartu yang tidak pernah bisa muncul?
Matt Timmermans
2
Ada beberapa suite tes angka acak seperti Diehard dari George Marsaglia atau TestU01 dari Pierre L'Ecuyer / Richard Simard yang dengan mudah menemukan anomali statistik dalam output acak. Untuk memeriksa kartu, Anda dapat menggunakan dua kotak. Anda menentukan urutan kartu. Kotak pertama menunjukkan posisi dua kartu pertama sebagai pasangan xy: Kartu pertama sebagai x dan posisi perbedaan (!) (-26-25) dari kartu kedua sebagai y. Kotak kedua menunjukkan kartu ke-3 dan ke-4 dengan (-25-25) relatif terhadap kartu ke-2 / ke-3. Ini akan segera menunjukkan kesenjangan dan kluster dalam distribusi Anda jika Anda menjalankannya untuk sementara waktu.
Thorsten S.
4
Ya, itu bukan ujian yang Anda katakan bisa Anda tulis, tetapi itu juga tidak berlaku. Mengapa Anda berasumsi bahwa ada kesenjangan dan klaster dalam distribusi yang akan diungkap oleh tes seperti itu? Itu akan menyiratkan "kelemahan spesifik dalam implementasi PRNG" seperti yang saya sebutkan, dan tidak ada hubungannya sama sekali dengan jumlah benih yang mungkin. Tes semacam itu bahkan tidak mengharuskan Anda untuk memasang kembali generator. Saya memang memperingatkan pada awalnya bahwa ini sulit dimengerti.
Matt Timmermans
3
@ ThorstenS. Suite tes tersebut sama sekali tidak akan menentukan apakah sumber Anda adalah PRNG yang aman secara kriptografis 64-bit-seeded atau RNG sejati. (Bagaimanapun, pengujian PRNG adalah untuk semua suite itu.) Sekalipun Anda tahu algoritma yang digunakan, PRNG yang baik membuatnya tidak mungkin untuk menentukan keadaan tanpa pencarian kasar dari ruang negara.
Sneftel
1
@ ThorstenS .: Dalam setumpuk kartu yang sebenarnya, sebagian besar kombinasi tidak akan pernah muncul. Anda tidak tahu yang mana. Untuk PRNG setengah layak itu sama - jika Anda dapat menguji apakah urutan output yang diberikan selama itu dalam gambar, itu cacat dalam PRNG. Negara / periode yang sangat besar seperti 52! tidak dibutuhkan; 128-bit sudah cukup.
R .. GitHub BERHENTI MEMBANTU ICE
10

Saya akan mengambil sedikit taktik berbeda dalam hal ini. Anda benar dengan asumsi Anda - PRNG Anda tidak akan bisa mencapai semua 52! kemungkinan.

Pertanyaannya adalah: apa skala permainan kartu Anda?

Jika Anda membuat gim bergaya klondike sederhana? Maka Anda pasti tidak perlu semua 52! kemungkinan. Sebaliknya, lihat seperti ini: seorang pemain akan memiliki 18 trilyun gim yang berbeda. Bahkan terhitung untuk 'Masalah Ulang Tahun', mereka harus bermain miliaran tangan sebelum mereka mengalami permainan duplikat pertama.

Jika Anda membuat simulasi monte-carlo? Maka Anda mungkin baik-baik saja. Anda mungkin harus berurusan dengan artefak karena 'P' di PRNG, tetapi Anda mungkin tidak akan mengalami masalah hanya karena ruang benih rendah (sekali lagi, Anda sedang melihat trilyunan kemungkinan unik.) Di sisi sebaliknya, jika Anda bekerja dengan jumlah iterasi yang besar, maka, ya, ruang benih rendah Anda mungkin menjadi pemecah kesepakatan.

Jika Anda membuat permainan kartu multi pemain, terutama jika ada uang di telepon? Maka Anda perlu melakukan beberapa googling tentang bagaimana situs-situs poker online menangani masalah yang sama yang Anda tanyakan. Karena sementara ruang masalah rendah tidak terlihat oleh pemain rata-rata, itu dapat dieksploitasi jika sepadan dengan investasi waktu. (Situs poker semua melewati fase di mana PRNG mereka 'diretas', membiarkan seseorang melihat kartu hole semua pemain lain, hanya dengan menyimpulkan benih dari kartu yang terbuka.) Jika ini adalah situasi Anda, don 't hanya menemukan PRNG yang lebih baik - Anda harus memperlakukannya sebagai serius sebagai masalah Crypto.

Kevin
sumber
9

Solusi singkat yang pada dasarnya sama dengan dasblinkenlight:

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

Collections.shuffle(deck, random);

Anda tidak perlu khawatir dengan kondisi internal. Penjelasan panjang mengapa:

Ketika Anda membuat SecureRandominstance dengan cara ini, itu mengakses generator nomor acak benar OS spesifik. Ini adalah salah satu kumpulan entropi di mana nilai diakses yang berisi bit acak (misalnya untuk timer nanosecond presisi nanosecond pada dasarnya acak) atau generator nomor perangkat keras internal.

Input ini (!) Yang mungkin masih mengandung jejak palsu dimasukkan ke hash yang kuat secara kriptografis yang menghapus jejak tersebut. Itulah alasan CSPRNG tersebut digunakan, bukan untuk membuat angka-angka itu sendiri! The SecureRandommemiliki counter yang menelusuri berapa banyak bit yang digunakan ( getBytes(), getLong()dll) dan isi ulang yang SecureRandomdengan bit entropi bila diperlukan .

Singkatnya: Lupakan saja keberatan dan gunakan SecureRandomsebagai penghasil bilangan acak sejati.

Thorsten S.
sumber
4

Jika Anda menganggap nomor sebagai hanya array bit (atau byte) maka mungkin Anda bisa menggunakan (Secure) Random.nextBytessolusi yang disarankan dalam pertanyaan Stack Overflow ini , dan kemudian memetakan array menjadi a new BigInteger(byte[]).

IvanK
sumber
3

Algoritma yang sangat sederhana adalah dengan menerapkan SHA-256 ke urutan bilangan bulat yang bertambah dari 0 ke atas. (Garam dapat ditambahkan jika diinginkan untuk "mendapatkan urutan yang berbeda".) Jika kita berasumsi bahwa output dari SHA-256 adalah "sebaik" bilangan bulat terdistribusi antara 0 dan 2 256 - 1 maka kita memiliki cukup entropi untuk tugas.

Untuk mendapatkan permutasi dari output SHA256 (ketika dinyatakan sebagai bilangan bulat) kita hanya perlu menguranginya modulo 52, 51, 50 ... seperti dalam pseudocode ini:

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]
Artelius
sumber