概率p输出1,概率1-p输出0,等概率输出0和1 & 【LeetCode】470. rand7()构造rand10() 系列变形(新浪、字节面试题)
这一题在我面试中遇到两次(均为2020年7月份),新浪的比较简单,字节的做了变形,具体看后面详细分析。1. 等概率输出0和11.1 题目描述有一个随机数发生器,以概率 P 产生0,概率 (1-P) 产生 1,请问能否利用这个随机数发生器,构造出新的发生器,以 1/2 的概率产生 0 和 1 。请写明结论及推理过程。变形:知随机数生成函数f(),返回0的概率是60%,返回1的概率是40%。根据f()
目录
这一题在我面试中遇到两次(均为2020年7月份),新浪的比较简单,字节的做了变形,具体看后面详细分析。
1. 等概率输出0和1
1.1 题目描述
有一个随机数发生器,以概率 P 产生0,概率 (1-P) 产生 1,请问能否利用这个随机数发生器,构造出新的发生器,以 1/2 的概率产生 0 和 1 。请写明结论及推理过程。( 注意:这里的 p 相当于是未知的,后文会提到已知 p 的类型,解题思路是不同的 )
变形:
知随机数生成函数f(),返回0的概率是60%,返回1的概率是40%。根据f()求随机数函数g(),使返回0和1的概率是50%,不能用已有的随机生成库函数。
注意:
这里的变形毫无区别,看后续分析即可知道,核心问题都是
P ( 0 , 1 ) = ( p ) ∗ ( 1 − p ) = P ( 1 , 0 ) = ( 1 − p ) ∗ p P(0,1) = (p)*(1-p) = P(1,0) = (1-p)*p P(0,1)=(p)∗(1−p)=P(1,0)=(1−p)∗p
所以让 01 的情况来模拟 0, 让 10 的情况来模拟 1 即可,代码一模一样
1.2 解题思路 & 代码
题目解答:
两次调用该 RANDOM 函数,如果其概率为 P(x),调用2次
P(1) = p
P(0) = 1-p
P’(1) =p
P’(0) = 1-p
概率如下:
P
(
1
,
1
)
=
p
∗
p
P(1,1) = p * p
P(1,1)=p∗p
P
(
1
,
0
)
=
p
∗
(
1
−
p
)
P(1, 0) = p * (1-p)
P(1,0)=p∗(1−p)
P
(
0
,
1
)
=
(
1
−
p
)
∗
p
P(0, 1) = (1-p)*p
P(0,1)=(1−p)∗p
P
(
0
,
0
)
=
(
1
−
p
)
∗
(
1
−
p
)
P(0, 0) = (1-p)*(1-p)
P(0,0)=(1−p)∗(1−p)
很明显,这四种情况中出现 (0,1) 和 (1,0) 的概率相等,那么把 (0,1) 看成是 0 , (1,0) 看成是 1 ,那么他们输出的概率均为 p (1 - p),其他的情况舍弃并重新调用。这样就得到了 0 和 1 均等生成的随机器了。
这种解法可以推广到n个数的情况,我们知道,取n个随机数发生器,存在n个概率相同的独立事件,我们只使用这n个事件就得到1/n的概率了。例如 n=3 ,有 8 中情况000,001,010,011,100,101,110,111,其中001,010,100的概率都是 p 2 ∗ ( 1 − p ) p^2*(1-p) p2∗(1−p)。
Python
import random
class Solution:
def random_index(self):
# """随机变量的概率函数"""
# 参数rate为list<int>
# 返回概率事件的下标索引
# 这是一个10%概率产生0,90%概率产生1的生成器
rate = [1, 9]
start = 0
index = 0
randnum = random.randint(1, sum(rate))
for index, scope in enumerate(rate):
start += scope
if randnum <= start:
break
return index
def Rand1(self):
# 第一题:可以用原发生器周期性地产生2个数,直到生成01或者10。
i1 = self.random_index()
i2 = self.random_index()
if i1 == 0 and i2 == 1:
return 1
elif i1 == 1 and i2 ==0:
return 0
else:
return self.Rand1()
if __name__ == '__main__':
solution = Solution()
rand1 = []
for i in range(20):
rand1.append(solution.Rand1())
print(rand1)
rand2 = []
for i in range(20):
rand2.append(solution.Rand2(100))
print(rand2)
C++
int random_0_1()
{
int i = RANDOM();
int j = RANDOM();
int result;
while (true)
{
if (i == 0 && j == 1)
{
result = 0;
break;
}
else if (i == 1 && j == 0)
{
result = 1;
break;
}
else
continue;
}
return result;
}
这里,等概率输出 0 和 1 的期望调用次数 K
K
=
2
2
p
(
1
−
p
)
=
1
p
(
1
−
p
)
K= \frac{2}{2p(1-p)}= \frac{1}{p(1-p)}
K=2p(1−p)2=p(1−p)1
可以看出,当 p 很大或者很小的时候,这个期望值会很大,也就是调用次数会很多,只有当 p 趋近于 0.5, 调用次数最少,所以这种方法的期望调用次数的时间复杂度是比较高的!
2. 以 1/N 的概率返回 1~N 之间的数
用 位运算,因为 i
个二进制位随机的选择 0 或 1,可以随机的构成 0~
2
i
2^i
2i 的数,而这些数构成了所有的组合数。因此是等概率出现的。比如:2位二进制位,这两位可以随机为0或1而互不影响,随机的构成了00, 01, 10, 11,它们代表了四个数,且这四个数是等概率的。
可以通过已知随机函数 rand() 产生等概率产生 0 和 1 的新随机函数 Rand1(),然后调用 k(k为整数n的二进制表示的位数)次 Rand1() 函数,得到一个长度为 k 的 0 和 1 序列,以此序列所形成的整数即为 1–n 之间的数字。
注意:从产生序列得到的整数有可能大于 n ,如果大于 n 的话,则重新产生直至得到的整数不大于n。
算法:
-
第一步:由
rand()
(在下面的程序中是random_index()
)函数产生Rand1()
函数,Rand1()
函数等概率产生0和1。 -
第二步:计算整数 n 的二进制表示所拥有的位数 k, k = 1 + l o g 2 n k = 1 +log_2n k=1+log2n
-
第三步:调用k次 Rand1() 产生随机数。
代码中 Rand2()和 Rand3()是两种写法
import random
class Solution:
def random_index(self):
# """随机变量的概率函数"""
# 参数rate为list<int>
# 返回概率事件的下标索引
# 这是一个10%概率产生0,90%概率产生1的生成器
rate = [1, 9]
start = 0
index = 0
randnum = random.randint(1, sum(rate))
for index, scope in enumerate(rate):
start += scope
if randnum <= start:
break
return index
def Rand1(self):
# 1. 等概率生成 0,1
i1 = self.random_index()
i2 = self.random_index()
if i1 == 0 and i2 == 1:
return 1
elif i1 == 1 and i2 == 0:
return 0
else:
return self.Rand1()
return -1
def Rand2(self, n: int):
# 2. 以 1/N 的概率返回 1~N 之间的数
# int(res, 2) 表示把 str 类型的二进制转成十进制 int('101',2) == 5
k = len(bin(n)[2:]) # k = 1 + np.log2(n)
print("k:", k) # 10
res = ''
for i in range(k):
res += str(self.Rand1())
if int(res, 2) > n:
return self.Rand2(n)
else:
return int(res, 2)
def Rand3(self, n: int):
# 3. 以 1/N 的概率返回 1~N 之间的数
# (1<<i) 表示 1 * 2^i
import numpy as np
result = 0
k = 1 + np.log2(n)
print("k:", int(k)) # 10
for i in range(0, int(k)):
if self.Rand1() == 1:
result |= (1<<i)
if result > n:
return self.Rand3(n)
return result
if __name__ == '__main__':
solution = Solution()
rand1 = []
for i in range(20):
rand1.append(solution.Rand1())
print(rand1)
rand2 = []
for i in range(10):
rand2.append(solution.Rand2(999))
print(rand2)
rand3 = []
for i in range(10):
rand3.append(solution.Rand3(999))
print(rand3)
3. 给定函数rand5() 构造rand7() 或 rand7()构造rand10()
3.1 rand5() 构造rand7()
给定一个函数rand5(),该函数可以随机生成1-5的整数,且生成概率一样。现要求使用该函数构造函数rand7(),使函数rand7()可以随机等概率的生成1-7的整数。
思路:
很多人的第一反应是利用 rand5() + rand()%3 来实现 rand7() 函数,这个方法确实可以产生1-7之间的随机数,但是仔细想想可以发现数字生成的概率是不相等的。rand()%3 产生 0 的概率是 1/5 ,而产生 1 和 2 的概率都是 2/5 ,所以这个方法产生 6 和 7 的概率大于产生 5 的概率。
分析:
要保证rand7()在整数1-7的均匀分布,可以构造一个 1-7*n
的均匀分布的随机整数区间(n为任何正整数)。假设x是这个1-7*n 区间上的一个随机整数,那么x%7+1就是均匀分布在1-7区间上的整数。由于(rand5()-1)*5+rand5()可以构造出均匀分布在1-25的随机数(原因见下面的说明),可以将 22~25 这样的随机数剔除掉,得到的数1-21仍然是均匀分布在1-21的,这是因为每个数都可以看成一个独立事件。
正确的方法是利用rand5()函数生成1-25之间的数字,然后将其中的 1~21 (1~7 * n,其中 n 为任意整数,这里取了 n = 3,即 1 ~ 3*7 ) 映射成1-7,丢弃 22-25 。例如生成 (1,1),(1,2),(1,3),则看成 rand7() 中的 1 ,如果出现剩下的4种,则丢弃重新生成。
(如果是rand7 ( ) 构造rand10 ( ) ,则下面的代码循环条件变成
while ( x > 40)
,因为 1 ~ 7 *7 之间满足 10 ∗ n 10 * n 10∗n 且最大的数是 10 * 4 == 40)
解释:
首先 rand5()-1 得到一个离散整数集合{0,1,2,3,4},其中每个整数的出现概率都是1/5。那么 ( r a n d 5 ( ) − 1 ) ∗ 5 (rand5()-1)*5 (rand5()−1)∗5 得到一个离散整数集合 A = {0,5,10,15,20},其中每个整数的出现概率也都是1/5。而 r a n d 5 ( ) rand5() rand5() 得到的集合B={1,2,3,4,5}中每个整数出现的概率也是1/5。显然集合A和B中任何两个元素相加可以与1- 25 之间的一个整数 一 一 对应,也就是说1-25之间的任何一个数,可以唯一确定 A 和 B 中两个元素的一种组合方式,反过来也成立。由于 A 和 B 中元素可以看成是独立事件,根据独立事件的概率公式 P ( A B ) = P ( A ) P ( B ) P(AB)=P(A)P(B) P(AB)=P(A)P(B),得到每个组合的概率是 1 / 5 ∗ 1 / 5 = 1 / 25 1/5*1/5=1/25 1/5∗1/5=1/25。因此 ( r a n d 5 ( ) − 1 ) ∗ 5 + r a n d 5 ( ) (rand5()-1)*5+rand5() (rand5()−1)∗5+rand5()生成的整数均匀分布在 1 − 25 1-25 1−25之间,每个数的概率都是 1 / 25 1/25 1/25。
C++
int rand7()
{
int x = 0;
do
{
x = 5 * (rand5() - 1) + rand5();
}while(x > 21);
return 1 + x % 7;
}
注:为什么用 while(x>20)而不用while(x>7)呢?原因是如果用while(x>7)则有20/25的概率需要循环 while,很有可能死循环了。
这种思想是基于,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()的产生器。
3.2 【LeetCode】470. rand7() 构造rand10()
法一、
Java
public int rand10() {
// 首先得到一个数
int num = (rand7() - 1) * 7 + rand7();
// 只要它还大于40,那你就给我不断生成吧
while (num > 40)
num = (rand7() - 1) * 7 + rand7();
// 返回结果,+1是为了解决 40%10为0的情况
return 1 + num % 10;
}
复杂度分析
- 时间复杂度:期望时间复杂度为 O(1),但最坏情况下会达到 O(∞)(一直被拒绝)。
- 空间复杂度:O(1)。
- 期望调用次数
这里先 int num = (rand7() - 1) * 7 + rand7();
和上面的 do...while...
是一样的
但是这时候我们舍弃了 9 个数,舍弃的还是有点多,效率还是不高,怎么提高效率呢?那就是舍弃的数最好再少一点
法二、(更快,因为拒绝的更少)
Java
/**
* The rand7() API is already defined in the parent class SolBase.
* public int rand7();
* @return a random integer in the range 1 to 7
*/
class Solution extends SolBase {
public int rand10() {
while (true){
int num = (rand7() - 1) * 7 + rand7();
// 如果在40以内,那就直接返回
if(num <= 40) return 1 + num % 10;
// 说明刚才生成的在41-49之间,利用随机数再操作一遍
num = (num - 40 - 1) * 7 + rand7();
if(num <= 60) return 1 + num % 10;
// 说明刚才生成的在61-63之间,利用随机数再操作一遍
num = (num - 60 - 1) * 7 + rand7();
if(num <= 20) return 1 + num % 10;
}
}
}
这样我们可以得到 1−21 之间的随机数,只要舍弃 1 个即可
复杂度分析
- 时间复杂度:期望时间复杂度为 O(1),但最坏情况下会达到 O(∞)(一直被拒绝)。
- 空间复杂度:O(1)。
- 期望调用次数
使用类似的期望计算方法,我们可以得到调用 Rand7 的期望次数约为 2.2123。
变形 3.1 random3() 构造 random5()
已知random3()这个随机数产生器生成[1, 3]范围的随机数,请用random3()构造random5()函数,生成[1, 5]的随机数?
如何从[1-3]范围的数构造更大范围的数呢?同时满足这个更大范围的数出现概率是相同的,可以想到的运算包括两种:加法和乘法
考虑下面的表达式:
3 ∗ ( r a n d o m 3 ( ) – 1 ) + r a n d o m 3 ( ) 3 * (random3() – 1) + random3() 3∗(random3()–1)+random3()
可以计算得到上述表达式的范围是[1, 9] 而且数的出现概率是相同的,即1/9
下面考虑如何从[1, 9]范围的数生成[1, 5]的数呢?
可以想到的方法就是 rejection sampling
方法,即生成[1, 9]的随机数,如果数的范围不在[1, 5]内,则重新取样
int random5()
{
int val = 0;
do
{
val = 3 * (random3() - 1) + random3();
}while(val > 5);
return val;
}
变形 3.2
给定一个函数rand()能产生0到n-1之间的等概率随机数,问如何产生0到m-1之间等概率的随机数?
int random(int m , int n)
{
int k = rand();
int max = n-1;
while(k < m)
{
k = k*n + rand();
max = max*n + n-1;
}
return k/(max/n);
}
变形 3.3
将这个问题进一步抽象,已知 random_m() 随机数生成器的范围是 [1, m] 求random_n() 生成 [1, n] 范围的函数,m < n && n <= m *m
其中 t 为 n 的最大倍数,且满足 t< m*m
如:
- rand3() 生成rand5() , t = 5 ∗ i , t < 3 ∗ 3 , ∴ t = 5 ∗ 1 = 5 t = 5 * i,t < 3 * 3,∴ t = 5 * 1 = 5 t=5∗i,t<3∗3,∴t=5∗1=5
- rand5() 生成rand7() , t = 7 ∗ i , t < 5 ∗ 5 , ∴ t = 7 ∗ 3 = 21 t = 7 * i,t < 5 * 5,∴ t = 7 * 3 = 21 t=7∗i,t<5∗5,∴t=7∗3=21
- rand7() 生成rand10() , t = 10 ∗ i , t < 7 ∗ 7 , ∴ t = 10 ∗ 4 = 40 t = 10 * i,t < 7 * 7,∴ t = 10 * 4 = 40 t=10∗i,t<7∗7,∴t=10∗4=40
int random_n()
{
int val = 0;
int t; # t为n的最大倍数,且满足t<m*m
do
{
val = m * (random_m() - 1) + random_m();
}while(val > t);
return val;
}
变形 3.4
如何产生如下概率的随机数?0出1次,1出现2次,2出现3次,n-1出现n次?
int random(int size)
{
while(true)
{
int m = rand(size);
int n = rand(size);
if(m + n < size)
return m+n;
}
}
4. 返回 (0, 1) 之间的均匀分布(字节跳动面试题)
题目描述:
有一个函数 weak_random() 能以 p 概率返回 0 , 以 1-p 概率返回 1 , 且 p 已知 , 使用weak_random 实现float_random()返回(0,1)之间的浮点数 要求均匀分布 精度为 1e-8
先回顾一下均匀分布:
均匀分布的概率密度函数为:
f
(
x
)
=
{
1
b
−
a
,
a < x < b
0
,
else
f(x)= \begin{cases} \frac{1}{b - a}, & \text {a < x < b} \\0, & \text{else} \end{cases}
f(x)={b−a1,0,a < x < belse
可以理解为落在 (a, b) 区间内都是等概率的,概率均为 1 b − a \frac{1}{b - a} b−a1
解题思路:
可以理解为把前者映射到后者: (0,1) -> (T - 5e-9, T+5e-9 )
前文提到的第一种最简单的类型,即等概率输出 0 和 1 的期望调用次数
K
=
2
2
p
(
1
−
p
)
=
1
p
(
1
−
p
)
K= \frac{2}{2p(1-p)}= \frac{1}{p(1-p)}
K=2p(1−p)2=p(1−p)1
可以看出,当 p 很大或者很小的时候,这个期望值会很大,也就是调用次数会很多,只有当 p 趋近于 0.5, 调用次数最少,所以这种方法的期望调用次数的时间复杂度是比较高的!
这一题,返回 (0,1)之间均匀分布的调用次数 K 的下限:
K
>
−
l
o
g
(
e
−
8
)
−
p
l
o
g
p
−
(
1
−
p
)
l
o
g
(
1
−
p
)
K>\frac{-log(e^{-8})}{-plogp - (1-p)log(1-p)}
K>−plogp−(1−p)log(1−p)−log(e−8)
这是面试官提示的,一看到带有 log,就想到用分治法。
可以这样求解:
- 因为这里的条件给了 p 是已知的,所以把一条长度为 1 的线段,左边划分长度 p, 右边划分长度 (1-p);
- 再把左边的 p 划分成两段,占比分别为 p 和 (1-p)
- 同样再把右边的 (1-p) 划分成两段,占比分别为 p 和 (1-p)
- 依次递归到满足精度 e − 8 e^{-8} e−8 为止
参考:
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)