【刷穿LeetCode】470.用Rand7()实现

题目描述

这是 LeetCode 上的 470. 用 Rand7() 实现 Rand10() ,难度为 中等

Tag : 「位运算」、「数学」

已有方法 rand7 可生成 1 到 7 范围内的均匀随机整数,试写一个方法 rand10 生成 1 到 10 范围内的均匀随机整数。

不要使用系统的 Math.random() 方法。

示例 1:

输入: 1

输出: [7]
复制代码

示例 2:

输入: 2

输出: [8,4]
复制代码

示例 3:

输入: 3

输出: [8,1,10]
复制代码

提示:

  1. rand7 已定义。
  2. 传入参数: n 表示 rand10 的调用次数。

进阶:

  • rand7()调用次数的 期望值 是多少 ?
  • 你能否尽量少调用 rand7() ?

基本分析

给定一个随机生成

11

~

77

的函数,要求实现等概率返回

11

~

1010

的函数。

首先需要知道,在输出域上进行定量整体偏移,仍然满足等概率,即要实现

00

~

66

随机器,只需要在 rand7 的返回值上进行

1-1

操作即可。

但输出域的 拼接/叠加 并不满足等概率。例如 rand7() + rand7() 会产生

[2,14][2, 14]

范围内的数,但每个数并非等概率:

  • 产生
    22

    的概率为:

    1717=149\frac{1}{7} * \frac{1}{7} = \frac{1}{49}

  • 产生
    44

    的概率为:

    1717+1717+1717=349\frac{1}{7} * \frac{1}{7} + \frac{1}{7} * \frac{1}{7} + \frac{1}{7} * \frac{1}{7} = \frac{3}{49}

[2,14][2, 14]

1313

个数里面,等概率的数值不足

1010

个。

因此,你应该知道「执行两次 rand7() 相加,将

[1,10][1, 10]

范围内的数进行返回,否则一直重试」的做法是错误的。

kk

进制诸位生成 + 拒绝采样

上述做法出现概率分布不均的情况,是因为两次随机值的不同组合「相加」的会出现相同的结果(

(1,3)(1, 3)

(2,2)(2, 2)

(3,1)(3, 1)

最终结果均为

44

)。

结合每次执行 rand7 都可以看作一次独立事件。我们可以将两次 rand7 的结果看作生成

77

进制的两位。从而实现每个数值都唯一对应了一种随机值的组合(等概率),反之亦然。

举个🌰,设随机执行两次 rand7 得到的结果分别是

44

(第一次)、

77

(第二次),由于我们是要

77

进制的数,因此可以先对 rand7 的执行结果进行

1-1

操作,将输出域偏移到

[0,6][0, 6]

(仍为等概率),即得到

33

(第一次)和

66

(第二次),最终得到的是数值

(63)7(63)_7

(63)7(63)_7

77

进制的数值。

那么根据「进制转换」的相关知识,如果我们存在一个 randK 的函数,对其执行

nn

次,我们能够等概率产生

[0,Kn1][0, K^n - 1]

回到本题,执行一次 rand7 只能产生

[0,6][0, 6]

范围内的数值,不足

1010

个;而执行

22

rand7 的话则能产生

[0,48][0, 48]

范围内的数值,足够

1010

个,且等概率。

我们只需要判定生成的值是否为题意的

[1,10][1, 10]

即可,如果是的话直接返回,否则一直重试。

代码:

class Solution extends SolBase {
    public int rand10() {
        while (true) {
            int ans = (rand7() - 1) * 7 + (rand7() - 1); // 进制转换
            if (1 <= ans && ans <= 10) return ans;
        }
    }
}
复制代码
  • 时间复杂度:期望复杂度为
    O(1)O(1)

    ,最坏情况下为

    O()O(\infty)

  • 空间复杂度:
    O(1)O(1)

进阶

  1. 降低对 rand7 的调用次数

我们发现,在上述解法中,范围

[0,48][0, 48]

中,只有

[1,10][1, 10]

范围内的数据会被接受返回,其余情况均被拒绝重试。

为了尽可能少的调用 rand7 方法,我们可以从

[0,48][0, 48]

中取与

[1,10][1, 10]

成倍数关系的数,来进行转换。

我们可以取

[0,48][0, 48]

中的

[1,40][1, 40]

范围内的数来代指

[1,10][1, 10]

首先在

[0,48][0, 48]

中取

[1,40][1, 40]

仍为等概率,其次形如

x1x1

的数值有

44

个(

11

1111

2121

3131

),形如

x2x2

的数值有

44

个(

22

1212

2222

3232

)... 因此最终结果仍为等概率。

代码:

class Solution extends SolBase {
    public int rand10() {
        while (true) {
            int ans = (rand7() - 1) * 7 + (rand7() - 1); // 进制转换
            if (1 <= ans && ans <= 40) return ans % 10 + 1;
        }
    }
}
复制代码
  • 时间复杂度:期望复杂度为
    O(1)O(1)

    ,最坏情况下为

    O()O(\infty)

  • 空间复杂度:
    O(1)O(1)

  1. 计算 rand7 的期望调用次数

[0,48][0, 48]

中我们采纳了

[1,40][1, 40]

范围内的数值,即以调用两次为基本单位的话,有

4049\frac{40}{49}

成功的概率为

4049\frac{40}{49}

4940=1.225\frac{49}{40} = 1.225

1.2252=2.451.225 * 2 = 2.45

最后

这是我们「刷穿 LeetCode」系列文章的第 No.470 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour…

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。