这是我最近遇到的这些非常简单的编程难题之一: 给定一个函数以相等的概率返回0
到4
随机整数,编写一个返回0
到6
随机整数的函数。
当然,解决方案还应该返回均布的数字。 因此,让我们从输入函数示例定义开始:
def rand4() = (math.random * 5).toInt
你的任务是实现rand6()
只用rand4()
给自己几分钟,继续阅读。
第一种方法非常简单,但是碰巧被彻底打破了:
def rand6() = rand4() * 3 / 2
就如此容易。 在理想的解决方案中,应该以1/7
的概率出现从0
到6
每个输出值。 您能否从上面的代码中看出rand6()
返回2
或5
的概率是rand6()
? 没错,它不超过0
,您永远不会得到这些值。 我希望很清楚为什么。 因此,让我们去做一些更复杂的事情:
def rand6() = (rand4() + rand4()) % 7
看起来更好,但仍然很远。 上面的代码有两个主要缺陷。 首先, rand4() + rand4()
表达式的结果范围是0
到8
但是我们需要0
到6
。 显而易见的解决方案是使用% 7
操作。 但是,这导致0
和1
返回的频率是两倍,因为7
和8
溢出到0
和1
。 那么呢:
def rand6(): Int = {
val rand8 = rand4() + rand4()
if(rand8 > 6)
rand6()
else
rand8
}
我希望递归(可以很容易地转换成循环,但我将工作留给Scala编译器)不会掩盖意图–如果两个rand4()
调用的总和超出预期结果,我们只需丢弃它并调用rand6()
再次。 但是,仍然存在一个细微但引人注目的错误,引用Wikipedia的统一分布 。 两个独立的,均匀分布的均匀分布之和产生对称的三角形分布 。 如果您还不了解以上内容,请使用<canvas/>
演示一下Wikipedia的含义, 以JavaScript演示此实时演示 :
该程序只是将像素放置在每个面板上的随机 (X, Y)
位置。 在第一个面板中,我使用了一个Math.random() * 300
调用,该调用按比例缩放以适合整个画布。 如您所见,分布或多或少是均匀的。 但是我们不能说第二和第三面板。 在第二个面板上,我原则上使用两个均匀分布的变量之和: (Math.random() + Math.random()) * 150)
。 即使此表达式可以返回0
到300
之间的任何0
,这些点还是非常偏向于画布的中间(三角形分布)。 在第三个面板上强调了相同的行为,其中使用了十个Math.random()
调用。
正确答案
我采用的方法基于rand4()
能够产生两个随机的最低有效位的观察。 因此,让我们从使用已知语义实现rand3()
开始:
def rand3(): Int = rand4() match {
case 4 => rand3()
case x => x
}
rand3()
通过拒绝rand4()
4
输出来返回从0
到3
均匀分布值。 那将如何帮助我们? 好了,我们现在有两个随机位,每个位是0
或1
,概率为50%。 我们可以轻松地将其扩展为更大的序列,例如rand15()
和rand7()
:
def rand15() = (rand3() << 2) + rand3()
def rand7() = rand15() >> 1
您应该对上面的摆弄感到相当自在。 具有产生两个随机位的能力,我可以轻松地生成4和3。现在rand6()
是rand6()
的事情:
def rand6() = rand7() match {
case 7 => rand6()
case x => x
}
为了使本课程更加有趣,让我们在randN(n: Int)
之上实现randN(n: Int)
rand4()
。 randN()
应该返回从0
到n
均匀分布自然值。 首先,我将实现辅助方法atLeastKrandBits(k: Int)
返回…… 至少K个随机位 :
def atLeastKrandBits(k: Int): Int = k match {
case 0 => 0
case 1 => rand3() >> 1
case 2 => rand3()
case b => (atLeastKrandBits(k - 2) << 2) + rand3()
}
foldLeft()
替代实现 :
def atLeastKrandBits(k: Int) = (0 to (k + 1) / 2).foldLeft(0){
(acc, _) => (acc << 2) + rand3()
}
…或者如果您真的讨厌那些人来维护您的代码:
def atLeastKrandBits(k: Int) = (0 /: (0 to (k + 1) / 2)){
(acc, _) => (acc << 2) + rand3()
}
具有randN(n: Int)
以上的任何实现randN(n: Int)
简单:
def randN(n: Int) = {
val bitsCount = java.lang.Integer.highestOneBit(n)
val randBits = atLeastKrandBits(bitsCount)
if(randBits > n)
randN(n)
else
randBits
}
结论
您可能会问自己一个问题: 我为什么还要关心? 如果您不了解概率分布,则您的应用程序可能会产生实际上很容易预测的随机输出。 如果您正在编写游戏,并且敌人更有可能出现在地图上的某些位置上,那没什么大不了的(但是玩家会发现并滥用它!)但是如果出于安全原因需要随机数,或者您依赖统一分配例如,出于负载平衡目的–任何偏差都可能致命。
参考: Java和社区博客中JCG合作伙伴 Tomasz Nurkiewicz 为程序员提供的概率分布 。
翻译自: https://www.javacodegeeks.com/2013/01/probability-distribution-for-programmers-2.html