LeetCode 470 用 Rand7() 实现 Rand10():python3 题解

目录


1. 题目理解

目标 :实现一个函数 rand10(),返回 1 到 10 之间的均匀随机整数。
限制 :只能使用给定的 rand7() 函数,它返回 1 到 7 之间的均匀随机整数。
核心要求均匀分布 。这意味着生成 1、2、...、10 的概率必须完全相等,都是 \(1/10\)。

什么是"均匀随机"?

如果 rand10() 是均匀的,那么运行很多次后,数字 1 出现的次数应该约等于数字 10 出现的次数。如果某个数字出现的概率偏高,那就不是均匀随机。


2. 为什么不能直接用数学公式?

以下方法的思路相对直接,初学者容易直接想到,但它们都是错误的:

  • 错误思路 1:取模

    python 复制代码
    return rand7() % 10 + 1

    原因rand7() 只能生成 1-7。1%10 是 1,7%10 是 7,永远生成不了 8、9、10。

  • 错误思路 2:相加

    python 复制代码
    return (rand7() + rand7()) % 10 + 1

    原因 :两个随机数相加,结果不是均匀分布的。

    例如:和为 2 只有 (1,1) 一种情况;但和为 8 有 (1,7), (2,6)...(7,1) 七种情况。8 出现的概率远大于 2。这违反了均匀性。


3. 核心解法:拒绝采样 (Rejection Sampling)

要生成均匀随机数,最可靠的方法是构造一个更大的均匀空间,然后从中"截取"我们需要的部分。

思路推导

  1. 扩大样本空间

    调用两次 rand7(),我们可以得到一个二维坐标 \((a, b)\),其中 \(a, b \in [1, 7]\)。

    这就像掷两个 7 面的骰子。总共有 \(7 \times 7 = 49\) 种可能的组合。

    由于 rand7() 是均匀的,这 49 种组合中,每一种出现的概率都是 \(1/49\)。

  2. 映射到一维

    我们可以把这 49 种组合映射到数字 1 到 49。

    公式:idx = (a - 1) * 7 + b

    • a - 1 的范围是 0 到 6。
    • (a - 1) * 7 的范围是 0, 7, 14, ..., 42。
    • 加上 b (1 到 7) 后,idx 的范围正好是 1 到 49
    • 且 1 到 49 中每个数字出现的概率相等。
  3. 拒绝采样

    我们需要 1 到 10 的均匀分布。

    49 不能被 10 整除。如果我们直接把 1-49 映射到 1-10,会导致某些数字概率偏高。
    策略 :找到 49 以内最大的、能被 10 整除的数,即 40

    • 如果生成的 idx1 到 40 之间:我们接受它,并将其映射到 1-10。
    • 如果生成的 idx41 到 49 之间:我们拒绝 它(丢弃),重新生成两个新的 rand7()
  4. 映射结果

    对于接受的 idx (1-40),使用公式 (idx - 1) % 10 + 1 即可得到 1-10。

    • 1 -> 1, 11 -> 1, 21 -> 1, 31 -> 1 (共 4 次)
    • 10 -> 10, 20 -> 10, 30 -> 10, 40 -> 10 (共 4 次)
    • 每个数字被选中的概率完全相等。

解法一:标准拒绝采样(推荐)

这是最清晰、最标准的面试解法。

python 复制代码
# 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. 调用两次 rand7(),生成两个独立的随机数 a 和 b
            a = rand7()
            b = rand7()
            
            # 2. 将二维坐标 (a, b) 映射为一维数字 idx,范围是 [1, 49]
            # 解释:(a-1)*7 产生 0, 7, 14...42
            # 加上 b (1-7) 后,产生 1 到 49 的均匀分布
            idx = (a - 1) * 7 + b
            
            # 3. 拒绝采样
            # 我们只保留 1 到 40 的数字,因为 40 是 10 的倍数
            # 41 到 49 的数字会被丢弃,重新循环生成
            if idx <= 40:
                # 4. 将 1-40 映射到 1-10
                # (idx - 1) % 10 产生 0-9,再加 1 变为 1-10
                return (idx - 1) % 10 + 1

复杂度分析

  • 时间复杂度 :期望时间复杂度为 \(O(1)\)。
    • 每次循环生成 1-49 的概率是 1。
    • 被接受(idx <= 40)的概率是 \(40/49\)。
    • 期望循环次数 = \(1 / (40/49) = 49/40 = 1.225\) 次。
    • 每次循环调用 2 次 rand7(),所以期望调用 rand7() 次数 = \(2 \times 1.225 = 2.45\) 次。
  • 空间复杂度 :\(O(1)\),只用了几个变量。

4. 进阶优化:减少 rand7() 调用次数

题目进阶部分问:能否尽量少调用 rand7()

在标准解法中,当 idx 在 41-49 之间时(共 9 个数),我们直接丢弃了。这浪费了随机性。

这 9 个数本身构成了一个均匀的 rand9()。我们可以利用这部分的随机性,结合新的 rand7() 继续生成,而不是完全重来。

优化思路

  1. 第一轮 :生成 1-49。
    • 若 1-40:返回结果。
    • 若 41-49(9 个数):保留,视为 rand9()
  2. 第二轮 :利用上一轮的 9 个数,再调用一次 rand7()
    • 新空间大小:\(9 \times 7 = 63\)。
    • 公式:idx = (prev_rand9 - 1) * 7 + rand7(),范围 1-63。
    • 若 1-60:返回结果(60 是 10 的倍数)。
    • 若 61-63(3 个数):保留,视为 rand3()
  3. 第三轮 :利用上一轮的 3 个数,再调用一次 rand7()
    • 新空间大小:\(3 \times 7 = 21\)。
    • 公式:idx = (prev_rand3 - 1) * 7 + rand7(),范围 1-21。
    • 若 1-20:返回结果。
    • 若 21:彻底丢弃,重新开始第一轮。

这种优化可以将期望调用次数从 2.45 降低到约 2.21 次。

解法二:优化版代码

python 复制代码
class Solution:
    def rand10(self):
        """
        :rtype: int
        """
        while True:
            # --- 第一轮:生成 1-49 ---
            a = rand7()
            b = rand7()
            idx = (a - 1) * 7 + b  # 范围 [1, 49]
            
            if idx <= 40:
                return (idx - 1) % 10 + 1
            
            # --- 第二轮:利用 41-49 (共 9 个数) ---
            # idx - 40 得到 1-9 的均匀随机数
            a = idx - 40  # 范围 [1, 9]
            b = rand7()
            idx = (a - 1) * 7 + b  # 范围 [1, 63] (9 * 7)
            
            if idx <= 60:
                return (idx - 1) % 10 + 1
            
            # --- 第三轮:利用 61-63 (共 3 个数) ---
            # idx - 60 得到 1-3 的均匀随机数
            a = idx - 60  # 范围 [1, 3]
            b = rand7()
            idx = (a - 1) * 7 + b  # 范围 [1, 21] (3 * 7)
            
            if idx <= 20:
                return (idx - 1) % 10 + 1
            
            # 如果到了这里 (idx == 21),说明运气很差,所有随机性都用光了
            # 只能回到 while 开头,完全重新开始

5. 总结与对比

特性 标准解法 (解法一) 优化解法 (解法二)
核心思想 拒绝采样 拒绝采样 + 随机性复用
代码可读性 ⭐⭐⭐⭐⭐ (非常清晰) ⭐⭐⭐ (逻辑稍复杂)
rand7() 期望调用次数 2.45 次 ~2.21 次
面试建议 首选。逻辑清晰,不易出错。 进阶。如果面试官追问优化,再提出此思路。

建议:

  1. 理解公式(a - 1) * 7 + b 是将两个独立随机变量组合成一个更大范围均匀分布的标准技巧(进制转换思想)。
  2. 理解拒绝:为什么是 40 而不是 49?因为 49 无法被 10 整除。为了保证均匀,必须舍弃多余的部分。
  3. 死循环问题 :虽然代码里有 while True,但因为每次循环都有大于 0 的概率成功(40/49),所以理论上几乎不可能无限循环,期望次数是有限的。