在阅读如何使用std :: rand时,我在cppreference.com上找到了此代码
int x = 7;
while(x > 6)
x = 1 + std::rand()/((RAND_MAX + 1u)/6); // Note: 1+rand()%6 is biased
右边的表达式有什么问题?尝试了一下,它完美地工作。
uniform_int_distribution
。)
在阅读如何使用std :: rand时,我在cppreference.com上找到了此代码
int x = 7;
while(x > 6)
x = 1 + std::rand()/((RAND_MAX + 1u)/6); // Note: 1+rand()%6 is biased
右边的表达式有什么问题?尝试了一下,它完美地工作。
uniform_int_distribution
。)
Answers:
有两个问题rand() % 6
(1+
不影响任何一个问题)。
首先,正如几个答案所指出的那样,如果的低位rand()
未适当统一,则余数运算符的结果也将不一致。
其次,如果所产生的不同值的数目rand()
不是6的倍数,则其余部分将产生比高值更多的低值。即使rand()
返回完美分布的值也是如此。
举一个极端的例子,假装rand()
在范围内产生均匀分布的值[0..6]
。如果查看这些值的余数,则rand()
返回范围内的值时[0..5]
,余数将生成范围内均匀分布的结果[0..5]
。当rand()
返回6时,rand() % 6
返回0,就像rand()
返回了0一样。因此,您得到的0分配是其他任何值的两倍。
第二个是真正的问题rand() % 6
。
避免该问题的方法是丢弃将产生非均匀重复项的值。您计算出小于或等于6的最大倍数RAND_MAX
,并且每当rand()
返回一个大于或等于该倍数的值时,您都会拒绝它并再次调用`rand(),次数需要多次。
所以:
int max = 6 * ((RAND_MAX + 1u) / 6)
int value = rand();
while (value >= max)
value = rand();
那是所讨论代码的另一种实现,旨在更清楚地显示正在发生的事情。
这里有隐藏的深度:
使用小的u
在RAND_MAX + 1u
。RAND_MAX
被定义为int
类型,并且通常是最大可能的类型int
。在您溢出类型的情况下,的行为RAND_MAX + 1
将是不确定的signed
。写入会1u
强制将类型转换RAND_MAX
为unsigned
,从而避免了溢出。
使用% 6
can(但是在std::rand
我见过的 每一种实现上都没有)会在所提供的替代方法之上引入任何其他统计偏差。这种% 6
危险的情况是数字生成器在低阶位具有相关平原,例如rand
我认为是1970年代的著名IBM实现(用C语言编写),将高低位翻转为“最终值”。繁荣”。进一步的考虑是6很小。RAND_MAX
,因此,如果RAND_MAX
不是6的倍数,可能会产生最小的影响,而实际上不是。
总之,这些天来,由于其易处理性,我会使用% 6
。除了生成器本身引入的统计异常之外,不可能引入任何统计异常。如果仍然不确定,请测试生成器以查看其是否具有适合您的用例的统计属性。
% 6
只要由生成的不同值的数量rand()
不是6的倍数,就会产生有偏差的结果。鸽子洞原理。当然,当偏差RAND_MAX
远大于6 时,偏差很小,但它确实存在。对于更大的目标范围,效果当然更大。
x==7
。通常,您将范围划分为[0, RAND_MAX]
7 个子范围,其中6个具有相同的大小,最后一个较小的子范围。最后一个子范围的结果将被丢弃。显然,这样一来,您就不能拥有两个较小的子范围。
此示例代码说明这std::rand
是一种传统的货物崇拜秃头案,每当您看到它时,都会使您的眉毛扬起。
这里有几个问题:
该合同人们通常认为,即使是穷人倒霉的灵魂不知道谁更好,并正是这些不会想到这方面,是rand
从样品均匀分布在0的整数,1,2,..., RAND_MAX
,并且每次调用都会产生一个独立的样本。
第一个问题是假设的合同,即每次通话中的独立统一随机样本,实际上不是文档所说的,而且在实践中,历来的实现甚至都无法提供最严格的独立性。 例如,C99§7.20.2.1'The rand
function'无需详细说明:
该
rand
函数计算范围为0到的伪随机整数序列RAND_MAX
。
这是一个毫无意义的句子,因为伪随机性是函数(或函数族)的属性,而不是整数的属性,但这甚至不会阻止ISO官僚滥用语言。毕竟,唯一对它感到不满的读者比rand
担心害怕脑细胞衰退的文档要好得多。
C语言中典型的历史实现如下:
static unsigned int seed = 1;
static void
srand(unsigned int s)
{
seed = s;
}
static unsigned int
rand(void)
{
seed = (seed*1103515245 + 12345) % ((unsigned long)RAND_MAX + 1);
return (int)seed;
}
不幸的是,即使单个样本可以均匀地分布在一个均匀的随机种子下(取决于的特定值RAND_MAX
),它也会在连续调用中的偶数和奇数整数之间交替
int a = rand();
int b = rand();
该表达式(a & 1) ^ (b & 1)
以100%的概率产生1,对于在偶数和奇数整数上支持的任何分布上的独立随机样本而言,情况并非如此。因此,出现了一种货物崇拜,人们应该丢弃低阶位来追逐难以捉摸的“更好的随机性”。(剧透警告:这不是一个技术术语。这是一个信号,表明您正在阅读的散文要么不知道他们在说什么,要么认为您无能为力,必须屈服。)
第二个问题是,即使每个调用都独立于0、1、2,...,的均匀随机分布独立采样RAND_MAX
,的结果rand() % 6
也不会像骰子一样均匀地分布在0、1、2、3、4、5中滚动,除非RAND_MAX
等于-1模6。 简单的反例:如果RAND_MAX
= 6,则从rand()
,所有结果的概率均等1/7,但是从rand() % 6
,结果0的概率为2/7,而其他所有结果的概率为1/7。 。
正确的方法是拒绝采样: 从0、1、2,...,重复绘制一个独立的统一随机样本,然后拒绝(例如)结果0、1、2,…,—如果得到以下结果之一:这些,重新开始;否则,屈服。s
RAND_MAX
((RAND_MAX + 1) % 6) - 1
s % 6
unsigned int s;
while ((s = rand()) < ((unsigned long)RAND_MAX + 1) % 6)
continue;
return s % 6;
这样rand()
,我们接受的结果集可以被6整除,并且每个可能的结果s % 6
都可以通过相同数量的接受结果来获得rand()
,因此如果rand()
均匀分布,则也是s
。试验次数没有限制,但是预期的数目小于2,并且成功的概率随试验次数呈指数增长。
如果您将相等数量的结果映射到低于6的每个整数,则选择哪个结果rand()
就无关紧要。由于上面的第一个问题,cppreference.com上的代码做出了不同的选择-不能保证的分布或输出的独立性,rand()
实际上,低阶位的显示模式看起来“不够随机”(不要介意下一个输出是前一个输出的确定性函数)。
读者练习:证明cppreference.com上的代码在rand()
0、1、2,...,RAND_MAX
。上均匀分布时,在模辊上产生均匀分布。
读者练习:为什么您更喜欢其中一个子集来拒绝?在这两种情况下,每个审判需要什么计算?
第三个问题是种子空间很小,以至于即使种子均匀分布,一个拥有您的程序知识和一个结果但不是种子的武装者却可以轻易预测种子和随后的结果,这使得他们似乎并非如此毕竟是随机的。 因此,甚至不要考虑将其用于加密。
您可以std::uniform_int_distribution
使用合适的随机设备和喜欢的随机引擎(如广受欢迎的Mersenne Twister)去花哨的过度工程路线和C ++ 11的类,std::mt19937
与您四岁的堂兄一起玩骰子,但即使那样也不会是适合生成的密码密钥材料和梅森捻线机是一个可怕的空间猪太关于您的CPU与一个下流的设置时间缓存多KB的状态肆虐,所以即使是坏的,比如,平行Monte Carlo模拟与子计算的可复制树;它的受欢迎程度可能主要来自其易记的名称。但是您可以像本例一样将它用于玩具骰子滚动!
另一种方法是使用具有小状态的简单密码伪随机数生成器,例如简单的快速密钥擦除PRNG,或者如果您有信心,则仅使用流密码(例如AES-CTR或ChaCha20)(例如,在Monte Carlo仿真中自然科学的研究),如果状态受到损害,则预测过去的结果不会有不利的后果。
(RAND_MAX + 1 )% 6
值。您如何细分可能的结果并不重要。您可以从范围内的任何位置拒绝它们[0, RAND_MAX)
,只要接受范围的大小是6的倍数即可。地狱,您可以拒绝任何结果x>6
,并且不再需要%6
。
无论如何,我都不是经验丰富的C ++用户,但是我很想知道其他有关std::rand()/((RAND_MAX + 1u)/6)
比1+std::rand()%6
实际少偏见的答案
是否成立。因此,我写了一个测试程序来列出这两种方法的结果(我已经很久没有写C ++了,请检查一下)。在此处找到用于运行代码的链接。它也被复制如下:
// Example program
#include <cstdlib>
#include <iostream>
#include <ctime>
#include <string>
int main()
{
std::srand(std::time(nullptr)); // use current time as seed for random generator
// Roll the die 6000000 times using the supposedly unbiased method and keep track of the results
int results[6] = {0,0,0,0,0,0};
// roll a 6-sided die 20 times
for (int n=0; n != 6000000; ++n) {
int x = 7;
while(x > 6)
x = 1 + std::rand()/((RAND_MAX + 1u)/6); // Note: 1+rand()%6 is biased
results[x-1]++;
}
for (int n=0; n !=6; n++) {
std::cout << results[n] << ' ';
}
std::cout << "\n";
// Roll the die 6000000 times using the supposedly biased method and keep track of the results
int results_bias[6] = {0,0,0,0,0,0};
// roll a 6-sided die 20 times
for (int n=0; n != 6000000; ++n) {
int x = 7;
while(x > 6)
x = 1 + std::rand()%6;
results_bias[x-1]++;
}
for (int n=0; n !=6; n++) {
std::cout << results_bias[n] << ' ';
}
}
然后,我将其输出,并使用chisq.test
R中的函数进行卡方检验,以查看结果是否与预期有显着差异。这个stackexchange问题更详细地介绍了如何使用卡方检验来测试裸片公平性:如何测试裸片是否公平?。以下是几次运行的结果:
> ?chisq.test
> unbias <- c(100150, 99658, 100319, 99342, 100418, 100113)
> bias <- c(100049, 100040, 100091, 99966, 100188, 99666 )
> chisq.test(unbias)
Chi-squared test for given probabilities
data: unbias
X-squared = 8.6168, df = 5, p-value = 0.1254
> chisq.test(bias)
Chi-squared test for given probabilities
data: bias
X-squared = 1.6034, df = 5, p-value = 0.9008
> unbias <- c(998630, 1001188, 998932, 1001048, 1000968, 999234 )
> bias <- c(1000071, 1000910, 999078, 1000080, 998786, 1001075 )
> chisq.test(unbias)
Chi-squared test for given probabilities
data: unbias
X-squared = 7.051, df = 5, p-value = 0.2169
> chisq.test(bias)
Chi-squared test for given probabilities
data: bias
X-squared = 4.319, df = 5, p-value = 0.5045
> unbias <- c(998630, 999010, 1000736, 999142, 1000631, 1001851)
> bias <- c(999803, 998651, 1000639, 1000735, 1000064,1000108)
> chisq.test(unbias)
Chi-squared test for given probabilities
data: unbias
X-squared = 7.9592, df = 5, p-value = 0.1585
> chisq.test(bias)
Chi-squared test for given probabilities
data: bias
X-squared = 2.8229, df = 5, p-value = 0.7273
在我进行的三个运行中,两种方法的p值始终大于用于检验显着性的典型alpha值(0.05)。这意味着我们不会认为它们中的任何一个都有偏见。有趣的是,所谓的无偏方法始终具有较低的p值,这表明它实际上可能有更大的偏倚。需要注意的是,我只进行了3次跑步。
更新:当我写我的答案时,康拉德·鲁道夫发布了一个采用相同方法的答案,但是得到了非常不同的结果。我没有信誉评论他的答案,所以我将在这里解决。首先,最主要的是,他使用的代码每次运行时都会为随机数生成器使用相同的种子。如果您更改种子,则实际上可以获得各种结果。其次,如果您不更改种子,但更改试验次数,那么您还将获得各种结果。尝试增加或减少一个数量级以了解我的意思。第三,在期望值不太准确的地方进行了一些整数截断或舍入。它可能不足以产生变化,但是就在那里。
基本上,总的来说,他只是碰巧得到了正确的种子和大量的试验,以至于他可能得到了错误的结果。
rand()%6
有rand()/(1+RAND_MAX)/6
。而是将剩余部分的简单获取与拒绝采样进行比较(有关说明,请参见其他答案)。因此,您的第二个代码是错误的(while
循环不执行任何操作)。您的统计测试也有问题(您不能仅仅为了健壮性而重复测试,就没有执行校正,……)。
std::srand
(不使用<random>
)随机种子,并且我不希望它的复杂性降低其余代码的难度。它与计算也无关紧要:在模拟中重复相同的序列是完全可以接受的。当然,不同的种子将产生不同的结果,而某些种子将是不重要的。完全可以根据p值的定义来预期。
std::rand
,在随机种子范围内,我的标准库实现的d6抛硬币模拟效果非常好。
RAND_MAX
,它决定了模偏差的影响大小。统计显着性是在无效假设下您错误地拒绝它的概率。什么是统计力量 -备择假设您的测试下的概率正确地拒绝零假设?rand() % 6
当RAND_MAX = 2 ^ 31-1时,您会以这种方式检测到吗?
可以认为随机数生成器可以处理二进制数字流。生成器通过将流切成块将流转换成数字。如果std:rand
函数使用RAND_MAX
32767的a 表示,则每个切片中使用15位。
当一个采用0到32767(含0和32767)之间的数字的模块时,会发现5462的“ 0”和“ 1”,但只有5461的“ 2”,“ 3”,“ 4”和“ 5”。因此,结果是有偏见的。RAND_MAX值越大,偏差将越小,但这是不可避免的。
没有偏差的是[0 ..(2 ^ n)-1]范围内的数字。通过提取3位,将它们转换为0..7范围内的整数,并拒绝6和7,可以在理论上更好地生成0..5范围内的数字。
人们希望,比特流中的每个比特都有相等的机会成为“ 0”或“ 1”,而不管它在比特流中的位置还是其他比特的值。这在实践中异常困难。软件PRNG的许多不同实现在速度和质量之间提供了不同的折衷。线性同余生成器(例如)可std::rand
提供最快的速度和最低的质量。密码生成器以最低的速度提供最高的质量。
std::uniform_int_distribution