咱们先来看这样一个问题, 已知rand5能等几率产生1, 2, 3, 4, 5, 现要用rand5来实现rand7(rand7的意思是要等几率产生1, 2, 3, 4, 5, 6, 7), 该怎么搞呢? 我看了一下网上资料, 不少都是凑出来一个结果, 没有什么过程思路, 我以为虽然结果正确, 但总感受所用的技巧性太强。 因此, 在文本中, 我也来凑凑热闹, 看看该如何下手, 并给出程序的实际验证结果。数组
咱们看看rand5 + rand5 行不行。 rand5 + rand5 的结果是2, 3, 4, 5, 6, 7, 8, 9, 10, 稍微思考一下, 就知道, 这些数确定不是等几率的, 好比2的几率要低于5的几率。 因此, 不靠谱。 咱们再来看看, 既然有了1, 2, 3, 4, 5, 那很容易就有10, 20, 30, 40, 50, 且是等几率的。 假设如今又有另一个fun函数, 能等几率随机生成0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 那么, 咱们不就很轻易地构造了等几率的10, 11, 12, 13, ....., 59么? 没错, 思路就是这样的。dom
因此, 咱们先要让rand5产生等几率的间距数组(好比上述的10, 20, 30, 40, 50,), 而后让rand5产生连续的待插入数字(好比上述的0, 1, 2, ..., 9,). 如今问题是, 要多大的间距才合适呢? 其实也很简单, 要让0, 1, 2, 3, 4恰好能插入到间距数组中。函数
到这里, 就比较俗套了:spa
第一步: 用rand5产生等几率的0, 1, 2, 3, 4,准备插入到下一步的等间距数组中, 使得插入后, 恰好合适。.net
第二步: 用rand5产生等几率的0, 1, 2, 3, 4, 而后为了被插入, 将其散开成0, 5, 10, 15, 20.递归
第三步: 将第一步插入 到第二步中, 因而, 就造成了0, 1, 2, 3, 4, 5, 6, 7, 8, ..., 20, 21, 22, 23, 24. 而后就很容易等几率地生成1, 2, 3, 4, 5, 6, 7了。get
题目:table
给定一个函数rand5(),该函数能够随机生成1-5的整数,且生成几率同样。现要求使用该函数构造函数rand7(),使函数rand7()能够随机等几率的生成1-7的整数。模板
思路:class
不少人的第一反应是利用rand5() + rand()%3来实现rand7()函数,这个方法确实能够产生1-7之间的随机数,可是仔细想一想能够发现数字生成的几率是不相等的。rand()%3 产生0的几率是1/5,而产生1和2的几率都是2/5,因此这个方法产生6和7的几率大于产生5的几率。
正确的方法是利用rand5()函数生成1-25之间的数字,而后将其中的1-21映射成1-7,丢弃22-25。例如生成(1,1),(1,2),(1,3),则当作rand7()中的1,若是出现剩下的4种,则丢弃从新生成。
简单实现:
Java代码
rand5() 它可以等几率生成 1-5 之间的整数。所谓等几率就是1,2,3,4,5 生产的几率均为 0.2 。如今利用rand5(), 构造一个可以等几率生成 1- 7 的方法。这里有两个特别重要的点,一是 若是 rand5() + rand5(), 咱们可以产生一个均匀分布的 1 - 10 吗? 答案是否认的。好比对于 6来说(4+2, 2+4, 3+3),它被生成的生成的几率比1 (1+0,0+1)要大。
第二个点就是咱们不可能用rand5()直接产生 1- 7 的数,无论你用加减乘除都不行。因此,咱们要构造一个更大的范围,使得范围里每个值被生成的几率是同样的,并且这个范围是7的倍数。
先产生一个均匀分布的 0, 5, 10, 15, 20的数,再产生一个均匀分布的 0, 1, 2, 3, 4 的数。相加之后,会产生一个 0到24的数,并且每一个数(除0外)生成的几率是同样的。咱们只取 1 - 21 这一段,和7 取余之后+1就能获得彻底均匀分布的1-7的随机数了。
个人备注:
这种思想是基于,rand()产生[0,N-1],把rand()视为N进制的一位数产生器,那么可使用rand()*N+rand()来产生2位的N进制数,以此类推,能够产生3位,4位,5位...的N进制数。这种按构造N进制数的方式生成的随机数,一定能保证随机,而相反,借助其余方式来使用rand()产生随机数(如 rand5() + rand()%3 )都是不能保证几率平均的。
此题中N为5,所以可使用rand5()*5+rand5()来产生2位的5进制数,范围就是1到25。再去掉22-25,剩余的除3,以此做为rand7()的产生器.
给定一个函数rand()能产生0到n-1之间的等几率随机数,问如何产生0到m-1之间等几率的随机数?
如何产生以下几率的随机数?0出1次,1出现2次,2出现3次,n-1出现n次?
5.1-2 描述random(a,b)过程的一种实现,它只调用random(0,1).做为a和b的函数,你的程序的指望运行时间是多少?
注:random(a,b)为产生a,a+1,a+2,...,b的函数发生器,且产生各整数的几率相等,同为1/(b - a + 1).
看到这个题目时,似曾相识,脑海浮现了利用random(0,1)产生0或1,从而组成二进制数,来完成random(a,b)的实现.可是细想之后,感受有个问题在脑海中有点不明不白.
运行random(0,1)函数k次,使得2k>=(b-a+1),将获得[0,2k)的整数区间,如何将[0,2k)映射到[a,b]的整数区间,保证产生各整数的几率相等,同为1/(b-a+1).
1.当存在k使得2k=(b-a+1)时,只需将产生的二进制数与[a,b]整数一一对应,便可知足几率同为1/(b-a+1)的要求.
例如,random(3,6),k=2. 此时,对应关系可为00~3,01~4,10~5,11~6.产生的几率为1/4.
2.当不存在k使得2k=(b-a+1)时,产生[0,2k)区间整数的几率为1/2k,小于1/(b-a+1).[0,2k)如何映射到[a,b]整数区间.
思路一:扩大[0,2k)区间,使得2k能够被(b-a+1)整除,这样能够把[0,2k)分红N段时,每一段对应[a,b]里的一个整数.
但这个思路,是不可行的,由于不存在这样的k值.要么2k=(b-a+1),要么2k>(b-a+1)且不可被(b-a+1)整除.
思路二:参取截断映射,即 [0,2k) 的前部分映射到[a,b],这样虽然能够达到产生整数的几率相等,但不等于1/(b-a+1),还有若是产生[0,2k)后部分的值如何处理.
这个思路,是可行的,若是产生后部分的值,就继续调用自身,从新random.从结果输出分析,最终random(a,b)最终输出的只有[a,b]里的整数,并且每一个整数的几率相等,于是其产生的几率值是1/(b-a+1).
具体的实现代码以下:
int random(int a,int b) { int m = 1; int len = b - a + 1; int k = 0; //计算最小的正整数k,使2^k >= len while(m < len) { k++; m *= 2; } m = 0; for(int i = 0;i < k;i++) { m += random(0,1) * (1<<i); } if(m + 1 > len) { return random(a,b); } else { return m + a; } }
因为冗余的存在,该方法运行时间最坏的状况是无究,就是无限地递归调用自身.运行时间的下限是O(log(b-a+1)).
由上述的练习题可扩展出更多相似的问题.
利用rand5()产生rand7().rand5()产生1到5的整数,rand7()产生1到7的整数.
解决思路与上述的练习题是同样的.利用rand5()产生的一个整数空间,而后将其映射到[1,7]的整数空间上,映射时保证几率相等,且等于1/7.
下面介绍几个有意思的实现.
1.利用预置数组 该方法简单,易理解,可是不具扩展性,须要额外存储空间.
1 int rand7() 2 { 3 int vals[5][5] = { 4 {1,2,3,4,5}, 5 {6,7,1,2,3}, 6 {4,5,6,7,1}, 7 {2,3,4,5,6}, 8 {7,0,0,0,0} 9 }; 10 int result = 0; 11 while(result == 0) 12 { 13 int i = rand5(); 14 int j = rand5(); 15 result = vals[i - 1][j - 1]; 16 } 17 return result; 18 }
2.常规实现方法 可扩展,主要分为三步,构造大的整数区间,限制整数区间,最后映射整数区间.
1 int rand7() 2 { 3 int i; 4 do{ 5 i = 5 * (rand5() - 1) + rand5(); //产生[1,25]的整数区间 6 }while(i > 21); //将[1,25]整数区间控制于[1,21] 7 return i%7 + 1; //将[1,21]映射到[1,7] 8 }
3.看似正确的方法 其实错误的方法
1 int rand7() 2 { 3 int i; 4 i = rand5() + rand5() + rand5() + rand5() + rand5() + rand5() + rand5(); 5 return i%7 + 1; 6 }
与方法2的思路同样,构造新的整数区间,可是方法3中构造的整数区间并非等几率的.
第4代码中,将会产生5^7种可能的计算,但最终这些可能映射到[7,35]的整数区间中,可是[7,35]区间内整数的产生的几率并不相等.
例如,经过累加区间[0,1]三次,能够获得[0,3]的区间,可是[0,3]每一个整数的几率并不相等,分别为1/8,3/8,3/8,1/8.
给你一个能生成1到5随机数的函数,用它写一个函数生成1到7的随机数。 (即:使用函数rand5()来实现函数rand7())。
rand5能够随机生成1,2,3,4,5;rand7能够随机生成1,2,3,4,5,6,7。 rand5并不能直接产生6,7,因此直接用rand5去实现函数rand7彷佛不太好入手。 若是反过来呢?给你rand7,让你实现rand5,这个好实现吗?
一个很是直观的想法就是不断地调用rand7,直到它产生1到5之间的数,而后返回。 代码以下:
int Rand5(){ int x = ~(1<<31); // max int while(x > 5) x = Rand7(); return x; }
1 2 3 4 5 6 |
int Rand5(){ int x = ~(1<<31); // max int while(x > 5) x = Rand7(); return x; } |
等等,这个函数能够等几率地产生1到5的数吗?首先,它确确实实只会返回1到5这几个数, 其次,对于这些数,都是由Rand7等几率产生的(1/7),没有对任何一个数有偏袒, 直觉告诉咱们,Rand5就是等几率地产生1到5的。事实呢?让咱们来计算一下, 产生1到5中的数的几率是否是1/5就OK了。好比说,让咱们来计算一下Rand5生成1 的几率是多少。上面的函数中有个while循环,只要没生成1到5间的数就会一直执行下去。 所以,咱们要的1多是第一次调用Rand7时产生,也多是第二次,第三次,…第n次。 第1次就生成1,几率是1/7;第2次生成1,说明第1次没生成1到5间的数而生成了6,7, 因此几率是(2/7)*(1/7),依次类推。生成1的几率计算以下:
P(x=1)=1/7 + (2/7) * 1/7 + (2/7)^2 * 1/7 + (2/7)^3 * 1/7 + ... =1/7 * (1 + 2/7 + (2/7)^2 + ...) // 等比数列 =1/7 * 1 / (1 - 2/7) =1/7 * 7/5 =1/5
1 2 3 4 5 6 |
P(x=1)=1/7 + (2/7) * 1/7 + (2/7)^2 * 1/7 + (2/7)^3 * 1/7 + ... =1/7 * (1 + 2/7 + (2/7)^2 + ...) // 等比数列 =1/7 * 1 / (1 - 2/7) =1/7 * 7/5 =1/5
|
上述计算说明Rand5是等几率地生成1,2,3,4,5的(1/5的几率)。从上面的分析中, 咱们能够获得一个通常的结论,若是a > b,那么必定能够用Randa去实现Randb。其中, Randa表示等几率生成1到a的函数,Randb表示等几率生成1到b的函数。代码以下:
// a > b int Randb(){ int x = ~(1<<31); // max int while(x > b) x = Randa(); return x; }
1 2 3 4 5 6 7 |
// a > b int Randb(){ int x = ~(1<<31); // max int while(x > b) x = Randa(); return x; } |
回到正题,如今题目要求咱们要用Rand5来实现Rand7,只要咱们将Rand5 映射到一个能产生更大随机数的Randa,其中a > 7,就能够套用上面的模板了。 这里要注意一点的是,你映射后的Randa必定是要知足等几率生成1到a的。好比,
Rand5() + Rand5() - 1
1 2 |
Rand5() + Rand5() - 1
|
上述代码能够生成1到9的数,但它们是等几率生成的吗?不是。生成1只有一种组合: 两个Rand5()都生成1时:(1, 1);而生成2有两种:(1, 2)和(2, 1);生成6更多。 它们的生成是不等几率的。那要怎样找到一个等几率生成数的组合呢?
咱们先给出一个组合,再来进行分析。组合以下:
5 * (Rand5() - 1) + Rand5()
1 2 |
5 * (Rand5() - 1) + Rand5()
|
Rand5产生1到5的数,减1就产生0到4的数,乘以5后能够产生的数是:0,5,10,15,20。 再加上第二个Rand5()产生的1,2,3,4,5。咱们能够获得1到25, 并且每一个数都只由一种组合获得,即上述代码能够等几率地生成1到25。OK, 到这基本上也就解决了。
套用上面的模板,咱们能够获得以下代码:
int Rand7(){ int x = ~(1<<31); // max int while(x > 7) x = 5 * (Rand5() - 1) + Rand5() // Rand25 return x; }
1 2 3 4 5 6 |
int Rand7(){ int x = ~(1<<31); // max int while(x > 7) x = 5 * (Rand5() - 1) + Rand5() // Rand25 return x; } |
上面的代码有什么问题呢?可能while循环要进行不少次才能返回。 由于Rand25会产生1到25的数,而只有1到7时才跳出while循环, 生成大部分的数都舍弃掉了。这样的实现明显很差。咱们应该让舍弃的数尽可能少, 因而咱们能够修改while中的判断条件,让x与最接近25且小于25的7的倍数相比。 因而判断条件可改成x > 21,因而x的取值就是1到21。 咱们再经过取模运算把它映射到1-7便可。代码以下:
int Rand7(){ int x = ~(1<<31); // max int while(x > 21) x = 5 * (Rand5() - 1) + Rand5() // Rand25 return x%7 + 1; }
1 2 3 4 5 6 |
int Rand7(){ int x = ~(1<<31); // max int while(x > 21) x = 5 * (Rand5() - 1) + Rand5() // Rand25 return x%7 + 1; } |
这个实现就比上面的实现要好,而且能够保证等几率生成1到7的数。
让咱们把这个问题泛化一下,从特殊到通常。如今我给你两个生成随机数的函数Randa, Randb。Randa和Randb分别产生1到a的随机数和1到b的随机数,a,b不相等 (相等就不必作转换了)。如今让你用Randa实现Randb。
经过上文分析,咱们能够获得步骤以下:
// A > b int Randb(){ int x = ~(1<<31); // max int while(x > b*(A/b)) // b*(A/b)表示最接近A且小于A的b的倍数 x = RandA(); return x%b + 1; }
1 2 3 4 5 6 7 |
// A > b int Randb(){ int x = ~(1<<31); // max int while(x > b*(A/b)) // b*(A/b)表示最接近A且小于A的b的倍数 x = RandA(); return x%b + 1; } |
Randab = b * (Randa - 1) + Randb Randab = a * (Randb - 1) + Randa
1 2 3 |
Randab = b * (Randa - 1) + Randb Randab = a * (Randb - 1) + Randa
|
若是再通常化一下,咱们还能够把问题变成:给你一个随机生成a到b的函数, 用它去实现一个随机生成c到d的函数。