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

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

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


当前回答

简单的解决方案,本质上是相同的dasblinkenlight:

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

Collections.shuffle(deck, random);

你不需要担心内部状态。解释原因:

当您以这种方式创建securerrandom实例时,它将访问特定于操作系统的实例 真正的随机数发生器。这是一个熵池,其中的值是 访问包含随机位(例如,对于纳秒计时器,纳秒 精度基本上是随机的)或内部硬件数字生成器。

此输入(!)可能仍然包含虚假的跟踪被送入 加密强哈希,删除这些跟踪。这就是使用这些csprng的原因,而不是为了自己创建这些数字!SecureRandom有一个计数器,用于跟踪使用了多少位(getBytes(), getLong()等),并在必要时用熵位重新填充SecureRandom。

简而言之:简单地忘记反对意见,使用securerrandom作为真正的随机数生成器。

其他回答

我的实证研究成果是Java。随机并不完全是随机的。如果你尝试使用随机类" next高斯()方法,并生成足够大的样本总体的数字在-1和1之间,图是正态分布场称为高斯模型。

芬兰政府拥有的赌博书签每天都有一次,全年每天都抽彩票,中奖表显示书签书签以正常分布的方式给予奖金。我的Java模拟500万次抽奖显示,与nextInt() - method使用数字抽奖,奖金正态分布与My Bookmarker在每次抽奖中处理奖金相同。

我最好的选择是避免在结尾的每一个数字中出现3和7,这是事实,它们很少有获胜的结果。有几次通过避免在整数1-70之间的一列中的3和7个数字,赢得了5个选择中的5个。

芬兰彩票每周周六晚上抽一次,如果你在39个数字中抽12个,也许你会避开3和7个数字,在优惠券中获得5或6个正确的选择。

芬兰彩票有数字1-40可供选择,它需要4张优惠券来覆盖所有的数字与12号系统。总花费是240欧元,从长远来看,对于经常赌博的人来说,这太贵了,不玩就会破产。即使你把优惠券分享给其他可以购买的客户,如果你想盈利,你也必须非常幸运。

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

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

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

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


虽然java.security.SecureRandom允许传入无限长度的种子,但SecureRandom实现可以使用底层的PRNG(例如,“SHA1PRNG”或“DRBG”)。这取决于PRNG的最大周期长度它是否能够从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不可能击中所有52个!的可能性。

问题是:你的纸牌游戏规模有多大?

如果你正在制作一款简单的克朗代克风格游戏?那么你绝对不需要全部52个!的可能性。相反,可以这样看:一个玩家将有18亿亿次不同的游戏。即使考虑到“生日问题”,他们也必须在遇到第一个重复游戏之前玩数十亿手牌。

如果你在做蒙特卡罗模拟?那你应该没事。您可能不得不处理由于PRNG中的“P”而产生的工件,但是您可能不会因为低种子空间而遇到问题(再次强调,您看到的是无数种独特的可能性)。另一方面,如果您正在处理大量的迭代计数,那么,是的,您的低种子空间可能是一个交易破坏者。

If you're making a multiplayer card game, particularly if there's money on the line? Then you're going to need to do some googling on how the online poker sites handled the same problem you're asking about. Because while the low seed space issue isn't noticeable to the average player, it is exploitable if it's worth the time investment. (The poker sites all went through a phase where their PRNGs were 'hacked', letting someone see the hole cards of all the other players, simply by deducing the seed from exposed cards.) If this is the situation you're in, don't simply find a better PRNG - you'll need to treat it as seriously as a Crypto problem.

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

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

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