一、题目

【LeetCode470】用 Rand7() 实现 Rand10()(拒绝采样)_概率分布

二、思路

这题要用到一个神奇的知识:

已知 rand_N() 可以等概率的生成[1, N]范围的随机数
那么:
(rand_X() - 1) × Y + rand_Y() => 可以等概率的生成[1, X * Y]范围的等概率随机数
即实现了 rand_XY()

要实现rand10(),就需要先实现rand_N(),并且保证N大于10且是10的倍数。这样再通过rand_N() % 10 + 1 就可以得到[1,10]范围的等概率随机数了。

对于随机数 randN,只要 K 是 N 的约数(或者说 N 是 K 的整数倍),都可以通过 randN 一步得到 randK:randK = (randN % K) + 1,这一条比较显然=。=

而实现rand_N(),我们可以通过最先介绍的神奇知识对rand7()进行改造,如下:
(rand7()-1) × 7 + rand7() ==> rand49()
由于此处的N不是10的倍数,就需要用到【拒绝采样】,即采样结果不在要求范围内就丢弃。

优化:利用这些范围外的数字,以减少丢弃的值,提高命中率总而提高随机数生成效率。此时可以将要拒绝的随机数看成一个新的额randM,然后对这个randM用一开始讲的方法。

如何利用一个小范围随机数,得到一个确定范围的等概率随机数?
先采用随机数的 k 进制,得到一个不小于确定范围的随机数 randK,然后对超过确定范围数进行拒绝即可。 注意,如果 K 比确定范围大太多,拒绝策略效率可能就会比较低(经常生成要拒绝的随机数),此时可以把要拒绝的随机数看成一个新的 randM,然后针对这个 randM 再思考怎么用这三个方法也得到确定范围的随机数

三、代码

# The rand7() API is already defined for you.
# def rand7():
# @return a random integer in the range 1 to 7

class Solution:
def rand10(self):
"""
:rtype: int
"""
while True :
# 等概率生成[1, 49]范围的随机数我
num = (rand7() - 1) * 7 +rand7()
# 拒绝采样,并返回[1, 10]范围的随机数
if(num <= 40):
return num % 10 +1

# 以下为优化部分
a = num -40 # rand 9
b = rand7()
num = (a - 1) * 7 + b # rand 63
if(num <= 60):
return num % 10 + 1

a = num - 60 # rand3
b = rand7()
num = (a - 1) * 7 + b # rand 21
if(num <= 20):
return num % 10 + 1

另外可以参考leetcode鲤鱼姐的题解:

impl Solution {
// 学习一下拒绝采样:
// 每一步都列出算式得出的数字分布情况, 可以加深理解
pub fn rand10() -> i32 {
// 首先我们清晰一下目标分布rand10(): {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 然后我们看看手中的rand7()能产生的分布: {1, 2, 3, 4, 5, 6, 7}
// 1. 我们手中能产生的分布范围是小于目标范围的, 所以我们需要尝试拓展
// 能等概率的拓展分布的操作有 '数乘' '加'
// 我们试试数乘:
// 1 * rand7() = {1, 2, 3, 4, 5, 6, 7}
// 2 * rand7() = {2, 4, 6, 8, 10, 12, 14}
// 3 * rand7() = {3, 6, 9, 12, 15, 18, 21}
// 4 * rand7() = {4, 8, 12, 16, 20, 24, 28}
// 5 * rand7() = {5, 10, 15, 20, 25, 30, 35}
// 6 * rand7() = {6, 12, 18, 24, 30, 36, 42}
// 7 * rand7() = {7, 14, 21, 28, 35, 42, 49} <-- 等会儿会使用这个
// 8 * rand7() = {8, 16, 24, 32, 40, 48, 56}
// ...
// 这里就有感觉啦, 为了能够产生无空的分布, 我们可以用 7 * rand7() - (rand7() - 1)
// rand7() - 1 = {0, 1, 2, 3, 4, 5, 6}
// 7 * rand7() - (rand7() - 1) = {1, 2, 3, 4, 5, ..., 48, 49}
// 这样我们就有了一个[1, 49]的随机数生成器

// 因为采用拒绝采样的思想, 首先我们进入一个无限循环, 当得到结果就退出, 如果得不到就一直循环
loop {
let range_1_to_49 = 7 * rand7() - (rand7() - 1);
if range_1_to_49 <= 40 {
// 因为我们要1..10的等概率分布, 所以我们只需要前40个数字, 拒绝剩下的9个
// 这里的range_1_to_49 = {1, 2, 3, 4, ..., 39, 40},
// range_1_to_49 - 1 = {0, 1, 2, 3, ..., 38, 39},
// (range_1_to_49 - 1) % 10 = {0, 1, 2, 3, ..., 8, 9}
// (range_1_to_49 - 1) % 10 + 1 = {1, 2, 3, ..., 9, 10} 即为rand10()
return (range_1_to_49 - 1) % 10 + 1;
}

// 到这里说明我们的range_1_to_49 = {41, 42, 43, 44, 45, 46, 47, 48, 49}
// 我们也不要浪费它们, 用它们我们可以得到一个[1, 9]的等概率分布
let range_1_to_9 = range_1_to_49 - 40;
// 我们可以用之前的思想, 使用9 * rand7()得到一个空隙为9的分布, 再把这个[1, 9]的分布插进去
// 9 * rand7() = {9, 18, 27, 36, 45, 54, 63}
// range_1_to_9 - 1 = {0, 1, 2, 3, 4, 5, 6, 7, 8}
// 9 * rand7() - (range_1_to_9 - 1) = {1, 2, 3, 4, 5, ..., 62, 63}
let range_1_to_63 = 9 * rand7() - (range_1_to_9 - 1);
if range_1_to_63 <= 60 {
// 类似之前的
// 因为我们要1..10的等概率分布, 所以我们只需要前60个数字, 拒绝剩下的3个
// 这里的range_1_to_63 = {1, 2, 3, 4, ..., 59, 60},
// range_1_to_63 - 1 = {0, 1, 2, 3, ..., 58, 59},
// (range_1_to_63 - 1) % 10 = {0, 1, 2, 3, ..., 8, 9}
// (range_1_to_63 - 1) % 10 + 1 = {1, 2, 3, ..., 9, 10} 即为rand10()
return (range_1_to_63 - 1) % 10 + 1;
}

// 走到这里说明range_1_to_63 = {61, 62, 63}
// 我们也不要浪费它们, 用它们我们可以得到一个[1, 3]的等概率分布
let range_1_to_3 = range_1_to_63 - 60;
// 我们可以用之前的思想, 使用3 * rand7()得到一个空隙为3的分布, 再把这个[1, 3]的分布插进去
// 3 * rand7() = {3, 6, 9, 12, 15, 18, 21}
// range_1_to_3 - 1 = {0, 1, 2}
// 3 * rand7() - (range_1_to_3 - 1) = {1, 2, 3, 4, 5, ..., 20, 21};
let range_1_to_21 = 3 * rand7() - (range_1_to_3 - 1);
if range_1_to_21 <= 20 {
// 类似之前的
// 因为我们要1..10的等概率分布, 所以我们只需要前20个数字, 拒绝剩下的1个
// 这里的range_1_to_21 = {1, 2, 3, 4, ..., 19, 20},
// range_1_to_21 - 1 = {0, 1, 2, 3, ..., 18 19},
// (range_1_to_21 - 1) % 10 = {0, 1, 2, 3, ..., 8, 9}
// (range_1_to_21 - 1) % 10 + 1 = {1, 2, 3, ..., 9, 10} 即为rand10()
return (range_1_to_21 - 1) % 10 + 1;
}

// 走到这里说明range_1_to_21 = {21}
// 我们没法使用它了
}

-1
}
}

关于拒绝采样的其他解释可以参考wiki百科:
​​​ https://en.wikipedia.org/wiki/Rejection_sampling​