【每天進步一點】毒葯和老鼠的研究


之前碰到過毒葯和老鼠,雞蛋和稱的問題,每次都拿筆在紙上推敲很久,這類問題今天終於有了完整的解決思路。

基礎:

1.整數的二進制表達式

1000的二進制表達式是什么呢?

1000/2=500  --(余)--0
500/2=250   --(余)--0
250/2=125   --(余)--0
125/2=62    --(余)--1
62/2=31     --(余)--0
31/2=15     --(余)--1
15/2=7      --(余)--1
7/2=3       --(余)--1
3/2=1       --(余)--1
1/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瓶水的情況。

001//1010//2011//3100//4101//5110//6111//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瓶水有毒了。這樣可以避免老鼠的犧牲,阿彌陀佛~~~~

 

算法總結:

根據園友Wossoneri的提示,仔細看了漢明碼的校驗方法,看到如下說明:

可發現一個比較直觀的規律:第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);
View Code

老鼠喂食方案:

Console.WriteLine("老鼠喂食方案:");
List<int>[] solution = new List<int>[mouseNum];
for (int i = 0; i < mouseNum; i++)
{
    solution[i] = new List<int>();
    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]));
View Code

根據實驗結果得出結論:

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);
View Code

 

發散的問題:6個質量相等的小球和1個質量稍重的球,不能用天平,只能用稱,設計一種方法,只能量三次就找出稍重的球。

類似的問題,這里數量變成了7個。測試數據也從死掉變成了重和輕,不過一樣可以轉換成0和1。

如果你看懂了上面,不妨想想這個的答案。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM