白鼠試毒酒問題
這道題有兩種問法,一種是問需要多少隻老鼠才能確定,一種是問要如何安排老鼠的喝法。
第一種問法相對簡單:
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 瓶是毒酒。