由rand7生成rand10以及隨機數生成方法的討論

ZZ 畢達哥拉斯半圓


問題:rand7是一個能生成1-7的整數隨機數。要求利用rand7生成1-10的整數隨機數。可以參看原帖。在lz提示下又找到了更簡潔的方法,同餘循環法,只需要一行代碼!我很淺的探討幾種方法,還需要更深入的學習。感慨一下知識的浩瀚和自己的渺小。

1.組合數學方法
我在帖子裏給出了這樣的方法,這個很簡單的算法,卻似乎不那麼容易被理解。

第1次 1 2 3 4 5 6 7 之中用rand7取一個數
第2次從 2 3 4 5 6 7 8 之中取一個數
第3次從 3 4 5 6 7 8 9 之中取一個數
第4次從 4 5 6 7 8 9 10 之中取一個數
第5次從 5 6 7 8 9 10 1 之中取一個數
第6次從 6 7 8 9 10 1 2 之中取一個數
...
第10次從 10 1 2 3 4 5 6 之中用取一個數

1-10每個數字在上表中都出現了7次,共有70個數字,這樣每個數字被命中的概率爲1/10。

這裏的關鍵詞是"命中".

第11次重新循環,從 1 2 3 4 5 6 7 之中取一個數
第12次從 2 3 4 5 6 7 8 之中取一個數
......

按照這樣的方法,1-10每個數字被命中的概率是均勻分佈。而1-10次的數組,本質上是生成了所有1-10按照次序的一個輪換結構,也就是組合問題中的生成所有排列。(參見Knuth第4卷第2冊(The Art Of Computer Programming, Volume4, Generating All Tuples and Permutations))在經過比較長的時間之後,就可以觀察到均勻分佈。事實上,任何隨機數算法都需要經過比較長的過程才能觀察出他的分佈,而概率分佈,是在一個統計意義上的概念。

由此還可以得出用m個隨機數生成n個隨機數的方法
1. 當m > n的時候,用捨去法,每次n個隨機數,超過這個範圍就捨棄,再來一次。
2. 當m < n的時候,用如上的方法建立m大小的數組,其中的數字在1到n按照次序循環輪換,這樣n*m個循環之後,就可以得到均勻的n個隨機數。

2. 同餘循環法
mingliang1212提示我上面這個方法,實際上等價於(rand7() + i)%10,i從0-9反覆循環,而每次計算的餘數恰好與上面的方法等價。也就是用利用同餘的性質生成所有排列,這真是一個巧妙的想法!於是:
1. i從0到9反覆循環
2. (rand7()+i)%10,產生0-9的隨機數,等效與第一種組合法(只需要把上面表中的1-10改成0-9)
3. 再+1得到1-10的隨機數
優化後只需要一行代碼!本文最後給出了這個代碼。

於是上面用m個隨機數生成n個隨機數的方法的,也有了更簡潔的算法,步驟與此類似就不寫了。感慨一下數學的力量!也謝謝mingliang1212

3. 捨去法

這個題在考試中大概是不讓用捨去法的,因爲他太平凡。但實際上舍入法也很有用,因此還是寫出來,後面還會再提到舍入法。
1. 第一次用rand7取出1到5的隨機數,記爲a
2. 第二次用rand7取出1或2,記爲b
3. 如果b = 1, 則c = a, 如果b = 2,則c = a + b
4. 返回c

4. 利用對rand7的組合計算
有很多網友包括我自己也曾經用(rand7()+rand7()+7+rand7()+14+....)%10的方法類生成隨機數,還有希望用rand7()*rand7()*rand7().....然後再做各種計算來生成。隨後我意識到這樣只能得到近似的均勻

從統計學的角度上看,n個rand7的計算結果是就是具有獨立同分布(iid)的n個隨機變量的聯合分佈,也就是若X1,X2,...Xn~U(1,7)(服從1-7的均勻分佈),而計算之後的結果符合f(x1,x2,...xn)的聯合概率分佈,現在將多元的f(x1,x2,...xn)分佈變換到U(1,10),如果不用篩選法,難度是非常大的。而在本例是不可能的,這是因爲:

1)對rand7的一元運算只能有7種結果,不可能產生10個隨機數
2)現在有二元運算(X)可以是加減乘除或者任何函數任何映射關係,rand7(X)rand7的可能運算方式是7*7種,,n次二元(X)運算後的可能是運算方式是7^(n+1)種,現在要用7^(n+1)種運算過程得到均勻的10種結果,這是不可能的,(因爲7^n不能被10整除),所以只能是近似均勻。

還有人希望用這樣的方法:調用兩次rand7從而生成一個7進制的數,然後轉換成0-49,剛好是50個數的均勻分佈,再取模10。這個方法貌似可行,可是很遺憾的是,這樣生成的7進制0-66對應到10進制是0-48,而不是49,少了一個數。

5. 連續隨機變量的分佈
本題的rand7是一個離散隨機變量,只取1-7的整數。離散變量的缺點是在數學計算上不方便,因此可以轉成連續隨機變量。也就是從rand7生成1-7的連續均勻分佈,獲得1-10的均勻分佈。雖然本題不適用這個方法,但是本題除了考試有用,在實際應用中不會出現,更多的方法是從一種分佈變換到另外一種分佈。

現在的答案很簡單,從幾何的角度上看,我們可以把[a,b]線段上的點按照一對一映射到另一個線段[c,d]上去,只需要做一個線性變換y=(x-a)/(b-a)*(d-c)+c. 那麼,若rand()~U(a,b),則y=(rand()-a)/(b-a)*(d-c)+c~U(c,d),也就是如果rand()是a到b上的均勻分佈,則y=(d-c)(x-a)/(b-a)+c是c到d上的均勻分佈。對於本例rand10=(rand()-1)/6*9+1. 下面是證明,更一般的情況同理可證:


另外有一個重要的定理來表明變換之後的分佈。這可以處理如Y=X^2, Y=e^X等多種變換。定理如下:


這個定理還可以更強一些,f(x)是分段還是也可以,甚至只是一個覆蓋(包括)就可以了。從符合一種分佈的隨機數生成另外一種分佈的隨機數是統計模擬的課題,其中有非常有趣的變換方法,例如,如果X是(0,1)上的均勻分佈,則Y=-a*log(X)是指數分佈。這些內容,參考《統計推斷》,或者更進一步的材料。

6. 再談舍入法
C語言的rand函數,可能是用了線性同餘算法獲得均勻分佈,這類叫直接方法。我118樓還提到了利用變換從均勻分佈獲得其他分佈的方法,這些都叫直接法。捨去法也是非常重要的一類隨機,用來生成各種分佈的隨機數,比如Metropolis算法,比較著名的還有Markov Chain Monte Carlo (MCMC)算法,這類方法可以看成是一個黑盒子,要求在算法內部通過幾次運算很快收斂到一種概率分佈,然後返回一個隨機數。參見Casella & Berger統計推斷(Statistical Inference)以及Kunth第2卷Seminumerical Algorithms, Random Numbers.

7. 本題的代碼
//1. 組合數學法的實現
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <time.h>

static int x[7] = {1, 2, 3, 4, 5, 6, 7}; //輪轉數組,初始爲1-7

//每調用1次,實現一次1-10的輪轉
void shift_array()
{
    memcpy(x, x + 1, 6 * sizeof(int));
    x[6] = (*x + 6) % 10;
    if (!(x[6])) x[6] = 10; //這裏可能有更好的寫法
}

//返回1-10的隨機數,不要忘記先調用srand()
int rand10()
{
    shift_array(); //輪轉一次
    return x[rand() % 7]; //隨機返回數組內的一個數字
}

void main(void)
{
    unsigned int result[10] = {0};
    int k; srand((unsigned int)time(0)); //設定種子

    for (k = 0; k < 100000; k++)
        result[rand10() - 1]++;
        
    for (k = 0; k < 10; k++)
        printf("%d : %.05f\n", k + 1, (double)result[k]gloabl_i/100000);

}


//2. 同餘循環法,本方法與上面的區別只在於實現循環方式的不同  
static unsigned int gi = 0;
//a = rand() % 7 + 1 :生成1-7的隨機數
//b = i++ % 10 : i從0-9循環
//(a + b) % 10,循環產生0-9的隨機數,詳見第一種方法的論述
//這個結果再+1,生成1-10隨機數
//再提醒不要忘記先調用srand()
int new_rand10()
{
   return (((rand() % 7 + 1) + (gi++ % 10)) % 10) + 1;
}


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