已知隨機函數RandN(),構造隨機函數RandM()

1. 題目

已知一個能產生 [0, n) 的隨機數的函數,設計一個能產生 [0, m)的隨機數的函數。

要產生 [0, m) 的隨機數,首先要確保輸出 0、1、2、…、m-1 的概率相同。

2. 驗證函數 RandN

其中,RandN 表示能產生 [0, n) 的隨機數的函數,如下所示:

unsigned int RandN(unsigned int N)
{
    return 0 == N ? 0 : rand() % N;
}

此處令 N = 10,進行 1 000 000 次重複實驗,統計 0,1,2,3,4,5,6,7,8,9出現的頻度。如下圖所示:

這裏寫圖片描述

0,1,2,3,4,5,6,7,8,9出現的頻度近似相等,可以理解爲等概率分佈。

3. 設計函數 RandM M 小於等於 N

如果 M = N 很簡單,就是同一個函數;

如果 M < N,可以考慮使用截斷的方式,因爲 RandN 產生的 [0, 1, 2, 3, …, N-1] 是等概率的,隨機的,因此 [0, 1, 2, 3, …, M-1] 也就等概率的,所以最簡單的方式就是:如果 RandN() 輸出小於 M,則直接作爲 RandM() 的輸出,否則繼續生成,直到小於 M。

上述方法又一個欠缺,如果 N 較大,比如 N =1000,M較小,M = 10,那麼從統計的角度考慮,要執行 100 次 RandN() 才能成功輸出一個 RandM(),也就是所成功的概率是:MN ,效率太低。

針對上述方案的解決方案如下:

取一個整數 K,令 KN ,並且滿足 K=aMN(a+1)M=K+M ,把 K 作爲結尾閾值,然後的到的結果對 K 取餘即可。例如:令 N = 27,M=13,K =26,如果直接把 M 作爲截尾閾值,則成功的概率爲 1327 ;如果把 K 作爲截尾閾值,則成功的概率爲 2627

代碼如下:

unsigned int RandmUsingRandn(unsigned int M, unsigned int N, unsigned int(*func)(unsigned int))
{
    if (0 == M || 0 == N)
    {
        return 0;
    }

    if (M <= N)
    {
        // 取小於 N 的 M 的最大倍數,提高效率
        int iNDivM = N / M;
        int iTimesM = iNDivM * M;

        int iRandN = func(N); // 截尾能保證前面的概率依然相同
        while (iRandN >= iTimesM)
        {
            iRandN = func(N);
        }
        return iRandN % M;
    }
}

例如:令 N = 27,M=13,K =26,把 K 作爲截尾閾值,進行 1 300 000 次重複實驗,統計 0,1,2,3,4,5,6,7,8,9,10,11,12 出現的頻度。如下圖所示:

這裏寫圖片描述

0,1,2,3,4,5,6,7,8,9,10,11,12 出現的頻度近似相等,可以理解爲等概率分佈。

4. 設計函數 RandM M大於等於N

由於 M > N,所以不能使用簡單的截斷方法了,但是如果能產生一個大於 M 的隨機數,那麼就可以借用上述 M < N的情況。很幸運的是,有 RandN(N) 可以很方便的設計 RandN2(N*N)。

N1=RandN(N) ,則:

N1=[0,1,2,3,...,(N1)]1N

N2=RandN(N)N ,則:

N2=[0,N,2N,3N,...,N(N1)]1N

則,N3=N1+N2 可表示爲:

N3=0N...N(N1)1N+1N(N1)+1.........N12N1N211N2

上述式子的含義就是任意從 N1中取一個數,任意從 N2 中取一個數,他們的和不會有重複,正好等於[0,1,2,...,N21] 中的一個,並且出現每個數的概率爲:1N2

所以下面的函數就可以很簡單的生成一個 [0, N*N)之間的隨機數:

int iRandN2 = RandN(N) * N + RandN(N);

此時,就可以借用 M < N 的情況,有如下代碼:

unsigned int RandmUsingRandn(unsigned int M, unsigned int N, unsigned int(*func)(unsigned int))
{
    if (0 == M || 0 == N)
    {
        return 0;
    }

    if (M > N)
    {
        // 取小於 N^2 的 M 的最大倍數,提高效率
        int iN2DivM = N * N / M;
        int iTimesM = iN2DivM * M;

        // 有 Rand(N * N) = Rand(N) * N + Rand(N)
        // 同理可以推出 Rand(N^3), Rand(N^4)
        int iRandN2 = func(N) * N + func(N); // 截尾能保證前面的概率依然相同
        while (iRandN2 >= iTimesM)
        {
            iRandN2 = func(N) * N + func(N);
        }

        return iRandN2 % M;
    }
}

例如:令 N = 7,M=15,K = 45,把 K = 45 作爲截尾閾值,進行 1 500 000 次重複實驗,統計 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14 出現的頻度。如下圖所示:

這裏寫圖片描述

0,1,2,3,4,5,6,7,8,9,10,11,12,13,14 出現的頻度近似相等,可以理解爲等概率分佈。

注意:上述部份仍然有問題,因爲我們人爲假設 N2M ,如果 N2M 呢,此時就需要產生更大的隨機數了。

由 RandN(N) 可以很簡單的推出 RandN2(N*N),同樣,也可以推出 RandN3(N*N*N)等等。

N1=RandN(N) ,則:

N1=[0,1,2,3,...,(N1)]1N

N2=RandN(N)N ,則:

N2=[0,N,2N,3N,...,N(N1)]1N

N3=RandN(N)NN ,則:

N3=[0,N2,2N2,3N2,...,N2(N1)]1N

N4=N3+N2+N1 ,則

N4=[0,1,2,3,...,N31]1N3

上述推導其實可以看成一個N進制的數字:
1. N1 表示最低位,其中N1=0,1,2,N1
2. N2 表示次低位,其中N2=0,N,2N,N(N1)
3. N3 表示次次位,其中N3=0,N2,2N2,N2(N1)

所以N4=N3+N2+N1=0,1,2,...,N31

根據上述規律,完全可以由 RandN() 來產生 [0, n),[0, n^2), [0, n^3), [0, n^4), [0, n^5),。。。

從上面的分析似乎我們總可以找到一個能產生大於 M 的隨機數,然而,我們沒有考慮另外一種情況,如果M 和 N 都較大時,N^2或者N^3就有可能超過整數的範圍現在,此時,可能需要需要使用 unsigned long long int作爲中間變量了。

5. 參考

《程序員面試筆試寶典(2014版)》7.15.12

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