Я использовал Random (java.util.Random) для перетасовки колоды из 52 карт. Их 52! (8.0658175e + 67) возможности. Тем не менее, я обнаружил, что семя для java.util.Random - это long, что намного меньше на 2 ^ 64 (1.8446744e + 19).

Отсюда я подозреваю, действительно ли java.util.Random случайный; действительно ли он способен генерировать все 52! возможности?

Если нет, то как я могу надежно сгенерировать лучшую случайную последовательность, которая может произвести все 52! возможности?

Ответы (9)

Выбор случайной перестановки требует одновременно большей и меньшей случайности, чем предполагает ваш вопрос. Позвольте мне объяснить.

Плохая новость: нужно больше случайности.

Фундаментальный недостаток вашего подхода заключается в том, что он пытается выбирать между ~ 2226 возможностями, используя 64 бита энтропии (случайное начальное число). Чтобы справедливо выбрать между ~ 2226 возможностями, вам нужно будет найти способ генерировать 226 бит энтропии вместо 64.

Существует несколько способов генерации случайных битов: выделенное оборудование, инструкции ЦП, интерфейсы ОС, онлайн-сервисы. В вашем вопросе уже есть неявное предположение, что вы можете каким-то образом сгенерировать 64 бита, поэтому просто сделайте то, что вы собирались делать, только четыре раза, и пожертвуйте лишние биты на благотворительность. :)

Хорошие новости: нужно меньше случайности.

Как только у вас есть эти 226 случайных битов, остальное можно сделать детерминированно, и поэтому свойства java.util.Random могут стать несущественными. Вот как.

Допустим, мы сгенерировали все 52! перестановки (несите меня) и отсортируйте их лексикографически.

Чтобы выбрать одну из перестановок, все, что нам нужно, - это одно случайное целое число от 0 до 52! -1. Это целое число - наши 226 бит энтропии. Мы будем использовать его как индекс в нашем отсортированном списке перестановок. Если случайный индекс равномерно распределен, вы не только гарантируете, что все перестановки могут быть выбраны, они будут выбраны равновероятно (что является более надежной гарантией, чем то, что задается в вопросе).

Теперь вам действительно не нужно генерировать все эти перестановки. Вы можете произвести его напрямую, учитывая его случайно выбранную позицию в нашем гипотетическом отсортированном списке. Это можно сделать за время O (n2), используя Lehmer[1] код (также см. перестановки нумерации и факториадная система счисления). Здесь n - размер вашей колоды, то есть 52.

В этом ответе StackOverflowесть реализация C. Там есть несколько целочисленных переменных, которые будут переполняться при 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] Не путать с Lehrer. :)

Как правило, генератор псевдослучайных чисел (ГПСЧ) не может выбирать из всех перестановок списка из 52 пунктов, если его максимальная длина цикла меньше 226 бит.

java.util.Random реализует алгоритм с модулем 248 и максимальной длиной цикла всего 48 бит, что намного меньше, чем 226 бит, о которых я говорил. Вам нужно будет использовать другой ГПСЧ с большей длиной цикла, в частности, с максимальной длиной цикла 52 факториала или больше.

См. Также «Перемешивание» в моей статье о генераторах случайных чисел.

Это соображение не зависит от природы ГПСЧ; он в равной степени применяется к криптографическим и некриптографическим ГПСЧ (конечно, некриптографические ГПСЧ не подходят, когда речь идет о информационной безопасности).


Although java.security.SecureRandom allows seeds of unlimited length to be passed in, the SecureRandom implementation could use an underlying PRNG (e.g., "SHA1PRNG" or "DRBG"). And it depends on that PRNG's maximum cycle length whether it's capable of choosing from among 52 factorial permutations.

Краткое решение, которое по сути аналогично dasblinkenlight:

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

Collections.shuffle(deck, random);

Вам не нужно беспокоиться о внутреннем состоянии. Длинное объяснение почему:

Когда вы создаете экземпляр SecureRandom таким образом, он обращается к конкретной ОС истинный генератор случайных чисел. Это либо пул энтропии, где значения доступ, который содержит случайные биты (например, для наносекундного таймера наносекундный точность по существу случайна) или внутренний аппаратный генератор чисел.

Этот вход (!), Который все еще может содержать ложные трассировки, передается в криптографически стойкий хеш, который удаляет эти следы. Вот почему используются эти CSPRNG, а не для создания самих чисел! SecureRandom имеет счетчик, который отслеживает, сколько бит было использовано (getBytes (), getLong () и т. Д.) И пополняет SecureRandom с битами энтропии при необходимости.

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

Результаты моих эмпирических исследований основаны на Java. Случайность не является полностью случайной. Если вы попробуете себя, используя метод случайного класса "nextGaussian ()" и сгенерируете достаточно большую выборку для чисел от -1 до 1, то график будет обычным распределенным полем, известным как модель Гаусса.

Принадлежащий финскому правительству букмаркер для азартных игр раз в день в течение всего года проводит розыгрыш лотереи, в которой таблица выигрышей показывает, что букмаркер дает выигрыши обычным распределенным способом. Моя симуляция Java с 5 миллионами розыгрышей показывает мне, что с использованием метода nextInt () -methdod, выигрыши обычно распределяются так же, как мой Bookmarker распределяет выигрыши в каждом розыгрыше.

My best picks are avoiding numbers 3 and 7 in each of ending ones and that's true that they are rarely in winning results. Couple of times won five out of five picks by avoiding 3 and 7 numbers in ones column in Integer between 1-70 (Keno).

Финская лотерея разыгрывается один раз в неделю субботним вечером Если вы играете в Систему с 12 числами из 39, возможно, вы получите 5 или 6 правильных выборов в своем купоне, избегая значений 3 и 7.

В финской лотерее есть номера от 1 до 40 на выбор, и требуется 4 купона, чтобы покрыть все номера с 12-ти числовой системой. Общая стоимость составляет 240 евро, и в долгосрочной перспективе это слишком дорого для обычного игрока, чтобы играть, не разорившись. Даже если вы делитесь купонами с другими покупателями, доступными для покупки, вы все равно должны быть очень удачливыми, если хотите получать прибыль.

Я собираюсь подойти к этому вопросу несколько иначе. Вы правы в своих предположениях - ваш ГПСЧ не сможет охватить все 52! возможности.

Вопрос: каков масштаб вашей карточной игры?

Если вы делаете простую игру в стиле клондайк? Тогда вам определенно не нужны все 52! возможности. Вместо этого посмотрите на это так: у игрока будет 18 квинтиллионов различных игр. Даже с учетом «проблемы дня рождения» им пришлось бы сыграть миллиарды рук, прежде чем они столкнулись бы с первой дублирующей игрой.

Если вы делаете симуляцию Монте-Карло? Тогда вы , вероятно, в порядке. Возможно, вам придется иметь дело с артефактами из-за 'P' в ГПСЧ, но вы, вероятно, не столкнетесь с проблемами просто из-за нехватки места для семян (опять же, вы видите квинтиллионы уникальных возможностей). обратная сторона: если вы работаете с большим количеством итераций, то, да, ваш низкий начальный объем может быть препятствием для сделки.

Если вы делаете многопользовательскую карточную игру, особенно если на кону есть деньги? Тогда вам нужно будет погуглить, как сайты онлайн-покера справились с той же проблемой, что и вы » спрашивают о. Потому что, хотя проблема с низким начальным пространством не заметна для среднего игрока, она может быть использована, если это стоит потраченного времени. (Все покерные сайты прошли фазу, когда их ГПСЧ были «взломаны», позволяя кому-то видеть закрытые карты всех других игроков, просто путем выведения семени из открытых карт.) Если это та ситуация, в которой вы находитесь, не просто найдите лучший ГПСЧ - вам нужно отнестись к нему так же серьезно, как и к проблеме криптографии.

Ваш анализ верен: заполнение генератора псевдослучайных чисел любым конкретным семенем должно давать ту же последовательность после перемешивания, ограничивая количество возможных перестановок до 264. Это утверждение легко проверить экспериментально, дважды вызвав Collection.shuffle, передав объект Random, инициализированный одним и тем же начальным значением, и заметив, что два случайных перемешивания идентичный.

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

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

A very simple algorithm is to apply SHA-256 to a sequence of integers incrementing from 0 upwards. (A salt can be appended if desired to "get a different sequence".) If we assume that the output of SHA-256 is "as good as" uniformly distributed integers between 0 and 2256 - 1 then we have enough entropy for the task.

Чтобы получить перестановку из вывода 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]

Если вы рассматриваете число как просто массив бит (или байтов), тогда, возможно, вы могли бы использовать решения (Secure)Random.nextBytes, предложенные в этом Stack Overflow вопросе, а затем сопоставьте массив с новым BigInteger (byte []).

Позвольте мне заранее извиниться, потому что это немного сложно понять ...

Прежде всего, вы уже знаете, что java.util.Random вовсе не случайный. Он генерирует последовательности совершенно предсказуемым образом из начального числа. Вы совершенно правы, поскольку семя имеет длину всего 64 бита, оно может генерировать только 2 ^ 64 различных последовательностей. Если бы вы каким-то образом сгенерировали 64 реальных случайных бита и использовали их для выбора начального числа, вы не смогли бы использовать это начальное число для случайного выбора между всеми из 52! возможные последовательности с равной вероятностью.

Однако этот факт не имеет значения до тех пор, пока вы на самом деле не собираетесь генерировать более 2 ^ 64 последовательностей, пока в этом нет ничего «особенного» или «заметно особенного». последовательности 2 ^ 64, которые он может генерировать.

Допустим, у вас был гораздо лучший ГПСЧ, который использовал 1000-битные начальные числа. Представьте, что у вас есть два способа инициализировать его - один способ инициализировать его с использованием всего семенного материала, а другой способ - хешировать семя до 64 бит перед его инициализацией.

Если бы вы не знали, какой инициализатор какой был, могли бы вы написать какой-нибудь тест, чтобы различать их? Если вам (не) повезло, и вы дважды инициализировали плохой бит с одинаковыми 64 битами, тогда ответ будет отрицательным. Вы не сможете различить два инициализатора, не зная некоторых слабых мест в конкретной реализации ГПСЧ.

В качестве альтернативы представьте, что класс Random имеет массив из 2 ^ 64 последовательностей, которые были выбраны полностью и случайным образом в какой-то момент в далеком прошлом, и что начальное число было просто индексом в этом массиве.

Таким образом, тот факт, что Random использует только 64 бита для своего начального числа, на самом деле не обязательно является проблемой статистически, если нет значительного шанса, что вы будете использовать то же самое начальное число. дважды.

Конечно, для криптографических 64-битного начального числа просто недостаточно, потому что заставить систему использовать одно и то же начальное число дважды вычислительно возможно.

РЕДАКТИРОВАТЬ:

Я должен добавить, что, хотя все вышеперечисленное верно, фактическая реализация java.util.Random не так хороша. Если вы пишете карточную игру, возможно, используйте API MessageDigest для генерации хэша SHA-256 для «MyGameName» + System.currentTimeMillis ()и используйте эти биты для перемешивания палуба. Согласно приведенному выше аргументу, если ваши пользователи на самом деле не играют в азартные игры, вам не нужно беспокоиться о том, что currentTimeMillis вернет long. Если ваши пользователи действительно играют в азартные игры, используйте SecureRandom без начального числа.

2022 WebDevInsider