利用random5 生成 random7
摘要:今天看算法,看到一个有意思的题目:给定一个函数 rand(5) 能随机生成 [1, 5] 之间的正整数,你能实现 rand(7) 吗?
尝试
如果我们用 rand(5) + rand(5) 呢? rand(5) + rand(5) 的结果是 [2, 10], 我们思考一下就知道,这些数肯定不是等概率的,比如 2 的概率要低于 5 的概率(生成 2 只有 1+1 这一种可能,而生成 5 可以有 1+4,2+ 3, 3+2,4+1)。我们再来看看,现在既然有了随机等概率的 1, 2, 3, 4, 5 ,那很容易就有随机等概率的 10, 20, 30, 40, 50。假设现在又有另外一个函数,能等概率的生成 0, 1, 2, 3, 4, 5, 6, 7, 8,9, 那么我们就能轻易的构造了等概率的 10, 11, 12, 13 ,... , 59(因为每一个数字只能有另外两个数字唯一相加得到),这就是我们的整体思路。
探索
基于上面的分析,我们首先要让 rand(5) 产生等概率的间距数组(比如上面的 10, 20, 30, 40, 50), 然后让 rand(5) 产生连续的待插入的数字(比如上面的 0, 1, 2 ,..., 9)。问题是,要多大的间距才合适呢?其实也很简单,要让 0, 1, 2, 3, 4 刚好能插入到间距数组中。
step1: 用 rand(5) 产生等概率的 0, 1, 2, 3, 4, 准备插入到下一步的等间距数组中,使得插入刚好合适。
step2: 用 rand(5)产生等概率的 0, 1, 2, 3, 4, 为了被插入,将其散开成 0, 5, 10, 15, 20。
step3: 将第一步的结果插入到第二步中,于是,就形成了 0, 1, 2, 3, 4 ,..., 21, 22, 23, 24。然后就很容易等概率地生成 1, 2, 3, 4, 5, 6, 7 。
/* * 输入:rand5 产生 [1, 5] 的随机数 * 输出:rand7 产生 [1, 7] 的随机数 */ int rand7() { int x = 22; while (x > 21) { //将区间控制到 [1, 21] x = rand5() + (rand5() - 1) * 5; //产生 [1, 25] 的整数区间 } return 1 + x % 7; //将[1, 21] 映射到 [1, 7] }
升华
上面的方法是基于这种思想:rand() 产生 [0, N-1], 把 rand() 视为 N进制的一位数产生器,那么可以使用 rand()*N + rand() 来产生 2位的 N进制数,以此类推,可以产生 3位, 4位...N位。这种按构造 N进制数的方式生成的随机数,必定能保证随机。
题目中 N为 5, 因此可以使用 rand5() * 5 + rand5() 来产生 2位的 5进制数,范围就是 1 到 25。再去掉 22-25, 剩余的除3, 映射到 [1, 7] 的区间上,以此作为 rand7() 的产生器。
思考:如果反过来呢?给你 rand7, 让我们实现 rand5, 这个该怎么实现呢?最直观的想法就是不断调用 rand7, 直到它产生 1到5 之间的数,然后返回。代码如下:
int rand5() { int x = 6; while (x > 5) { //将区间控制到 [1, 5] x = rand7() } return x; }
根据以上,我们可以得出一个一般性结论:如果 A > B, 那么一定可以用 randA 去实现 randB, 其中 randA 表示等概率生成[1, a] 的函数, randB 表示等概率生成[1, b] 的函数。
其它解法
对于上述题目,我们也可以采用 预置数组 的方式来解决,该方法简单,容易理解,但是不具有兼容性,需要额外的存储空间。
/* * 输入:rand5 产生 [1, 5] 的随机数 * 输出:rand7 产生 [1, 7] 的随机数 */ 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 res = 0; while (res == 0) { int i = rand5(); int j = rand5(); res = vals[i][j]; } return res; }
参考资料: