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

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

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


当前回答

让我提前道歉,因为这有点难以理解……

首先,您已经知道java.util.Random根本不是完全随机的。它以完全可预测的方式从种子中生成序列。你是完全正确的,因为种子只有64位长,它只能生成2^64个不同的序列。如果你要以某种方式生成64个真实的随机比特,并使用它们来选择一颗种子,你不能使用该种子在所有52个种子中随机选择!概率相等的可能序列。

然而,只要你实际上不会生成超过2^64的序列,只要它所生成的2^64序列没有什么“特别”或“明显特别”的地方,这个事实就没有什么意义。

假设你有一个更好的使用1000位种子的PRNG。假设您有两种方法来初始化它——一种方法是使用整个种子初始化它,另一种方法是在初始化它之前将种子散列到64位。

如果你不知道哪个初始化式是哪个,你能写一些测试来区分它们吗?除非你足够幸运,最终用相同的64位初始化了坏的那个,那么答案是否定的。如果不详细了解特定PRNG实现中的一些弱点,就无法区分这两个初始化器。

或者,想象Random类有一个2^64个序列的数组,这些序列是在遥远的过去的某个时间完全随机选择的,而种子只是这个数组的一个索引。

所以从统计学上来说,《Random》只使用64位的种子并不一定是个问题,只要你不太可能两次使用相同的种子。

当然,出于加密目的,64位种子是不够的,因为让一个系统两次使用相同的种子在计算上是可行的。

编辑:

I should add that, even though all of the above is correct, that the actual implementation of java.util.Random is not awesome. If you are writing a card game, maybe use the MessageDigest API to generate the SHA-256 hash of "MyGameName"+System.currentTimeMillis(), and use those bits to shuffle the deck. By the above argument, as long as your users are not really gambling, you don't have to worry that currentTimeMillis returns a long. If your users are really gambling, then use SecureRandom with no seed.

其他回答

在这个问题上,我要采取一点不同的方法。你的假设是对的——你的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.

一个非常简单的算法是将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]

简单的解决方案,本质上是相同的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.util.Random根本不是完全随机的。它以完全可预测的方式从种子中生成序列。你是完全正确的,因为种子只有64位长,它只能生成2^64个不同的序列。如果你要以某种方式生成64个真实的随机比特,并使用它们来选择一颗种子,你不能使用该种子在所有52个种子中随机选择!概率相等的可能序列。

然而,只要你实际上不会生成超过2^64的序列,只要它所生成的2^64序列没有什么“特别”或“明显特别”的地方,这个事实就没有什么意义。

假设你有一个更好的使用1000位种子的PRNG。假设您有两种方法来初始化它——一种方法是使用整个种子初始化它,另一种方法是在初始化它之前将种子散列到64位。

如果你不知道哪个初始化式是哪个,你能写一些测试来区分它们吗?除非你足够幸运,最终用相同的64位初始化了坏的那个,那么答案是否定的。如果不详细了解特定PRNG实现中的一些弱点,就无法区分这两个初始化器。

或者,想象Random类有一个2^64个序列的数组,这些序列是在遥远的过去的某个时间完全随机选择的,而种子只是这个数组的一个索引。

所以从统计学上来说,《Random》只使用64位的种子并不一定是个问题,只要你不太可能两次使用相同的种子。

当然,出于加密目的,64位种子是不够的,因为让一个系统两次使用相同的种子在计算上是可行的。

编辑:

I should add that, even though all of the above is correct, that the actual implementation of java.util.Random is not awesome. If you are writing a card game, maybe use the MessageDigest API to generate the SHA-256 hash of "MyGameName"+System.currentTimeMillis(), and use those bits to shuffle the deck. By the above argument, as long as your users are not really gambling, you don't have to worry that currentTimeMillis returns a long. If your users are really gambling, then use SecureRandom with no seed.

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

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

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