这道题有两种问法,一种是问需要多少只老鼠才能确定,一种是问要如何安排老鼠的喝法。
第一种问法相对简单:
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 瓶是毒酒。