1. 程式人生 > >白鼠試毒酒問題

白鼠試毒酒問題

這道題有兩種問法,一種是問需要多少隻老鼠才能確定,一種是問要如何安排老鼠的喝法。

第一種問法相對簡單:
1000 瓶無色無味的白酒,其中有一瓶毒酒, 白鼠喝了毒酒一個星期(或一天,無所謂)後會死去。 那麼問你:最少需要多少隻白鼠,可以在最短時間內(一個星期或者一天,反正只能實驗一次)即可找出那瓶毒酒。

第二種問法比較更難一點:同樣1000瓶白酒(其中只有一瓶毒酒),用10只小白鼠拿過來做實驗。如何在最短時間(一週或一天,反正只能做一次實驗)之內找出這瓶有毒的藥水?

首先看第一種問法。

最少需要多少隻老鼠?

其實是一個編碼問題。1000 瓶白酒如果不考慮成本問題(即老鼠數目沒有限制),那麼用 1000 只老鼠分別喝一瓶,很容易確定那瓶白酒有毒。那隻老鼠死了,就是那瓶酒有問題。但是實驗結果就會有 1000 份。

1000 個實驗資料太大了,我們可以縮小實驗規模,然後類推。假設有 4 瓶白酒,用 4 只小鼠來做實驗。

那麼分別讓每隻老鼠喝一瓶白酒,那麼可能得出 4 種結果(排列組合):

第1種結果:x o o o

第2種結果:o x o o

第3種結果:o o x o

第4種結果:o o o x

在這個表格中,每一行表示一種可能的實驗結果。在每一種結果中,使用了 4 個 o 或 x 來分別表示 4 只老鼠的死/活狀態。打 x 表示這隻老鼠死了,打 o 則表示老鼠還活著。這樣,只消看第幾只老鼠的位置上打了 x,就知道是哪瓶白酒有問題。

如果你將上表換成用二進位制表示,即 x 換成 1,o 換成 0,你會發現它們其實和白酒的編號(轉換成二進位制)有一定的對映關係:

第1種結果:1 0 0 0 --> 第一瓶酒:1 --> 0 0 0 1

第2種結果:0 1 0 0 --> 第二瓶酒:2 --> 0 0 1 0

第3種結果:0 0 1 0 --> 第三瓶酒:3 --> 0 1 0 0

第4種結果:0 0 0 1 --> 第四瓶酒: 4 --> 1 0 0 0

其實無非就是老鼠的編號的高低位和白酒編號的高低位順序相反了。如果我們實驗結果的編碼顛倒一下,也按照“高位在前”的原則編碼,那麼你會發現,其實老鼠的編號和白酒編號恰恰是一致的:

第1種結果:0 0 0 1 --> 第一瓶酒:1 --> 0 0 0 1

第2種結果:0 0 1 0 --> 第二瓶酒:2 --> 0 0 1 0

第3種結果:0 1 0 0 --> 第三瓶酒:3 --> 0 1 0 0

第4種結果:1 0 0 0 --> 第四瓶酒:4 --> 1 0 0 0

這裡我們把白酒按照每瓶白酒佔一個二進位制位的方式編碼,所以有多少瓶白酒,就需要多少位二進位制位來編碼。

如果白酒的編碼用二進位制編碼需要 4 位,那麼就需要用 4 只老鼠來做實驗。如果白酒編碼的長度為 100 位,那麼就需要 100 只老鼠來實驗。

但問題是,上面的二進位制編碼並不是最優的(最短的)。我們知道如果要表示 4 瓶白酒,其實只需要 2 位二進位制就足以表示。注意看上面的編碼,4 屏白酒分別佔用了 4 個 4 位二進位制編碼: 0001,0010,0100,1000,但除此之外,其實還有 4 個 4 位二進位制編碼 0000,0011,0101,0111 是沒用到的。有整整一半的編碼被閒置了,顯得有些浪費。

那麼要對 4 個數字進行編碼,需要多少位二進位制就能編完呢?答案是 2 位。因為 22 等於 4。

第 1 瓶酒:0 0

第 2 瓶酒:0 1

第 3 瓶酒:1 0

第 4 瓶酒:1 1

注意,這裡為了最大化利用編碼,第一瓶酒的編碼從 0 開始而非從 1 開始。

那麼如果是 10 瓶酒呢?需要幾位二進位制進行編碼?首先 3 位肯定不夠(它只能表示 8 個數),4 位稍有點多(16個數),但是 5 位就更多了。所以只能選 4 位。於是要表示 n 個數,只需要計算出最接近這個數(同時不能小於這個數)的 2 的整數次方即可,即存在 2m >= n >= 2m-1 。m 就是二進位制數的位數。

因此,1000 瓶酒的編碼方案應該是 210 = 1024。於是答案就出來了,1000 瓶酒的實驗方案最少需要 10 只老鼠。

每隻老鼠喝哪幾瓶酒?

其實,實驗的方案同樣暗示在了瓶子的編碼上。還是用 4 瓶白酒作為例子吧:

第1種結果:0 0 0 1 --> 第一瓶酒:1 --> 0 0 0 1

第2種結果:0 0 1 0 --> 第二瓶酒:2 --> 0 0 1 0

第3種結果:0 1 0 0 --> 第三瓶酒:3 --> 0 1 0 0

第4種結果:1 0 0 0 --> 第四瓶酒:4 --> 1 0 0 0

我們把每種‘答案’,也就是實驗結果都和一瓶白酒進行了一一匹配(將它們的編碼都統一了)。

這樣做的好處很明顯,酒瓶編碼中的二進位制序列就暗示了這瓶酒的終極‘答案’,即是否是毒酒的線索。也就是說,如果實驗結束後,將實驗結果編碼成二進位制,如果和某隻酒瓶的編碼一致,則說明這瓶酒就是毒酒。於是實驗結果出來後,要知曉哪瓶白酒有問題,只需要將實驗結果編碼拿去上表中比照一下即可。

但是還不僅僅如此,實驗結果的編號同樣表明了 4 只老鼠的每一隻分別喝了哪瓶酒,也就是實驗方案。有 4 種實驗結果,也就對應了 4 種實驗組合。而且和實驗只有一個結果不同,為了儘快出結果(題目中有此要求),我們最好將 4 種實驗組合都同時進行測試,這樣就能一次效能遍歷所有可能的實驗結果。不管毒酒是哪一瓶,只需一次測試。

因此上述實驗方案就是(4 種實驗方案一起進行):

第1種結果:0 0 0 1 --> 第一瓶酒:1 --> 0 0 0 1 --> 給第1只老鼠喝

第2種結果:0 0 1 0 --> 第二瓶酒:2 --> 0 0 1 0 --> 給第2只老鼠喝

第3種結果:0 1 0 0 --> 第三瓶酒:3 --> 0 1 0 0 --> 給第3只老鼠喝

第4種結果:1 0 0 0 --> 第四瓶酒:4 --> 1 0 0 0 --> 給第4只老鼠喝

發現規律了沒有?就是實驗可能性、酒瓶編碼、實驗方案一一對應了。

如果用最短編碼來實現,就是:

第1種結果:0 0 --> 第一瓶酒:1 --> 0 0 --> 兩隻老鼠喝

第2種結果:0 1 --> 第二瓶酒:2 --> 0 1 --> 給第1只老鼠喝

第3種結果:1 0 --> 第三瓶酒:3 --> 1 0 --> 給第2只老鼠喝

第4種結果:1 1 --> 第四瓶酒:4 --> 1 1 --> 給兩老鼠都喝

注意,這裡為了最大化利用編碼,第一瓶酒的編碼從 0 開始而非從 1 開始。

那麼 1000 瓶酒的實驗方案,用 C 語言實現其實就是連續打印出 0~999 的二進位制數。

// 白鼠試毒問題
void findPoison(int bottles){
    int digits = 0;
    int tmp = bottles - 1;// 有一瓶酒不用試,因為根據其它酒的測試結果,很容易就判斷這瓶酒是否有毒
    // 計算需要幾隻老鼠
    while(tmp > 0){
        tmp = tmp / 2;
        digits ++;
    }
    NSLog(@"%d瓶酒需要幾隻老鼠:%d",bottles,digits);
    
    // 列印每瓶酒的編號
    for(int i= 0; i < bottles; i++){
        printBits(i, digits);
    }
    
}
// 列印十進位制數的二進位制形式
void printBits(int a,int digits){
    char ch[digits+1];
    int tmp=a;
    
    for(int i = digits-1;i>= 0;i--){
        if(tmp % 2 == 0){
            ch[i] = '0';
        }else{
            ch[i] = '1';
        }
        tmp = tmp / 2;
    }
    ch[digits] = '\0';
    NSLog(@"%2d ==> %s",a,ch);
}

列印結果類似:

需要幾隻老鼠:10
0   ==> 0000000000
1   ==> 0000000001
2   ==> 0000000010
3   ==> 0000000011
4   ==> 0000000100
5   ==> 0000000101
6   ==> 0000000110
7   ==> 0000000111
8   ==> 0000001000
......
998 ==> 1111100110
999 ==> 1111100111

實驗結果驗證

前面說過,“正確答案”都寫在酒瓶(編碼)上。也就是說每一種實驗結果對應了一瓶酒。假設實驗結果是這個:

1111100111

即“除了 4、5 兩隻老鼠外其它老鼠都死了”

那麼你可以根據這個編號去查表(或者自己換算成 10 進位制):

999 ==> 1111100111

第 999 瓶是毒酒。