1. 问题背景与需求分析
在算法面试和实际工程中,随机数生成是一个经典问题。我们经常会遇到这样的需求:给定一个能生成均匀随机数的基本方法,如何基于它构建更复杂的随机数生成器?题目"用 Rand7() 实现 Rand10()"就是这类问题的典型代表。
Rand7() 是一个假设的基础函数,它能以均等概率(1/7)返回1到7的整数。我们的目标是构建一个Rand10()函数,使其以均等概率(1/10)返回1到10的整数。这个问题的难点在于7和10不是倍数关系,无法通过简单的缩放来实现。
提示:在解决这类问题时,拒绝采样(Rejection Sampling)是一种常用且有效的方法。它的核心思想是通过多次调用基础随机函数,构造一个足够大的均匀采样空间,然后拒绝掉超出目标范围的样本。
2. 拒绝采样原理详解
2.1 基本概念
拒绝采样是一种概率抽样方法,用于从一个复杂的分布中生成样本。其基本步骤如下:
- 找到一个容易采样的建议分布q(x),使得对于所有x,p(x) ≤ Mq(x),其中p(x)是目标分布,M是一个常数。
- 从q(x)中采样得到一个候选样本x。
- 以概率p(x)/(Mq(x))接受这个样本,否则拒绝并重复步骤2。
在我们的场景中:
- 基础分布q(x)是Rand7()生成的均匀分布
- 目标分布p(x)是Rand10()要求的均匀分布
- 我们需要构造一个足够大的均匀空间,使得可以均匀地映射到1-10
2.2 数学基础
为了用Rand7()实现Rand10(),我们需要构造一个采样空间,其大小是10的倍数。因为7和10的最小公倍数是70,我们可以考虑使用两个Rand7()调用来构造一个7×7=49的采样空间。但是49不是10的倍数,我们需要找到一个更大的均匀空间。
更优的方案是使用以下公式构造采样空间:
num = 7 * (Rand7() - 1) + Rand7()
这样num的取值范围是1到49(因为7×6+7=49),且每个数字出现的概率都是1/49。然后我们可以:
- 如果num ≤ 40,返回num % 10 + 1
- 否则拒绝这个样本,重新采样
这样每个1-10的数字被选中的概率都是4/49,保证了均匀性。
3. 具体实现与优化
3.1 基础实现
基于上述理论,我们可以写出以下伪代码:
code复制function Rand10():
while true:
num = 7 * (Rand7() - 1) + Rand7()
if num <= 40:
return num % 10 + 1
这个实现中:
- 每次循环需要调用2次Rand7()
- 接受概率是40/49 ≈ 81.63%
- 期望调用Rand7()的次数是2/(40/49) = 2.45次
3.2 性能优化
我们可以通过利用被拒绝的样本来提高效率。对于num > 40的情况(即41-49),我们可以将这些值映射到一个更小的范围内:
code复制function Rand10():
while true:
num = 7 * (Rand7() - 1) + Rand7()
if num <= 40:
return num % 10 + 1
# 利用被拒绝的样本(41-49)
num = num - 40 # 现在num是1-9
num2 = 7 * (Rand7() - 1) + Rand7() # 1-49
if num2 <= 60: # 60是9*6.666...的整数部分
return (num - 1) * 10 + (num2 % 10 + 1)
这个优化版本:
- 减少了被拒绝样本的浪费
- 提高了整体效率
- 期望调用Rand7()的次数降低到约2.21次
4. 正确性证明与边界分析
4.1 均匀性证明
对于基础实现:
- 采样空间是1-49,每个数字概率1/49
- 我们接受1-40,分成4组1-10,11-20,...,31-40
- 每组正好有4个数字映射到每个1-10的输出
- 因此每个输出概率=4/49≈1/10
对于优化版本:
- 第一阶段的41-49被映射到1-9
- 第二阶段生成1-60,可以分成6组1-10,...,51-60
- 每个最终输出概率=(9/49)*(60/49)/10≈1/10
- 保持了均匀性
4.2 期望调用次数分析
基础版本:
- 每次尝试需要2次Rand7()调用
- 成功概率p=40/49
- 期望尝试次数=1/p=49/40
- 期望总调用次数=2*49/40=2.45
优化版本:
- 第一阶段成功概率p1=40/49
- 第二阶段成功概率p2=(9/49)*(60/49)
- 总成功概率=p1 + (1-p1)*p2 ≈ 0.942
- 期望调用次数≈2.21
5. 实际应用与扩展
5.1 其他类似问题
这种方法可以推广到其他类似的随机数生成问题:
- 用Rand5()实现Rand7()
- 用RandN()实现RandM()
- 非均匀分布的转换
5.2 工程实践中的考虑
在实际工程中,还需要考虑:
- 随机数生成的质量(避免伪随机数的周期性)
- 性能与随机性的权衡
- 多线程环境下的安全性
- 随机种子的管理
5.3 测试验证方法
验证实现正确性的方法:
- 统计测试:生成大量样本,验证分布均匀性
- χ²检验:检验观察频数与期望频数的差异
- 序列测试:验证随机数的独立性
6. 常见误区与注意事项
6.1 错误方法示例
初学者常犯的错误包括:
-
简单相加:
code复制Rand7() + Rand7() % 4这样会导致分布不均匀
-
直接相乘:
code复制Rand7() * Rand7() % 10 + 1这样会导致某些数字出现概率偏高
-
错误拒绝:
code复制num = Rand7() + Rand7() while num > 10: num = Rand7() + Rand7() return num这样会导致1和10的概率低于中间数字
6.2 性能陷阱
需要注意的性能问题:
- 无限循环风险(虽然概率极低)
- 基础随机数生成器的质量影响
- 在严格要求性能的场景下,可能需要预生成随机数
6.3 随机性质量
高质量的随机数生成需要考虑:
- 基础随机源的熵
- 避免可预测性
- 周期性处理
- 统计特性检验
在实际项目中,通常会使用经过严格测试的随机数库(如C++的
