隨機數——百度面試題
題目如下:
已知一隨機發生器,產生0的概率是p,產生1的概率是1-p,現在要你構造一個發生器,使得它構造0和1的概率均為1/2;構造一個發生器,使得它構造1、2、3的概率均為1/3;...,構造一個發生器,使得它構造1、2、3、...n的概率均為1/n,要求複雜度最低。
分析:
在這裡有三個小問題,但是都可以歸結到n的情況。
(1)首先針對1/2的情況,我們可以產生兩位隨機數,如果是00或者11則丟棄,如果是01或者10則保留,他們的概率均為p*(1-p),因此兩者概率相等為1/2。
(2)對於1/3的情況,同樣可以產生三位隨機數,保留100、010、001,捨棄其他的,他們的概率均為p*(1-p)^2。
(3)思維擴充套件一下,對於生成1,2,...n的概率分別是1/n,只需要每個數的概率相等即可。自然想到產生n位隨機數,保留只有一位為1的組合,捨棄其餘的。雖然這樣可以產生1到n的隨機數,但是效率明顯十分低.假設產生一位0,1的隨機數需一個單位的時間,那麼產生n位隨機數需要o(n)時間,期望迴圈次數E = 1/(n/2^n) = 2^n/n次,所以時間複雜度為o(2^n)。那麼怎樣才能降低時間複雜度呢?自然會想到選取1的位數不止1位,假設選取x位為1,總位數為y位,那麼需要滿足C(x,y) >= n.並且可以計算得到這些概率為p^x*(1-p)^y.注意到題目中需要複雜度最低,因此考慮到組合數中中間值取到最大C(x/2,x),所以只需要取最小的x使得C(x/2,x) >= n就能達到最小得複雜度。
舉個例子:n = 7,我們可以取到最小的x = 5,使得C(2,5) >= 7。這樣共有10個組合他們的概率相等:00011,00101,00110,01001,01010,01100,10001,10010,10100,11000.
捨棄後面三個組合,就可以得到概率相等的7個組合。
接下來就是如何對其中一個組合賦值,比如:00011對應隨機數1,01010對應隨機數5.其實發現其中的規律我想了有點久,最後發現了一個規律,我舉兩個例子吧,比如找01010對應的數,可以找最高位的1所在的位置3,然後取組合數C(2,3),接著往低位找第二個1所在位置1,取組合數C(1,1),最後把這兩個數加起來再加1.
找11000對應的數,同樣可以計算得到組合數:C(2,4),C(1,3),加起來為6+3+1 = 10,當然這個數大於n(7),所以被捨棄。
在貼程式碼之前我再講一個寫程式碼過程碰到的一個很奇怪的問題,這個和我之前對Java中的Random這個類不是很熟悉有關。
在我第一次寫下面這個產生隨機數0和1的方法時,每次都產生一個Random物件,導致了在迴圈中呼叫的時候產生的隨機數並不隨機,這個和Random使用的種子是當前系統的時間有關,可以想象在迴圈中如果多次呼叫,由於系統時間變化很小,所以產生的隨機數不隨機。這個問題讓我檢查了好幾遍程式碼都發現不了問題,特此mark一下,大神們可以忽略,不過對於之前不怎麼清楚的朋友,給個提醒吧。
開始寫的產生0和1的隨機函式
完整程式碼:public static boolean random0Or1() { double p = 0.1; if (new Random().nextDouble() <= p) { return false; } else { return true; } }
public static int randomWithN(int n, Random random) {
int x = 2, y = 1;
while (combine(x, x / 2) < n) {
x++;
}
y = x / 2;
boolean[] a = new boolean[x];
int count = 0;
do {
count = 0;
for (int i = 0; i < a.length; i++) {
a[i] = random0Or1(random);
if (a[i]) {
count++;
if (count > y) {
break;
}
}
}
} while (count != y);
int sum = 0;
for (int i = a.length - 1; i >= 0 && y != 0; i--) {
if (a[i]) {
sum += combine(i, y);
y--;
}
}
sum++;
if (sum > n) {
return randomWithN(n, random);
}
return sum;
}
public static int combine(int n, int r) {
if (r > n) {
return 0;
}
if (r > n / 2) {
r = n - r;
}
double s = 0;
for (int i = n - r + 1; i <= n; i++) {
s += Math.log(i);
}
for (int i = 2; i <= r; i++) {
s -= Math.log(i);
}
return (int) (Math.pow(Math.E, s) + 0.1);
}
public static boolean random0Or1(Random random) {
double p = 0.1;
if (random.nextDouble() <= p) {
return false;
} else {
return true;
}
}