如何用一個1-8隨機生成器製作一個1-7隨機數生成器?

假設 rand8() 可以生成自然數 1 到 8,

現在我要用 rand8() 實現一個 rand7(),

那麼如何才能最高效(假設 rand8() 成本比較大)?


其實最簡單可行的方法就是重複調用rand8(),如果抽到8就重抽,直到結果小於8為止。這個方法看起來很簡單,但其實是有道理的。它利用了Rejection sampling技術,這是一種常用的採樣演算法。

如果寫成C代碼的話,大概是這樣:

int rand7(){
int a;
while ( (a=rand8())==8 );
return a;
}

題注還提到了rand8()的開銷比較大,那我們就來分析一下這種基於Rejection sampling方法構造的rand7()的效率。

設成功執行一次rand7()所需要的rand8()的次數為N,N是個隨機變數,服從參數為7/8的幾何分布,其數學期望E[N]=8/7=1.143,方差D[N]=8/49=0.163,標準差大約是0.404。

從這個結果來看,上述rand7()的平均開銷和波動都不算大。執行超過五次rand8()的概率約為十萬分之三,而超過十次的概率不足十億分之一。十億分之一什麼概念?假如你有1GB的數據,你希望針對其中的每一個位元組做一個和rand7有關的處理,那麼你把整個資料庫處理完,估計也就能碰上一次倒霉需要抽十次rand8()。而我認為這種程度的「倒霉」是完全可以接受的。

問題本身就算是答完了,再補充一些內容:

第一、關於樓主提到的「最高效率」,我想只能從資訊理論的角度給出一個界限。一次rand7的演算法所能提供的信息量是log7,而一次rand8是log8,所以無論怎樣,平均一次rand7所需要執行的rand8的次數至少為log7/log8=0.9358。我們的演算法需要1.143次,效率也還可以。

理論上能接近這個界限的方法大概就是王贇 Maigo基於七、八進位小數轉換的方法,但該方法需要生成無窮多位八進位小數才能保證完全準確的rand7()。實際應用中,為了提高rand7()的精度,也不得不多產生一些八進位小數。當所需的rand7()的樣本較少時,效率也不一定很高。

第二、簡要介紹一下Rejection sampling。

Rejection Sampling解決的是這樣一類問題:我們希望對目標分布f(x)進行採樣,但直接構造這樣一個隨機數生成器可能比較困難。我們就先構造一個簡單的隨機數生成器,使之能產生與f(x)近似的分布g(x),然後再利用如下方法就可以得到f(x)的樣本:

1、對g(x)採樣,得到樣本x

2、對0~1之間的均勻分布採樣,得到樣本a,如果a<f(x)/(Mg(x)),則接受這個x,否則重複1操作。(這一步其實就是以概率f(x)/(Mg(x))接受x)。這裡M是一個常數,要求對於任意的x,有f(x)/(Mg(x))le 1,證明也不太複雜,如果記A為隨機事件「接納樣本」,則經過篩選的x分布其實就是條件概率p(x|A),簡單推導一下p(x|A)propto p(x) p(A|x)=g(x)*f(x)/(Mg(x))propto f(x)。因此接受的樣本x服從分布f(x)

把這個方法應用到rand7()問題上就得到了開始的演算法。

1、調用rand8()進行採樣,得到一個整數x

2、如果x等於8,則接受概率為0,扔掉這個樣本,如果x等於7,則接受概率為1,保留這個樣本。

第三、關於多次採樣時樣本間的獨立性問題。這是隨機數生成器的一個重要指標。基於Rejection Sampling的這個演算法可以保證每次產生的隨機數是獨立的,因為新樣本的產生完全不依賴現有的結果。有人在評論里問我,如果產生的樣本等於8則取上一次輸出的結果作為本次結果,可以不可以。回答是不可以,雖然這種方案可以保證每一個樣本的邊緣分布都是1到7的均勻分布,但是這種方法產生的隨機數樣本之間是有相關性的。假設本次生成的樣本是1,考察下一個樣本還是1的條件概率,顯然執行rand8()的結果無論是1還是8,輸出的結果都是1,因此條件概率等於2/8。而獨立性要求這個值等於1/7。所以這種方法不能滿足獨立性的要求。而且這個方法還有一個問題,需要先執行一次rand7(),拿到第一個樣本,才能夠保證後續樣本的邊緣分布都是1到7的均勻分布。

第四、關於能不能保證演算法在有限次內結束。這個演算法在有限次內結束的概率是1,需要執行無窮多次rand7()的概率是0。但概率是0並不代表不會發生,樣本空間里是有這種情況的。而能保證有限次內結束的非近似演算法,我想是不存在的。假設演算法在M次內結束,那麼樣本空間就有8的M次方個基本元素,每個基本元素的概率相同。而這個數字又不能被7整除,怕是沒辦法了。我想這就好比是用尺規三等分任意角,如果能無限次操作的話,也是可以做到的,因為1/3=1/4+1/16+1/64+1/256...。而對於這個問題如果採用Rejection Sampling方法,則是1/7=1/8+1/64+1/512+1/4096...這麼搞出來個1/7。


如果一定要是嚴格等概率分布的話,本質上都要靠出現8時重新產生隨機數直到產生1-7之間的,不可能保證在給定步數內結束,因為產生n次1-8隨機數的sigma域里沒有概率為1/7的事件。

如果要近似的話,可以在連續產生m個8後強行輸出1,這樣誤差是(1/8)的m次方。


為敘述方便,設rand8等概率地產生0~7的隨機整數,我們想要用它製造一個rand7,等概率地產生0~6的隨機整數。

效率最高的方法如下:用rand8反覆生成0~7的隨機數,把它們連起來看作一個八進位純小數。把這個純小數轉換成七進位,以它的各位數作為rand7的輸出。每能確定七進位小數的一位數,就輸出一位數。

從長遠來看,這種方法輸出一位0~6的隨機數,需要消耗log(7)/log(8)位0~7的隨機數,信息利用率為100%。之所以能做到這一點,是因為產生上一位隨機數時沒有用完的信息,被用在了下一位隨機數的產生中。

上面的分析沒有考慮運算誤差。輸出一位數所需的時間沒有上限,但無限運行下去的概率為0。這也是已有答案中的各種方法的共同點。


這是幾年前大公司特別喜歡出的筆試/面試題,不過他們都喜歡反過來,用小的生成大的,比這個稍微複雜一點點,我當時還寫了一篇博客。

關於面試中經常出現的根據一個隨機數構造另外的隨機數的解法

其實從大的隨機數生成小的隨機數是簡單的,一般面試會反過來,從小的隨機數生成大的,會稍微複雜一點點,需要對原有的random進行想加或者相乘,再推導出新的random,還要保證生成新的隨機各個結果的概率相同。

我直接上代碼:(親測正確,有什麼不懂的可以看博客)

#include &
#include &

using namespace std;

int Rand8()
{
int n =1 + rand()%8;
return n;
}
int Rand7()
{
int n;
do
{
n = Rand8();
} while (n&>7);

return 1+n%7;
}
int main()
{
int result[] = {0,0,0,0,0,0,0};
int before[] = {0,0,0,0,0,0,0,0};
for (int i = 0 ; i &< 10000; i++) { before[Rand8()-1] += 1; } for(int i = 0; i&< 8; i++) { cout&<&< "num:"&<&

結果:

一萬次實驗:Rand8,次數都在1200多左右,random7次數都在1400左右。


先說結論:

@崔恩馳

個人覺得,在8的 n次方的空間上,大概不存在對1~7的均勻映射。

(見評論區)

還有 @小科的答案:

如果一定要是嚴格等概率分布的話,本質上都要靠出現8時重新產生隨機數直到產生1-7之間的,不可能保證在給定步數內結束,因為產生n次1-8隨機數的sigma域里沒有概率為1/7的事件。

從信息的角度來說,我們每次使用rand8(),獲得的信息量都是3bit,或者說8種狀態中的一種,那麼n次的狀態總量是8的n次方,或者說3n bit。以這樣的一個空間,大概沒有什麼優雅的方案可以均勻的映射到一個每次都是7種狀態的空間的。

如果我們將信息視作一進位,那麼信息利用方式就只能對7取餘數,則可以完全的利用所有的信息。簡而言之,演算法如下:

int rand7()
{
static int sum;
sum+=rand8();
if(sum&>7)sum-=7;
return (sum%7 + 1);
}

此種演算法有知友驗證了從概率來說,7個數字產生概率都是相同的,但是數字之間的獨立性卻會有很大問題。

這個簡單來說,假設當前sum=0,那麼下次rand7返回值中,1會是2/8,其他是1/8。數據的獨立性會有問題。但是每個數字的概率是相同的——其實如果你依次返回1-7,概率也是相同的,但顯然這不能算是啥隨機數。

如果要獨立性好,但對性能沒有要求,則可以用如下的方法:

int rand7()
{
int temp=rand8();
if(temp==8)return rand7();
else return temp;
}

這個很好理解,如果等於8則再取一次。但是理論上存在一長串8,導致效率低下,並且原理上是可能會有無限個連續的8出現以至於沒有結果。

所以我們可以結合上面兩種方案:

int rand7()
{
static int sum;
int temp=rand8();

sum+=rand8();
if(sum&>7)sum-=7;
if(temp==8)return (sum%7 + 1);
else return temp;
}

這個方法是,一方面記錄總和,如果rand8返回的是8,那利用sum對7取餘數來得到隨機值。這個方案的隨機性我還沒去驗證,不過至少可以避免上一種方法的極端情況,但是還是犧牲了隨機性的。

更高級的解法大概是要等數學專業的人來做解答了,大概這應該可以對應到數論或者某個數學現成的猜想或者命題。

如果你觀察一下,會發現存在「if(之前的情況)輸出現在的數字」這種語句的,幾乎不可能獨立,因為if本來就是一種關聯。同樣,求和為什麼用static?static本身就意味著和之前的數據有關係,所以也是簡單就能判斷數據之間獨立性是不完全滿足隨機數的要求的。


@Eidosper 的求和只寫了一次,應該是typo吧……

實際上 @D Flip Flop的方法(累加n次並對7取余)和 @小科的方法(取n次,若n次都為8則強行輸出1)是等價的。

證明:顯然。

……這個逼是我掉的我裝完就走……

證明:因為每次累加都使得有一個數的概率比其它6個數高一點,第1次累加後這個數為1,第2次累加後這個數為2,依此類推。第一次累加時高出的部分是1/8,由歸納法,若第n-1次時高出的部分是(1/8)^{n-1},即

有6個數的概率為(1-(1/8)^{n-1})/7,有1個數的概率為(1-(1/8)^{n-1})/7+(1/8)^{n-1}

那麼第n次後高出的部分為

{(1/4)[(1-(1/8)^{n-1})/7+(1/8)^{n-1}]+(6/8)(1-(1/8)^{n-1})/7}

-{(1/8)[(1-(1/8)^{n-1})/7+(1/8)^{n-1}]+(5/8+1/4)(1-(1/8)^{n-1})/7}

=(1/8)(1/8)^{n-1}=(1/8)^n

這恰好就是「出現n次8後強行輸出這個數」這一做法給這個數所增加的概率。

既然等價那收斂性就沒問題了,因為 @小科 的做法誤差顯然是收斂的O(8^{-n})


似乎並不能通過有限個rand(8) 獲得一個rand(7) 因為8^n不是7的倍數......


對於給定randN()([0, N-1]),求randM()的問題,可以分兩種情況;

1. N &> M,這種情況很簡單:

while ((r = randN()) &>= M) {}

當然,這樣對於N比M大得多時這樣會導致必須調用很多次randN函數,這時可以利用下面的代碼:

int k = N / M;

while (true) {
int tmp = randN();
if (tmp &> k * M) continue;
return tmp % M;
}

就相當於對取M的同餘數,對於多出來的部分直接丟掉。

2. M &> N, 對這種情況,可以想辦法構造某個數f(N),使得f(N) &> M,然後就可以按照1的解法來求了。

  • f(N) = k*N,其中k = iggllceilfrac{M}{N}iggr
ceilqquad,這時生成的序列為(0, k , 2k,...,kN),我們需要連續的數字,我們必須補齊0, k中的數,

  • 如果k&<=N,那麼我們只需要再加上randk(),即可,也就是k * randN() + randK(),這樣就生成了(0, 1, ...,kN+k-1)這個序列。

  • 如果k &> N, 那麼繼續步驟1,最終需要構造一個連續的序列(0,1,...P)其中P&>M,然後由randP()生成randM();

例如: 由rand5()構造rand7(),由於iggllceilfrac75iggr
ceilqquad=2 ,則生成了(0, 2, 4, 6, 8),這個序列,只需要再加上rand2()就可以構造出(0, 1, 2, .., 9)這個序列,也就是rand10().


首先用隨機用rand8() 生成rand7() 是沒有固定步數內的映射方案。原因是,8^n不可能被7整除,導致無法你不可能通過rand8()找到一種有限步驟內的組合,使得映射到rand7()的概率是均勻分布的。

所以唯一的方法就是獲取一次rand8(),如果得到的是8,那麼重新獲取。

這就導致了,有可能多次獲取到是8的可能。這裡有幾種方法優化,使得獲取的最大步驟會縮減,但是這些優化方法都製造了「偽隨機」,導致了不是依賴於rand8()的均勻概率分布。

反過來討論一種情況,就是用rand7()如何生成rand8()。

同樣,也沒有一種可能在固定步驟內映射方案,可是使得rand7()可以均勻映射到rand8()。

我們同樣需要和上面用的方法一樣需要丟棄一些狀態來使得概率是均勻的。

例如,通過獲取兩次rand7()的數值(x,y)。那麼(x,y)共有7*7等於49種值。那麼我們可以丟棄一個值,把剩下的48個值,任意取6種值映射為rand8()的一個值,就可以保證rand8()的各個值是基於rand7()均勻分布的了。例如(x,y)如果x等於y,那麼返回8,其他情況則返回x,但是如果x=y=7時候,不能返回8,而是要再次取(x,y)再次計算,一直到不出現x=y=7為止。

其他的奇技淫巧,實際上都製造了「偽隨機」是的不原來原來的隨機數都是耍流氓。

因為通過製造偽隨機,甚至不需要調用原來的隨機函數就可以生成看起來隨機的任意範圍內的隨機數,那麼這個題目還有什麼意義?


int rand7() {

while(true) {

int ret = random8();

if (ret != 8) {

return ret;

}

}

return 0l

}


抽到 8 重抽。

這個方法有人指出這種 rejection sampling「不能保證有限步數內結束」。

於是有個改進方案:

每生成一次 [1, 7] 內的結果,在輸出的同時也把它丟到熵池裡去。如果抽到 8 的話,就用那個熵池來生成一個 [1, 7] 之間的隨機數(雖然感覺這樣相當於完全重寫了一個 rand7 的樣子)。


首先題目肯定要求的是隨機分布。

1-8的隨機丟掉8來當作1-7的隨機發生器用,沒有任何問題,任何一個取值概率都是相同的,不過不能加和到1罷了。

再來看用1-7來構造1-8的隨機數生成器,用兩個1-7發生器構造一個1-49的發生器(想像7*7的二維數組),丟掉49,現在你能隨機均勻的得到1~48,再對8求余就行了。

之前小結了一點


不讓8輸出不行嗎?


import random
random.seed(int("".join([str(rand8()) for i in xrange(8)])))
rand7 = lambda: random.randint(0, 7)

當然是用從低質量隨機源生成高質量隨機數的一般方法啊(光速逃


用rand8得到的隨機數作為seed輸入然後上線性同餘法(逃


首先,上面答案所說的「遇到8就重取,直到出現1~7」這個方法是可以的,標準的Acceptance-Rejection method

其次,關於用這個方法的最大效率,我們要先定義「效率」,即「每生成一個rand7需要rand8的期望次數」,這個數越小越好(當然我們可以用這個數的倒數,效率應該越大越好嘛),所以就像高票答案里提到的(@lixin liu 算錯了,他的boundary好像也錯了,應該是0.93578...),這種方法的效率是8/7=1.143

接下來是我的改進方案:

一次性用rand8生成n個1~8的隨機數,並用它們產生m個1~7的隨機數。為了保證rejection method可以用,我們有frac{7^m}{8^n}leq 1,(m,nin Z^+)

那麼此時的效率就是frac{8^n	imes n}{7^m	imes m}

例如m=16, n=15時,效率=0.993

又或者m=3535, n=3308時,效率=0.9360

再或者m=8892, n=8321時,效率=0.9358, 很接近boundary了,n,m繼續增加還會有更接近的解

ps:這是個integer programming的問題,我太菜了不會求,但是如果m,n可以不是整數,上述問題變成了minimization with linear constraint,那麼可以證明在不等式條件取等號時我們有frac{7^m}{8^n}=1Leftrightarrow frac{m}{n}=frac{ln8}{ln7} ,此時效率=frac{ln7}{ln8}=0.93578...,即最高效率的情況(不會資訊理論,求指教)


return (rand8()-1) *6/7+1


碰到8重新抽樣,或者搞個轉盤

轉盤上只有0-6

每次隨機出來的數都是指針撥動的次數

把隨機值往往當前值上加。

應該是均勻的吧(滑稽


突然想到有錯。但不能刪就改成這樣


呃。。。我想請教一下為什麼不能

得到的數/8*7

這樣算?


推薦閱讀:

TAG:演算法 | 隨機數 | 蒙特卡洛方法 | 隨機演算法 | 隨機數發生器 |