现在的位置: 首页 > 综合 > 正文

(12)取样问题

2018年04月01日 ⁄ 综合 ⁄ 共 2857字 ⁄ 字号 评论关闭

一、简介

    问题描述:程序的输入包含两个整数m和n,其中m<n。输出是0~n-1范围内m个随机整数的有序列表,不允许重复。从概率的角度说,我们希望得到没有重复的有序选择,其中每个选择出现的概率相等。

    (1)一般情况下,如果要从r个剩余的整数中选出s个,我们以s/r的概率来选择下一个数。如下伪代码所示:

    select = m  
    remaining = n  
    for i = [0, n)  
      if (bigrand() % remaining) < select  
          print i  
          select—  
      remaining--  

    下面给出一个C++的实现:

void getknuth(int m, int n)
{
    for(int i = 0; i < n; i ++)
    {
        //select m of remaining n - i
        if(bigrand() % (n - i) < m)
        {
            cout << i << " ";
            m--;
        }
    }
    cout << endl;
}

    (3)另一个解决方法是在一个初始为空的集合里插入随机整数,直到个数足够。伪代码如下:

initialize set S to empty  
  size = 0  
  while size < m do  
      t = bigrand() % n  
      if t is not in S  
        insert t into S  
        size++  
  print the elements of S in sorted order

    利用C++的标准模板库实现如下所示:

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

    (3)生成随机整数的有序子集的另一种方法时把包含整数0~n-1的数组顺序打乱,然后把前m个元素排序输出即可。

for i = [0, n)  
   swap(i, randint(i, n-1) )   // randint(i, j)从i...j范围内均匀选择的随机整数的函数

    其实在这个问题中,我们只需要打乱数组的前m个元素,对应的C++代码如下所示:

void 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);
        int t = x[i]; 
        x[i] = x[j]; 
        x[j] = t;
    }
    //排序数组中的前m个元素
    sort(x, x+m);
    for (i = 0; i < m; i++)
    {
        cout << x[i] << " ";
    }
    cout << endl;
}

二、原理

    (1)正确理解所遇到的问题,即将问题抽象成能用数学或逻辑表示的命题;

    (2)提炼出抽象问题,有助于我们把解决方案应用到其他问题中;

    (3)考虑尽可能多的解法,多思考,然后会发觉写代码的时间会很短;

    (4)实现一种解决方案。

三、习题

    1、C库函数rand()通常返回约15个随机位,使用该函数实现函数bigrand()和randint(l,u),前者返回至少30个随机位,后者返回[l,u]范围内的一个随机整数,解答如下:

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

    2、当m接近于n时,就集合的算法生成的很多随机数都要丢弃,因为他们之前已经存在于集合中了。能否给出一个算法,使得即使在随坏情况下也能使用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; 
}

    3、如何从n个对象中随机选择一个?具体说来,如何在实现不知道文本文件行数的情况下读取该文件,从中随机选择并输出一行?

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

    i = 0
    while more input lines
         with probability 1.0/++i
             choice = 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个数中仍然以等概率抽取其中的一个。

抱歉!评论已关闭.