问题描述:程序的输入包含两个整数m和n,其中m<n。输出是0~n-1范围内的m个随机整数,要求:每个数选择出现的概率相等,且按序输出。该题目是从《编程珠玑》的第12章看到的。
学习过概率统计的同学应该都知道每一个数字被抽取的概率都应该为m/n. 那么我们怎么构造出这样的概率呢?在《编程珠玑》上面是这样解析的:
依次考虑整数0,1,2,.....,n-1,并通过一个适当的随机测试对每个整数进行选择。通过按序访问整数,我们可以保证输出结果是有序的。 假如我们考虑m = 2,n = 5的情况,那么选择的每一个数字的概率都应该是2/5,我们怎么样才能做到呢?不慌张,慢慢来。
下面给出我的分析过程:在0,1,2,3,4这五个数字中,我们依次对每一个数进行分析,第一次遇到0时,它的选择概率应该是2/5,如果选中了,我们开始测试第二个数1,这个时候因为1选中了,所以1这个数字的选中概率就变小了,变成1/4了,有人说这似乎不对吧,因为题目说让每一个数字选中的概率是一样大的,而现在?一个2/5,一个1/4,这怎么行呢?其实不是这样的,认真思考一下就知道了,数字1选中的概率等于什么? 数字1选中的概率p(1) = 数字0选中的概率 * (1/4) + 数组0没选中的概率*(2/4)这样推算下 (2/5 * 1/4) + (3/5 * 2/4) = 8/20 = 2/5 。这不就一样了吗?呵呵!下面给出来自Knuth的《The Art of Computer Programming, Volume2:Seminumerical Algorithms》的伪代码:
select = m remaining = n for i = [0,n) if (rand() % remaining) < select print i select -- remaining--
代码很精简,代码遵守的规则应该是要从r个剩余的整数中选出s个,我们以概率s/r选择下一个数。这个概率的选择方式和我们上面证明的是一样的。所以在程序结束的时候一定会打印出m个数字,且每一个数字的被选择概率相同,为m/n。 (不要小看任何一段小代码,其背后隐含着很多的意义!)
当然了,这个题目还有其他的解法,这是在网上看到的其他的解法。他们将这样的问题抽象的定义为蓄水池抽样问题。其思路是这样的,先把前k个数放入蓄水池中,对第k+1,我们以k/(k+1)的概率决定是否要把它换入蓄水池,换入时我们可以随机挑选一个作为替换位置,这样一直到样本空间N遍历完,最后蓄水池中留下的就是结果。这样的方法得到的结果也是正确的,且每一个数字被选择的概率也是k/n。
证明:1) 当遍历到到m+1个元素时, 该元素被保存在A’中的概率为 m/(m+1), 前面m个元素被保存在A’中的概率为 1- (m/m+1 * 1/m) = m/m+1
2) 当遍历到第i个元素时,设前面i-1个元素被保存在A’中的概率为 m/(i-1)。根据算法, 第i个元素被保存在A’中的概率为m/i , 前面i-1各个元素留在A’中的概率为 m/(i-1) * (1-(m/i* 1/m) = m/i;
3)通过归纳,即可得到每个元素留在A’中的概率为 m/n;
参见:http://blog.csdn.net/hackbuteer1/article/details/6878627#reply 九楼smqjzzm1988 回复
这个问题其实还可以扩展一下:
如何从n个对象(可以以此看到这n个对象,但事先不知道n的值)中随机选择一个?比如在不知道一个文本中有多少行,在这样的情况下要求你随机选择文件中一行,且要求文件的每一行被选择的概率相同。 在知道n这个总对象个数的情况下,谁都知道概率是1/n. 但是我们现在不知道,怎么办呢?
考虑这样是不是可以,我们总是以1/i的概率去选择每一次遍历的对象,比如从1,2,3,4,5,6,....,N, 每一次遍历到x时,总是以1/x的概率去选择它.
整体思路如下:
我们总选择第一个数字(文本行),并以概率1/2选择第二个(行),以1/3选择第三行,也就是说设结果为result,遍历第一个时result = 1,第二个时以1/2的概率替让result = 2,这样一直遍历概率性的替换下去,最终的result就是你的结果。他被选择的概率就是1/n。
证明思路如下:
第x个数被选择的概率等于x被选择的概率 * (x+1没被选择的概率) * (x+2没有被选择的概率) *......*(N没有被选择的概率) 具体化一下
2被选择的概率 = 1/2 * 2/3 * 3/4 * 4/5 .....* (n-1/n) 我想你知道答案了吧? 对! 是1/n.这样就可以在不知道N的大小的情况下等概率的去选择任意一个对象了!
参考伪代码如下:
i = 0 while more input lines with probability 1.0/++i choice = this input line print choice
由于本文章中的伪代码思路很清晰,所以不必要贴出代码了。
以上转自:http://www.cnblogs.com/fxplove/archive/2012/07/25/2607544.html
现对取样做一个小结:
1.输出是0~n-1范围内的m个随机整数,可重复。
void genRandoms1(int n,int m){ for(int i=0;i<m;i++){ int r=rand()%n; cout<<r<<' '; } cout<<endl; }
2.输出是0~n-1范围内的m个随机整数,不可重复。
//generate random digit in [l,u)
int rand(int l,int u){
//srand(time(0));
return rand()%(u-l)+l;
}
void genRandoms2(int n,int m){ int* a=new int[n]; for(int i=0;i<n;i++){ a[i]=i; } srand(time(0)); for(int i=0;i<m;i++){ int j=rand(i,n); swap(a[i],a[j]); cout<<a[i]<<' '; } cout<<endl; delete []a; }
3.输出是0~n-1范围内的m个有序的随机整数,可重复。
方法一,先生成可重复m个随机整数,然后排序。方法二,将生成的可重复m个随机整数插入到非降序的集合中。
4.输出是0~n-1范围内的m个有序的随机整数,不可重复。
void genRandoms4(int n,int m){ srand(time(0)); for(int i=0;i<n;i++){ if(rand()%(n-i)<m){ cout<<i<<' '; m--; } } cout<<endl; }