最近看到一個阿里的面試題目,覺得很有意思,遂記下。
阿里2014年筆試題目,(選擇題)是給定生成1-7的隨即函式rand_7,看是否能生成其它隨機數?
A. rand_3 B. rand_21 C. rand_23 D.rand_47
首先從最簡單的開始,給定一個能夠隨機生成1到7之間數字的rand_7方法,能否用其實現rand_3?
非常直觀的方法就是直接使用rand_7生成數字,如果為1 ~ 3則返回即可,程式碼如下:
1 int rand_3() 2 { 3 int x; 4 while(x = rand_7()) 5 { 6 if(x <= 3) 7 { 8 return x; 9 } 10 } 11 }
需要證明該方法能夠以1/ 3概率生成1 , 2, 3 證明過程如下:
1. rand_7()以1/7概率生成1~7;
2. 迴圈中第一次生成1並返回的概率為1/7;
3. 迴圈中第二次生成1並返回的概率應該為 (4 / 7) * (1 * 7) (第一次迴圈沒有退出的概率 + 本次生成1的概率);
4. 同理,之後的每一次生成1的概率均為前幾次沒有退出(生成4, 5, 6, 7)的概率加上本次生成1的概率 有 P(n)= (4 / 7) ^ (n - 1) * (1 / 7); (n >= 1)
5. 這樣,rand_3()生成1的概率為 P = 1/ 7 + (4 / 7) * (1 * 7) + (4 / 7) ^ 2 * (1 / 7) + (4 / 7) ^ 3 * (1 / 7) + .... + (4 / 7) ^ (n - 1) * (1 / 7); ( n -> 無窮)
6. 以上求和公式求得P = 1 / 3; (該等比數列收斂)
同理,生成2 ,3 的概率也為1/3,說明該方法是可行的。
這裡其實還可以優化一下,減少while迴圈命中失敗率(以下還有說明)。
1 int rand_3() 2 { 3 int x; 4 while(x = rand_7()) 5 { 6 if(x <= 6) 7 { 8 return x % 3 + 1; 9 } 10 } 11 }
同時引申出一個通用的結論, 如果 b > a, 我們可以使用能夠等概率生成1~b之間數字的rand_b() 去實現rand_a(). 而且rand_a()等概率生成1 ~ a之間的數字.
虛擬碼如下:
1 // a < b 2 int rand_a() 3 { 4 int x; 5 while(x = rand_b()) 6 { 7 if(x <= a) 8 { 9 return x; 10 } 11 } 12 }
那如果a > b呢? 也就是說能不能使用一個生成範圍小的隨機數方法去實現一個範圍大的隨機數呢,也就是說還能不能用rand_b()去實現rand_a()?
既然這樣我們可以用rand_b()的組合去實現rand_a()對嗎?(a > b),但是問題就是如何保證等概率生成1 ~ a
例如: rand_5() + rand_5() - 1
能夠生成1~9的數字,可是並不能等概率生成1~9,例如生成1的組合只有(1, 1),生成2的組合有(1, 2)和(2, 1),生成3的有(1, 3),(3, 1),(2, 2). 等,因此僅僅簡單組合是不能保證等概率的。那怎麼找到一個等概率生成數的組合呢?
首先,再來看一個組合,然後再進行分析。組合如下:
5 * (rand_5() - 1) + rand_5()
加號前面部分等概率生成0, 5, 10, 15, 20, 後面部分等概率生成1 ~ 5,這樣1 ~ 25的每一個數字僅能通過一種組合實現,而且每一個數字的生成概率均為 1/5 * 1/ 5 = 1/ 25。
這樣,相當於我們實現了rand_25(), 我們可以利用這個方法去實現rand_7(), 套用以上的程式碼:
1 int rand_7() 2 { 3 int x; 4 while(x = 5 *(rand_5() - 1) + rand_5()) 5 { 6 if(x <= 7) 7 { 8 return x; 9 } 10 } 11 }
目前看來使用rand_5()實現了rand_7()方法,但是以上的while迴圈次數過多,因為rand_5()組合生成1 ~ 25,只有1~7命中才能退出,我們應該儘量增大命中範圍,減小捨棄數目的個數。根據分析,if語句判斷的閾值越接近25越好,演算法命中率就增加。我們可以這樣(if語句中的閾值為接近25並小於25的7的倍數):
1 int rand_7() 2 { 3 int x; 4 while(x = 5 *(rand_5() - 1) + rand_5()) 5 { 6 if(x <= 21) 7 { 8 return x % 7 + 1; 9 } 10 } 11 }
同樣我們可以證明生成1的概率是P = 3 / 25 + ( 4 / 25 ) * (3 / 25) + (4 / 25)^2 * (3 / 25) + ... = 1 / 7
好了,重點來了,我們將整個隨機數生成問題泛化一下:
有兩個隨機數生成方法rand_a() 和rand_b(),其中a,b不相等,能夠使用rand_a()去實現rand_b() ?
1. 如果a > b,進入步驟2;否則構造rand_a^2 = a * (rand_a - 1) + rand_a, 表示生成1到a^2隨機數的函式。如果a^2 仍小於b,繼教構造 rand_a^3 = a * (rand_a^2 - 1) + rand_a 直到a^k > b,這時我們得到rand_a^k, 我們記為rand_A。
2.步驟1中得到rand_A()和rand_b() (A > b),我們就可用以下的程式碼去構造rand_b()
1 int rand_b() 2 { 3 int x; 4 while(x = rand_A()) 5 { 6 if(x <= b * (A / b)) // b*(A/b)表示最接近A且小於A的b的倍數 7 { 8 return x % b + 1; 9 } 10 } 11 }
大功告成,無論a和b的大小關係如何(如何相等就不需要轉化了對吧),我們總能用其中一個隨機數生成方法去實現另一個隨機數生成方法。
回過頭,我們來看阿里巴巴的這道筆試題,有rand_7(),問是否能夠生成其他的隨機數。顯而易見,所有答案選項均符合,因為我們無論如何都能構造出一個隨機數生成器來,大於7或者小於7.