为什么rand()%6有偏见?

yO_*_*yO_ 106 c++ random std

在阅读如何使用std :: rand时,我在cppreference.com上找到了这段代码

int x = 7;
while(x > 6) 
    x = 1 + std::rand()/((RAND_MAX + 1u)/6);  // Note: 1+rand()%6 is biased
Run Code Online (Sandbox Code Playgroud)

右边的表达有什么问题?尝试过,它完美无缺.

Pet*_*ker 136

有两个问题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();
Run Code Online (Sandbox Code Playgroud)

这是有问题的代码的不同实现,旨在更清楚地显示正在发生的事情.

  • 我做了一个图表,如果rand_max是32768,这个技术会引入多少偏差,这在某些实现中是这样.https://ericlippert.com/2013/12/16/how-much-bias-is-introduced-by-the-remainder-technique/ (30认同)
  • @MSalters:你的第一个句子对于*true*生成器是正确的,对于伪生成器不一定正确.当我退休时,我打算写一篇论文. (4认同)
  • 我已经承诺在这个网站上至少有一个常规的人就此发表一篇论文,但我认为*抽样和拒绝*可能会让人失望.例如,过度膨胀方差. (2认同)
  • @Bathsheba:确实有些拒绝函数会导致这种情况,但这种简单的拒绝会将统一的IID转换为不同的统一IID分布.没有位继承,所以独立,所有样本使用相同的拒绝如此相同,并且显示均匀性是微不足道的.均匀积分随机变量的较高矩由其范围完全定义. (2认同)
  • @Anthony从骰子角度考虑。您需要一个1到3之间的随机数,并且只有标准的6面模具。如果掷4-6,则只需减去3就可以得到。但是,假设您想要一个介于1到5之间的数字。如果在滚动6时减去5,那么最终得到的1就是其他任何数字的两倍。基本上,这就是cppreference代码正在做的事情。正确的做法是重新滚动6s。这就是Pete在这里所做的:将骰子划分为多个,以便用相同的方式滚动每个数字,然后重新滚动任何不适合偶数划分的数字 (2认同)
  • @RyanBeesley - `rand()`不需要交替奇数/偶数,好的实现不需要. (2认同)

Bat*_*eba 19

这里有隐藏的深度:

  1. 使用小的uRAND_MAX + 1u.RAND_MAX被定义为一种int类型,并且通常是最大的类型int.在您遇到类型溢出的情况下,行为RAND_MAX + 1将是未定义的signed.写入1u强制类型转换RAND_MAXunsigned,从而避免溢出.

  2. % 6 can的使用(但在std::rand我所见过的 每一个实现中都没有)引入任何额外的统计偏差,超出了所提出的替代方案.这种情况下% 6危险的情况是数字生成器具有低阶位的相关平台,例如rand,我认为,在20世纪70年代将高位和低位翻转为"最终"的相当着名的IBM实现(在C中)繁荣".进一步的考虑是6是非常小的参考.RAND_MAX,如果RAND_MAX不是6的倍数,那么将会产生最小的影响,这可能不是.

总而言之,这些天,由于其易处理性,我会使用% 6.除了发电机本身引入的统计异常之外,它不太可能引入任何统计异常.如果您仍然有疑问,请测试您的生成器,看它是否具有适合您的用例的统计属性.

  • 只要`rand()`生成的不同值的数量不是6的倍数,`%6`就会产生偏差的结果.鸽孔原理.当然,当"RAND_MAX"远大于6时,偏差很小,但它就在那里.对于较大的目标范围,效果当然更大. (12认同)
  • @PeteBecker:的确,我应该说清楚.但请注意,由于整数除法截断效应,当采样范围接近RAND_MAX时,您也会获得信息. (2认同)
  • @Bathsheba不是截断效应导致大于6的结果,因此重复执行整个操作? (2认同)

Squ*_*age 13

这个示例代码说明std::rand了传统货物崇拜balderdash的情况,每次看到它时都应该让你的眉毛升起.

这里有几个问题:

合同人们通常认为 - 即使是那些不知道更好的穷人倒霉灵魂,也不会用这些术语来思考 - 是0,1,2,......中整数均匀分布的rand样本,每个调用产生一个独立的样本.RAND_MAX

第一个问题是假定的合同,每次调用中独立的统一随机样本,实际上并不是文档所说的 - 实际上,实现历史上甚至无法提供最独立的模拟. 例如,C99§7.20.2.1' rand函数'说,没有详细说明:

rand函数计算0到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;
}
Run Code Online (Sandbox Code Playgroud)

这具有令人遗憾的特性,即使单个样本可以均匀地分布在均匀随机种子下(取决于具体值RAND_MAX),它在连续的呼叫之后在偶数和奇数整数之间交替.

int a = rand();
int b = rand();
Run Code Online (Sandbox Code Playgroud)

表达式(a & 1) ^ (b & 1)产生1,概率为100%,而在偶数和奇数整数上支持的任何分布上的独立随机样本不是这种情况.因此,出现了一种货币崇拜,人们应该抛弃低阶位来追逐难以捉摸的"更好随机性"的野兽.(剧透警报:这不是一个技术术语.这表明你正在阅读的散文或者不知道他们在谈论什么,或者认为是无知的,必须屈服于.)

第二个问题是,即使每次调用独立于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 .

正确的方法是使用拒绝采样: 重复绘制一个独立的均匀随机样本,s从0,1,2,... RAND_MAX,并拒绝(例如)结果0,1,2,...,((RAND_MAX + 1) % 6) - 1- 如果你得到其中一个那些,重新开始; 否则,收益率s % 6.

unsigned int s;
while ((s = rand()) < ((unsigned long)RAND_MAX + 1) % 6)
    continue;
return s % 6;
Run Code Online (Sandbox Code Playgroud)

这样rand(),我们接受的结果集合可以被6整除,并且每个可能的结果s % 6都是通过相同数量的可接受结果获得的rand(),所以如果rand()是均匀分布则那么s.试验数量没有限制,但预期数量小于2,成功概率随试验次数呈指数增长.

您拒绝哪些结果的选择rand()并不重要,前提是您将相同数量的结果映射到6以下的每个整数.cppreference.com上的代码做出了不同的选择,因为上面的第一个问题 - 没有任何保证输出的分布或独立性,rand()实际上低阶位表现出的模式并不"看起来足够随机"(更不用说下一个输出是前一个输出的确定性函数).

为读者练习:证明cppreference.com上的代码在掷骰子上产生均匀分布,如果rand()在0,1,2,......上产生均匀分布RAND_MAX.

为读者练习:为什么你更喜欢拒绝一个或另一个子集?在这两种情况下,每次试验需要什么计算?

第三个问题是种子空间太小,即使种子均匀分布,对手掌握了你的程序和一个结果而不是种子的对手可以很容易地预测种子和随后的结果,这使得它们看起来不那么毕竟是随机的. 因此,甚至不要考虑将其用于加密.

你可以std::uniform_int_distribution使用适当的随机设备和你最喜欢的随机引擎,比如流行的Mersenne twister std::mt19937与你四岁的堂兄一起玩骰子,你可以选择花哨的过度工程路线和C++ 11的课程,但即使这样也不会适合生成加密密钥材料 - 而Mersenne twister也是一个可怕的太空猪,因为一个数千字节的状态会对你的CPU缓存造成严重的破坏性设置时间,所以即使对于例如并行的蒙特卡罗模拟也很糟糕可重现的子计算树; 它的受欢迎程度可能主要来自其吸引人的名字.但你可以像玩这个例子一样用它来玩玩具骰子!

另一种方法是使用具有小状态的简单加密伪随机数发生器,例如简单的快速密钥擦除PRNG,或者如果您有信心则使用诸如AES-CTR或ChaCha20之类的流密码(例如,在蒙特卡罗模拟中自然科学研究)如果国家受到损害,对预测过去的结果没有不利后果.

  • 我对这个答案不太满意.Rants可能很好,但你会把它带入错误的方向.例如,您抱怨"更好的随机性"不是技术术语,而且没有意义.这是正确的一半.是的,这不是一个技术术语,但它在上下文中是一个非常有意义的速记.暗示这样一个术语的用户要么是无知的,要么是恶意的,这本身就是其中之一."良好的随机性"可能很难精确定义,但很容易掌握函数何时产生具有更好或更差随机性的结果. (12认同)
  • 对我而言恰恰相反.虽然它确实包含了很好的信息,但是除了意见之外,它只是一种咆哮.除了有用之外. (10认同)
  • "一个淫秽的设置时间"你不应该真的使用多个随机数发生器(每个线程),所以设置时间将摊销,除非你的程序运行时间不长. (4认同)
  • 我喜欢这个答案.这有点咆哮,但它有很多很好的背景信息.请记住,真正的专家只使用硬件随机发生器,问题就是很难. (3认同)
  • Downwote BTW是因为没有理解问题中的循环正在进行完全相同的拒绝抽样,完全相同的`(RAND_MAX + 1)%6`值.没有关系_how_你的细分可能的结果.你可以从"[0,RAND_MAX)"范围内的任何地方拒绝它们,只要接受范围的大小是6的倍数.地狱,你可以平掉掉任何结果`x> 6`,你赢了再也不需要'%6`了. (2认同)
  • 其实我非常喜欢读这个; 有一个upvote! (2认同)