Чи справді випадковий java.util.Random? Як я можу генерувати 52! (факторіальні) можливі послідовності?


202

Я використовував Random (java.util.Random)для переміщення колоди з 52 карт. Є 52! (8.0658175e + 67) можливості. Але я дізнався, що насіння для java.util.RandomA long- це набагато менше на 2 ^ 64 (1,8446744e + 19).

Звідси мені підозріло, чи java.util.Random справді це випадковість ; чи насправді він здатний генерувати всі 52! можливості?

Якщо ні, то як я можу надійно генерувати кращу випадкову послідовність, яка може створити всі 52! можливості?


21
"як я точно можу генерувати реальне випадкове число понад 52!" Числа з Randomніколи не є реальними випадковими числами. Це PRNG, де P означає "псевдо". Для справжніх випадкових чисел вам потрібне джерело випадковості (наприклад, random.org).
TJ Crowder

7
@JimGarrison Це не те, що ОП після. Він говорить про 10 ^ 68 можливих послідовностей. Оскільки кожна псевдовипадкова послідовність ідентифікується за насінням, ОП каже, що може бути не більше 2 ^ 64 різних послідовностей.
dasblinkenlight

6
Я думаю, що це цікаве питання, і над цим варто подумати. Але я не можу не задатися питанням про ваш проблемний контекст: що саме це призводить до вимоги мати можливість генерувати всі 52! перестановки? Наприклад, в реальному мості ми можемо переміщати колоду і передавати по одній картці одночасно, але є лише ~ 6e11 різних рук, оскільки багато різних перестановок призводять до однієї руки. Розмірковуючи в іншому напрямку, чи потрібне вам рішення спеціально для 52! Або вам потрібне таке, яке узагальнює, скажімо, дві колоди, перемішані разом (104! / (2 ** 52) можливості, або ~ 2e150)?
NPE

9
@NPE - Візьміть, наприклад, пасьянс (Клондайк), 52! саме стільки можливих рук ..
Серж Ардович

3
Я думаю, що це цікаве прочитання: superuser.com/a/712583
Dennis_E

Відповіді:


153

Вибір випадкової перестановки вимагає одночасно більшої та меншої кількості випадкових випадків, ніж те, що має на увазі ваше запитання. Дозволь пояснити.

Погана новина: потрібно більше випадковості.

Основний недолік у вашому підході полягає в тому, що він намагається вибирати між 2 226 можливостями, використовуючи 64 біти ентропії (випадкове насіння). Щоб справедливо вибрати між ~ 2 226 можливостями, вам доведеться знайти спосіб генерувати 226 біт ентропії замість 64.

Існує кілька способів генерування випадкових біт: спеціальне обладнання , інструкції процесора , інтерфейси ОС , онлайн-сервіси . У вашому питанні вже є неявне припущення, що ви можете якось генерувати 64 біти, тому просто робіть все, що ви збиралися зробити, лише чотири рази, і надлишкові надбавки даруйте на благодійність. :)

Хороша новина: потрібно менше випадковості.

Коли у вас є ці 226 випадкових бітів, решта можна зробити детерміновано, і тому властивості java.util.Randomможна зробити невідповідними . Ось як.

Скажімо, ми генеруємо всі 52! перестановки (візьміть із собою) і сортуйте їх лексикографічно.

Щоб вибрати одну з перестановок, все, що нам потрібно, - це одне випадкове ціле число між 0і 52!-1. Ціле число - це наші 226 біт ентропії. Ми будемо використовувати його як індекс у нашому відсортованому списку перестановок. Якщо випадковий індекс розподілений рівномірно, ви не тільки гарантуєте, що всі перестановки можуть бути обрані, вони будуть обрані однозначно (що є більш надійною гарантією, ніж запитання).

Тепер вам фактично не потрібно генерувати всі ці перестановки. Ви можете виготовити його безпосередньо, враховуючи його випадково вибране положення у нашому гіпотетичному відсортованому списку. Це можна зробити за O (n 2 ) час, використовуючи код Lehmer [1] (також див. Перестановку нумерації та фактичну діагностичну систему числення ). Тут розмір вашої колоди, тобто 52.

Існує реалізація C в цьому відповіді StackOverflow . Є кілька цілих змінних, які переповнюватимуться за n = 52, але, на щастя, у Java ви можете використовувати java.math.BigInteger. Решта обчислень можна переписати майже як є:

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] Не плутати з Лерером . :)


7
Хе, і я був впевнений, що ланкою в кінці буде New Math . :-)
TJ Crowder

5
@TJCrowder: Це майже майже було! Саме гойдалки нескінченно відрізнялися римановими колекторами. :-)
NPE

2
Приємно бачити людей, які цінують класику. :-)
TJ Crowder

3
Звідки ви берете випадкові 226 біт на Java ? На жаль, ваш код не відповідає на це.
Торстен С.

5
Я не розумію, що ви маєте на увазі, Java Random () також не забезпечить 64 біт ентропії. ОП має на увазі не визначене джерело, яке може створити 64 біт для засіву PRNG. Є сенс припустити, що ви можете запитати одне і те ж джерело для 226 біт.
Перестаньте шкодити Моніці

60

Ваш аналіз правильний: посів генератора псевдовипадкових чисел з будь-яким конкретним насінням повинен отримати ту саму послідовність після перетасування, обмеживши кількість перестановок, які ви могли отримати до 2 64 . Це твердження легко перевірити експериментально , зателефонувавши Collection.shuffleдвічі, передавши Randomоб'єкт, ініціалізований одним і тим же насінням, і помітивши, що дві випадкові перемішання однакові.

Вирішенням цього питання є використання генератора випадкових чисел, який дозволяє отримати більше насіння. Java надає SecureRandomклас, який може бути ініціалізований byte[]масивом практично необмеженого розміру. Потім можна передати екземпляр SecureRandomдля Collections.shuffleдля виконання завдання:

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

8
Безумовно, велике насіння не є гарантією того, що всі 52! можливості були б створені (про що конкретно йдеться в цьому питанні)? В якості продуманого експерименту розглянемо патологічний PRNG, який бере довільно велике насіння і генерує нескінченно довгий ряд нулів. Здається, досить зрозуміло, що PRNG потрібно задовольнити більше вимог, ніж просто взяти достатньо велике насіння.
NPE

2
@SerjArdovic Так, будь-який насіннєвий матеріал, переданий об’єкту SecureRandom, повинен бути непередбачуваним відповідно до документації Java.
dasblinkenlight

10
@NPE Ви маєте рацію, хоча занадто маленьке насіння є гарантією верхньої межі, але достатньо велике насіння не є гарантією нижньої межі. Все це - це зняття теоретичної верхньої межі, що дозволяє РНГ генерувати всі 52! комбінації.
dasblinkenlight

5
@SerjArdovic Найменша кількість байтів, необхідних для цього, становить 29 (вам потрібно 226 біт, щоб представити 52! Можливі комбінації бітів, що становить 28,25 байт, тому ми повинні їх округлити). Зауважте, що використання 29 байт насіннєвого матеріалу знімає теоретичну верхню межу щодо кількості мішаних місць, які ви могли отримати, не встановлюючи нижню межу (див. Коментар NPE про хитрий RNG, який займає дуже велике насіння і генерує послідовність усіх нулів).
dasblinkenlight

8
У SecureRandomреалізації майже напевно буде використаний базовий PRNG. І це залежить від періоду PRNG (і в меншій мірі, від тривалості штату), чи здатний він вибрати серед 52 факторних перестановок. (Зауважимо, що в документації йдеться про те, що SecureRandomреалізація "мінімально відповідає" певним статистичним тестам і генерує результати, "які повинні бути криптографічно сильними", але не встановлює явного нижнього обмеження щодо основної довжини штату PRNG або періоду його дії.)
Пітер О.

26

Взагалі генератор псевдовипадкових чисел (PRNG) не може вибрати серед усіх перестановок списку 52 елементів, якщо його довжина стану менше 226 біт.

java.util.Randomреалізує алгоритм з модулем 2 48 ; таким чином, його довжина стану становить лише 48 біт, що набагато менше 226 біт, про які я згадував. Вам потрібно буде використовувати інший PRNG з більшою довжиною стану - конкретно, один із періодом 52 або більше.

Дивіться також "Перемішування" в моїй статті про генератори випадкових чисел .

Цей розгляд не залежить від характеру PRNG; він однаковою мірою застосовується до криптографічних та некриптографічних PRNG (звичайно, некриптографічні PRNG є недоречними, коли йдеться про захист інформації).


Хоча java.security.SecureRandomдозволяє передавати насіння необмеженої довжини, SecureRandomреалізація може використовувати базовий PRNG (наприклад, "SHA1PRNG" або "DRBG"). І це залежить від періоду PRNG (і в меншій мірі, від тривалості штату), чи здатний він вибрати серед 52 факторних перестановок. (Зверніть увагу, що я визначаю "довжину стану" як "максимальний розмір насіння, який може взяти PRNG для ініціалізації свого стану без скорочення або стискання цього насіння ").


18

Дозвольте мені заздалегідь вибачитися, тому що це трохи важко зрозуміти ...

Перш за все, ви вже знаєте, що java.util.Randomце зовсім не випадково. Це генерує послідовності ідеально передбачуваним способом із насіння. Ви абсолютно вірні, що оскільки насіння має лише 64 біти, воно може генерувати лише 2 ^ 64 різних послідовності. Якби ви якось генерували 64 справжні випадкові біти і використовували їх для вибору насіння, ви не могли б використовувати це насіння для випадкового вибору між усіма 52! можливі послідовності з однаковою ймовірністю.

Однак цей факт не має жодного наслідку , доки ви насправді не збираєтесь генерувати більше 2–64 послідовностей, доки немає нічого «особливого» чи «помітно особливого» щодо 2 ^ 64 послідовностей, які він може генерувати. .

Скажімо, у вас був набагато кращий PRNG, який використовував 1000-бітове насіння. Уявіть, що у вас було два способи ініціалізації - один із способів ініціалізував би його за допомогою всього насіння, а один із способів перемістив насіння до 64 біт перед ініціалізацією.

Якщо ви не знали, який це ініціалізатор, чи можете ви написати будь-який тест, щоб їх розрізнити? Якщо вам (не) пощастило закінчити ініціалізацію поганого з тими ж 64 бітами двічі, то відповідь - ні. Ви не могли розрізнити два ініціалізатори без детального знання про деякі слабкі сторони в конкретній реалізації PRNG.

Крім того, уявіть, що Randomклас мав масив з 2 ^ 64 послідовностей, які були відібрані повністю і випадковими в певний час у далекому минулому, і що насіння було лише індексом цього масиву.

Тож факт, що Randomвикористовує лише 64 біти для свого насіння, насправді не обов'язково є статистично проблемою, доки немає значних шансів, що ви будете використовувати одне і те ж насіння двічі.

Звичайно, для криптографічних цілей 64-бітового насіння просто недостатньо, тому що отримати систему для використання одного і того ж насіння двічі обчислювально.

Редагувати:

Я хочу додати, що, хоча все вищезазначене є правильним, реальна реалізація java.util.Randomне є приголомшливою. Якщо ви пишете карткову гру, можливо, використовуйте MessageDigestAPI, щоб генерувати хеш SHA-256 "MyGameName"+System.currentTimeMillis(), і використовуйте ці біти для переміщення колоди. Наведеним вище аргументом, якщо ваші користувачі не дуже грають в азартні ігри, вам не доведеться турбуватися, що currentTimeMillisповертається надовго. Якщо ваші користувачі справді грають в азартні ігри, використовуйте SecureRandomбез насіння.


6
@ThorstenS, як ти міг написати будь-який тест, який міг би визначити, що є комбінації карт, які ніколи не можуть скластися?
Метт Тіммерманс

2
Існує кілька тестових наборів випадкових чисел, таких як Дієхард від Джорджа Марсалья або TestU01 від П'єра Л'Екуєра / Річарда Сімара, які легко знаходять статистичні аномалії у випадковому виході. Для перевірки картки можна використовувати два квадрати. Ви визначаєте замовлення картки. Перший квадрат показує положення перших двох карт як пара xy: Перша карта як x та різниця (!) Позиція другої карти як y. У другому квадраті зображені 3-я та 4-я картки з (-25-25) щодо 2-ї / 3-ї. Це покаже негайно прогалини та кластери у вашому розповсюдженні, якщо ви запускаєте його певний час.
Торстен С.

4
Ну, це не тест, який ви сказали, що можете написати, але він також не застосовується. Чому ви вважаєте, що в розподілі існують прогалини та кластери, які б виявити такі тести? Це означало б "специфічну слабкість у впровадженні PRNG", як я вже згадував, і не має нічого спільного з кількістю можливих насінин. Такі випробування навіть не вимагають повторного перезавантаження генератора. Я на початку попередив, що це важко зрозуміти.
Метт Тіммерманс

3
@ThorstenS. Ці тестові набори абсолютно не визначатимуть, чи є ваш джерело 64-бітовим криптографічно захищеним PRNG або справжнім RNG. (Зрештою, тестування PRNG - це те, для чого призначені ці набори.) Навіть якщо ви знали алгоритм, який використовується, хороший PRNG робить неможливим визначити стан без грубого пошуку державного простору.
Sneftel

1
@ThorstenS. У справжній колоді карт переважна більшість комбінацій ніколи не вийде. Ви просто не знаєте, що це. Для напівпристойного PRNG це те саме - якщо ви можете перевірити, чи відповідає дана послідовність виводу, яка є довгою у своєму зображенні, це недолік у PRNG. Смішно величезний стан / період, як 52! не потрібен; 128-розрядної повинно вистачити.
R .. GitHub СТОП ДОПОМОГА ВІД

10

Я збираюся взяти трохи іншого питання. Ви праві в своїх припущеннях - ваш PRNG не зможе вдарити всіх 52! можливості.

Питання: який масштаб вашої карткової гри?

Якщо ви робите просту гру в стилі клондайк? Тоді вам точно не потрібно всіх 52! можливості. Натомість погляньте на це так: у гравця буде 18 квінтільйонних ігор. Навіть припадаючи на «проблему з днем ​​народження», їм доведеться грати в мільярди рук, перш ніж потрапити в першу гру-дублікат.

Якщо ви робите моделювання Монте-Карло? Тоді ти, мабуть, добре. Можливо, вам доведеться мати справу з артефактами через "P" в PRNG, але ви, мабуть, не зіткнетеся з проблемами просто через низький насіннєвий простір (знову ж, ви дивитеся на квінтільйони унікальних можливостей.) відверніть сторону, якщо ви працюєте з великою кількістю ітерацій, то, так, ваш низький простір насіння може бути виправданим.

Якщо ви робите багатокористувацьку карточну гру, особливо якщо на лінії є гроші? Тоді вам потрібно буде трохи погуглювати, як веб-сайти в покер вирішили ту саму проблему, про яку ви питаєте. Тому що, хоча проблема з невеликим простором насіння не помітна середньому гравцеві, це корисно, якщо варто витратити час. (Усі покерні сайти пройшли фазу, коли їх PRNG були «зламані», дозволяючи комусь побачити луночні карти всіх інших гравців, просто вилучивши насіння з відкритих карт.) Якщо це така ситуація, в якій ви знаходитесь, не «т просто знайти кращий ПСЧ - ви повинні ставитися до нього так само серйозно , як проблема Crypto.


9

Коротке рішення, яке по суті є тим же, що і у dasblinkenlight:

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

Collections.shuffle(deck, random);

Вам не потрібно турбуватися про внутрішній стан. Довге пояснення, чому:

Коли ви створюєте SecureRandomекземпляр таким чином, він отримує доступ до специфічного генератора справжніх випадкових чисел для ОС. Це або пул ентропії, в якому отримують доступ до значень, які містять випадкові біти (наприклад, для наносекундного таймера точність наносекунди по суті є випадковим) або внутрішній апаратний генератор чисел.

Цей вхід (!), Який все ще може містити помилкові сліди, подається в криптографічно сильний хеш, який видаляє ці сліди. Ось чому ці CSPRNG використовуються, а не для створення самих цих чисел! У SecureRandomлічильнику є лічильник, який відстежує, скільки бітів було використано ( getBytes()і getLong()т.д.) і поповнює SecureRandomбіти ентропії, коли це необхідно .

Якщо коротко: просто забудьте заперечення та використовуйте SecureRandomяк справжній генератор випадкових чисел.


4

Якщо ви розглядаєте число як лише масив бітів (або байтів), можливо, ви могли б використовувати рішення (Secure), Random.nextBytesзапропоновані в цьому запитанні щодо переповнення стека , а потім зіставити масив у new BigInteger(byte[]).


3

Дуже простий алгоритм полягає в застосуванні SHA-256 до послідовності цілих чисел, що збільшуються від 0 вгору. (Сіль може бути додана за бажанням, щоб "отримати іншу послідовність".) Якщо вважати, що вихід SHA-256 "настільки ж хороший, як" рівномірно розподілені цілі числа між 0 і 2 256 - 1, то у нас є достатня ентропія для завдання.

Щоб отримати перестановку з виводу SHA256 (коли виражається як ціле число), потрібно просто зменшити його по модулю 52, 51, 50 ..., як у цьому псевдокоді:

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]
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.