【问题标题】:<stdlib.h> rand() example code, unnecessary check for larger than max?<stdlib.h> rand() 示例代码,不必要的检查是否大于最大值?
【发布时间】:2019-10-10 11:43:38
【问题描述】:

我一直在研究 C11 中 &lt;stdlib.h&gt; 中的 int rand() 函数,当时我偶然发现了以下 cppreference-example 用于滚动六面骰子。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
 
int main(void)
{
    srand(time(NULL)); // use current time as seed for random generator
    int random_variable = rand();
    printf("Random value on [0,%d]: %d\n", RAND_MAX, random_variable);
 
    // roll a 6-sided die 20 times
    for (int n=0; n != 20; ++n) {
        int x = 7;
        while(x > 6) 
            x = 1 + rand()/((RAND_MAX + 1u)/6); // Note: 1+rand()%6 is biased
        printf("%d ",  x); 
    }
}

具体这部分:

[...]
        while(x > 6) 
            x = 1 + rand()/((RAND_MAX + 1u)/6); // Note: 1+rand()%6 is biased
[...]

问题:

  1. 为什么要添加+ 1u?因为rand()[0,RAND_MAX] 我猜 那在做什么rand()/(RAND_MAX/6) -&gt; [0,RAND_MAX/(RAND_MAX/6)] -&gt; [0,6]?和 因为它是整数除法(LARGE/(LARGE+small)) &lt; 1 -&gt; 0,添加1u 给它所需的[0,5] 范围?

  2. 在上一个问题的基础上,假设[0,5]1 + (rand()/((RAND_MAX+1u)/6)) 应该只经过[1,6] 并且永远不会触发第二个循环?

一直在四处寻找rand() 是否在某个时候返回了float,但是 这似乎是对旧代码的巨大破坏?我猜是支票 如果您添加 1.0f 而不是 1u 使其成为浮点数,则有意义 分配?

试图把我的头包裹起来,有一种我可能会失踪的感觉 东西..

(P.s. 这不是任何安全关键的基础,我只是在探索 标准库。 D.s)

【问题讨论】:

  • 不,rand 从未返回浮点数。
  • "为什么要加+1u"怎么知道呢?代码应该做什么?
  • rand() 返回 [0..RAND_MAX] 范围内的数字,因此存在 RAND_MAX+1 个可能的值。 here 解释了为什么 % 运算符有偏见。
  • @Lundin *咳嗽* Note: 1+rand()%6 is biased
  • "应该只经过 [1,6] 并且永远不会触发第二个循环?" --> 假装RAND_MAX == 9 浏览代码。

标签: c random c11


【解决方案1】:

代码通过确保 [1, 6] 中的每个可能结果是来自 rand 的完全相同数量的返回值的输出来避免偏差。

根据定义,rand 返回从 0 到 RAND_MAXint 值。所以它可以返回1+RAND_MAX 可能的值。如果1+RAND_MAX 不是 6 的倍数,则不可能将其划分为 6 个完全相等的整数区间。因此,代码将其划分为 6 个尽可能大的相等间隔和一个奇数大小的片段间隔。然后将rand的结果映射到这些区间:前六个区间对应1到6的结果,最后一个区间被拒绝,代码再次尝试。

当我们将1+RAND_MAX 除以 6 时,有一些商 q 和一些余数 r。现在考虑rand() / q的结果:

  • rand在[0,q−1]中产生一个数字时,rand() / q将为0。
  • rand 在 [q, 2q−1] 中产生一个数字时,rand() / q 将为 1。
  • rand在[2q, 3q-1]中产生一个数字时,rand() / q将是2。
  • rand 在 [3q, 4q−1] 中产生一个数字时,rand() / q 将是 3。
  • rand 在 [4q, 5q-1] 中产生一个数字时,rand() / q 将是 4。
  • rand 在 [5q, 6q−1] 中产生一个数字时,rand() / q 将是 5。
  • rand 产生一个大于等于6q 的数字时,rand() / q 将是6。

请注意,在前六个区间的每一个中,都有 q 个数字。在第七个区间,可能的返回值在[6q,RAND_MAX]中。该区间包含 r 个数字。

此代码通过拒绝最后一个间隔来工作:

int x = 7;
while(x > 6) 
    x = 1 + rand()/((RAND_MAX + 1u)/6);

每当rand 在最后一个分段间隔中产生一个数字时,此代码都会拒绝它并重试。当rand 在整个区间之一中产生一个数字时,此代码接受它并退出(在加 1 后,x 中的结果是 1 到 6 而不是 0 到 5)。

因此,从 1 到 6(含)的每个输出都映射到完全相同数量的 rand 值。

这是从rand 生成均匀分布的最佳方法,因为我们使用的方案是这样的,所以它的拒绝率最低。1rand 的范围被分成了尽可能大的六个区间。无法使用剩余的碎片区间,因为余数 r 小于六,因此 r 未使用的值不能平均分配到 x 的六个所需值上。

脚注

1 这不一定是使用rand 在整体[1, 6] 中生成随机数的最佳方式。例如,从单个 rand 调用中,RAND_MAX 等于 32767,我们可以将值视为从 000000 到 411411 的以六为基数的数字。如果它小于 400000,我们可以取最后五位数字,它们是每个均匀分布在 [0, 5] 中,并添加一个 gts 我们所需的 [1, 6]。如果在 [400000, 410000) 中,我们可以使用最后四位。如果在 [410000, 411000) 中,我们可以使用最后三个,以此类推。此外,可能会在多个 rand 调用中汇集其他被丢弃的信息(例如前导数字),以将每次调用获得的平均输出数量增加到 rand

【讨论】:

  • 我还会注意到一些rand() 实现已知具有非常差的低位随机分布。直接模运算将导致这种rand() 实现的非随机结果。只要RAND_MAX 远大于除数,直接模实现引入的偏差在完美rand() 实现的结果中可能完全不显着。
  • 啊,绝妙的回答!我以某种方式假设 RAND_MAX 可以被 6 整除。这与@chux 评论一起澄清了这一点。总之->9/6 &gt; 0.5 -&gt; 1 != 0。接受!
  • @GlassShark 请注意,这可能不是一般的“最佳方式”,请参阅pcg-random.org/posts/bounded-rands.html 了解其他一些这样做的方式和各种权衡
  • @SamMason Ty 链接,超级有趣!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-04-30
相关资源
最近更新 更多