关于随机数

随机数是一个很神奇的东西, 基于随机数衍生出来很多有意思的算法

洗牌算法

得到一个M以内的所有自然数的随机顺序数组

洗牌之后, 如果能够保证每一个数出现在所有位置上的概率是相等的, 那么这种算法是符合要求的

当然, 最直观的想法就是随机一个数, 如果已经取过, 则重新抽取, 否则加入数组

1

这个思路有两种实现方式:

1.1

每次随机 [0,M-1] 得到一个数

​ 如果该位置不为-1, 则取出并置位-1;

​ 否则重抽

​ 这种算法会导致多次抽到相同的数字, 开销很大

1.2

每次随机 [0,M-1] 得到一个数

​ 将该数取出, 并删除, M–

相比于法1, 每次随机一定会得到一个结果, 但是删除的开销也很大

2

换一个思路

每次随机选两张牌, 交换他们, 随机一定的次数, 得到结果

如何确定这个次数来保证绝对公平? 我还没学会

3

Knuth-Shuffle 发明了洗牌算法

众所周知, 经典的算法都是简易而高效的

将最后一个数和前面任意 n-1 个数中的一个数进行交换(也可以不换),然后倒数第二个数和前面任意 n-2 个数中的一个数进行交换,如此往复直到最后一个元素

1
2
for (int i = n - 1; i >= 0; i--) 
swap(arr[i], arr[rand() % (i + 1)]);

算法不需要开辟额外空间, 时间复杂度为 $o(n)$ 相当完美!

下面来简单论证一下这个算法的正确性

需要证明洗牌算法的正确性, 需要论证两点

  1. 一张牌出现在每个位置的概率是相等的

​ 2.一个位置出现牌的概率是相等的

先来证明1

对于第一张牌, 他出现在最后一个位置的概率为 $1/n$ (第一轮抽到1号牌的概率)

出现在倒数第二个位置的概率为 $(n-1)/n * 1/(n-1) = 1/n$ (第一轮没抽到1号牌且第二轮抽到1号牌)

接下来证明2

对于最后一个位置, 装1的概率为 $1/n$

装2的概率为$(n-1)/n * 1/(n-1) = 1/n$

好耶

4

但是这种算法也有一定的局限性

最明显的就是原数组顺序被打乱了

当然, 要解决这个问题, 只需要另外开辟一个空间做个拷贝即可

接下来是一个不可避免的问题: 假设我们不知道M的大小, 换一句话说, M是一个动态增长(只有增)的数字呢?

Inside-Out Algorithm解决了这一问题

从前向后扫描数据, 把位置 i 的数据随机插入到前 i 个 (包括第 i 个) 位置 (假设为k)

然后把原始数据中位置 k 的数字替换新数组位置 i 的数字, 其实效果相当于新数组中位置 k 和位置 i 的数字进行交换

蓄水池抽样算法

我更喜欢叫它抽牌算法

从n个数据中随机取得m个数,n>m

从前往后遍历数组, 当 i < m 时, 放入新数组

当 i >= m 时, 随机[0, m), 设为k, 若 k < m, 则将新数组中的 k 替换

随机数的映射

简单的说, 就是给一个随机数, 要求设计某种变换得到另一个随机数

假设randx可以生成 [1,x] 的随机数

从大范围rand中得到小范围

这个很简单, 因为我们平时就会用, mod一下就行

注意丢弃掉最后一部分数

eg:
rand18 -> rand5

如果随机出来的是17或18, 则丢弃

否则 (rand18)%4 + 1

从小范围rand中得到大范围

这种情况稍微复杂一点

那么我们先来考虑一种简单的情况

现有随机数rand可以随机 [0, 9]

实现0~99的随机函数

很简单, 分为个, 十 两位数, 分别随机后即为答案

那么, 考虑生成

0~150之间的随机数呢

当然我们可以生成0~999间的随机数,然后利用1中的方法得到答案

接下来回到问题

其实我们上述已经解决了10进制下的问题

那么, 解决n进制下的问题也是一样的思路