给定一个生成范围在1到5之间随机整数的函数,编写一个生成范围在1到7之间随机整数的函数。
这与Adam Rosenfield的解决方案等效,但对于某些读者来说可能更清晰。假设rand5()是返回范围在1到5(包括1和5)之间的统计随机整数的函数。
int rand7()
{
int vals[5][5] = {
{ 1, 2, 3, 4, 5 },
{ 6, 7, 1, 2, 3 },
{ 4, 5, 6, 7, 1 },
{ 2, 3, 4, 5, 6 },
{ 7, 0, 0, 0, 0 }
};
int result = 0;
while (result == 0)
{
int i = rand5();
int j = rand5();
result = vals[i-1][j-1];
}
return result;
}
它是怎么工作的呢?就像这样:想象一下在纸上打印出这个双维数组,将其钉在飞镖板上并随机投掷飞镖。如果你打中了一个非零值,那么这是一个统计学意义下的1到7之间的随机值,因为有相等数量的非零值可以选择。如果你打中了一个零,就一直投掷飞镖直到你打中一个非零值。这就是这段代码所做的事情:i和j索引随机选择飞镖板上的一个位置,如果我们没有得到一个好的结果,我们就继续投掷飞镖。
就像Adam所说,最坏情况下它可能永远运行下去,但从统计角度来看,最坏情况永远不会发生:)
rand5
是均匀的,那么vals
网格中的每个单元格被选中的概率相等。该网格恰好包含区间[1,7]中每个整数的三个副本,以及四个零。因此,“原始”的结果流趋向于均匀混合的[1,7]值,再加上一些比任何单个允许值更频繁出现的零。但这不重要,因为零被去除了,只留下均匀混合的[1,7]值。 - Daniel Earwicker由于1/7在5进制下是一个无限小数,因此没有(完全正确的)能够以恒定时间运行的解决方案。一种简单的解决方案是使用拒绝抽样,例如:
int i;
do
{
i = 5 * (rand5() - 1) + rand5(); // i is now uniformly random between 1 and 25
} while(i > 21);
// i is now uniformly random between 1 and 21
return i % 7 + 1; // result is now uniformly random between 1 and 7
这个循环的预计运行时间为25/21 = 1.19次迭代,但是有无限小的可能会一直循环下去。
rand5()
不超过N
次且运行时间为常数的解决方案。那么,对于每个1≤k≤7的输出,将所有可能的调用序列的输出为“k”的概率相加,得到输出为“k”的概率是m/5^N,其中m是这样的序列数量。因此,m/5^N = 1/7,但是没有可能的整数解(N,m)满足此等式==>矛盾。 - Adam Rosenfield除了我的第一个答案,我还想再添加一个答案。这个答案试图最小化对于每次调用rand7()
需要调用rand5()
的次数,以最大化随机性的使用。也就是说,如果您认为随机性是一种宝贵的资源,我们希望尽可能地使用它,而不会浪费任何随机位。这个答案也与Ivan的答案中提出的逻辑有一些相似之处。
随机变量的熵是一个定义明确的量。对于一个等概率(即均匀分布)取N种状态的随机变量,其熵为log2 N。因此,rand5()
大约具有2.32193比特的熵,而rand7()
大约具有2.80735比特的熵。如果我们希望最大化随机性的使用,我们需要使用每次调用rand5()
返回的全部2.32193比特熵,并将它们应用于生成每次调用rand7()
所需的2.80735比特熵。那么,基本限制就是我们最多只能进行log(7)/log(5) = 1.20906次rand5()
调用来生成一次rand7()
调用。
附注:本答案中所有对数都以2为底,除非另有说明。rand5()
被假定返回范围在[0,4]的数字,而rand7()
则被假定返回范围在[0,6]的数字。将范围调整到分别为[1,5]和[1,7]是微不足道的。
那么我们该怎么做呢? 我们生成一个0到1之间的无限精确随机实数(暂时假设我们可以实际计算和存储这样一个无限精确的数字--我们稍后会解决这个问题)。我们可以通过在基数为5时生成其数字来生成这样的数字:我们选择随机数0。a
1a
2a
3...,其中每个数字ai
都是通过调用rand5()
来选择的。例如,如果我们的RNG对所有i
都选择ai
= 1,那么忽略它不太随机的事实,那将对应于实数1/5 + 1/52 + 1/53 + ... = 1/4(几何级数的总和)。
好了,我们已经选定了0到1之间的随机实数。我现在声称这样的随机数是均匀分布的。直观地说,这很容易理解,因为每个数字都是均匀选择的,并且该数字具有无限的精度。然而,对此进行正式证明要复杂得多,因为现在我们正在处理连续分布而不是离散分布,因此我们需要证明我们的数字落在区间[a
, b
]内的概率等于该区间的长度,即b-a
。读者可以把证明留作练习=)。
现在,我们已经从范围[0,1]中均匀地选择了一个随机实数,我们需要将其转换为范围[0,6]中的一系列均匀随机数,以生成rand7()
的输出。我们该怎么做?刚才所做的反向操作——将其转换为基数为7的无限精确小数,然后每个基数为7的数字将对应于一个rand7()
的输出。
以前面的例子为例,如果我们的rand5()
产生无限数量的1,则我们的随机实数将为1/4。将1/4转换为基数为7的数字,我们得到无限小数0.15151515...,因此我们将依次产生输出1、5、1、5、1、5等。
好了,我们有了主要思路,但还有两个问题:我们实际上无法计算或存储无限精确实数,那么我们如何处理它的有限部分?其次,我们实际上如何将其转换为基数为7的数字呢?
我们可以将0到1之间的数字转换为基数为7的数字的一种方法如下:
为了解决精度无限的问题,我们计算一个部分结果,并存储一个可能的上限。也就是说,假设我们两次调用rand5()
并且都返回了1,那么我们生成的数字是0.11(五进制)。无论后续对rand5()
的无限次调用生成什么,我们生成的随机实数永远不会大于0.12:始终成立0.11 ≤ 0.11xyz... < 0.12。
因此,我们跟踪当前的数字和它可能达到的最大值,然后将这两个数都转换为七进制。如果它们在前面的k
位相同,则我们可以安全地输出下一个k
位 -- 不管基于五进制的无限流产生什么,它们永远不会影响到七进制表示法中下一个k
位的值!
这就是算法 -- 要生成下一个rand7()
的输出,我们仅生成我们需要确保下一个数字在随机实数转换为七进制时的值的rand5()
位数。以下是一个带有测试工具的Python实现:
import random
rand5_calls = 0
def rand5():
global rand5_calls
rand5_calls += 1
return random.randint(0, 4)
def rand7_gen():
state = 0
pow5 = 1
pow7 = 7
while True:
if state / pow5 == (state + pow7) / pow5:
result = state / pow5
state = (state - result * pow5) * 7
pow7 *= 7
yield result
else:
state = 5 * state + pow7 * rand5()
pow5 *= 5
if __name__ == '__main__':
r7 = rand7_gen()
N = 10000
x = list(next(r7) for i in range(N))
distr = [x.count(i) for i in range(7)]
expmean = N / 7.0
expstddev = math.sqrt(N * (1.0/7.0) * (6.0/7.0))
print '%d TRIALS' % N
print 'Expected mean: %.1f' % expmean
print 'Expected standard deviation: %.1f' % expstddev
print
print 'DISTRIBUTION:'
for i in range(7):
print '%d: %d (%+.3f stddevs)' % (i, distr[i], (distr[i] - expmean) / expstddev)
print
print 'Calls to rand5: %d (average of %f per call to rand7)' % (rand5_calls, float(rand5_calls) / N)
注意,rand7_gen()
返回一个生成器,因为它具有内部状态,涉及将数字转换为七进制。测试框架调用 next(r7)
10000 次以产生 10000 个随机数,然后测量它们的分布。仅使用整数运算,因此结果是完全正确的。
还要注意,这里的数字会非常快地变得非常大。5 和 7 的幂增长很快。因此,在生成大量随机数后,由于 bignum 算术,性能将开始明显下降。但请记住,我的目标是最大化使用随机比特位,而不是最大化性能(尽管这是次要目标)。
在一次运行中,我对 rand7()
进行了 10000 次调用,对 rand5()
进行了 12091 次调用,平均每次调用需要调用 log(7)/log(5) 次,精确到 4 个有效数字,并且生成的输出是均匀分布的。
为了将此代码移植到没有内置任意大整数的语言中,您必须将 pow5
和 pow7
的值限制为本机整数类型的最大值——如果它们变得太大,则重置所有内容并重新开始。这将稍微增加每次调用 rand7()
对 rand5()
的平均调用次数,但希望即使对于 32 或 64 位整数,它也不会增加太多。
(我抄袭了 Adam Rosenfeld 的答案 并让它运行速度提高了约7%。)
假设 rand5() 均匀返回{0,1,2,3,4}中的一个数字,目标是均匀返回{0,1,2,3,4,5,6}。
int rand7() {
i = 5 * rand5() + rand5();
max = 25;
//i is uniform among {0 ... max-1}
while(i < max%7) {
//i is uniform among {0 ... (max%7 - 1)}
i *= 5;
i += rand5(); //i is uniform {0 ... (((max%7)*5) - 1)}
max %= 7;
max *= 5; //once again, i is uniform among {0 ... max-1}
}
return(i%7);
}
我们正在跟踪循环可以在变量max中产生的最大值。如果到目前为止的结果在max%7和max-1之间,则结果将在该范围内均匀分布。如果不是,则我们使用余数,它在0和max%7-1之间随机,并使用另一个rand()调用来生成新数字和新的max。然后我们重新开始。x = 2 * 21/25
+ 3 * 4/25 * 14/20
+ 4 * 4/25 * 6/20 * 28/30
+ 5 * 4/25 * 6/20 * 2/30 * 7/10
+ 6 * 4/25 * 6/20 * 2/30 * 3/10 * 14/15
+ (6+x) * 4/25 * 6/20 * 2/30 * 3/10 * 1/15
x = about 2.21 calls to rand5()
5 * rand5() + rand5()
中恰好符合这种情况。 - Ted Hopp算法:
数字7可以用3位二进制数表示。
使用rand(5)随机填充每个二进制位,填充0或1。
例如:调用rand(5)并且
如果结果是1或2,则填充0
如果结果是4或5,则填充1
如果结果是3,则忽略并重新进行(拒绝采样)
通过这种方式,我们可以随机填充3位二进制数为0/1,并得到一个1-7之间的数字。
编辑: 这似乎是最简单和最有效的答案,因此这里提供一些代码:
public static int random_7() {
int returnValue = 0;
while (returnValue == 0) {
for (int i = 1; i <= 3; i++) {
returnValue = (returnValue << 1) + random_5_output_2();
}
}
return returnValue;
}
private static int random_5_output_2() {
while (true) {
int flip = random_5();
if (flip < 3) {
return 0;
}
else if (flip > 3) {
return 1;
}
}
}
int randbit( void )
{
while( 1 )
{
int r = rand5();
if( r <= 4 ) return(r & 1);
}
}
int randint( int nbits )
{
int result = 0;
while( nbits-- )
{
result = (result<<1) | randbit();
}
return( result );
}
int rand7( void )
{
while( 1 )
{
int r = randint( 3 ) + 1;
if( r <= 7 ) return( r );
}
}
rand7() = (rand5()+rand5()+rand5()+rand5()+rand5()+rand5()+rand5())%7+1
编辑:那并不完全正确。假设 rand5 完美无误,它的偏差约为千分之二。桶会得到以下结果:
value Count Error%
1 11158 -0.0035
2 11144 -0.0214
3 11144 -0.0214
4 11158 -0.0035
5 11172 +0.0144
6 11177 +0.0208
7 11172 +0.0144
通过切换到求和
n Error%
10 +/- 1e-3,
12 +/- 1e-4,
14 +/- 1e-5,
16 +/- 1e-6,
...
28 +/- 3e-11
每增加2个,似乎就会增加一个数量级。
顺便说一下:上面的错误表并非通过抽样生成,而是通过以下递归关系生成的:
p[x,n]
是在给定n
次调用rand5
的情况下,output=x
可能发生的次数。
p[1,1] ... p[5,1] = 1
p[6,1] ... p[7,1] = 0
p[1,n] = p[7,n-1] + p[6,n-1] + p[5,n-1] + p[4,n-1] + p[3,n-1]
p[2,n] = p[1,n-1] + p[7,n-1] + p[6,n-1] + p[5,n-1] + p[4,n-1]
p[3,n] = p[2,n-1] + p[1,n-1] + p[7,n-1] + p[6,n-1] + p[5,n-1]
p[4,n] = p[3,n-1] + p[2,n-1] + p[1,n-1] + p[7,n-1] + p[6,n-1]
p[5,n] = p[4,n-1] + p[3,n-1] + p[2,n-1] + p[1,n-1] + p[7,n-1]
p[6,n] = p[5,n-1] + p[4,n-1] + p[3,n-1] + p[2,n-1] + p[1,n-1]
p[7,n] = p[6,n-1] + p[5,n-1] + p[4,n-1] + p[3,n-1] + p[2,n-1]
int ans = 0;
while (ans == 0)
{
for (int i=0; i<3; i++)
{
while ((r = rand5()) == 3){};
ans += (r < 3) >> i
}
}
ans += (r < 3) << i
。 - woolfieint rand7() {
int value = rand5()
+ rand5() * 2
+ rand5() * 3
+ rand5() * 4
+ rand5() * 5
+ rand5() * 6;
return value%7;
}
和所选方案不同,该算法将在常数时间内运行。但是,与所选方案的平均运行时间相比,它会调用 rand5 函数多 2 次。
请注意,这个生成器并不完美(数字0的概率比其他数字高出0.0064%),但对于大多数实际目的来说,稳定的运行时间保证可能比这种不准确性更为重要。
说明
这个解决方案基于一个事实,即数字 15,624 可以被 7 整除,因此如果我们可以随机均匀地生成从 0 到 15,624 的数字,然后取模 7,我们就可以得到一个接近均匀的 rand7 生成器。0 到 15,624 的数字可以通过掷 6 次 rand5 并使用它们来形成一个基数为 5 的数字的位数来均匀生成:
rand5 * 5^5 + rand5 * 5^4 + rand5 * 5^3 + rand5 * 5^2 + rand5 * 5 + rand5
然而,模7的性质使我们能够简化方程:
5^5 = 3 mod 7
5^4 = 2 mod 7
5^3 = 6 mod 7
5^2 = 4 mod 7
5^1 = 5 mod 7
所以。rand5 * 5^5 + rand5 * 5^4 + rand5 * 5^3 + rand5 * 5^2 + rand5 * 5 + rand5
变成
rand5 * 3 + rand5 * 2 + rand5 * 6 + rand5 * 4 + rand5 * 5 + rand5
理论
15624这个数字并非随机选择,而是可以通过费马小定理来发现。该定理指出,如果p是一个质数,则有:
a^(p-1) = 1 mod p
因此,这给了我们,
(5^6)-1 = 0 mod 7
(5^6)-1 等于
4 * 5^5 + 4 * 5^4 + 4 * 5^3 + 4 * 5^2 + 4 * 5 + 4
这是一个基于5进制的数,因此我们可以看出,这种方法可以用于从任何随机数生成器转换到另一个随机数生成器。虽然在使用指数p-1时会始终引入对0的小偏差。
为了更加通用和准确,我们可以使用以下函数:
def getRandomconverted(frm, to):
s = 0
for i in range(to):
s += getRandomUniform(frm)*frm**i
mx = 0
for i in range(to):
mx = (to-1)*frm**i
mx = int(mx/to)*to # maximum value till which we can take mod
if s < mx:
return s%to
else:
return getRandomconverted(frm, to)
public static int random_7(Random rg) {
int returnValue = 0;
while (returnValue == 0) {
for (int i = 1; i <= 3; i++) {
returnValue = (returnValue << 1) + SimulateFairCoin(rg);
}
}
return returnValue;
}
private static int SimulateFairCoin(Random rg) {
while (true) {
int flipOne = random_5_mod_2(rg);
int flipTwo = random_5_mod_2(rg);
if (flipOne == 0 && flipTwo == 1) {
return 0;
}
else if (flipOne == 1 && flipTwo == 0) {
return 1;
}
}
}
private static int random_5_mod_2(Random rg) {
return random_5(rg) % 2;
}
private static int random_5(Random rg) {
return rg.Next(5) + 1;
}
7 * rand5() / 5
怎么样? - kiwixz