我在这里有一点不同的想法。我们可以使用原始范围,并在该范围内的某处选择一个映射到范围限制的受害者编号,而不是尝试将范围扩展到一个 epsilon。我们可以通过随机选择一个并不时更改它来避免硬编码的受害者:
(defun make-random-gen (range)
(let ((victim nil)
(count 1))
(lambda ()
(when (zerop (decf count))
(setf count 10000
victim (random range)))
(let ((out (random range)))
(if (eql out victim) range out)))))
(defun testit ()
(loop with r = (make-random-gen 1.0)
for x = (funcall r)
until (eql x 1.0)
counting t))
在听者处:
[5]> (testit)
23030093
这里有一个小偏差,victim 永远不会等于range。也就是说,range 值如 1.0 永远不会是victim,因此总是有一定的发生几率。而其他所有值都可能会转变为victim,其出现的机会暂时减少到零。在对输出的统计分析中应该可以隐约检测到这一点,因为 range 值的出现频率将略高于任何其他值。
用更正来更新这种方法会很有趣,尝试这样做:
(defun make-random-gen (range)
(let ((victim nil)
(count 1))
(labels ((gen ()
(when (zerop (decf count))
(setf count 10000
victim (gen)))
(let ((out (random range)))
(if (eql out victim) range out))))
#'gen)))
现在,当我们选择victim 时,我们会递归我们自己的函数,该函数可能会选择range。每当range 被选为victim 时,该值就会被正确抑制:range 不会出现在输出中,因为out 永远不会是eql 到range。
我们可以用以下挥手的论据来证明这一点:
让我们假设对gen 的递归调用稍微偏向于输出range。但无论何时发生这种情况,range 都会被选为victim,这会阻止它出现在gen 的输出中。
有一种负反馈几乎可以完全纠正这种偏见。
注意:如果我们的随机数生成lambda 也捕获了一个随机状态对象并使用它,那么它的设计会更好。然后它产生的序列将不受伪随机数生成器的其他用途的干扰。那是另一个话题。
在理论上,请注意 [0, 1) 和 [0, 1] 都不会产生严格正确的分布。如果我们有一个数学上理想的 PRNG,它将产生这些范围内的实际实数。由于该范围包含无数个实数值,因此每个数值的出现概率为零:1/aleph-null,我猜它是如此之小,以至于无法与真正的零区分开来。
我们想要的是近似理想 PRNG 的浮点 PRNG。
问题在于每个浮点值都近似于一个实数值范围。所以这意味着如果我们有一个 0.0 到 1.0 范围内的值生成器,它实际上代表了从 -epsilon 到 1.0 + epsilon 的实数范围。如果我们从此 PRNG 中获取值并绘制值的条形图,则图中的每个条形都必须具有一些非零宽度。 0.0 柱以 0 为中心,1.0 柱以 1 为中心。实数的分布从左柱的左边缘延伸到右柱的右边缘。
为了创建一个模拟 0.0 到 1.0 区间内值的均匀分布的 PRNG,我们必须以一半的概率包含 0.0 和 1.0 的值。也就是说,当我们从 PRNG 中收集大量值时,图表的 0.0 和 1.0 条应该是所有其他条的一半左右。
在这些条件下,我们无法区分 [0, 1.0) 区间和 [0, 1.0] 区间,因为它们完全一样大。我们必须包括 1.0 值,大约是通常概率的一半,以解决上述均匀性问题。如果我们简单地排除该值,我们会在错误的方向上产生偏差,因为直方图中的 1.0 条现在具有零值。
我们可以挽救这种情况的一种方法可能是采用直方图的 1.0-epsilon 条,并使该值的可能性高出 50%,从而使条比平均值高 50%。基本上,我们重载了 1.0 之前范围的最后一个值,以表示不包括 1.0 的所有内容,要求该值更有可能。然后,我们从输出中排除 1.0 值。从左边算起接近 1.0 的所有值都映射到 1.0 - epsilon 的额外 50% 概率。