随机算法涉及大量概率论知识,有时候难得去仔细看推导过程,当然能够完全了解推导的过程自然是有好处的,如果不了解推导过程,至少记住结论也是必要的。本文总结最常见的一些随机算法的题目,也当作面试的准备工作吧。需要说明的是,这里用到的随机函数都假定它能随机的产生范围[a,b]内的整数,即产生每个整数的概率相等。(虽然在实际中并不一定能实现,不过,谁在乎呢?这个世界都是这么随机)
假设给定一个数组A,它包含元素1到N,我们的目标是构造这个数组的一个随机排列。
一个常用的方法是为数组每个元素A[i]赋一个随机的优先级P[i],然后依据优先级对数组进行排序。比如我们的数组为A={1, 2, 3, 4},如果选择的优先级数组为P={36, 3, 97, 19},那么就可以得到数列B={2, 4, 1, 3},因为3的优先级最高(为97),而2的优先级最低(为3)。这个算法需要产生优先级数组,还需使用优先级数组对原数组排序,这里就不详细描述了,还有一种更好的方法可以得到随机排列数组。
产生随机排列数组的一个更好的方法是原地排列给定数组(in-place),可以在O(N)的时间内完成。伪代码如下:
RANDOMIZE-IN-PLACE ( A , n )
for i ←1 to n
do swap A[i] ↔A[RANDOM(i , n )]
如代码中所示,第i次迭代时,元素A[i]是从元素A[i]到A[n]中随机选取的,在第i次迭代后,我们就再也不会改变A[i]。
A[i]位于任意位置j的概率为1/n。这个是很容易推导的,比如A[1]位于位置1的概率为1/n,这个显然,因为A[1]不被1到n的元素替换的概率为1/n,而后就不会再改变A[i]了。而A[1]位于位置2的概率也是1/n,因为A[1]要想位于位置2,则必须在第一次与A[k]交换(k=2...n),同时第二次A[2]与A[k]替换,第一次与A[k]交换的概率为(n-1)/n,而第二次替换概率为1/(n-1),所以总的概率是(n-1)/n * 1/(n-1) = 1/n。同理可以推导其他情况。
当然这个条件只能是随机排列数组的一个必要条件,也就是说,满足元素A[i]位于位置j的概率为1/n不一定就能说明这可以产生随机排列数组。因为它可能产生的排列数目少于n!,尽管概率相等,但是排列数目没有达到要求,算法导论上面有一个这样的反例。
算法RANDOMIZE-IN-PLACE可以产生均匀随机排列,它的证明过程如下:
首先给出k排列的概念,所谓k排列就是从n个元素中选取k个元素的排列,那么它一共有n!/(n-k)!个k排列。
循环不变式:for循环第i次迭代前,对于每个可能的i-1排列,子数组A[1...i-1]包含该i-1排列的概率为(n-i+1)! / n!。
初始化:在第一次迭代前,i=1,则循环不变式指的是对于每个0排列,子数组A[1...i-1]包含该0排列的概率为(n-1+1)! / n! = 1。A[1...0]为空的数组,0排列则没有任何元素,因此A包含所有可能的0排列的概率为1。不变式成立。
维持:假设在第i次迭代前,数组的i-1排列出现在A[1...i-1]的概率为(n-i+1) !/ n!,那么在第i次迭代后,数组的所有i排列出现在A[1...i]的概率为(n-i)! / n!。下面来推导这个结论:
考虑一个特殊的i排列p = {x1, x2, ... xi},它由一个i-1排列p' ={x1, x2,..., xi−1}后面跟一个xi构成。设定两个事件变量E1和E2:
E1为该算法将排列p‘放置到A[1...i-1]的事件,概率由归纳假设得知为Pr(E1) = (n-i+1)! / n!。
E2为在第i次迭代时将xi放入到A[i]的事件。
因此我们得到i排列出现在A[1...i]的概率为Pr {E2 ∩ E1} = Pr {E2 | E1} Pr {E1}.而Pr {E2 | E1} = 1/(n − i + 1),所以
Pr {E2 ∩ E1} = Pr {E2 | E1} Pr {E1}= 1 /(n − i + 1) * (n − i + 1)! / n! = (n − i )! / n!。
结束:结束的时候i=n+1,因此可以得到A[1...n]是一个给定n排列的概率为1/n!。
如果上面的随机排列算法写成下面这样,是否也能产生均匀随机排列?
PERMUTE-WITH-ALL( A , n )
for i ←1 to n
do swap A[i] ↔A[RANDOM(1 , n )]
注意,该算法不能产生均匀随机排列。假定n=3,则该算法可以产生3*3*3=27个输出,而3个元素只有3!=6个不同的排列,要使得这些排列出现概率等于1/6,则必须使得每个排列出现次数m满足m/27=1/6,显然,没有这样的整数符合条件。而实际上各个排列出现的概率如下,如{1,2,3}出现的概率为4/27,不等于1/6。
题目:给定一个未知长度的整数流,如何随机选取一个数?(所谓随机就是保证每个数被选取的概率相等)
解法1:
如果数据流不是很长,可以存在数组中,然后再从数组中随机选取。当然题目说的是未知长度,所以如果长度很大不足以保存在内存中的话会很麻烦。这种解法有其局限性。
解法2:
如果数据流在第1个数字后结束,那么必选第1个数字。
如果数据流在第2个数字后结束,那么我们选第2个数字的概率为1/2,我们以1/2的概率用第2个数字替换前面选的随机数,得到新的随机数。
.........
如果数据流在第n个数字后结束,那么我们选择第n个数字的概率为1/n,即我们以1/n的概率用第n个数字替换前面选的随机数,得到新的随机数。
一个简单的方法就是使用随机函数f(n)=bigrand()%n,其中bigrand()返回很大的随机整数,当数据流到第n个数时,如果f(n)==0,则替换前面的已经选的随机数,这样可以保证每个数字被选中的概率都是1/n。如当n=1时,则f(1)=0,则选择第1个数,当n=2时,则第2个数被选中的概率为1/2,以此类推,当数字长度为n时,第n个数字被选中的概率为1/n。
select = m
remaining = n
for i = [0, n)
if (bigrand() % remaining < select)
print i
select--
remaining--
void genknuth(int m, int n)
{
for (int i=0; i
void gensets(int m, int n)
{
set S;
while (S.size() < m)
S.insert(bigrand() % n);
set::iterator it;
for (it = S.begin(); it != S.end(); it++)
cout << *it << endl;
}
答案:还是1:1。在所有出生的第一个小孩中,男女比例是1:1;在所有出生的第二个小孩中,男女比例是1:1;.... 在所有出生的第n个小孩中,男女比例还是1:1。所以总的男女比例是1:1。
2)约会问题:两人相约5点到6点在某地会面,先到者等20分钟后离去,求这两人能够会面的概率。
答案:设两人分别在5点X分和5点Y分到达目的地,则他们能够会面的条件是|X-Y| <= 20,而整个范围为S={(x, y): 0 =< x <= 60, 0=< y <= 60},所以会面的情况为图中表示的面积,概率为(60^2 - 40^2) / 60^2 = 5/9。
3)帽子问题:有n位顾客,他们每个人给餐厅的服务生一顶帽子,服务生以随机的顺序归还给顾客,请问拿到自己帽子的顾客的期望数是多少?
答案:使用指示随机变量来求解这个问题会简单些。定义一个随机变量X等于能够拿到自己帽子的顾客数目,我们要计算的是E[X]。对于i=1, 2 ... n,定义Xi =I {顾客i拿到自己的帽子},则X=X1+X2+...Xn。由于归还帽子的顺序是随机的,所以每个顾客拿到自己帽子的概率为1/n,即Pr(Xi=1)=1/n,从而E(Xi)=1/n,所以E(X)=E(X1+X2+...Xn)=E(X1)+E(X2)+...E(Xn)=n*1/n = 1。即大约有1个顾客可以拿到自己的帽子。5)如果在高速公路上30分钟内看到一辆车开过的几率是0.95,那么在10分钟内看到一辆车开过的几率是多少?(假设常概率条件下)
答案:假设10分钟内看到一辆车开过的概率是x,那么没有看到车开过的概率就是1-x,30分钟没有看到车开过的概率是(1-x)^3,也就是0.05。所以得到方程(1-x)^3 = 0.05 ,解方程得到x大约是0.63。
《算法导论》
《编程珠玑2》
http://blog.csdn.net/wxwtj/article/details/6621430