我一直在使用Random (java.util.Random)来洗牌一副52张牌。有52个!(8.0658175 e + 67)的可能性。然而,我发现java.util.Random的种子很长,它要小得多,为2^64 (1.8446744e+19)。

从这里开始,我怀疑java.util.Random是否真的那么随机;它真的能产生全部52个吗?可能性?

如果不是,我如何可靠地生成一个更好的随机序列,可以产生所有52!可能性?


当前回答

选择一个随机排列同时需要比你的问题所暗示的更多和更少的随机性。让我解释一下。

坏消息是:需要更多的随机性。

你的方法的根本缺陷在于,它试图使用64位熵(随机种子)在~2226种可能性之间进行选择。为了公平地在2226种可能性中进行选择,你必须找到一种方法来产生226位而不是64位的熵。

生成随机比特的方法有几种:专用硬件、CPU指令、操作系统接口、在线服务。在你的问题中已经有一个隐含的假设,你可以以某种方式生成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"))));
}

别和莱勒搞混了。:)

其他回答

一般来说,伪随机数生成器(PRNG)如果最大周期长度小于226位,则不能从52项列表的所有排列中进行选择。

random实现了一个算法,其模量为248,最大周期长度不超过248,因此远小于2226(对应于我提到的226位)。您将需要使用另一个周期长度更大的PRNG,特别是最大周期长度为52 !或更大的PRNG。

请参见我关于随机数生成器的文章中的“洗牌”。

这一考虑与PRNG的性质无关;它同样适用于密码学和非密码学的prng(当然,非密码学的prng在涉及信息安全时是不合适的)。


虽然java.security.SecureRandom允许传入无限长度的种子,但SecureRandom实现可以使用底层的PRNG(例如,“SHA1PRNG”或“DRBG”)。这取决于PRNG的最大周期长度它是否能够从52个阶乘排列中进行选择。

一个非常简单的算法是将SHA-256应用于从0开始递增的整数序列。(如果想要“获得不同的序列”,可以添加盐。)如果我们假设SHA-256的输出与0到2256 - 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]

您的分析是正确的:在伪随机数生成器中植入任何特定的种子都必须在洗牌后产生相同的序列,从而将您可以获得的排列数量限制为264。这个断言很容易通过调用Collection进行实验验证。shuffle两次,传递一个用相同种子初始化的随机对象,并观察两次随机shuffle是相同的。

因此,解决这个问题的方法是使用允许更大种子的随机数生成器。Java提供了securerrandom类,可以使用大小几乎无限制的byte[]数组进行初始化。然后,您可以将securerrandom的一个实例传递给集合。洗牌完成任务:

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

如果你认为数字只是一个比特(或字节)的数组,那么也许你可以使用(安全)随机。在这个堆栈溢出问题中建议的nextBytes解决方案,然后将数组映射到一个新的BigInteger(byte[])。

选择一个随机排列同时需要比你的问题所暗示的更多和更少的随机性。让我解释一下。

坏消息是:需要更多的随机性。

你的方法的根本缺陷在于,它试图使用64位熵(随机种子)在~2226种可能性之间进行选择。为了公平地在2226种可能性中进行选择,你必须找到一种方法来产生226位而不是64位的熵。

生成随机比特的方法有几种:专用硬件、CPU指令、操作系统接口、在线服务。在你的问题中已经有一个隐含的假设,你可以以某种方式生成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"))));
}

别和莱勒搞混了。:)