问题起源于编程珠玑Column 12中的题目10,其描述如下:
How could you select one of n objects at random, where you see the objects sequentially but you do not know the value of n beforehand? For concreteness, how would you read a text file, and select and print one random line, when you don’t know the number of lines in advance?
问题定义可以简化如下:在不知道文件总行数的情况下,如何从文件中随机的抽取一行?
首先想到的是我们做过类似的题目吗?当然,在知道文件行数的情况下,我们可以很容易的用C运行库的rand函数随机的获得一个行数,从而随机的取出一行,但是,当前的情况是不知道行数,这样如何求呢?我们需要一个概念来帮助我们做出猜想,来使得对每一行取出的概率相等,也即随机。这个概念即蓄水池抽样(Reservoir Sampling)。
有了这个概念,我们便有了这样一个解决方案:定义取出的行号为choice,第一次直接以第一行作为取出行 choice ,而后第二次以二分之一概率决定是否用第二行替换 choice ,第三次以三分之一的概率决定是否以第三行替换 choice ……,以此类推,可用伪代码描述如下:
i = 0 while more input lines with probability 1.0/++i choice = this input line print choice
这种方法的巧妙之处在于成功的构造出了一种方式使得最后可以证明对每一行的取出概率都为1/n(其中n为当前扫描到的文件行数),换句话说对每一行取出的概率均相等,也即完成了随机的选取。
证明如下:
回顾这个问题,我们可以对其进行扩展,即如何从未知或者很大样本空间随机地取k个数?
类比下即可得到答案,即先把前k个数放入蓄水池,对第k+1,我们以k/(k+1)概率决定是否要把它换入蓄水池,换入时随机的选取一个作为替换项,这样一直做下去,对于任意的样本空间n,对每个数的选取概率都为k/n。也就是说对每个数选取概率相等。
伪代码:
Init : a reservoir with the size: k for i= k+1 to N M=random(1, i); if( M < k) SWAP the Mth value and ith value end for
证明如下:
蓄水池抽样问题是一类问题,在这里总结一下,并由衷的感叹这种方法之巧妙,不过对于这种思想产生的源头还是发觉不够,如果能够知道为什么以及怎么样想到这个解决方法的,定会更加有意义。
类似面试题:
谷歌面试题:如何随机选取1000个关键字
给定一个数据流,其中包含无穷尽的搜索关键字(比如,人们在谷歌搜索时不断输入的关键字)。如何才能从这个无穷尽的流中随机的选取1000个关键字?(注:这一题和2012年百度校招浙大站其中一个分析题很相似)
解:定义长度为1000的数组。
对于数据流中的前1000个关键字,显然都要放到数组中。
对于数据流中的的第n(n>1000)个关键字,我们知道这个关键字被随机选中的概率为 1000/n。所以我们以 1000/n 的概率用这个关键字去替换数组中的随机一个。这样就可以保证所有关键字都以 1000/n的概率被选中。
对于后面的关键字都进行这样的处理,这样我们就可以保证数组中总是保存着1000个随机关键字。
类似面试题:随机洗牌程序
问题:给定一个有序序列1~n,要你将其完全打乱,要求每个元素在任何一个位置出现的概率均为1/n。
解决方案:依次遍历数组,对第n个元素,以1/n的概率与前n个元素中的某个元素互换位置,最后生成的序列即满足要求,1/n的概率可通过rand() % n实现。见如下程序:
void swap(int &p, int &q) { int tmp = p; p = q; q = tmp; } void shuffle(int *arr, int n) { int i; for(i = 0; i < n; i++) { int idx = rand() % (i + 1); //选取互换的位置 swap(arr[idx], arr[i]); } }
使用数学归纳法证明:
(1)当n=1时,idx必为0,所以元素arr[0]在任何一个位置的概率为1/1,命题成立。
(2)假设当n=k时,命题成立,即n=k时,原数组中任何一个元素在任何一个位置的概率为1/k。
则当n=k+1时,当算法执行完k次时,前k个元素在前k个位置的概率均为1/k。
当执行最后一步时,前k个元素中任何一个元素被替换到第k+1位置的概率为:(1-1/(k+1)) * 1/k = 1/(k+1); 在前面k个位置任何一个位置的概率为(1-1/(k+1)) * 1/k = 1/(k+1);
故前k个元素在任意位置的的概率都为1/(k+1)
所以,对于前k个元素,它们在k+1的位置上概率为1/(k+1)。
对于第k+1个元素,其在原位置的概率为1/k+1,在前k个位置任何一个位置的概率为:(1-k/(k+1)) * (1/k)=1/(k+1),所以对于第k+1个元素,其在整个数组前k+1个位置上的概率也均为1/k+1。
综上所述,对于任意n,只要按照方案中的方法,即可满足每个元素在任何一个位置出现的概率均为1/n。