假设我们有一个二进制随机数生成器int r();
,它将返回零或一个两者,可用性为0.5。
我查看了Boost.Random,然后他们生成32位并执行类似的操作(伪代码):
x = double(rand_int32());
return min + x / (2^32) * (max - min);
我对此有一些严重怀疑。 double有53位尾数,32位不能正确生成完全随机的尾数,其中包括舍入误差等等。
假设IEEE754,在半开放范围float
中创建均匀分布的double
或[min, max)
的快速方法是什么?这里强调的是分配的正确性,而不是速度。
为了正确定义正确,正确的分布将等于我们将采用无限精确均匀分布的随机数发生器时得到的正确分布,并且对于每个数字,我们将舍入到最接近的IEEE754表示,如果该表示将仍然在[min, max)
之内,否则该数字将不计入分配。
P.S。:我也会对开放范围的正确解决方案感兴趣。
答案 0 :(得分:3)
这是一种正确的方法,没有尝试提高效率。
我们从一个bignum类开始,然后是一个理性的bignums包装器。
我们生成一个“远大于”[min, max)
范围的范围,因此我们smaller_min
和bigger_max
的舍入会产生超出该范围的浮点值,在我们的理性基础上构建BIGNUM。
现在我们将范围细分为两个部分,完全在中间(我们可以做,因为我们有一个合理的bignum系统)。我们随机选择其中一个部分。
如果在舍入后,拾取范围的顶部和底部将是(A)在[min, max)
之外(在同一侧,请注意!)您拒绝并从头重新开始。
如果(B)你的范围的顶部和底部四舍五入到相同的double
(如果你要返回一个浮点数,则为float
),你就完成了,你就会返回这个值。
否则(C)你在这个新的较小范围内进行递归(细分,随机选择,测试)。
无法保证此过程停止,因为您可以不断深入查看两个舍入double
之间的“边缘”,或者您可以不断选择[min, max)
范围之外的值。发生这种情况的概率是(从不停止),但是,为零(假设一个好的随机数生成器,以及一个非零大小的[min, max)
)。
这也适用于(min, max)
,甚至可以在圆润的Cantor集合中选取一个数字。只要舍入到正确浮点值的有效实数范围的度量不为零,并且该范围具有紧凑支持,则此过程可以运行并且具有100%终止的概率,但没有硬上限受限于可以花费的时间。
答案 1 :(得分:1)
这里的问题是在IEEE754中,可以表示的双精度不是等分布的。也就是说,如果我们有一个生成实数的生成器,比如(0,1),然后映射到IEEE754可表示的数字,结果就不会是等分布的。
因此,我们必须定义“等分布”。也就是说,假设每个IEEE754数字只是代表IEEE754舍入定义的区间的概率,首先生成等分布“数字”和圆形到IEEE754的过程将生成(根据定义)“ “均匀分布”的IEEE754号码。
因此,如果我们只选择足够高的准确度,我相信上述公式将变得接近这样的分布。如果我们将问题限制为在[0,1)中找到一个数字,这意味着限制到一组一对一到53位整数的非经典化IEEE 754数字。因此,通过53位二进制随机数生成器生成尾数应该是快速和正确的。
IEEE 754算术始终是“无限精度算术,然后舍入”,即代表 b的IEEE754数字是最接近 b的那个(换句话说,你可以想到* b以无限精度计算,然后四舍五入到关闭的IEEE754数字)。因此我认为min +(max-min)* x,其中x是一个非正规数,是一种可行的方法。
(注意:从我的评论中可以清楚地看到,我首先不知道你指向最小和最大不同于0,1的情况。非规范化数字具有它们均匀间隔的属性。因此你得到了通过将53位映射到尾数来进行均衡分布。接下来你可以使用浮点运算,因为它在机器精度方面是正确的。如果使用反向映射,你将恢复等分布。
有关此问题的另一方面,请参阅此问题:Scaling Int uniform random range into Double one
答案 2 :(得分:1)
std::uniform_real_distribution
S.T.L有一个really good talk。来自今年的Going Native会议,解释了为什么你应该尽可能使用标准发行版。简而言之,手卷代码往往具有可笑的低质量(想象std::rand() % 100
),或者具有更微妙的均匀性缺陷,例如在(std::rand() * 1.0 / RAND_MAX) * 99
中,这是谈话中给出的示例,并且是问题中张贴的特殊情况。
std::uniform_real_distribution
的实现,这就是我发现的:
该实现通过使用范围[dist_min, dist_max)
中生成的某个数字的简单线性变换,生成[0, 1)
范围内的数字。它使用std::generate_canonical
,the implementation of which my be found here(在文件末尾)生成此源编号。 std::generate_canonical
确定分布范围的范围的次数(表示为k
),表示为整数,此处表示为r
*,将适合目标类型的尾数。它的作用主要是在[0, r)
中为每个r
大小的尾数段生成一个数字,并使用算术相应地填充每个段。结果值的公式可以表示为
Σ(i=0, k-1, X/(r^i))
其中X
是[0, r)
中的随机变量。该范围的每个除法等于用于表示它的比特数的移位(即log2(r)
),因此填充相应的尾数段。这样,使用了目标类型的整个精度,并且因为结果的范围是[0, 1)
,所以指数保持0
**(模偏差)并且你没有得到当你开始搞乱指数时你会遇到一致性问题。
我不相信这种方法在加密方面是安全的(我怀疑在计算r
的大小时可能出现一个错误),但我认为它在统一性条款比您发布的Boost实施方案更好,绝对比摆弄std::rand
更好。
值得注意的是,Boost代码实际上是此算法的退化情况k = 1
,这意味着它等效如果输入范围至少需要23位到表示其大小(IEE 754单精度)或至少52位(双精度)。这意味着最小范围分别为~840万或~4.5e15。根据这些信息,我不认为如果您使用二进制生成器,Boost实现相当将削减它。
在简要了解libc++’s implementation之后,看起来他们正在使用相同的算法,实现方式略有不同。
(*)r
实际上是输入加上一个的范围。这允许使用urng的max
值作为有效输入。
(**)严格地说,编码的指数不是0
,因为IEEE 754在有效数的基数之前编码隐式前导1。然而,从概念上讲,这与此算法无关。
答案 3 :(得分:1)
AFAIK,正确(也可能也是最快)的方法是首先创建一个64位无符号整数,其中52个小数位是随机位,指数是1023,如果类型被打成(IEEE 754)双重将是[1.0,2.0]范围内的均匀分布随机值。所以最后一步是从中减去1.0,得到均匀分布的随机双值,范围为[0.0,1.0]。
在伪代码中:
rndDouble = bitCastUInt64ToDouble(1023<<<<<< 52"        
这里提到了这个方法: http://xoroshiro.di.unimi.it (参见“在单位间隔内生成均匀的双精度”)
编辑:推荐的方法后来改为: (x>> 11)*(1. /(UINT64_C(1)<< 53))
有关详细信息,请参阅上面的链接。