【问题标题】:Explanation of card shuffling algorithm (java)洗牌算法讲解(java)
【发布时间】:2020-10-29 17:36:48
【问题描述】:

我试图在一个常见的 Java shuffle 纸牌算法中更好地理解这段代码:

// Random for remaining positions. 
int r = i + rand.nextInt(52 - i); 

为什么需要“填充”或将i 索引添加到生成的随机数中?看起来当您迭代并添加 i 时,通过减去 i,您可以保持随机数的最大可能范围从 0 到 51,但为什么不这样做:

int r = rand.nextInt(52);

完整代码:

      
    // Function which shuffle and print the array 
    public static void shuffle(int card[], int n) 
    { 
          
        Random rand = new Random(); 
          
        for (int i = 0; i < n; i++) 
        { 
            // Random for remaining positions. 
            int r = i + rand.nextInt(52 - i); 
              
             //swapping the elements 
             int temp = card[r]; 
             card[r] = card[i]; 
             card[i] = temp; 
               
        } 
    } 

【问题讨论】:

    标签: java algorithm random shuffle


    【解决方案1】:

    Fisher–Yates shuffle 的工作原理如下:

    • 从数组中取出一个随机元素,并将其交换到首位
    • 剩余值中取出一个随机元素,并将其交换到第二个位置
    • 剩余值中取出一个随机元素,并将其交换到第三个位置
    • 等等

    这是您要询问的“剩余值”部分。

    例如在 10 次迭代之后,您已将 10 个随机值交换到数组的前 10 个位置,因此对于下一次迭代,您需要在 10-end 范围内的随机位置,因此从 10 的随机范围的偏移量为 10 小于全范围,又名i + rand.nextInt(52 - i)

    【讨论】:

    • 啊,所以这本质上是与 Knuth 算法不同的 Fisher-Yates 算法,嗯?
    • @ennth 正如Wikipedia 所说:现代版本的 Fisher-Yates shuffle 专为计算机使用而设计,由 Richard Durstenfeld 于 1964 年推出,并由 Donald E. Knuth 在 The计算机编程艺术作为“算法 P(洗牌)”。
    • 它是essentially the same algorithm。这里的代码基于 Knuth 的描述。该算法背后的理论首先由 Fisher & Yates 阐述(在 1938 年,当时还没有计算机可以编程),这就是为什么他们通常会获得荣誉。
    【解决方案2】:

    其他答案还没有解决您的问题“为什么不这样做: int r = rand.nextInt(52);"。这有时被称为 naive shuffle,答案是因为这会导致有偏见的 shuffle。

    理想情况下,您希望结果的数量能够反映同样可能的结果所涉及的概率计算。这意味着洗好的一副牌中的第一张牌可以是 52 张牌中的任何一张,第二张可以是剩余 51 张牌中的任何一张,第三张可以是剩余 50 张牌中的任何一张,......换句话说,有A = 52*51*50*...*3*2*1(即 52 阶乘)可能的卡片排列方式。这就是 Fisher-Yates 洗牌及其变体所做的。但是,如果您按照您的建议选择任何一张卡片在第 ith 次迭代中移动,则会生成 C = 5252 个案例。结果是有偏见的洗牌。

    为了说明在幼稚的洗牌过程中出现偏差的方式和原因,请考虑一个小得多的牌组,其中只有 3 张牌。在这种情况下,A = 3*2*1 = 6 个排列,而 C = 33 = 27 是到达最终排列的路径数。为什么这是个问题?因为鸽子洞原理。 C 不是 A 的整数倍,因此如果我们将 C 视为鸽子而将 A 视为鸽笼,则某些洞必须获得比其他洞更多的鸽子。在洗牌方面,有更多的途径可以达成某些安排。因此,并非所有安排都会同样频繁地发生,因此结果不是“公平”的洗牌。

    如果你分析地计算它并从一个初始化为['a','b','c'] 的数组开始,当使用一个简单的随机播放时,你会发现以下概率:

    outcome         probability
    -------         -----------
    ['a','b','c']       4/27
    ['a','c','b']       5/27
    ['b','a','c']       5/27
    ['b','c','a']       5/27
    ['c','a','b']       4/27
    ['c','b','a']       4/27
    

    而在无偏洗牌的情况下,6 种可能排列中的每一种的概率都是 1/6。这就是创建 Fisher-Yates 算法的原因。

    【讨论】:

      【解决方案3】:

      您的代码所做的是创建牌组的洗牌。在每次迭代中,它会随机抽取一张牌并将其放回牌组的前面,从 i=0 到 i=n。

      如果您使用int r = rand.nextInt(52);,则意味着在每次迭代中您都可以取回任何牌,即使是牌组开头已经是牌组新顺序一部分的牌。

      通过减去 i 你只选择剩下的 52-i 牌中的一张牌,然后你需要加上 +i 才能得到它的实际位置,因为你已经在牌组的开头设置了第一张 i 牌作为新的洗牌。

      例如,假设您已经将前 10 张卡片放在了新位置。现在你需要得到第 11 张牌,所以你从剩下的 52-10=42 张牌中随机抽取一张。假设我们找回了数字 5,不是card[4] 的卡,而是card[10+4] 的卡

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-07-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-12-10
        相关资源
        最近更新 更多