(12)取樣問題

一、簡介

    問題描述:程序的輸入包含兩個整數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個數中仍然以等概率抽取其中的一個。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章