题目:给一副扑克牌和一个随机数函数,设计一个洗牌算法。
解析:
最直观的思路是什么?很简单,每次从牌堆中随机地拿一张出来。那么, 第一次拿有52种可能,拿完后剩下51张;第二次拿有51种可能,第三次拿有50种可能, …,一直这样随机地拿下去直到拿完最后1张,我们就从52!种可能中取出了一种排列, 这个排列对应的概率是1/(52!),正好是题目所要求的。
接下来的问题是,如何写代码去实现上面的算法?假设扑克牌是一个52维的数组cards, 我们要做的就是从这个数组中随机取一个元素,然后在剩下的元素里再随机取一个元素… 这里涉及到一个问题,就是每次取完元素后,我们就不会让这个元素参与下一次的选取。 这个要怎么做呢。
我们先假设一个5维数组:1,2,3,4,5。如果第1次随机取到的数是4, 那么我们希望参与第2次随机选取的只有1,2,3,5。既然4已经不用, 我们可以把它和1交换,第2次就只需要从后面4位(2,3,1,5)中随机选取即可。同理, 第2次随机选取的元素和数组中第2个元素交换,然后再从后面3个元素中随机选取元素, 依次类推。
代码:
#include <iostream> #include <cstdlib> using namespace std; void Swap(int &a, int &b){// 有可能swap同一变量,不能用异或版本 int t = a; a = b; b = t; } void RandomShuffle(int a[], int n){ for(int i=0; i<n; ++i){ int j = rand() % (n-i) + i;// 产生i到n-1间的随机数 Swap(a[i], a[j]); } } int main(){ srand((unsigned)time(0)); int n = 9; int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; RandomShuffle(a, n); for(int i=0; i<n; ++i) cout<<a[i]<<endl; return 0; }
讲义讲解版本:
for i in 1...n
randomly select a card j from [1, i]
swap card i with card j
证明:
我们使⽤用数学归纳法进⾏行证明算法1的正确性:
每张牌出现在各个位置的概率相等(1/N)
当N = 1时,显然成⽴立
当N=2,每张牌出现在两个位置的概率都是1/2
假设当N=k时候成⽴立,我们现在证明N = k +1的时候也成⽴立,即每张牌出现出现在各个位置的概率均为1/(k + 1)
我们分三部分来看:第k+1张牌到所有位置,前k张牌到第k+1个位置,前k张牌到前k个位置
第k+1张牌到所有位置
显然,第k + 1张牌到所有位置的概率均为1/(k +1),算法就是这么写的:)
前k张牌到第k+1个位置
跟上⾯面同理,前k张牌被交换到第k + 1个位置的概率为1/(k +1)
前k张牌到前k个位置(最复杂):
我们知道N = k的时候成⽴立,所以不考虑第k+1张牌,前k张牌在前k个位置的概率是1/k
但现在,因为第k+1张牌的出现,我们还要保证前k张牌中的某⼀一张不被交换⾛走,这个概率是:(1 - 1/(k + 1))
所以根据⻉贝叶斯公式,最终的概率为:
1/k * (1 - 1/(k + 1)) = 1/(k + 1)