为什么这个简单的shuffle算法会产生偏差的结果呢?什么是一个简单的原因?

时间:2009-05-13 17:18:26

标签: algorithm math shuffle

似乎这种简单的混洗算法会产生偏差的结果:

# suppose $arr is filled with 1 to 52

for ($i < 0; $i < 52; $i++) { 
  $j = rand(0, 51);

  # swap the items

  $tmp = $arr[j];
  $arr[j] = $arr[i];
  $arr[i] = $tmp;
}

你可以尝试...而不是使用52,使用3(假设只使用3张卡),并运行10,000次并计算结果,你会看到结果偏向某些模式..

问题是......它会发生什么简单的解释?

正确的解决方案是使用类似

的内容
for ($i < 0; $i < 51; $i++) {  # last card need not swap 
  $j = rand($i, 51);        # don't touch the cards that already "settled"

  # swap the items

  $tmp = $arr[j];
  $arr[j] = $arr[i];
  $arr[i] = $tmp;
}

但问题是......为什么第一种方法,似乎也是完全随机的,会使结果产生偏差?

更新1:感谢大家在这里指出它需要rand($ i,51)才能正确洗牌。

12 个答案:

答案 0 :(得分:34)

见这个:
The Danger of Naïveté (Coding Horror)

让我们看一下你的三张牌组。使用3张牌组,洗牌后甲板上只有6种可能的订单:123, 132, 213, 231, 312, 321.

使用第一个算法,代码有27种可能的路径(结果),具体取决于rand()函数在不同点的结果。这些结果中的每一个都是同等可能的(无偏见的)。这些结果中的每一个都将映射到上面6个可能的“真实”混洗结果列表中的相同单个结果。我们现在有27个项目和6个桶用于放入它们。由于27个不能被6整除,因此这些6个组合中的一些必须过度表示。

使用第二种算法,有6种可能的结果可以准确地映射到6种可能的“真实”混洗结果,并且它们应该随时间平均表示。

这很重要,因为在第一个算法中过度表示的存储桶不是随机的。为偏见选择的存储桶是可重复的,并且可预测。因此,如果您正在构建在线扑克游戏并使用第一种算法,黑客可能会发现您使用了天真的排序并从中确定甲板安排比其他安排更容易发生。然后他们可以相应地下注。他们会失去一些,但他们会赢得比失败更多的东西,并迅速让你破产。

答案 1 :(得分:23)

以下是这些替换的完整概率树。

假设您从序列123开始,然后我们将列举所有使用相关代码生成随机结果的方法。

123
 +- 123          - swap 1 and 1 (these are positions,
 |   +- 213      - swap 2 and 1  not numbers)
 |   |   +- 312  - swap 3 and 1
 |   |   +- 231  - swap 3 and 2
 |   |   +- 213  - swap 3 and 3
 |   +- 123      - swap 2 and 2
 |   |   +- 321  - swap 3 and 1
 |   |   +- 132  - swap 3 and 2
 |   |   +- 123  - swap 3 and 3
 |   +- 132      - swap 2 and 3
 |       +- 231  - swap 3 and 1
 |       +- 123  - swap 3 and 2
 |       +- 132  - swap 3 and 3
 +- 213          - swap 1 and 2
 |   +- 123      - swap 2 and 1
 |   |   +- 321  - swap 3 and 1
 |   |   +- 132  - swap 3 and 2
 |   |   +- 123  - swap 3 and 3
 |   +- 213      - swap 2 and 2
 |   |   +- 312  - swap 3 and 1
 |   |   +- 231  - swap 3 and 2
 |   |   +- 213  - swap 3 and 3
 |   +- 231      - swap 2 and 3
 |       +- 132  - swap 3 and 1
 |       +- 213  - swap 3 and 2
 |       +- 231  - swap 3 and 3
 +- 321          - swap 1 and 3
     +- 231      - swap 2 and 1
     |   +- 132  - swap 3 and 1
     |   +- 213  - swap 3 and 2
     |   +- 231  - swap 3 and 3
     +- 321      - swap 2 and 2
     |   +- 123  - swap 3 and 1
     |   +- 312  - swap 3 and 2
     |   +- 321  - swap 3 and 3
     +- 312      - swap 2 and 3
         +- 213  - swap 3 and 1
         +- 321  - swap 3 and 2
         +- 312  - swap 3 and 3

现在,第四列数字,即交换信息之前的数字,包含最终结果,有27种可能的结果。

让我们计算每种模式出现的次数:

123 - 4 times
132 - 5 times
213 - 5 times
231 - 5 times
312 - 4 times
321 - 4 times
=============
     27 times total

如果您运行随机交换无限次的代码,则模式132,213和231将比模式123,312和321更频繁地发生,这仅仅是因为代码交换的方式使得更多可能会发生。

现在,当然,你可以说,如果你运行代码30次(27 + 3),你最终可能会出现5次所有模式,但在处理统计数据时你必须看一下长期趋势。

这是用于探索每种可能模式之一的随机性的C#代码:

class Program
{
    static void Main(string[] args)
    {
        Dictionary<String, Int32> occurances = new Dictionary<String, Int32>
        {
            { "123", 0 },
            { "132", 0 },
            { "213", 0 },
            { "231", 0 },
            { "312", 0 },
            { "321", 0 }
        };

        Char[] digits = new[] { '1', '2', '3' };
        Func<Char[], Int32, Int32, Char[]> swap = delegate(Char[] input, Int32 pos1, Int32 pos2)
        {
            Char[] result = new Char[] { input[0], input[1], input[2] };
            Char temp = result[pos1];
            result[pos1] = result[pos2];
            result[pos2] = temp;
            return result;
        };

        for (Int32 index1 = 0; index1 < 3; index1++)
        {
            Char[] level1 = swap(digits, 0, index1);
            for (Int32 index2 = 0; index2 < 3; index2++)
            {
                Char[] level2 = swap(level1, 1, index2);
                for (Int32 index3 = 0; index3 < 3; index3++)
                {
                    Char[] level3 = swap(level2, 2, index3);
                    String output = new String(level3);
                    occurances[output]++;
                }
            }
        }

        foreach (var kvp in occurances)
        {
            Console.Out.WriteLine(kvp.Key + ": " + kvp.Value);
        }
    }
}

输出:

123: 4
132: 5
213: 5
231: 5
312: 4
321: 4

因此,虽然这个答案实际上是重要的,但这不是一个纯粹的数学答案,你只需要评估随机函数可以采用的所有可能方式,并查看最终输出。

答案 2 :(得分:18)

根据您对其他答案的评论,您似乎不仅仅在寻找解释为什么分布不是 统一分布(对于哪个分配答案很简单),也是一个“直观”的解释,说明为什么它实际上远非统一

这是一种看待它的方法。假设您从初始数组[1, 2, ..., n]开始(其中n可能是3或52或其他)并应用这两种算法中的一种。如果所有排列均匀可能,则1保持在第一位置的概率应为1/n。事实上,在第二个(正确的)算法中, 1/n,因为当且仅当第一次没有交换时,它仍保持在原位,即如果初始调用为rand(0,n-1)返回0.
但是,在第一个(错误的)算法中,只有当 时,它才会保持不变 - 即,只有第一个{{1}其他rand s返回0并且 none 返回0,其概率为(1 / n)*(1-1 / n)^(n-1)...“抓鸟”英语词典1 /(ne)≈0.37/ n,而不是1 / n。

这就是“直观”的解释:在你的第一个算法中,早期的项目比后来的项目更有可能被替换掉,所以你得到的排列倾向于早期项目的模式。 在原来的位置。

(它比这更微妙,例如1可以换成后来的位置,并且最终通过一系列复杂的掉期交换回来,但这些概率相对不太重要。)

答案 3 :(得分:15)

我见过这个效果的最佳解释来自Jeff Atwood的 CodingHorror 博客(The Danger of Naïveté)。

使用此代码模拟3张牌随机随机播放......

for (int i = 0; i < cards.Length; i++)
{
    int n = rand.Next(cards.Length);
    Swap(ref cards[i], ref cards[n]);
}

...你得到了这个发行版。

Distribution of 3-card shuffle

随机播放代码(上图)导致3 ^ 3(27)个可能的套牌组合。但是数学告诉我们真的只有3个!或3张牌组的6种可能组合。所以有些组合过度代表了。

您需要使用Fisher-Yates shuffle来正确(随机)洗牌。

答案 4 :(得分:3)

这是另一种直觉:单一的随机交换不能在占据位置的概率中产生对称性,除非至少已经存在双向对称。调用三个位置A,B和C.现在让a是卡2在位置A的概率,b是卡2在位置B的概率,并且c是它在位置C的概率,先前交换动作。假设没有两个概率是相同的:a!= b,b!= c,c!= a。现在计算交换后卡在这三个位置的概率a',b'和c'。假设这个交换移动包括位置C随机交换三个位置之一。然后:

a' = a*2/3 + c*1/3
b' = b*2/3 + c*1/3
c' = 1/3.

也就是说,卡在位置A上升的概率是它已经存在的概率,即时间位置A的2/3没有参与交换,加上它在位置C的概率乘以C与A交换的概率的1/3等。现在减去前两个方程式,得到:

a' - b' = (a - b)*2/3

这意味着因为我们假设了一个!= b,然后是'!= b'(尽管差异将随着时间推移接近0,给定足够的交换)。但由于'+ b'+ c'= 1,如果是'!= b',则两者都不能等于c',即1/3。因此,如果三个概率在交换之前开始全部不同,那么在交换之后它们也将全部不同。无论交换哪个位置,这都会成立 - 我们只是在上面交换变量的作用。

现在,第一次交换是通过将位置A中的卡1与其他一个交换而开始的。在这种情况下,在交换之前存在双向对称,因为位置B中的卡1的概率=卡1在位置C = 0的概率。因此实际上,卡1可以结束对称概率并且它最终结束在三个位置中的每个位置具有相同的概率。所有后续掉期都是如此。但是卡片2在第一次交换之后在三个位置卷起概率(1 / 3,2 / 3,0),同样卡3在三个位置卷起概率(1 / 3,0,2 / 3) 。因此,无论我们进行多少次后续交换,我们都不会因为卡2或3的概率占据所有三个位置而完全相同。

答案 5 :(得分:2)

请参阅编码恐怖帖The Danger of Naïveté

基本上(附3张卡):

  

天真的洗牌导致33(27)   可能的甲板组合。那是   奇怪,因为数学告诉我们   真的只有3个!或6   3张牌的可能组合   甲板。在KFY shuffle中,我们开始   初始订单,从交换   三个中的任何一个的第三个位置   卡,然后从第二个交换   其余两张牌的位置。

答案 6 :(得分:1)

简单的答案是该算法有52 ^ 52种可能的运行方式,但只有52种!可能安排52张卡。为了使算法公平,它需要同样可能产生这些安排中的每一个。 52 ^ 52不是52的整数倍!因此,某些安排必须比其他安排更有可能。

答案 7 :(得分:1)

一种说明性的方法可能是这样的:

1)只考虑3张牌。

2)为了使算法得到均匀分布的结果,“1”结束为[0]的概率必须为1/3,“2”结束于[1]的概率必须为1 / 3也是如此。

3)所以如果我们看第二个算法:

  

“1”在[0]处结束的概率:   当0是生成的随机数时,   因此,(0,1,2)中的1例,   是1/3 = 1/3

     

“2”在[1]处结束的概率:   当它没有被交换到[0]时   第一次,它没有被交换   第二次到[2]:2/3 * 1/2 =   1/3

     

“3”以[2]结束的概率:   当它没有被交换到[0]时   第一次,它没有被交换   到[1]第二次:2/3 * 1/2 =   1/3

     

他们都是完美的1/3,我们   这里没有看到任何错误。

4)如果我们试图计算第一个算法中“1”结束为[0]的概率,计算会有点长,但正如lassevk的答案所示,它是9 / 27 = 1/3,但是以[1]结束的“2”有8/27的机会,而以[2]结束的“3”有可能是9/27 = 1/3。

结果,作为[1]结尾的“2”不是1/3,因此算法会产生相当偏差的结果(大约3.7%的误差,而不是任何可忽略的情况,如3/10000000000000 = 0.00000000003 %)

5)Joel Coehoorn所拥有的证据,实际上可以证明某些案件将被过度代表。我认为为什么它是n ^ n的解释是这样的:在每次迭代时,有可能随机数可以,所以在n次迭代之后,可能有n ^ n个情况= 27.这个数字不会变暗在n = 3的情况下,均匀的数量(n!= 3!= 6),因此一些结果被过度表示。它们的代表性过高,而不是出现4次,它会出现5次,所以如果你从最初的1到52的顺序洗牌数百万次,过度代表的情况将会出现500万时间而不是400万次,这是相当大的差异。

6)我认为过度表现会被显示出来,但过度表现会“为什么”发生?

7)对于算法的正确性的最终测试是任何数字在任何时隙都有1 / n概率结束。

答案 8 :(得分:0)

以下是card shuffling Markov chains的精彩分析。哦等等,这都是数学。抱歉。 :)

答案 9 :(得分:0)

Naive算法选择n的值,如下所示:

n = rand(3)

n = rand(3)

n = rand(3)

3 ^ 3种可能的n

组合

1,1,1,1,1,2 ...... 3,3,2 3,3,3(27种组合)lassevk的答案显示了这些组合卡片之间的分布。

更好的算法:

n = rand(3)

n = rand(2)

N! n

的可能组合

1,1,1,2,1 2,1 2,2 3,1 3,2(6种组合,它们都给出不同的结果)

正如其他答案中所提到的,如果你试图获得6次结果,你就不可能达到均匀分布的6个结果,因为27不能被6整除。将27个弹珠放入6个桶中,无论你是什么有些水桶比其他水桶有更多的弹珠,你能做的最好的是1到6桶的4,4,4,5,5,5弹珠。

天真洗牌的根本问题在于交换太多次,完全洗牌3张牌,你只需要进行2次掉期,而第二次掉牌只需要在前两张牌中,因为第3张牌已经有了交换机会的1/3。如果您的总交换组合可以被6整除,那么继续交换卡片会给予更换一张特定卡牌的机会,这些机会只会达到1 / 3,1 / 3,1 / 3。

答案 10 :(得分:0)

不是需要另一个答案,但我发现值得尝试找出Fisher-Yates 统一的原因。

如果我们谈论的是一个包含N个项目的牌组,那么这个问题是:我们怎样才能显示

Pr(Item i ends up in slot j) = 1/N?

使用条件概率将其分解,Pr(item i ends up at slot j)等于

Pr(item i ends up at slot j | item i was not chosen in the first j-1 draws)
* Pr(item i was not chosen in the first j-1 draws).

然后从那里递归地扩展回第一次抽奖。

现在,第一次抽奖中未绘制元素i的概率为N-1 / N。并且它没有在第二次抽签中绘制的概率条件是它没有在第一次抽签上绘制的事实是N-2 / N-1等等。

因此,我们得出第一个i抽奖中未绘制元素j-1的概率:

(N-1 / N) * (N-2 / N-1) * ... * (N-j / N-j+1)

当然我们知道它是以j 为基础绘制的概率,而不是之前绘制的只是1 / N-j

请注意,在第一个词中,分子全部取消后续分母(即N-1取消,N-2取消,一直到N-j+1取消,只留下{{1} })。

因此,N-j / N插槽中出现元素i的总体概率为:

j

正如所料。

为了更加全面地了解“简单随机播放”,它缺少的特定属性称为exchangeability。由于shuffle创建方式的“路径依赖性”(即,遵循27条路径中的哪条路径来创建输出),您无法将不同的分量随机变量视为可以按任何顺序出现。事实上,这可能是 激励的例子,说明为什么可交换性在随机抽样中很重要。

答案 11 :(得分:0)

显示第一个算法失败的最明确答案是,将有问题的算法视为n个图上n个步骤的马尔可夫链! n个自然数的所有置换的顶点。该算法以转移概率从一个顶点跳到另一个顶点。第一种算法为每个跃点给出1/n的转移概率。存在n ^ n条路径,每条路径的概率为1/n^n。假设降落在每个顶点上的最终概率为1/n!,这是减少的分数。要实现这一点,必须有m个具有相同最终顶点的路径,使得m/n^n=1/n!n^n = mn!代表某个自然数m,或者n^n被{{1}整除}。但这是不可能的。否则,n必须被n!整除,这仅在n-1时才可能。我们有矛盾。