使用 rand7 实现 rand10 的解决办法及扩展
一个误区引出问题的本质
初看这个问题应该很简单啊,7 个盒子分为两分部:5 + 2。第二部分 2 ,有两种状态:乘以/不乘以 到第一部分上去,不就得到 10 个状态了?刚好可以表示 10 空间。那也就得到 rand10 了。
细想一下不对:如果"第一部分数字"是 1,乘2得到2。如果"第一部分数字"是2,乘以1得到2。这丙种情况都得到 2:比得到 1 的情况要多。则不是等概率的了。
问题的关键点1:在实现 randn 的过程中,需要多次调用 randm,需要保证:后续调用的 randm 得到的值,与之前得到的值,进行运算之后得到的值,不能有重复。否则等概率就无法得到保证了。
问题的关键点2:用 randm 来实现 randn,考虑当 m>n 的情况。稍加思考,这是很简单的,只需要判断 randm() 值 <n 时,返回即可,因为 [0, m-1] 是等概率的,则 [0, n-1] 上每个数字也是等概率的。
如何实现及扩展 (randm -> randn)
由 “后续调用得到的值,与之前得到的值,进行运算,不能有重复”。这一模型,很容易想到二进制数模型:第 n+1 次得到数字 1,计算得到 2^(n+1) ,与之前 n 次得到的数字必然都不一样。
故得到最终的方案:将每次 randm() 的值串起来为一个 m 进制的数字,则它的大小必然会在第 p 次超过 n,记下 p:则调用 p 次 randm 一定可实现 randn ,且此时次数是最少的。
具体代码如下:
function rand(m){
return Math.floor(Math.random() * m);
}
//利用 rand_m 实现 rand_n
//先求最小的 p 满足: m^p >= n, 比如 m=7, n=10 时, p=2, 表示需要调用 rand7 两次用来生成 rand10
function byRand(m, n){
let p = Math.floor(Math.log(n) / Math.log(m))
if(0 === p || Math.pow(m, p) < n){
++ p;
}
//生成的随机数小于 n 则返回, 否则循环生成.
while(true){
let res = 0;
for(let i=0;i<p;++i){
res = res*m + rand(m);
}
if(res < n)
return res;
}
return NaN;
}
function Test(){
const cases = 10000000;
let statMap = new Map();
for(let i=0;i<cases;++i){
let r = byRand(7, 10); //rand(10)
let count = statMap.get(r)
if(!count){
count = 1;
}
else{
++ count;
}
statMap.set(r, count);
}
let entryList = [];
for(let [k,v] of statMap){
entryList.push({num: k, count: v});
}
entryList.sort(function(left, right){
if(left.num < right.num) return -1;
else if(left.num > right.num) return 1;
else return 0;
});
for(let e of entryList){
console.info(e.num, e.count)
}
}
Test()
Test
函数进行等概率验证:byRand(7, 10) 和原生的 rand(10),分别测试 1000 万次,分别得到的数字分布如下图: