如何真正洗牌

Posted

技术标签:

【中文标题】如何真正洗牌【英文标题】:How to really shuffle a deck of cards 【发布时间】:2013-04-28 00:25:31 【问题描述】:

当我需要在 Java/android 中洗牌时,我当然会使用Collections.shuffle(List<?> list)。我曾经这样做过,结果似乎可以接受。但他们不是。

如this paper 中所述,共有 52 个! 52张扑克牌的可能独特的洗牌。这相当于大约 2^226。

Collections.shuffle(List<?> list) 默认使用new Random(),它使用48-bit seed,因此只能创建 2^48 个唯一的随机播放 - 这仅占所有可能随机播放的 3.49*10^(-52)%!

那么我该如何正确洗牌呢?

我已经开始使用SecureRandom,但最后就够了吗?

List<Card> cards = new ArrayList<Card>();
...
SecureRandom secureRandom;
try 
    secureRandom = SecureRandom.getInstance("SHA1PRNG");

catch (NoSuchAlgorithmException e) 
    secureRandom = new SecureRandom();

secureRandom.nextBytes(new byte[20]); // force SecureRandom to seed itself
Collections.shuffle(cards, secureRandom);

【问题讨论】:

来自论文:首先要意识到的是,一种算法能够产生 52 个中的每一个!洗牌并不是真正需要的。 @Philipp Sander:重复洗牌不会增加随机性,是吗? blog.uncommons.org/2008/04/10/… 这可能会有所帮助,我还根据文章更新了我对非完全废话的回答。 @MarcoW.Mersenne Twister 产生 32 位或 64 位结果,但它基于更大的内部状态 - 它的循环长度为 2^19937-1。 @MarcoW。我相信确实如此。在任何单种子生成器中,例如 LCG 的,一旦您第二次看到给定值,整个序列将被相同地重复。当较大的状态折叠/投影到较小的输出状态时,看到相同值的重复并不意味着您将获得相同序列的重复。至于播种,那只是选择进入大周期的切入点。 64 位种子意味着您选择大约 10^18 个入口点之一进入长度为 2^19937-1 的循环。 【参考方案1】:

您可能只能从特定的起始安排中获得 248 个不同的手牌,但并不要求您每次都以相同的安排开始。

大概,在牌组完成后(扑克牌、二十一点等),它的顺序是不确定的,任何一种重新排列都是合适的。

而且,如果您担心每次启动程序时都从固定排列开始,只需在退出时保持顺序并下次重新加载即可。

无论如何,248 仍然是一个巨大的可能性(大约 280,000,000,000,000),对于纸牌游戏来说已经足够了,当你意识到它限制洗牌而不是限制洗牌时更是如此安排。除非您是认真的统计学家或密码学家,否则您所拥有的应该没问题。

【讨论】:

Presumably, after the deck is finished (poker hands, blackjack and so on), it will be in an indeterminate order 可能二十一点并不适合这里,因为策略太固定了。我也怀疑其他游戏,因为规则几乎排除了大多数排列。 它不会是不确定的顺序。它将按照您在上一款游戏中实际操作的顺序。 如果只有0.000000000000000000000000000000000000000000000000000349% 是所有可能的洗牌,2^48 怎么可能足够,即使是纸牌游戏? “一些”永远都达不到。而且由于实施问题,您通常每次都必须从相同的固定安排开始。 @aMarco,我的建议是解决这个问题。其他选项可能是引入真正的随机性,例如使用来自用户输入的时间来变形随机数序列(例如,在空闲时间每 0.1 秒调用一次 rand)。但我真的认为这是过度工程,我自己。不从相同的安排开始的简单解决方法就足够了。 @Marco,我不确定我是否明白我的意思。随机序列的“少量”结果是无关,因为您没有从一致的起始安排中使用它。考虑一个(非常糟糕的)洗牌功能,它只交换卡片 1 和 2。如果你洗牌,然后玩一个游戏,让卡片处于不同的起始排列,尽管它只允许 2,但下次洗牌不会产生相同的排列^0 个转换。提供额外随机性的是中间的游戏,以及所有 52!安排是可能的。【参考方案2】:

虽然您使用的是SecureRandom,但仍处于受限状态。只要输入种子的范围小于 52!它不可能是完全随机的。

其实SHA1PRNG is 160 bit seeded,表示还是不够随机。关注this link,它在几年前就有一个解决方案,就是使用名为UnCommons Math 的第三方库。

【讨论】:

好的,谢谢! 160 位绝对比 48 位好得多,所以这是一个很好的第一步。除了您链接到的页面之外,具有 160 位的 SHA1PRNG 的来源在哪里? 维基有它:en.wikipedia.org/wiki/SHA-1。根据***,SHA-2 或 3 也可以工作。 所以“SHA2PRNG”就足够了,SHA-256 有足够的位来生成所有可能的洗牌。 虽然种子空间与可能的 shuffle 数量一样大是一种很好的感觉,但种子不需要与问题空间一样大。它只需要足够大以至于无法破解,并且随机数应该分布得足够均匀,以至于手无法预测。错过可能的冲水并不重要,只要您错过的冲水次数不会多于非冲水次数。 也许您还应该在链接中包含标题,以便更好地搜索参考(如果 URL 解析为 404)。换句话说,我建议将 Follow this link 替换为 Read New Adventures in Software » A Java Programmer’s Guide to Random Numbers, Part 3: Seeding【参考方案3】:

如果您想要真正的随机性,您可以跳过伪随机生成器并使用更好的东西,例如从大气噪声生成的随机数。

random.org 提供API 以将这种方式生成的随机数集成到您自己的软件中。

【讨论】:

【参考方案4】:

从您链接的文章中窃取答案:

START WITH FRESH DECK
GET RANDOM SEED
FOR CT = 1, WHILE CT <= 52, DO
X = RANDOM NUMBER BETWEEN CT AND 52 INCLUSIVE
SWAP DECK[CT] WITH DECK[X]

随机数生成器应该是好的,并且使用一个 64 位的种子,你无法预测地选择,最好使用硬件。

【讨论】:

改组算法不是问题。 Java 的Collections.shuffle(...) 应该使用足够的Fisher-Yates shuffle。但 PRNG 及其种子是关键。 同意,关键在随机数生成中。 (尽管这篇文章描述了人们是如何弄错洗牌的。)【参考方案5】:

如何真正地洗牌?

有几个shuffling techniques。

任一(剥离/上手):

Cut the deck in two
    Add a small (pseudorandom) amount of one half to the front of the front of the other
    Add a small (pseudorandom) amount of one half to the front of the back of the other
Do this until one hand is empty
Repeat

或者(Riffle):

Cut the deck in two
    Set down a small (pseudorandom) portion of one half
    Set down a small (pseudorandom) portion of the other
Do this until both hands are empty, and you have a new deck
Repeat

除此之外还有更多内容,如我上面的链接中所述。


不管怎样,有太多的组合,即使是完美的洗牌算法也需要一台机器每秒探索2*10^50 独特的排列,以完成探索宇宙存在时间中的每个排列。到 2019 年,现代计算机预计只会达到 1 ExaFLOP(1*10^18 每秒浮点运算)。

也没有人类洗牌器会探索这种可能性范围,而且我相信(在最基本的层面上)你是在模拟人类洗牌,对吗?你会发现荷官可能会在一次洗牌中将递增顺序的牌组洗牌为降序吗? 一次洗牌

我不认为在每次洗牌时将自己限制在该相空间的一个(尽管非常)小部分(2^48 可能的随机数)是不可接受的,只要您不以相同的方式连续播种等.

一副 52 张牌的牌组中恰好有 52 种阶乘(简写为 52!)可能的顺序。这大约是 8×1067 个可能的排序,或者具体来说:80,658,175,170,943,878,571,660,636,856,403,766,975,289,505,440,883,277,824,000,000,000,000这个数字的大小意味着两个随机选择的真正随机的牌组是极不可能的永远,即使在宇宙的历史上,也是一样的。然而,虽然随机牌组中所有牌的确切顺序是不可预测的,但有可能对牌组不够充分的牌组做出一些概率预测随机。 ~Wikipedia

另外,值得注意的是,Bayer & Diaconis 在 1992 年证明只需 7 次良好的洗牌即可正确随机化一副​​牌,here 是***的相关部分,其中有大量讨论此问题的论文链接。

【讨论】:

以上是关于如何真正洗牌的主要内容,如果未能解决你的问题,请参考以下文章

行业|自动驾驶遭遇技术资金瓶颈,行业开始洗牌

打造数字供应链综合能力,迎接行业新一轮洗牌

SIMD:更通用的随机播放功能

h如何才能真正加入示例?

如何获得 TrieMap.getOrElseUpdate 的真正原子更新

如何*真正*编写 UML 基数?