等概率无重复的从n个数中选取m个数

问题描述:程序的输入包含两个整数m和n,其中m<n。输出是0~n-1范围内的m个随机整数,要求:每个数选择出现的概率相等,且按序输出。

学习过概率统计的同学应该都知道每一个数字被抽取的概率都应该为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--
int gen(int m,int n)
{
    int i, select = m,remaining = n;
    for(i=0;i<n;i++) {
        if(rand() % remaining <select) {
            printf("%d\n",i);
            select--;
        }
        remaining--;
    }
    return 0;
}

可以优化为这样:

int genknuth(int m,int n)
{
    int i;
    for(i=0;i<n;i++)
        if(rand()%(n -i) < m) {
            printf("%d\n",i);
            m--;
        }
    return 0;
}

代码很精简,代码遵守的规则应该是要从r个剩余的整数中选出s个,我们以概率s/r选择下一个数。这个概率的选择方式和我们上面证明的是一样的。所以在程序结束的时候一定会打印出m个数字,且每一个数字的被选择概率相同,为m/n。
首先是一个循环,这个循环确保了输出的数是不重复的,因为每次的i都不一样

其次是m个数,在每次循环中都会用rand()%(n-i)<m来判断这个数是否小于m,如果符合条件则m减1,直到为0,说明已经取到m个数了

再次是如何保证这m个数是等概率取到的

在第一次循环中i=0, n-i=n, 则随机数生成的是0-n-1之间的随机数,那么此刻0被取到的概率为 m/n-1

在第二次循环中i=1,n-i=n-1,则随机数生成的是0-n-2之间的随机数,这时1被取到的概率就和上一次循环中0有没有取到有关系了。假设在上一次循环中,没有取,则这次取到的1的概率为 m/n-2;假设上一次循环中,已经取到了,那么这次取到1的概率为m-1/n-2,所以总体上这次被取到的概率为 (1-m/n-1)*(m/n-2)+(m/n-1)*(m-1/n-2),最后通分合并之后的结果为m/n-1和第一次的概率一样的

同理,在第i次循环中,i被取上的概率也为m/n-1

2、等概率顺序取数据的第二种方法,可以使用集合的思想

由于集合元素不重复,如果按等概率选择一个随机数,不在集合中就把它插入,反之直接抛弃,直到集合元素个数达到m个,同样可以满足要求,并且用C++的STL很容易实现:

void gensets(int m,int n) {
    set<int> S;
    while(S.size() < m)
        S.insert(rand()%n);
    set<int>::iterator i;
    for(i = S.begin();i!=S.end();++i)
        cout<<*i<<"\n";
}

这个算法的主要问题是,如果抛弃已存在的元素的次数过多,相当于多次产生随机数并进行集合操作,性能将明显下降。比如当n=100而m=99,取第99个元素时,算法“闭着眼睛乱猜整数,直到偶然碰上正确的那个为止”(《编程珠玑(续)》,13.1节)。虽然这种情况会在“从一般到特殊”提供解决方案,但下面的Floyd算法明显规避了产生随机数超过m次的问题。

  习题12.9提供了一种基于STL集合的随机数取样方法,可以在最坏情况下也只产生m个随机数:限定当前从中取值的区间的大小,每当产生重复的随机数,就把这一次迭代时不会产生的第一个随机数拿来替换。

int genfloyd(int m,int n){
    set<int> S;
    set<int>::iterator i;
    for(int j = n-m; j<n;j++) {
        int t = rand()%(j+1);
        if(S.find(t) == S.end())
            S.insert(t);
        else
            S.insert(j);
    }
    for(i=S.begin();i!=S.end();++i)
        cout<<*i<<"\n";
}

从“打乱顺序”出发

  这是个来源于实际的想法:将所有n个元素打乱,取出前m个。更快的做法是,打乱前m个即可。对应的C++代码如下:

int genshuf(int m,int n)
{
    int i,j;
    int *x = new int[n];
    for(i = 0;i<n;i++)
        x[i] = i;
    for(i = 0;i<m;i++) {
        j = randint(i,n-1);
        //randint产生i到n-1之间的随机数
        int t = x[i];x[i] = x[j];x[j] = t;
    }
    //sort(x,x+m);
    //sort是为了按序输出
    for(i=0;i<m;i++)
        cout<<x[i]<<"\n";
}    

当然了,这个题目还有其他的解法,这是在网上看到的其他的解法。他们将这样的问题抽象的定义为蓄水池抽样问题。其思路是这样的,先把前k个数放入蓄水池中,对第k+1,我们以k/(k+1)的概率决定是否要把它换入蓄水池,换入时我们可以随机挑选一个作为替换位置,这样一直到样本空间N遍历完,最后蓄水池中留下的就是结果。这样的方法得到的结果也是正确的,且每一个数字被选择的概率也是k/n。

这个问题其实还可以扩展一下:

  如何从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

  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

参考http://blog.csdn.net/hackbuteer1/article/details/7971328

时间: 2024-10-05 07:21:51

等概率无重复的从n个数中选取m个数的相关文章

C++从多n个数中选取m个数的组合

1 //start 是从哪个开始取, picked代表已经取了多少个数 2 //process和data是全局变量数组 3 //语言说明比较难,我举个例子吧 4 //从[ 1, 2, 3, 4 ]中选取 2 个数 5 //然后可以依次得到 6 // 1 2 7 // 1 3 8 // 1 4 9 // 2 3 10 // 2 4 11 // 3 4 12 void combination(int start, int picked) 13 { 14 if (picked == m) { 15 f

小易邀请你玩一个数字游戏,小易给你一系列的整数。你们俩使用这些整数玩游戏。每次小易会任意说一个数字出来,然后你需要从这一系列数字中选取一部分出来让它们的和等于小易所说的数字。 例如: 如果{2,1,2,7}是你有的一系列数,小易说的数字是11.你可以得到方案2+2+7 = 11.如果顽皮的小易想坑你,他说的数字是6,那么你没有办法拼凑出和为6 现在小易给你n个数,让你找出无法从n个数中选取部分求和

小易邀请你玩一个数字游戏,小易给你一系列的整数.你们俩使用这些整数玩游戏.每次小易会任意说一个数字出来,然后你需要从这一系列数字中选取一部分出来让它们的和等于小易所说的数字. 例如: 如果{2,1,2,7}是你有的一系列数,小易说的数字是11.你可以得到方案2+2+7 = 11.如果顽皮的小易想坑你,他说的数字是6,那么你没有办法拼凑出和为6 现在小易给你n个数,让你找出无法从n个数中选取部分求和的数字中的最小数. 输入描述: 输入第一行为数字个数n (n ≤ 20) 第二行为n个数xi (1

查找一个数中1的个数

for (t=0;m;m&=m-1) t++; 查找一个数中1的个数

排列组合问题:n个数中取k个数

/************************************有0~n-1共n个数,从其中任取k个数,*已知这k个数的和能被n整除,求这样的*k个数的组合的个数sum,*输入:n,k*输出:符合条件的个数sum************************************/ #include <malloc.h>#include <iostream>#include <stdio.h>using namespace std; int k, *a,

Java 实现m个数全排列组合以及从M中选取N个数(有序)

(1)全排列组合的递归规律: 集合s的全排列组合 all(s)=n+all(s-n);其中n为已经取出的集合 以集合 s={1,2,3}为例,则s的全排列组合为all(s)={1}+all({2,3});其中n={1},s-n={2,3} 通过以上例子,我们可以知道上述算法可以用递归来解决. 我们取极端情况,如果集合s为空,那么说明不需要再进行递归. 全排列组合,如果集合有4个元素,则全排列组合的个数为 A(4,4)=4*3*2*1=24种,代码如下: package dataStructer;

(hdu step 4.3.3)Sum It Up(从n个数中选出m个数让他们的和达到指定和targetSum,输出所有的合法序列)

题目: Sum It Up Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) Total Submission(s): 140 Accepted Submission(s): 73   Problem Description Given a specified total t and a list of n integers, find all distinct sums using

编程实现:输入n个数,在这n个数中查找某个数

/*要求: 写四个函数 void input(float arr[], int n) void output(float arr[], int n) void bubblesort(float arr[], int n) int search(float arr[], int n, float num) */ #include <stdio.h> #include <stdlib.h> #define MAXN 1000 float a[MAXN]; void input(floa

给定n个整数和一个整数C,问n个数中那几个数的和等于C。

void function(vector<int> vecS,vector<int> vecD,vector< vector<int> > & vecGroup,int iSum) { for(vector<int>::iterator itr = vecS.begin(); itr != vecS.end(); ++itr) { if(iSum - *itr == 0) { vecD.push_back(*itr); // 去掉重复组合

从给定的N个正数中选取若干个数之和为M

#include <iostream> #include <list> using namespace std; void find_seq(int sum, int index, int * value, list<int> & seq) { if(sum <= 0 || index < 0) return; if(sum == value[index]) { printf("%d ", value[index]); for(l