这种混洗算法有什么问题(如果有的话),我怎么知道?


77

就像背景一样,我知道Fisher-Yates完美的随机播放。它的O(n)复杂度和保证的均匀性是一个很好的改组,如果在允许就地更新数组的环境中使用它,我真是个傻瓜(所以在大多数情况下,如果不是全部,命令式编程环境)。

遗憾的是,函数式编程世界无法让您访问可变状态。

但是,由于Fisher-Yates的缘故,我没有太多关于如何设计改组算法的文献。在解决这个问题的几个地方都做了简短的说明,然后才说“实际上是Fisher-Yates,这是您需要知道的所有内容”。最后,我必须提出自己的解决方案。

我想出的解决方案是这样的,它可以随机排列任何数据列表:

  • 如果列表为空,则返回空集。
  • 如果列表中有单个项目,则返回该单个项目。
  • 如果列表非空,请使用随机数生成器对列表进行分区,然后将算法递归应用于每个分区,以组合结果。

在Erlang代码中,它看起来像这样:

shuffle([])  -> [];
shuffle([L]) -> [L];
shuffle(L)   ->
  {Left, Right} = lists:partition(fun(_) -> 
                                    random:uniform() < 0.5 
                                  end, L),
  shuffle(Left) ++ shuffle(Right).

(如果对您来说,这看起来像是一种疯狂的快速排序,那么基本上就是这样。)

因此,这就是我的问题:使得费舍尔-耶茨(Fisher-Yates)很难找到改组算法的情况使寻找工具来分析改组算法同样困难。在分析PRNG的均匀性,周期性等方面,我可以找到很多文献,但是关于如何分析混洗的信息却很少。(实际上,我发现的有关分析混洗的一些信息完全是错误的,很容易通过简单的技术来欺骗。)

所以我的问题是:我该如何分析我的改组算法(假设random:uniform()调用是由生成具有良好特征的适当随机数的任务决定的)?我可以使用哪些数学工具来判断,例如,在1..100范围内的整数范围内的100,000次混洗运行是否给了我很好的混洗结果?我已经进行了一些测试(例如,将随机播放中的增量与减量进行比较),但是我想知道更多一些内容。

并且,如果对这种随机播放算法本身有任何见解,也将不胜感激。


这个问题的答案可能会有所帮助:stackoverflow.com/questions/1685339/…值得一看一下Knuth对Fisher-Yates的分析(请参阅您链接的Wikipedia文章以获取引用)。
Alex Mendes da Costa

4
我建议您将此内容带到MathOverflow。归纳证明它按预期工作,似乎可以归结为计算一行总和。(但我可以肯定,这正确的,尽管不能保证在任何给定的时间停止)。
2010年

doublep>我也认为此算法有效。请参阅我的帖子以获取详细说明。
加什

我认为无限减速在排序算法中被认为是非常糟糕的吗?而且,不是lists:splitlists:droplast并且lists:append使实现标准算法变得微不足道吗?

Answers:


76

一般说明

我个人关于使用概率算法的正确性的方法:如果您知道如何证明它是正确的,那么它可能是正确的。如果不这样做,那肯定是错误的。

换句话说,尝试分析每种可能的算法通常是没有希望的:必须一直寻找一种算法,直到找到可以证明正确的算法为止。

通过计算分布分析随机算法

我知道一种“自动”分析混洗(或更普遍地说是随机使用算法)的方法,该方法比简单的“进行大量测试并检查均匀性”要强。您可以机械地计算与算法的每个输入关联的分布。

通常的想法是,随机使用算法探索了可能性世界的一部分。每次算法要求一组中的随机元素(掷硬币时{ truefalse}),算法都会有两种可能的结果,并且选择其中一种。您可以更改算法,以使其不再返回可能的结果之一,而是并行探索所有解决方案,并返回具有相关分布的所有可能的结果。

通常,这需要深度重写算法。如果您的语言支持带分隔符的延续,则不必这样做。您可以在函数内实现“探索所有可能的结果”,以请求一个随机元素(这种想法是,随机生成器而不是返回结果,而是捕获与程序关联的延续并以所有不同的结果运行它)。有关此方法的示例,请参见olegHANSEI

一个中间的解决方案,可能是较不为人知的解决方案,是将这种“可能的结果世界”表示为monad,并使用Haskell这样的语言来进行monadic编程。这是在Haskell中使用概率包的概率monad实现算法变式¹的示例实现:

import Numeric.Probability.Distribution

shuffleM :: (Num prob, Fractional prob) => [a] -> T prob [a]
shuffleM [] = return []
shuffleM [x] = return [x]
shuffleM (pivot:li) = do
        (left, right) <- partition li
        sleft <- shuffleM left
        sright <- shuffleM right
        return (sleft ++ [pivot] ++ sright)
  where partition [] = return ([], [])
        partition (x:xs) = do
                  (left, right) <- partition xs
                  uniform [(x:left, right), (left, x:right)]

您可以为给定的输入运行它,并获得输出分布:

*Main> shuffleM [1,2]
fromFreqs [([1,2],0.5),([2,1],0.5)]
*Main> shuffleM [1,2,3]
fromFreqs
  [([2,1,3],0.25),([3,1,2],0.25),([1,2,3],0.125),
   ([1,3,2],0.125),([2,3,1],0.125),([3,2,1],0.125)]

您可以看到,该算法对于大小为2的输入是统一的,但是对于大小为3的输入是不一致的。

与基于测试的方法的区别在于,我们可以在有限的步骤中获得绝对的确定性:它可以很大,因为它相当于对可能性世界的详尽探索(但通常小于2 ^ N,因为(有相似结果的分解),但是如果它返回非均匀分布,我们可以肯定算法是错误的。当然,如果它返回一个均匀分布[1..N]1 <= N <= 100,你只知道你的算法是一致的高达100大小的名单; 可能仍然是错误的。

¹:由于特定的数据透视处理,该算法是您Erlang实现的一种变体。如果我不使用数据透视,就像您的情况一样,输入大小不会在每一步都减小:该算法还会考虑所有输入都在左列表(或右列表)中并在无限循环中丢失的情况。这是monad概率实现的弱点(如果算法的非终止概率为0,则分布计算可能仍会发散),我尚不知道如何解决。

基于排序的洗牌

这是一个简单的算法,我确信自己可以证明是正确的:

  1. 为集合中的每个元素选择一个随机密钥。
  2. 如果键不是完全不同的,请从步骤1重新开始。
  3. 通过这些随机键对集合进行排序。

如果您知道发生碰撞的概率(选择的两个随机数相等)足够低,则可以省略第2步,但是如果没有这种可能性,则混洗不是完全均匀的。

如果在[1..N]中选择键,其中N是集合的长度,则会发生很多冲突(生日问题)。如果将密钥选择为32位整数,则在实践中发生冲突的可能性较低,但仍然会遇到生日问题。

如果将无限(懒惰求值)的位串用作键而不是有限长度的键,则发生碰撞的可能性为0,并且不再需要检查区别性。

这是OCaml中的改组实现,使用惰性实数作为无限位字符串:

type 'a stream = Cons of 'a * 'a stream lazy_t

let rec real_number () =
  Cons (Random.bool (), lazy (real_number ()))

let rec compare_real a b = match a, b with
| Cons (true, _), Cons (false, _) -> 1
| Cons (false, _), Cons (true, _) -> -1
| Cons (_, lazy a'), Cons (_, lazy b') ->
    compare_real a' b'

let shuffle list =
  List.map snd
    (List.sort (fun (ra, _) (rb, _) -> compare_real ra rb)
       (List.map (fun x -> real_number (), x) list))

还有其他方法可以“纯改组”。apfelmus的基于mergesort的解决方案是一个不错的选择

算法上的考虑:先前算法的复杂性取决于所有键都不同的概率。如果将它们选择为32位整数,则特定键与另一个键发生冲突的概率约为40亿。假设选择一个随机数为O(1),则按这些键排序的结果为O(n log n)。

如果您使用无限的位串,则不必重新启动选择,但是复杂度则与“平均计算流的元素数”有关。我猜想它平均为O(log n)(因此总共仍然为O(n log n)),但没有证据。

...而且我认为您的算法有效

经过更多的反思,我认为(像douplep一样)您的实现是正确的。这是一个非正式的解释。

列表中的每个元素都经过几次random:uniform() < 0.5测试。对于一个元素,您可以将这些测试的结果列表关联为布尔值或{ 01}列表。在算法开始时,您不知道与这些数字相关的列表。第一次partition调用后,您知道每个列表的第一个元素,等等。当您的算法返回时,测试列表是完全已知的,并且根据这些列表对元素进行排序(按字典顺序排序,或视为实数的二进制表示形式)数字)。

因此,您的算法等效于按无限位字符串键排序。对列表进行分区的操作,使人想起quicksort在数据透视元素上的分区,实际上是一种方法,对于位串中的给定位置,将具有评估0的元素与具有评估的元素分开1

排序是统一的,因为位串都不同。确实,n在递归shuffle调用depth期间,两个实数等于第-位的元素位于分区的同一侧n。仅当分区产生的所有列表为空或单例时,该算法才会终止:所有元素均已通过至少一项测试分隔,因此具有一个不同的二进制十进制数。

概率终止

关于您的算法(或等效的基于排序的方法)的一个微妙之处是终止条件是概率性的。Fisher-Yates总是在已知数量的步骤(数组中元素的数量)之后终止。对于您的算法,终止取决于随机数生成器的输出。

有可能的输出会使您的算法发散,而不是终止。例如,如果随机数生成器始终输出0,则每次partition调用将返回未更改的输入列表,在该列表上递归调用shuffle:您将无限期循环。

但是,如果您确信自己的随机数生成器是公平的,那么这不是问题:它不会作弊,并且始终返回独立的均匀分布结果。在这种情况下,测试random:uniform() < 0.5始终返回true(或false)的概率恰好为0:

  • 前N个电话返回的概率true为2 ^ {-N}
  • 所有呼叫返回true的概率是前N个呼叫返回的事件对于所有N的无限交集的概率0;它是2 ^ {-N}的最小极限¹,为0

¹:有关数学的详细信息,请参见http://en.wikipedia.org/wiki/Measure_(mathematics)#Measures_of_infinite_intersections_of_measurable_sets

更一般而言,仅当某些元素与同一布尔流相关联时,算法才会终止。这意味着至少两个元素具有相同的布尔流。但是,两个随机布尔流相等的概率再次为0:在位置K的数字相等的概率为1/2,因此N个第一位数相等的概率为2 ^ {-N},并且相同分析适用。

因此,您知道您的算法以1终止。这总是会终止的Fisher-Yates算法的保证较弱。特别是,您很容易受到邪恶的对手的攻击,该攻击会控制您的随机数生成器。

利用更多的概率论,您还可以计算给定输入长度下算法运行时间的分布。这超出了我的技术能力,但是我认为这很好:我想您只需要平均查看O(log N)个第一位数字,即可检查所有N个惰性流是否不同,以及运行时间更长的可能性呈指数下降。


3
不过,我真正的问题是,我可以在混洗器的输出端进行哪些经验检验,以查看其是否被混洗了?例如,即使我的能力有限,这种“将每个元素的随机权重配对”的方法也进行了糟糕的测试。(我反复测试了序列[1,2],发现了巨大的失衡。)
我的正确观点是2010年

[min_int..max_int]由于您提到的生日问题,不足以使冲突概率接近0:使用32位整数,您仅通过约77,000个项目的列表就已经达到了0.5的冲突机会。
Pi Delport

另外,请注意,通常来说,使所有基于排序的混洗完美统一/正确可能比起初看起来要难得多:有关某些问题,请参阅Oleg的文章和我的回答。如果完美的混洗根本很重要,那么仅使用Fisher-Yates算法无疑会容易得多。
Pi Delport

我编辑提到您的警告[min_int..max_int]:您是正确的,并且不会按大顺序进行扩展。我还包括了基于实数排序的实现。我同意费舍尔·耶茨(Fisher-Yates)更简单,但是我不确定奥列格(Oleg)的提议是否如此。
加什

1
@AJMansfield:实际上,使用64位密钥,您只需要约50亿个选择,即可预期发生50%概率的碰撞。经过100亿次选择,发生碰撞的可能性增加到〜93%。这个违反直觉的结果就是生日问题。
2014年

23

您的算法是基于排序的随机播放,如Wikipedia文章所述。

一般来说,基于排序洗牌的计算复杂度是一样的底层的排序算法(例如O(ñ日志ñ)平均值,O(ñ ²)用于基于快速排序洗牌最坏的情况下),和而分布不完全均匀,因此对于大多数实际用途,它应足够接近均匀。

Oleg Kiselyov提供以下文章/讨论:

涵盖基于排序洗牌的详细计划书费-耶茨战略的两个改编的局限性,也:一个天真的O(ñ ²)之一,基于二叉树O(ñ日志ñ)之一。

遗憾的是,函数式编程世界无法让您访问可变状态。

事实并非如此:虽然纯函数式编程避免了副作用,但它支持访问具有一流效果的可变状态,而无需副作用。

在这种情况下,您可以使用Haskell的可变数组来实现本教程中所述的突变Fischer-Yates算法:

附录

随机排序的特定基础实际上是无限键基数排序:正如gasche指出的那样,每个分区都对应一个数字分组。

此方法的主要缺点与其他任何无限键排序洗牌相同:没有终止保证。尽管终止的可能性随着比较的进行而增加,但是永远不会有上限:最坏情况下的复杂度是O(∞)。


抱歉,我不够精确。 有效访问可变状态。;)
只是我的正确观点,2010年

4
是什么让您认为效率不高?
德尔波特

无需经过Oleg纯解决方案的额外复杂性,就可以轻松地将基于排序的简单混搭固定为完全统一(前提是基础随机数生成器也完全统一)。在排序过程中比较两个元素相等时,您将失去一致性:必须做出任意选择才能对它们进行排序。您可以选择保证永远不相等的权重,例如随机选择的实数(浮点数,甚至更好的惰性布尔流)。Cf
Haskell

gasche:这仍然接近统一,而不是完全统一。有四个要解决的问题:(1)从有限的键空间中进行任何选择,根据定义,重复是不可避免的。(2)如果像延迟布尔流那样使键空间无限,则不再保证算法会终止。(3)如果丢弃并重新选择重复项,则会造成偏差,并且密钥不再统一。(4)正如Oleg指出的那样,即使您可以解决前面的三个问题,您仍然必须证明键分配的配置空间可以被N!整除。
Pi Delport

具有惰性布尔流的算法以1概率终止。这与“总是终止”不同,特别是您容易受到邪恶的随机数生成器(例如始终输出“ 1”)的影响,但是一个相当有力的保证。回覆。N!:当然,如果您统一选择N个不同的权重,则其订购的配置空间大小为N!。
加什

3

我前段时间在做类似的事情,尤其是您可能对Clojure的向量感兴趣,这些向量功能齐全且不可变,但仍具有O(1)随机访问/更新特性。这两个要点具有“从这个M大小的列表中随机抽取N个元素”的几种实现方式;如果让N = M,则至少其中之一会变成Fisher-Yates的功能实现。

https://gist.github.com/805546

https://gist.github.com/805747


1

基于如何测试随机性(以点洗牌为例),我建议:

随机排列(中等大小)的数组,其中包含相等数量的零和一。重复并串联直到无聊。使用这些作为顽固测试的输入。如果您的混洗效果很好,那么您应该生成零和一的随机序列(注意:在中等大小的数组的边界处,零(或一)的累积过量为零),您希望测试能够检测到,但“媒介”越大,他们这样做的可能性就越小)。

请注意,测试可以拒绝您的随机播放,原因有以下三个:

  • 随机播放算法不好,
  • 随机播放器或初始化期间使用的随机数生成器不正确,或者
  • 测试实现不好。

如果任何测试拒绝,则必须解决这种情况。

顽固性测试的各种改编(为了解析某些数字,我使用了顽固性页面源代码)。自适应的原理机制是使混洗算法充当均匀分布的随机比特的来源。

  • 生日间隔:在n个零的数组中,插入log n个。随机播放。重复直到无聊。构造一个一间距离的分布,并与指数分布进行比较。您应该使用不同的初始化策略来执行此实验-前端的策略,末端的策略,中间的策略以及随机分散的策略。(后者最大的危险是初始化随机化较差(相对于混洗随机化而言),导致混洗被拒绝。)这实际上可以用相同值的块完成,但是存在的问题是在分布中引入了相关性(一个和两个不能在一个随机播放中位于同一位置)。
  • 重叠排列:将5个值混洗一堆。验证120个结果的可能性大致相同。(卡方检验为119个自由度-顽固性检验(cdoperm5.c)使用99个自由度,但这(主要是(由于使用输入序列的重叠子序列而引起的)序列相关伪像。)
  • 矩阵的等级:从2 *(6 * 8)^ 2 = 4608个比特(通过混洗相等数量的零和1)中选择6个不重叠的8位子字符串。将它们视为6×8二进制矩阵并计算其等级。重复100,000个矩阵。(将0-4的等级汇总在一起,则等级分别为6、5或0-4。)等级的预期分数为0.773118、0.217439、0.004433。卡方与具有两个自由度的观察分数进行比较。31 x 31和32 x 32测试相似。排名分别为0-28和0-29。预期分数为0.2887880952、0.5775761902、0.1283502644、0.0052854502。卡方检验具有三个自由度。

等等...

您也不妨利用dieharder和/或耳鼻喉科作出类似的调整测试。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.