从一道概率问题说起

本文选取面试过程中有意思的概率问题来进行讨论和分享,具体如下:
​ 给定一个函数rand7(),每次调用都能够等概率的生成1-7这7个数字,利用这个函数如何等概率地生成1~9呢,即实现函数rand9()。

分析

我们能够利用的有限条件是rand7()这个函数,它能够等概率地生成1-7,而我们需要等概率地生成1~9,这中间必然是有联系,且能够进行转换的。

换一个角度,如果我们有rand9(),那么如何实现rand7()呢?

实际上,如果我们有rand9()这个函数的话,每次生成的数据是1,2,3,4,5,6,7,8,9;对于数字8和9其实并不是我们需要的,我们直接丢弃就可以了,再次获取rand9()的值,直至小于等于7。

int rand7(){
  int num = rand9();
 while(num>7){
    num = rand9();
 }
  return num;
}

但是这种丢弃元素重新获取的方式能够保证等概率地获取1~7这7个数字么?是公平的么?
img

我们可以实际求取一下这样获得数字1的概率,有以下情况:

  1. 第一次调用rand9(),获得了1,概率p为

  2. 第二次调用rand9(),数字大于7,再次调用rand9(),获得了1,概率p为

  3. 第三次调用rand9(),数字大于7,后续再调用了两次rand9(),获得了1,概率p为

  4. 第n次调用rand9(),获得1的概率为

    由此可以得出用这种方式获取到元素1~7的概率

    可知,这种方式是能够等概率地获取1~7每一个数字的。这种方式也称之为拒绝采样,满足一定条件就接受,不满足就拒绝。

基于这个认识,即

m,n为正整数,如果能够等概率地获取1-m这m个数字,那么同样可以等概率地获取1-n(n<m)这n个数字

此时,我们可以回过头来看本文最初的问题,即给定一个rand7(),如何实现rand9()。这个问题与我们的具体问题相比较,等概率生成的范围较小,而要求的范围较大。如果我们能够基于rand7(),实现一个更大范围的等概率数(这个数的至少比9大),那么就可以通过拒绝采样的方式,筛选出小于9的数,实现rand9()。

联想

要求:基于rand7(),实现一个randk(),k>9

rand7()函数能够等概率生成1,2,3,4,5,6,7;

那么2*rand7()就能等概率生成2,4,6,8,10,12,14,但是中间的数1,3,5,7,9,11,13都没有生成;

7*rand7()就能等概率生成7,14,21,28,35,42,49,中间的差值都是7;

可以看到7*rand7()等概率生成的这些数,中间的差值都是7,因此可以利用rand7()生成1-7来填补中间这些数。

由此可以实现7*(rand7()-1)+rand7(),能够等概率返回 1~49这49个数,那么再筛选过程中,去掉其中大于9的数,肯定是能够实现rand9(),即:

int rand9(){
  //实现1-49
  int num = (rand7()-1)*7+rand7();
 while(num>9){
    num = (rand7()-1)*7+rand7();
 }
  return num;
}

这个方法虽然是对的,但是耗时会很多,因为很多大于9的数都被过滤了,需要多调用几次rand7(),时间复杂度高,获取1~9的数学期望会很高。可以基于此作一些改进:

int rand9(){
  //实现1-49
  int num = (rand7()-1)*7+rand7();
  //对于大于45的数,拒绝本次采样,开始下一轮采样
 while(num>45){
    num = (rand7()-1)*7+rand7();
 }
  //num 1~45,num%9 等概率返回0-8
  return num%9+1;
}

可以在实现等概率范围的1-49范围内,选择9的最大倍数45,将大于45的数过滤,开启下次采样,实现等概率返回1-45,此时再对9进行取模,能够等概率返回0-8这9个数。通过这种方式,每次最多丢弃的数字为46~49,一次获得1-45范围的数的概率为

实际上对于要过滤的46~49这4个数字,还可以做一次映射

可以将46-49映射到1-28,在对大于27(在28范围内9的最大倍数)舍去就可,最终舍去的就是28一个数字,能够进一步提高效率。

img
rand7()实现rand9()映射关系图

int rand9(){
  //实现1-49
  int num = (rand7()-1)*7+rand7();
  while(true){
    num = (rand7()-1)*7+rand7();
    if(num <= 45){
      return num%9+1;
    }
    
    num = (num-45-1)*7+rand7();
    if(num<=27){
      return num%9+1;
    }
  //num=28 舍去
 }
}

刚刚我们是从rand7生成rand9(),因为7<9,所以需要将数据范围扩充到7*7=49。如果要生成rand300()呢?实际上就可以在生成rand49()的基础上再乘以7,得到rand343(),然后筛选出小于等于300的数即可。

从这里我们可以总结规律:

已知randM(),求取randN(),其中M<N。

我们可以不断地调用randM()并进行映射,直到生成一个范围大于N的数。

从上述例子来看,我们每次扩充范围的时候,都是要乘以M,因此我们可以从M进制及位运算的角度来看下这个问题。

  1. 将N转化为M进制数

  2. 然后从N的高位开始使用randM()进行随机生成一个M进制数,如果生成的数小于等于高位上的数字,则进行下一个高位数字的生成与选择,否则在此高位继续使用randM()直至小于等于高位上的数字。

img
rand7()实现rand300()的7进制原理图

最后

本文对等概率生成数字问题做了一个分析,其核心思想就是,将复杂问题转化为经典的已知基础问题,最主要的有两个方面:

  1. 如何扩大生成的数字范围

  2. 如何保证生成的各个数的概率相同,即保证公平

参考

《程序员代码面试指南》
《Leetcode》

更多信息欢迎关注微信公众号:惊鸿只为卿
惊鸿只为卿

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值