之前碰到过毒药和老鼠,鸡蛋和称的问题,每次都拿笔在纸上推敲很久,这类问题今天终于有了完整的解决思路。
基础:
1.整数的二进制表达式
1000的二进制表达式是什么呢?
1000/2=500 --(余)--0500/2=250 --(余)--0250/2=125 --(余)--0125/2=62 --(余)--162/2=31 --(余)--031/2=15 --(余)--115/2=7 --(余)--17/2=3 --(余)--13/2=1 --(余)--11/2=0 --(余)--1
1000的二进制表达式为 1111101000 = 29 + 28 + 27 + 26 + 25 + 23 = 512 + 256 + 128 + 64 +32 + 8
还需要其他的吗?不需要了,有上面的基础足矣。
二进制和上面的题目有什么关系呢?
在计算机基础里面,函数B2Uw表示Binary to Unsigned(长度为w的二进制到无符号整数),它能够被定义为一个映射:
B2Uw{0,1}w --> {0,1,...,2w-1}
也就是说表达0~2w-1的整数值,只需要w位的二进制即可。
再转换一下:w位的二进制可以表达(2w-1)+1=2w个整数值。
反应在我们的命题中,n个瓶子对应n个可能的结果,转换成二进制之后,只需要x位的0101就可以表达出来了。(n已知,x未知。x的求值参考上面的结论)
命题:8瓶水,其中一瓶有毒,中毒的老鼠会在一个星期之后死亡。给你一个星期,请问最少几只老鼠可以测试出哪瓶有毒?
步骤一:确定需要几只老鼠
8 = (二进制)1000 = 23。
即三位的二进制即可表达8种情况。
步骤二:为药品分配准备数据
将三位的二进制所有的情况罗列出来,剔除0的情况
计算机的整数(索引)从0开始,生活中的计数习惯从1开始。而实际的操作中,我们可以拿出一瓶不做测试,当所有老鼠都存活下来时,就说明拿出来的这一瓶是最后的结果。这里可以取个巧,将0理解为索引,映射到最后一个数字,在这里就是8;剔除0,也就是剔除了第8瓶水的情况。
┆ 0 ┆ 0 ┆ 1 ┆ //1┆ 0 ┆ 1 ┆ 0 ┆ //2┆ 0 ┆ 1 ┆ 1 ┆ //3┆ 1 ┆ 0 ┆ 0 ┆ //4┆ 1 ┆ 0 ┆ 1 ┆ //5┆ 1 ┆ 1 ┆ 0 ┆ //6┆ 1 ┆ 1 ┆ 1 ┆ //7
从右往左,第一列对应的是1010101,其中1对应的数即为{1,3,5,7};第二列对应0110011=>{2,3,6,7};第三列对应0001111=>{4,5,6,7}
步骤三:提取上面的数字,得出喂食方案。
第一只老鼠给他喂食(1,3,5,7)瓶水的混合液,第二只老鼠喂食(2,3,6,7)的混合液,第三只老鼠喂食(4,5,6,7)的混合液。
步骤四:根据实验结果推算答案。
如果第一只死了,第二只和第三只活着,我们依旧从右往左写成001(死掉为1,活着为0),转成十进制整数为1。那么结论就是第1瓶水有毒。
如果第一只死了,第三只也死了,第二只活着。二进制为101,转十进制为5。那么结论就是第5瓶水有毒。
如果全部死亡。111,对应7,那么就是第7瓶水有毒。
如果全部存活。000,那就是0瓶水(也就是上面预留的第8瓶水)有毒。
步骤五:验证答案。
101的情况:第二只存活,说明2,3,6,7没有问题,而第一只和第三只,取他们的交集,有可能的是5和7,但是前面已经排除了7,所以答案是5。
再来验证110的情况:第一只存活,二、三只光荣了,说明1,3,5,7没问题,后两只的混合液取交集,有可能是6,7。前面排除了7,答案只能为6。
继续思考:
1000瓶水,找出其中有毒的那一瓶水,需要多少小白鼠,实际操作怎么样呢?
29 < 1000 < 210 所以需要的白老鼠个数为10只,也就是10只老鼠才能充分的反应1000种情况。当然,10老鼠最多可以反映多少情况呢,应该是210=1024种。
得出了需要十只老鼠,接下来十只老鼠应该怎么操作才能得出结论呢?
0000000001 ------1
0000000010 ------2
0000000011 ------3
0000000100 ------4
0000000101 ------5
...
1111100111 ------999
那么第一只老鼠应该喝1,3,5,7,9,11,...,997,999瓶水的混合液
第二只老鼠则是2,3,6,7,...,998,999瓶水的混合液
第三只老鼠则是4,5,6,7,12,13,14,15,...,996,997,998,999瓶水的混合液
...
第十只老鼠则是512,513,514,...,999瓶水的混合液
好吧,每个老鼠的指标都是接近500瓶的混合液,这也是自然的,每个位上有0,1两种可能。这么多,老鼠估计喝都喝死了。还想到一个问题啊,稀释这么严重,能毒死老鼠吗?这年头老鼠的生命力很顽强啊,这得上好的毒药啊......
......最后观察老鼠的牺牲情况,从右至左,死掉为1,活着为0,得到最终的二进制表达式,然后转换成十进制,即为有毒瓶子的答案。
注意:这里1000<210的情况,也可以将1111101000 -----1000的情况列出来。最后如果第四、六七八九十只老鼠死了的话,就可以判断是第1000瓶水有毒。但是也可以将1000空出来,不喂食任何老鼠。最后没有老鼠死掉的话,也可以判断是第1000瓶水有毒了。这样可以避免老鼠的牺牲,阿弥陀佛~~~~
算法总结:
根据园友的提示,仔细看了汉明码的校验方法,看到如下说明:
可发现一个比较直观的规律:第i个检验位是第2i-1位,从该位开始,检验2i-1位,跳过2i-1位……依次类推。例如上表中第3个检验位p4从第23-1=4位开始,检验4、5、6、7共4位,然后跳过8、9、10、11共4位,再检验12、13、14、15共4位……
由上面的思路写了下面的代码
计算小白鼠数量:
Console.WriteLine("请输入总瓶数:"); int poisonNum = int.Parse(Console.ReadLine()); var x = Math.Log(poisonNum, 2); var mouseNum = (int)x + (x % 1 > 0 ? 1 : 0); Console.WriteLine("需要小白鼠数量:" + mouseNum);
老鼠喂食方案:
Console.WriteLine("老鼠喂食方案:");List [] solution = new List [mouseNum];for (int i = 0; i < mouseNum; i++){ solution[i] = new List (); int segment = 1 << i; int idx = segment; while (idx < poisonNum) { solution[i].AddRange(Enumerable.Range(idx, segment)); idx += 2 * segment; } solution[i].RemoveAll(aa => aa >= poisonNum);}for (int i = 0; i < mouseNum; i++) Console.WriteLine("第{0}只老鼠的喂食方案:{1}", i + 1, string.Join(",", solution[i]));
根据实验结果得出结论:
Console.WriteLine("假设有毒瓶数为:");int poisonOne = int.Parse(Console.ReadLine());Console.WriteLine("死掉的老鼠有:" + string.Join(",", solution.Select((aa, idx) => aa.Contains(poisonOne) ? idx + 1 : -1).Where(idx => idx != -1)));var resBinary = new string(solution.Select(aa => aa.Contains(poisonOne) ? '1' : '0').Reverse().ToArray());Console.WriteLine("二进制表达式为:" + resBinary);var res = Convert.ToInt32(resBinary, 2);if (res == 0) res = poisonNum;Console.WriteLine("测试结果为:第{0}瓶有毒", res);
发散的问题:6个质量相等的小球和1个质量稍重的球,不能用天平,只能用称,设计一种方法,只能量三次就找出稍重的球。
类似的问题,这里数量变成了7个。测试数据也从死掉变成了重和轻,不过一样可以转换成0和1。
如果你看懂了上面,不妨想想这个的答案。