Answers:
所以rand()
是选择0之间的自然数和伪随机数发生器RAND_MAX
,它是在定义的常数cstdlib
(见本文章有关的一般概述rand()
)。
现在,如果您想生成一个介于0和2之间的随机数,会发生什么?为了便于说明,假设RAND_MAX
是10,我决定通过调用生成一个介于0和2之间的随机数rand()%3
。但是,rand()%3
不会以相等的概率产生0到2之间的数字!
当rand()
返回0、3、6或9时, rand()%3 == 0
。因此,P(0)= 4/11
当rand()
返回1、4、7或10时 rand()%3 == 1
。因此,P(1)= 4/11
当rand()
返回2,5或8时, rand()%3 == 2
。因此,P(2)= 3/11
这不会以相等的概率生成介于0和2之间的数字。当然,对于小范围而言,这可能不是最大的问题,但对于大范围而言,这可能会使分布偏斜,从而偏向较小的数字。
那么,何时rand()%n
以相等的概率返回从0到n-1的数字范围?什么时候RAND_MAX%n == n - 1
。在这种情况下,连同我们先前的假设rand()
确实以0且RAND_MAX
相等的概率返回一个数字,n的模数类也将平均分配。
那么我们如何解决这个问题呢?一种粗略的方法是不断生成随机数,直到获得所需范围内的数字:
int x;
do {
x = rand();
} while (x >= n);
但这对于的低值而言效率不高n
,因为您仅有n/RAND_MAX
机会获得您范围内的值,因此您需要平均RAND_MAX/n
调用rand()
。
一种更有效的公式方法是采用某个较大的范围,其长度可以被整除n
,例如RAND_MAX - RAND_MAX % n
,继续生成随机数,直到得到位于该范围内的一个,然后取模数:
int x;
do {
x = rand();
} while (x >= (RAND_MAX - RAND_MAX % n));
x %= n;
对于较小的值n
,很少需要调用多个rand()
。
作品引用和进一步阅读:
继续选择随机是消除偏差的好方法。
更新资料
如果搜索范围可被整除的x,我们可以使代码变快n
。
// Assumptions
// rand() in [0, RAND_MAX]
// n in (0, RAND_MAX]
int x;
// Keep searching for an x in a range divisible by n
do {
x = rand();
} while (x >= RAND_MAX - (RAND_MAX % n))
x %= n;
上面的循环应该非常快,平均说1次迭代。
rand()
可以返回的值的数量不是的倍数n
,那么无论您做什么,都将不可避免地获得“模偏差”,除非您丢弃其中一些值。user1413793很好地说明了这一点(尽管该答案中提出的解决方案确实令人讨厌)。
RAND_MAX+1 - (RAND_MAX+1) % n
工作正常进行,但是RAND_MAX+1 - ((RAND_MAX+1) % n)
为了清晰起见,我仍然认为应该这样写。
RAND_MAX == INT_MAX
(与大多数系统一样)则无法使用。请参阅上面对@ user1413793的第二条评论。
@ user1413793关于此问题是正确的。除了要指出一点之外,我将不做进一步讨论:是的,对于的小值n
和的大值RAND_MAX
,模偏差可能非常小。但是,使用引起偏差的模式意味着每次计算随机数并针对不同情况选择不同的模式时,都必须考虑偏差。而且,如果您选择错误,它引入的错误将非常微妙,几乎无法进行单元测试。与仅使用适当的工具(例如arc4random_uniform
)相比,这是额外的工作,而不是更少的工作。做更多的工作并获得更糟糕的解决方案是一项糟糕的工程,尤其是在大多数平台上,每次正确地做到这一点很容易。
不幸的是,该解决方案的实现都是不正确的或效率不高。(每个解决方案都有各种注释来解释问题,但是没有一种解决方案可以解决这些问题。)这很可能会使临时寻求答案的人感到困惑,因此我在这里提供了一个已知的良好实现。
同样,最好的解决方案是仅arc4random_uniform
在提供该解决方案的平台上使用,或者针对您的平台使用类似的远程解决方案(例如Random.nextInt
在Java上)。它将为您做正确的事情,而无需花费任何代码。这几乎总是正确的选择。
如果没有arc4random_uniform
,则可以使用开源的功能来确切地了解如何在更大范围的RNG之上实现它(ar4random
在这种情况下,但是类似的方法也可以在其他RNG之上工作)。
这是OpenBSD的实现:
/*
* Calculate a uniformly distributed random number less than upper_bound
* avoiding "modulo bias".
*
* Uniformity is achieved by generating new random numbers until the one
* returned is outside the range [0, 2**32 % upper_bound). This
* guarantees the selected random number will be inside
* [2**32 % upper_bound, 2**32) which maps back to [0, upper_bound)
* after reduction modulo upper_bound.
*/
u_int32_t
arc4random_uniform(u_int32_t upper_bound)
{
u_int32_t r, min;
if (upper_bound < 2)
return 0;
/* 2**32 % x == (2**32 - x) % x */
min = -upper_bound % upper_bound;
/*
* This could theoretically loop forever but each retry has
* p > 0.5 (worst case, usually far better) of selecting a
* number inside the range we need, so it should rarely need
* to re-roll.
*/
for (;;) {
r = arc4random();
if (r >= min)
break;
}
return r % upper_bound;
}
值得注意的是,对于需要实施类似操作的人员,此代码上的最新提交注释:
更改arc4random_uniform()以将其计算
2**32 % upper_bound
为-upper_bound % upper_bound
。通过使用32位余数而不是64位余数,可以简化代码并使之在ILP32和LP64体系结构上相同,并且在LP64体系结构上也稍快一些。Jorden Verwer在tech @ ok deraadt上指出;没有来自djm或otto的异议
Java实现也很容易找到(请参见前面的链接):
public int nextInt(int n) {
if (n <= 0)
throw new IllegalArgumentException("n must be positive");
if ((n & -n) == n) // i.e., n is a power of 2
return (int)((n * (long)next(31)) >> 31);
int bits, val;
do {
bits = next(31);
val = bits % n;
} while (bits - val + (n-1) < 0);
return val;
}
arcfour_random()
实际在实现中使用真正的RC4算法,则输出肯定会有一些偏差。希望您的图书馆作者已改在同一界面后使用更好的CSPRNG。我记得现在有一个BSD实际上使用ChaCha20算法来实现arcfour_random()
。有关RC4输出偏差的更多信息,这些偏差使其对于安全性或其他重要应用(例如视频扑克)无用:blog.cryptographyengineering.com/2013/03/…– rmalayter
/dev/random
过去也曾在某些平台上使用RC4(Linux在计数器模式下使用SHA-1)。不幸的是,我通过搜索发现的手册页表明RC4在提供的各种平台上仍在使用arc4random
(尽管实际代码可能有所不同)。
-upper_bound % upper_bound == 0
??
-upper_bound % upper_bound
如果int
大于32位,则@JonMcClung 确实为0 。应该是(u_int32_t)-upper_bound % upper_bound)
(假设u_int32_t
是BSD主义uint32_t
)。
模偏差是使用模算法将输出集减少为输入集的子集时的固有偏差。通常,每当输入集和输出集之间的映射不均等时,就会存在偏差,例如在输出集的大小不等于输入集的大小的除数时使用模运算的情况。
在数字表示为比特串(0和1)的计算中,很难避免这种偏差。寻找真正的随机性随机来源也非常困难,但超出了本讨论的范围。对于此答案的其余部分,假定存在无限数量的真正随机位。
让我们考虑使用这些随机位来模拟骰子滚动(0到5)。有6种可能性,因此我们需要足够的位来表示数字6,即3位。不幸的是,3个随机位会产生8种可能的结果:
000 = 0, 001 = 1, 010 = 2, 011 = 3
100 = 4, 101 = 5, 110 = 6, 111 = 7
通过取模6值,我们可以将结果集的大小减小到恰好为6,但这会带来模偏差问题:110
产生0,111
产生1 。
从理论上讲,可以依靠一支小兵整天掷骰子并将结果记录在数据库中,然后仅使用每个结果一次,而不是依赖随机位。这听起来很实用,反正很可能不会产生真正的随机结果(双关语)。
除了使用模量,天真但数学正确的办法是丢弃结果产量110
和111
和简单的3个新位再试一次。不幸的是,这意味着每卷都有25%的机会需要重新卷制,包括每个重新卷制本身。对于最琐碎的用途,这显然是不切实际的。
使用更多位:使用4,而不是3位。这将产生16种可能的结果。当然,在结果大于5的任何时候重新滚动都会使情况变得更糟(10/16 = 62.5%),因此仅靠这一点是无济于事的。
请注意,2 * 6 = 12 <16,因此我们可以安全地获取小于12的任何结果,并减少该模6以平均分配结果。必须丢弃其他4个结果,然后像以前的方法一样重新滚动。
首先听起来不错,但让我们检查一下数学:
4 discarded results / 16 possibilities = 25%
在这种情况下,多加一点一点都没有帮助!
这个结果很不幸,但是让我们用5位重试:
32 % 6 = 2 discarded results; and
2 discarded results / 32 possibilities = 6.25%
确实有改进,但在许多实际情况下还不够好。好消息是增加更多位将永远不会增加需要丢弃和重新滚动的机会。这不仅适用于骰子,而且适用于所有情况。
如所示 但是,,增加1位可能不会改变任何内容。实际上,如果将滚动增加到6位,则概率仍然为6.25%。
这提出了另外两个问题:
幸运的是,第一个问题的答案是肯定的。6的问题是2 ^ x mod 6在2和4之间翻转,这恰好是彼此的2的倍数,因此对于x> 1的偶数
[2^x mod 6] / 2^x == [2^(x+1) mod 6] / 2^(x+1)
因此6是一个例外,而不是规则。可以找到较大的模量,以相同的方式产生连续的2的幂,但是最终必须环绕,并且将降低丢弃的可能性。
在不提供进一步证据的情况下,通常使用所需位数的两倍将提供较小的(通常不重要的)丢弃机会。
这是一个使用OpenSSL的libcrypo提供随机字节的示例程序。编译时,请确保链接到-lcrypto
大多数人都应该可以使用的库。
#include <iostream>
#include <assert.h>
#include <limits>
#include <openssl/rand.h>
volatile uint32_t dummy;
uint64_t discardCount;
uint32_t uniformRandomUint32(uint32_t upperBound)
{
assert(RAND_status() == 1);
uint64_t discard = (std::numeric_limits<uint64_t>::max() - upperBound) % upperBound;
uint64_t randomPool = RAND_bytes((uint8_t*)(&randomPool), sizeof(randomPool));
while(randomPool > (std::numeric_limits<uint64_t>::max() - discard)) {
RAND_bytes((uint8_t*)(&randomPool), sizeof(randomPool));
++discardCount;
}
return randomPool % upperBound;
}
int main() {
discardCount = 0;
const uint32_t MODULUS = (1ul << 31)-1;
const uint32_t ROLLS = 10000000;
for(uint32_t i = 0; i < ROLLS; ++i) {
dummy = uniformRandomUint32(MODULUS);
}
std::cout << "Discard count = " << discardCount << std::endl;
}
我鼓励使用MODULUS
和ROLLS
值来查看在大多数情况下实际上发生了多少次重新滚动。持怀疑态度的人也可能希望将计算出的值保存到文件中并验证分布是否正常。
randomPool = RAND_bytes(...)
行将始终存在randomPool == 1
。这总是会导致丢弃并重新滚动。我认为您想在另一行中声明。因此,这导致RNG 1
每次迭代都返回。
对于模数的使用,通常有两个抱怨。
一个对所有发电机都有效。在极限情况下更容易看到。如果您的生成器的RAND_MAX为2(不符合C标准),并且您只希望将值设为0或1,则使用modulo生成的频率是生成器的0倍(生成器生成0和2时)两倍生成1(当生成器生成1时)。请注意,只要您不丢弃值,这就是事实,无论您使用的是从生成器值到所需值的映射,一个值的发生频率都是另一个值的两倍。
至少对于某些参数,某种类型的生成器的低有效位的随机性要比另一种低,但是可悲的是,这些参数还具有其他有趣的特征(这样才能使RAND_MAX的幂小于2)。这个问题是众所周知的,很长时间以来,库的实现可能会避免该问题(例如,C标准中的示例rand()实现使用这种类型的生成器,但是会删除低16位的有效位),但是有些人喜欢抱怨这样,您可能会遇到厄运
使用类似
int alea(int n){
assert (0 < n && n <= RAND_MAX);
int partSize =
n == RAND_MAX ? 1 : 1 + (RAND_MAX-n)/(n+1);
int maxUsefull = partSize * n + (partSize-1);
int draw;
do {
draw = rand();
} while (draw > maxUsefull);
return draw/partSize;
}
生成介于0和n之间的随机数将避免两个问题(并且避免RAND_MAX == INT_MAX的溢出)
顺便说一句,C ++ 11为还原和除rand()之外的其他生成器引入了标准方法。
马克的解决方案(公认的解决方案)几乎完美。
int x; do { x = rand(); } while (x >= (RAND_MAX - RAND_MAX % n)); x %= n;
16年3月25日在23:16编辑
马克·阿默里39k21170211
但是,它有一个警告,在RAND_MAX
(RM
)小于1的倍数N
(其中N
=可能的有效结果数)的任何情况下,它都会丢弃1组有效结果。
即,当“丢弃的值的数量”(D
)等于时N
,它们实际上是有效的集合(V)
,而不是无效的集合(I
)。
是什么原因导致这在某些时候马克失去视力之间的差异N
和Rand_Max
。
N
是一个集合,其有效成员仅包含正整数,因为它包含有效的响应计数。(例如:Set N
= {1, 2, 3, ... n }
)
Rand_max
但是,这是一组(按我们的定义定义)包括任意数量的非负整数。
以最通用的形式,这里定义为 Rand Max
是所有有效结果的集合,理论上可以包括负数或非数字值。
因此Rand_Max
,最好将其定义为“可能的响应”集。
但是N
,它会根据有效响应集中的值计数进行运算,因此即使按照我们的特定情况定义,Rand_Max
该值也将比其包含的总数小一个。
使用马克的解决方案,在以下情况下会舍弃值:X => RM-RM%N
EG:
Ran Max Value (RM) = 255
Valid Outcome (N) = 4
When X => 252, Discarded values for X are: 252, 253, 254, 255
So, if Random Value Selected (X) = {252, 253, 254, 255}
Number of discarded Values (I) = RM % N + 1 == N
IE:
I = RM % N + 1
I = 255 % 4 + 1
I = 3 + 1
I = 4
X => ( RM - RM % N )
255 => (255 - 255 % 4)
255 => (255 - 3)
255 => (252)
Discard Returns $True
如您在上面的示例中看到的那样,当X的值(我们从初始函数获得的随机数)为252、253、254或255时,即使这四个值组成了一组有效的返回值,我们也会将其丢弃。
IE:当值的计数Discarded(I)= N(有效结果数)时,原始函数将丢弃一组有效的返回值。
如果我们将值N和RM之间的差描述为D,即:
D = (RM - N)
然后,随着D的值变小,由于此方法而导致的不需要重新滚动的百分比在每个自然乘法中都会增加。(当RAND_MAX不等于素数时,这是有效的关注点)
例如:
RM=255 , N=2 Then: D = 253, Lost percentage = 0.78125%
RM=255 , N=4 Then: D = 251, Lost percentage = 1.5625%
RM=255 , N=8 Then: D = 247, Lost percentage = 3.125%
RM=255 , N=16 Then: D = 239, Lost percentage = 6.25%
RM=255 , N=32 Then: D = 223, Lost percentage = 12.5%
RM=255 , N=64 Then: D = 191, Lost percentage = 25%
RM=255 , N= 128 Then D = 127, Lost percentage = 50%
由于所需的Rerolls百分比随着N越接近RM而增加,因此,根据运行代码的系统约束和所寻找的值,对于许多不同的值,这可能是值得关注的问题。
要否定这个,我们可以做一个简单的修改,如下所示:
int x;
do {
x = rand();
} while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );
x %= n;
这提供了更通用的公式版本,该公式考虑了使用模量定义最大值的其他特性。
对RAND_MAX使用较小值的示例,该值是N的乘积。
标记的原始版本:
RAND_MAX = 3, n = 2, Values in RAND_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X >= (RAND_MAX - ( RAND_MAX % n ) )
When X >= 2 the value will be discarded, even though the set is valid.
通用版本1:
RAND_MAX = 3, n = 2, Values in RAND_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X > (RAND_MAX - ( ( RAND_MAX % n ) + 1 ) % n )
When X > 3 the value would be discarded, but this is not a vlue in the set RAND_MAX so there will be no discard.
另外,在N应该是RAND_MAX中的值数的情况下;在这种情况下,除非RAND_MAX = INT_MAX,否则您可以设置N = RAND_MAX +1。
从循环角度来看,您可以仅使用N = 1,但是X的任何值都将被接受,然后将IF语句放入您的最终乘数。但是也许您的代码可能有正当理由,当以n = 1调用函数时返回1 ...
因此,当您希望拥有n = RAND_MAX + 1时,最好使用0,通常会提供Div 0错误。
通用版本2:
int x;
if n != 0 {
do {
x = rand();
} while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );
x %= n;
} else {
x = rand();
}
这两种解决方案都以不必要的有效结果来解决此问题,当RM + 1为n的乘积时,会出现不必要的有效结果。
当您需要n等于RAND_MAX中包含的可能的总值集时,第二个版本还讨论了极端情况。
两者中的修改方法是相同的,并且允许提供更有效的解决方案,以提供有效的随机数并最小化丢弃的值。
重申:
扩展标记示例的基本通用解决方案:
// Assumes:
// RAND_MAX is a globally defined constant, returned from the environment.
// int n; // User input, or externally defined, number of valid choices.
int x;
do {
x = rand();
} while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) ) );
x %= n;
扩展通用解决方案,它允许RAND_MAX + 1 = n的另一种情况:
// Assumes:
// RAND_MAX is a globally defined constant, returned from the environment.
// int n; // User input, or externally defined, number of valid choices.
int x;
if n != 0 {
do {
x = rand();
} while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) ) );
x %= n;
} else {
x = rand();
}
在某些语言(特别是解释语言)中,在while条件之外进行比较运算的计算可能会导致更快的结果,因为这是一次性计算,无论需要重试多少次。YMMV!
// Assumes:
// RAND_MAX is a globally defined constant, returned from the environment.
// int n; // User input, or externally defined, number of valid choices.
int x; // Resulting random number
int y; // One-time calculation of the compare value for x
if n != 0 {
y = RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n)
do {
x = rand();
} while (x > y);
x %= n;
} else {
x = rand();
}
RAND_MAX%n = n - 1
随着RAND_MAX
价值3
(实际上它应该是高于很多,但偏置仍存在),它是有道理的,从这些计算是有偏差:
1 % 2 = 1
2 % 2 = 0
3 % 2 = 1
random_between(1, 3) % 2 = more likely a 1
在这种情况下,% 2
当您想要0
和之间的随机数时,您不应该这样做1
。通过这样做,您可能会在0
和之间获得一个随机数,因为在这种情况下:是的倍数。2
% 3
RAND_MAX
3
另一种方法
有一个简单得多的方法,但是要添加到其他答案中,这是我的解决方案,用于在0
和之间获得一个随机数n - 1
,因此有n
不同的可能性,而不会产生偏差。
>= n
,则重新启动(不取模)。真正的随机数据不容易获得,所以为什么要使用比所需更多的位。
下面是Smalltalk中的一个示例,其中使用了来自伪随机数生成器的位缓存。我不是安全专家,因此使用风险自负。
next: n
| bitSize r from to |
n < 0 ifTrue: [^0 - (self next: 0 - n)].
n = 0 ifTrue: [^nil].
n = 1 ifTrue: [^0].
cache isNil ifTrue: [cache := OrderedCollection new].
cache size < (self randmax highBit) ifTrue: [
Security.DSSRandom default next asByteArray do: [ :byte |
(1 to: 8) do: [ :i | cache add: (byte bitAt: i)]
]
].
r := 0.
bitSize := n highBit.
to := cache size.
from := to - bitSize + 1.
(from to: to) do: [ :i |
r := r bitAt: i - from + 1 put: (cache at: i)
].
cache removeFrom: from to: to.
r >= n ifTrue: [^self next: n].
^r
正如公认的答案所表明的,“模偏差”源于的低值RAND_MAX
。他使用极小的值RAND_MAX
(10)来表明,如果RAND_MAX为10,则您尝试使用%生成一个介于0和2之间的数字,结果将是:
rand() % 3 // if RAND_MAX were only 10, gives
output of rand() | rand()%3
0 | 0
1 | 1
2 | 2
3 | 0
4 | 1
5 | 2
6 | 0
7 | 1
8 | 2
9 | 0
因此,有4个0的输出(4/10机会),只有3个1和2的输出(每个3/10机会)。
所以这是有偏见的。较低的数字更有可能出现。
但这只有在RAND_MAX
很小的时候才会如此明显地显示出来。更具体地说,当您要修改的数字比时大RAND_MAX
。
一个比循环更好的解决方案(效率极低,甚至不建议使用循环)是使用输出范围更大的PRNG。在梅森倍捻机算法的4,294,967,295最大输出。MersenneTwister::genrand_int32() % 10
出于所有意图和目的,这样做将平均分配,并且模偏置效应将几乎消失。
MT::genrand_int32()%2
选择0(50 + 2.3e-8)%的时间和1(50-2.3e-8)%的时间。除非您要建立赌场的RGN(您可能会使用更大范围的RGN),否则任何用户都不会注意到额外的2.3e-8%的时间。您所说的数字太小,在这里无关紧要。
RAND_MAX
值将减少模偏置,但不会消除它。循环会。
RAND_MAX
它大于要修改的数字,则重新生成随机数所需的次数将逐渐减少,并且不会影响效率。我说保持循环,只要您针对最大倍数进行测试,n
而不是n
按照公认的答案所建议的那样进行测试。
我刚刚为冯·诺伊曼(Von Neumann)的“无偏硬币翻转法”编写了代码,从理论上讲应该消除随机数生成过程中的任何偏差。可以在(http://en.wikipedia.org/wiki/Fair_coin)上找到更多信息。
int unbiased_random_bit() {
int x1, x2, prev;
prev = 2;
x1 = rand() % 2;
x2 = rand() % 2;
for (;; x1 = rand() % 2, x2 = rand() % 2)
{
if (x1 ^ x2) // 01 -> 1, or 10 -> 0.
{
return x2;
}
else if (x1 & x2)
{
if (!prev) // 0011
return 1;
else
prev = 1; // 1111 -> continue, bias unresolved
}
else
{
if (prev == 1)// 1100
return 0;
else // 0000 -> continue, bias unresolved
prev = 0;
}
}
}
rand() % 100
100次。B)如果所有结果都不相同,则取第一个。C)否则,转到A。这将起作用,但是预期的迭代次数约为10 ^ 42,因此您必须非常耐心。和不朽。
else if(prev==2) prev= x1; else { if(prev!=x1) return prev; prev=2;}
RAND_MAX%n == n - 1
_的另一种思考方式是(RAND_MAX + 1) % n == 0
。在阅读代码时,我倾向于% something == 0
比其他计算方式更容易理解为“均匀可分”。 当然,如果您的C ++ stdlib具有RAND_MAX
与相同的值INT_MAX
,那(RAND_MAX + 1)
肯定是行不通的。因此Mark的计算仍然是最安全的实施方式。