【编程珠玑】第十二章 取样问题

一,概述

        问题描述:如何生成0~n-1内的m个随机整数(不重复

               需求:按序输出,并且保证每个子集被选中的可能性相等。

        1)给出下面代码

#include "stdio.h"
#include "stdlib.h"
#include "time.h"

void getRandNumber(int m,int n)//在0 -- n-1 中挑选m个 随机数 
{
	srand(time(NULL));//这个很关键 
	
	int i,j;
	for(i=0;i<n;++i)
	{		
		if( rand()%(n-i) < m)
		{
			printf("%d  ",i);
			m--;
		}
	} 
	
}
int main()
{
    getRandNumber(5,10);
	return 0;
}


        其中for循环保证 按序输出,rand()%(n-i) 保证输出概率符合要求。

     算法时间复杂度 O(n)

         2)非常规求法:

               将n个数写到大小相等的纸片上,摇匀。然后取出m个纸片,按序输出m个纸片


        3)解决算法时间复杂度问题,提出以下优化方案

              给定一个集合S,每次插入一个元素。插入之前检查S中个数是否达到m,且随机数在不在m中。

#include <iostream>
#include <set>
using namespace std;

void getSet(int m,int n)//在0 -- n-1 中挑选m个 随机数 
{
	srand(time(NULL));//这个很关键 
	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<<" "; 
}
int main()
{
    getSet(5,10);
	return 0;
}


          C++模板插入操作在O(logm)时间内完成,而遍历集合需要O(m)时间。所以完整程序需要O(mlogm)时间
         

         4)生成随机数的另一种方式:把包含0 - n-1的数组顺序打乱,然后把前m个元素输出。

               更好的方式是,我们只需要打乱前m个元素,然后排序输出。

                或者生成大于n个1 -  n范围的随机数,然后去掉重复的,输出前面的m个元素

#include <iostream>
#include <algorithm>  
using namespace std;

void sort(int a[],int m)
{
    for(int i=1;i<m;i++)
		for(int j=i;j>0&&a[j-1]>a[j];j--)	   	    	   		
   	   			  swap(a[j-1],a[j]);
}

void getShuf(int m,int n)//在0 -- n-1 中挑选m个 随机数 
{
    srand(time(NULL));//这个很关键 
	int i,j;
	int a[n];
	for(int i=0;i<n;++i)
		a[i]=i;
	for(int i=0;i<m;++i)
	{
		swap(a[i],a[rand()%(n-i)]);
	}
	sort(a,m);
	
	for(i=0;i<m;++i)
		cout<<a[i]<<" ";
}
int main()
{
    getShuf(5,10);
	return 0;
}


二,习题

       1)

int bigrand()
{
      return RAND_MAX*rand() + rand();
}
int region(int l, int u)  //[l, u]
{
     ++u;
      return l + rand() % (u - l);


       2)选择的m子集的概率相等,如何做?

             在1 - n范围内随机选择一个数,然后其后的m-1 个数为所选择的子集(有可能到头,然后从0开始)


       3)

当m < n/2时,

总共试了k次,则前面k-1次找到的数都是在集合中,那么只有第k次不在里面,那么概率

p = (m/n)^(k-1) * (n-m)/n

那么期望是 连加 k = 1至无穷大,根据二项式分布可知,期望等于

n/(n-m) < 2

从而可知得证

       4)参考算法导论中文版64页。

             搜集n张随机赠送的赠券,需要多少次? nlogn次


       7)先输出再递归,改成先递归再输出

       9)给出一个算法,在最坏情况下只使用m个随机数。而不用丢弃已经生成的随机数

#include <iostream>
#include <set> 
using namespace std;
void getSet(int m,int n)//在0 -- n-1 中挑选m个 随机数 
{
    srand(time(NULL));//这个很关键 
    set<int> S;
    for(int i=n-m;i<n;++i)
    {
        int t=rand()%(i+1);
        if(S.find(t) == S.end())
                S.insert(t);
        else
                S.insert(i);
    } 
    set<int>::iterator j;
    for(j=S.begin();
         j!=S.end();++j)
    cout<<*j<<" "; 
}
int main()
{ 
    getSet(5,10);
    return 0;
} 


       10)问题:如何随机从n个对象中选择一个对象,这n个对象是按序排列的,但是在此之前你并不知道n的值?

             具体些说,在事先并不知道行数的情况下,如何读一个文本文件,随机选择并输出一行?            

       解答:我们总是选择第一行,并使用二分之一的概率选择第二行,使用三分之一的概率选择第三行,以此类推。在该过程结束的时候,每一行具有相同的选中概率(1/n,其中n是文件的总行数):      

              i = 0    while more input lineswith probability 1.0/++ichoice = this input line  //如果前面做了选择,并不会break,而是直到最后一个为止。print choice          

              这里比较有些疑惑的是第一行:总是选第一行 为什么概率还是1/n?

            概率=1*(1/2)*(2/3)*(3/4)……(n-1/n) =1/n

            证明:当做第i步选择(选择第i行)时,选择该行的概率为1/i,则不选择的概率为(i-1)/i对于一篇有n行的文档,现需证明最终选定第i行的概率为1/n。

                       当最终选择第i行,前(i-1)步的选择对最终结果不会产生影响,第i步选择的概率为1/i,即选择第i行,第(i+1~n)步中均采取不选择的动作,即对于任意j(i+1<=j<=n),当前步的概率为(j-1)/j,那么最终的概率为:(1/i)*((i)/(i+1))*...*((n-1)/n) = 1/n

                     以一篇只有6行的文档为例,最终选择第2行的概率为:1/2*(2/3)*(3/4)*(4/5)*(5/6) = 1/6


扩展:原问题可简化为:如何从n个有序对象中等概率地任意抽取1个,简记为sample(n,1),其中n未知;

           若将该问题改为:如何从n个有序对象中等概率地任意抽取m个,简记为sample(n,m),其中n未知;

分析:若n已知,sample(n,m)是普通的抽样问题;当n未知时,可否根据上述算法进行相应的转化求解?

解决方案:将sample(n,m)问题转化为m个sample(n*,1)问题,更具体一点是,转化为sample(n,1);sample(n-1,1);sample(n-2,1)....;sample(n-m+1,1)问题。仍然以一篇6行文档为例,任取其中2行,做法如下:第一遍,以如下概率选中一行:1(1)   2(1/2)  3(1/3)  4(1/4)  5(1/5)  6(1/6)假设选中第2行,接着概率修改如下:3(1)  4(1/2)  5(1/3)  6(1/4)  1(1/5)


【说明】:当选中第2行,从第3行开始修改概率,并将第2行排除在外,继续扫描,这样能保证在剩下的5个数中仍然以等概率抽取其中的一个。

      11)这个题看似很复杂,其实很简单。只需要关注1,2,3如何输出即可。要想获胜,只需要1,2先输出,三个数的全排列中这种情况有2种。所以获胜概率为2/6

=1/3

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值