洗牌算法分析

Posted

技术标签:

【中文标题】洗牌算法分析【英文标题】:Shuffling algorithm analysis 【发布时间】:2011-09-03 06:02:02 【问题描述】:

我遇到了以下洗牌算法的分析:

Q:给定一个不同整数的数组,给一个算法随机 重新排序整数,以便每个可能的重新排序都是平等的 可能。换句话说,给定一副牌,你怎么能洗牌? 它们使得卡片的任何排列都有同样的可能性?

好答案:按顺序浏览元素,将每个元素与 数组中不早于 元素。这需要 O(n) 时间。注意有几种可能 这个问题的解决方案,以及几个好看的答案 这是不正确的。比如对上面的稍作修改 算法,其中一个切换每个元素与任何元素 数组不会以相同的概率进行每次重新排序

我想知道为什么将每个元素与数组中的任何其他元素切换不会产生良好的随机播放,而不是使用 Knuth 随机播​​放(已描述)。另外,Knuth 如何以相等的概率随机选择值?非常感谢任何数学或证明。

【问题讨论】:

【参考方案1】:

该算法不产生均匀随机排列的最简单证明

for (int i = 0; i < 3; ++i) 
   swap(a[i], a[rand() % 3]);

是不是它产生了 27 种可能的结果,但只有 3 种! = 6 个排列。由于 6 不除 27,所以一定有一些排列是采得太多,有些是采得少。

为什么 O(n) 算法是最优的?好吧,随机洗牌有时必须触及每个输入(以更改它们),因此任何最佳算法都需要至少完成 O(n) 的工作。

为什么 Knuth 算法是正确的?这需要更多的洞察力。您可以通过归纳证明以正确的概率选择第一个项目(每个项目同样可能是第一个),然后证明当您通过循环前进时,归纳步骤成立,第二个,第三个等项目是也以正确的概率从数组的其余部分中选择。

【讨论】:

嘿,谢谢您的回答。最优,我的意思是 - 为什么 Knuth shuffle 为每个选择的元素提供相等的概率? 不会用数组中的 any 元素切换为:swap(a[i], a[rand() % 3]);? 我喜欢 Knuth shuffle 的一件事是它的直观正确性。将数组中洗过的部分(最初什么都没有)想象成一堆纸牌,将尚未洗好的部分想象成一堆纸牌。在每一步,您从牌堆中随机选择一张牌并将其添加到牌堆顶部。很明显,只有一种方法可以让给定的排序做到这一点,而且所有排序的可能性都相同。 对不起,我很傻,你能解释一下 op 的算法是如何产生 27 个结果的吗?我不明白,@Mankarse 的答案是我评估为什么这个算法不相等的逻辑过程,但你的推理似乎更直接,你能帮忙解释一下吗。 @javarookie:算法迭代 3 次,在这 3 次中的每一次中,代码都会随机选择三个选项之一。这是 3^3,或 27 种不同的可能代码路径。【参考方案2】:

考虑一个三元素列表。它有这些可能的状态和相关的概率:

1 [a, b, c] (0)

在第一次洗牌操作中,a 有 1/3 的机会与任何元素交换,因此可能的状态和相关概率如下:

From (0)
1/3 [a, b, c] (1)
1/3 [b, a, c] (2)
1/3 [c, b, a] (3)

在第二次洗牌操作中,除了第二个槽外,同样的事情再次发生,所以:

From (1) ([a, b, c])
1/9 [b, a, c] (4)
1/9 [a, b, c] (5)
1/9 [a, c, b] (6)
From (2) ([b, a, c])
1/9 [a, b, c] (7)
1/9 [b, a, c] (8) 
1/9 [b, c, a] (9)
From (3) ([c, b, a])
1/9 [b, c, a] (10)
1/9 [c, b, a] (11)
1/9 [c, a, b] (12)

在第三次洗牌操作中,同样的事情发生了,除了第三个槽,所以:

From (4) ([b, a, c])
1/27 [c, a, b] (13)
1/27 [b, c, a] (14)
1/27 [b, a, c] (15)
From (5) ([a, b, c])
1/27 [c, b, a] (16)
1/27 [a, c, b] (17)
1/27 [a, b, c] (18)
From (6) ([a, c, b])
1/27 [b, c, a] (19)
1/27 [a, b, c] (20)
1/27 [a, c, b] (21)
From (7) ([a, b, c])    
1/27 [c, b, a] (22)
1/27 [a, c, b] (23)
1/27 [a, b, c] (24)
From (8) ([b, a, c])
1/27 [c, a, b] (25)
1/27 [b, c, a] (26)
1/27 [b, a, c] (27)
From (9) ([b, c, a])
1/27 [a, c, b] (28)
1/27 [b, a, c] (29)
1/27 [b, c, a] (30)
From (10) ([b, c, a])
1/27 [a, c, b] (31)
1/27 [b, a, c] (32)
1/27 [b, c, a] (33)
From (11) ([c, b, a])
1/27 [a, b, c] (34)
1/27 [c, a, b] (35)
1/27 [c, b, a] (36)
From (12) ([c, a, b])
1/27 [b, a, c] (37)
1/27 [c, b, a] (38)
1/27 [c, a, b] (39)

结合相似的术语,我们得到:

4/27 [a, b, c] From (18), (20), (24), (34)
5/27 [a, c, b] From (17), (21), (23), (28), (31)
5/27 [b, a, c] From (15), (27), (29), (32), (37)
5/27 [b, c, a] From (14), (19), (26), (30), (33)
4/27 [c, a, b] From (13), (25), (35), (39)
4/27 [c, b, a] From (16), (22), (36), (38)

这显然是不平衡的。

仅从尚未选择的元素中选择的随机播放是正确的。为了证明,我提出这个:

假设您有一袋元素。如果您从该袋子中随机挑选并将结果元素放入列表中,您将获得一个随机排序的列表。这本质上就是仅与尚未选择的那些元素进行交换(考虑将放置东西的列表作为列表的开头,将袋子作为可以交换的列表的尾部)。

【讨论】:

【参考方案3】:

首先,所描述的算法为 O(n) 并不是完全,尽管它非常接近。实际上应该是 O(n*log(n))。

原因如下:第一次交换需要从 n 个元素中提取,然后是 n-1 ... 2。但是从 n 个元素中选择的复杂度应该是 log(n),因为你必须随机生成 log(n)位。

rrenaud 给出了一个很好的论点,即“坏”算法不是统一的,所以我会尝试论证“好”算法是统一的。每一步你从 n, n-1, ... 1 个选项中选择一个,所以最终总共有 n 个!你可以做出的选择。既然有n!排列列表的方法,如果每个排列都可以通过至少一个选择序列到达,那么每个排列都可以通过一个选择序列到达。因此,为了证明它是一致的,我们只需要证明给定一些可能的排序,我们可以通过一系列选择来达到它。

现在问题看起来很简单。假设你从

a b c d e

你想得到

b c d e a

将光标放在第 0 个元素上。你应该换哪个? b,因为你想把它移到0位置。现在进展。在每一步,你“身后”的所有元素都在正确的位置,所以当你走到尽头时,所有的元素都在正确的位置。

【讨论】:

谢谢欧文。不幸的是,我不同意你的第一个说法。该算法是 O(n),因为从 n 个元素中选择的复杂性是 O(1),因为您依赖于数组的连续形式来选择数组索引范围内的随机元素。假设随机数生成器在 O(1) 时生成一个数字,则算法在 O(n) 时运行。另一点:“既然有 n! 排序列表的方法”实际上应该是“因为有 n! 排列列表的方法”。你说的和你的意思有很大的不同。但再次感谢您的帮助。 @OckhamsRazor 如果您需要从 n 个元素中进行选择,您至少需要 log2(n) 个随机位来做出该决定。 欧文的问题是生成随机数不是 O(1)。例如,rand() 的某些实现具有 32K 的 RAND_MAX。 (16 位。)因此,如果您想在 2^16 和 2^32 个项目之间随机播放,则需要 2 次调用 rand,并且随着数量的增长逐渐增加。 (如果您需要确保结果由于 mod 剪辑而真正无偏见,那就更棘手了:(但您可能很乐意忽略这种偏见。) 所以随机数生成不在 O(1) 运行?好吧,那么欧文可能是对的。另外,您提到了“至少一个选择序列”。虽然我明白你的意思,但措辞具有误导性。如果有 x 个序列要进行排序,则所有排序都必须为真,以便使分布符合均匀分布。您的措辞意味着获得一些订单只需要至少有 1 个序列,而获得其他一些订单是否有更多的方法可以到达那里并不重要。否则,我很喜欢你的回答。 @OckhamsRazor 说得很好,我有点不清楚。我的意思是,既然有 n!选择和n!排序,如果每个排序至少有一个选择,那么每个排序只有一个选择。如果你能提出改写的建议,我会编辑它。【参考方案4】:

首先,请注意 Knuth 的方式必须是一致随机的,因为这本质上等同于从堆栈 A 中随机抽取卡片并通过以随机顺序放置这些卡片来形成堆栈 B。这必须是均匀随机的。

要看出另一种方式不好,只需表明不同结果的数量会排除一致的结果。有 52^52 种方法可以选择 1 到 52 之间的 52 个随机整数。但是,有 52 个!这些整数的排列。 52!有 47 作为一个因素,而 52^52 没有;所以52!不均分 52^52。这意味着至少一个排列比其他排列具有更多导致它的结果……要看到这一点,请尝试将结果均分,直到用完为止。由于结果的数量不是排列数量的倍数,因此您不能给每个人相同的数量。换句话说,如果你把所有的吸盘都给了,你不能把 12 个吸盘平均分配给 5 个孩子。原理相同。

【讨论】:

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

洗牌问题

程序员的算法趣题Q50: 完美洗牌

关于数组方面的算法分析

游戏常用算法-洗牌算法

洗牌算法

每日算法洗牌算法