甲乙兩人互猜數字(鬼谷子問題)的邏輯推理與算法建模

1、問題

這是一道歷史悠久,又很困難的邏輯推理題,有的公司還會將其做爲面試題。有人將其稱爲「鬼谷子問題」,但筆者至今沒有找到任何可靠來源。先給出問題。ios

你在旁觀主持人和甲、乙兩個天才數學家玩猜數字遊戲。主持人準備了兩個數,告知甲乙:這兩個數不一樣,且大於等於1,小於等於30。而後主持人將兩數之積告訴甲,把兩數之和告訴乙。甲知道乙拿到兩數之和,乙也知道甲拿到兩數之積。主持人讓甲乙猜這兩個數字,讓甲先發言。面試

甲:「我不知道這兩個數是什麼」算法

乙:「我也不知道」數據結構

甲:「那我知道了」less

乙:「那我也知道了」函數

請問你,這兩個數是什麼?性能

 

另外一種等價表述(即所謂的鬼谷子問題):spa

一天,鬼谷子隨意從2-99中選取了兩個數。他把這兩個數的和告訴了龐涓,把這兩個數的乘積告訴了孫臏。但孫臏和龐涓彼此不知到對方獲得的數。次日,龐涓頗有自信的對孫臏說:雖然我不知到這兩個數是什麼,但我知道你必定也不知道。隨後,孫臏說:那我知道了。龐涓說:那我也知道了。code

 

網上有很多對這道題的討論和答案,但幾乎都沒有準確的推理過程,有些甚至是錯誤的。本文用盡可能清晰的語言給出詳細的推理過程,而後給出了計算機建模和程序實現,以及進一步的發散思考。但建議在參閱下面的答案前,先自行認真思考。blog

 

2、分析與推理

1. 約定

因爲推斷的邏輯很複雜,因此必須用約定的語言來描述。本文所用的推斷名稱格式以下:

「1甲n」表示若甲拿到的兩數之積爲n,第1次發言時作的推斷。

「1乙m」表示若乙拿到的兩數之和爲m,根據甲的第1次發言,乙作出的推斷。

「2甲n」表示若甲拿到的兩數之積爲n,根據乙的第1次發言,甲作出的推斷。

「2乙m」表示若乙拿到的兩數之和爲m,根據甲的第2次發言,乙作出的推斷。

前提是甲乙都是天才數學家,所以必定會先假設兩個數,而後將本身作爲對方進行推斷。若是能夠推斷出,則必定不會失誤。

 

推斷的書寫格式爲:

推斷名:可能拆分1,結論1;可能拆分2,結論2;……

推斷名爲紅色表示可知推斷,便可推斷出確切的兩個數;綠色表示未知推斷,即有多種可能。

 

2. 推理過程

甲說:「我不知道」

下面列出甲拿到的積爲2到12的所有狀況。(A)若兩數之積只有一種拆分的狀況下甲會作出已知推斷,與甲此次未知的事實不符;(B)若至少有兩種可能,則甲作出未知推斷,符合甲此次未知的事實。

1甲2:1*2,可知1和2。(A)

1甲3:1*3,可知1和3。(A)

1甲4:1*4,可知1和4。(A)

1甲5:1*5,可知1和5。(A)

1甲6:1*6,2*3。(B)

1甲7:1*7,可知1和7。(A)

1甲8:1*8,2*4。(B)

1甲9:1*9,可知1和9。(A)

1甲10:1*10,2*5。(B)

1甲11:1*11,可知1和11。(A)

1甲12:1*12,2*6,3*4。(B)

如下略,易證得兩數之積爲素數或素數的平方時爲已知推斷,不然爲未知推斷。

乙說:「我也不知道」

1. 對於乙,若兩數之和只有一種拆分可能,則乙會作出已知推斷,與乙第一次未知的事實不符。

2. 若至少有兩種拆分可能,則乙可在假設某一種拆分的狀況下,算得兩數之積,而後假設本身爲甲作出推斷,並獲得相應的結論:(A)若在假設的某一種拆分的狀況下甲會作出已知推斷,則該狀況與甲第一次未知的事實矛盾;(B)如有且只有一種拆分的狀況下甲會作出未知推斷,則乙可作出已知推斷(就是這種拆分),與乙此次未知的事實矛盾;(C)如有至少兩種拆分的狀況下甲都會作出未知推斷,則乙作出未知推斷,符合乙此次未知的事實。

1乙3:1+2,可知1和2。(A)

1乙4:1+3,可知1和3。(A)

1乙5:1+4,則1甲4;2+3,則1甲6。(B)

1乙6:1+5,則1甲5;2+4,則1甲8。(B)

1乙7:1+6,則1甲6;2+5,則1甲10;3+4,則1甲12。(C)

1乙8:1+7,1甲7;2+6,則1甲12;3+5,則1甲15。(C)

1乙9:1+8,則1甲8;2+7,則1甲14;3+6,則1甲18。(C)

1乙10:1+9,1甲9;2+8,則1甲16;3+7,則1甲21;4+6,則1甲24。(C)

如下略,可算得皆爲未知推斷。

甲說:「那我知道了」

對於甲,在排除第一次的已知推斷後,在剩下的推斷中兩數之積必有兩個或以上的拆分可能。那麼甲可在假設某一種拆分的狀況下,算得兩數之和,而後假設本身爲乙作出推斷,並獲得相應的結論:(A)若至少有兩種拆分的狀況下乙都會作出未知推斷,則甲只能作出未知推斷,與甲此次已知的事實矛盾;(B)如有一種拆分的狀況下乙會作出未知推斷,符合乙第一次未知的事實,則甲可作出已知推斷,符合甲此次已知的事實。

2甲6:1*6,則1乙7;2*3,則1乙5。(B)

2甲8:1*8,則1乙9;2*4,則1乙6。(B)

2甲10:1*10,則1乙11;2*5,則1乙7。(A)

2甲12:1*12,則1乙12; 2*6,則1乙8;3*4,則1乙7。(A)

如下略,可算得皆爲未知推斷。

乙說:「那我也知道了」

對於乙,在排除上次的已知推斷後,在剩下的推斷中兩數之和必有兩個或以上的拆分可能。那麼乙可在假設某一種拆分的狀況下,算得兩數之積,而後假設本身爲甲作出推斷,並獲得相應的結論:(A)若假設的全部拆分狀況下甲都會在第二次作出未知推斷,則該狀況與甲第二次已知的事實矛盾;(B)如有一種拆分的狀況下甲會在第二次作出已知推斷,符合甲第二次已知的事實,則乙可作出已知推斷,符合乙此次已知的事實。

2乙7:1+6,則2甲6;2+5,則2甲10;3+4,則2甲12。(B)

2乙8:1+7,則2甲7;2+6,則2甲12;3+5,則2甲15。(A)

2乙9:1+8,則2甲8;2+7,則2甲15;3+6,則2甲18;4+5,則2甲20。(B)

2乙10:1+9,則2甲9;2+8,則2甲16;3+7,則2甲21;4+6,則2甲24。(A)

藍色標註的狀況早在第一次推斷就被排除,不予考慮。如下略,可算得皆爲未知推斷。

 

3. 結論

當兩數爲1和6時或1和8時,甲乙各自的兩次推斷結論均知足題目所描述的事實。

 

3、計算機建模與實現

1. 模型

下面將用計算機程序來對這一問題進行建模,並在最後給出C++代碼的實現。先給出一些定義(不要怕,仔細看看會發現其實都很簡單)。

  1. 「拆分」是由兩個取值範圍內不一樣的兩個數構成二元組;
  2. 給定取值範圍的全部拆分構成「全集」;
  3. 通過推導排除掉全集中的一些拆分後的集合後造成「可能解集」;
  4. 「拆分之積」是指拆分的兩數乘積;
  5. 「拆分之和」是指拆分的兩數加和;
  6. 可能解集中,全部拆分之積等於同一數值的全部拆分構成的子集稱爲「兄弟積拆分」;
  7. 可能解集中,全部拆分之和等於同一數值的全部拆分構成的子集稱爲「兄弟和拆分」。

例如:取值範圍給定爲[1,4],那麼全部拆分構成的全集爲:{<1 2>, <1 3>, <1 4>, <2 3>, <2 4>, <3 4>}。拆分<1 4>的拆分之和爲1+4=5,拆分<2 3>的拆分之積爲2*3=6。上述全集中的一個兄弟和拆分爲:{<1 4>, <2 3>},這是由於1+4=2+3=5。

分析前文的推導過程可知,當一種拆分在一次推導中被排除後,這種拆分的全部兄弟拆分也一同被排除。此外,因爲取值範圍設定的不一樣,拆分的數量是很難找到規律的,結果也很難經過推導直接算出。所以咱們須要用計算機來模擬推導過程,不斷排除不可能的解,最後剩下的可能解集就是全部解。

根據上面的理論,可將甲乙的推導過程建模以下。甲的第一次推導中排除的是隻包含一種拆分的「兄弟積拆分」(如1甲4和1甲5)。乙的第一次推導是在甲的第一次推導中已經排除掉一些拆分(如1甲4和1甲5)後的基礎上進行的,所以乙一樣排除掉了只包含一種拆分的「兄弟和拆分」(如1乙6,注意,1乙6的拆分1+5以前已被1甲5排除)。甲的第二次推導還是在以前排除掉一些拆分(如1乙6)後的基礎上進行的,而這一次甲會排除掉包含多於一種拆分的「兄弟積拆分」(如2甲10和2甲12)。乙的第二次推導和甲的第二次推導相似,也會排除掉包含多一種拆分的「兄弟和拆分」。

進一步建模,可獲得程序過程以下。

  1. 構造全集Q;
  2. 刪除Q中全部成員數小於或等於1的兄弟積拆分;
  3. 刪除Q中全部成員數小於或等於1的兄弟和拆分;
  4. 刪除Q中全部成員數不等於1的兄弟積拆分;
  5. 刪除Q中全部成員數不等於1的兄弟和拆分;
  6. 輸出Q中所剩的解。

 

2. 數據結構與算法

爲實現上述模型,須要如下幾種基本操做:構造全集、求兄弟和拆分、求兄弟積拆分、推導排除。因爲兄弟拆分須要知足兩個條件:1) 運算結果相同;2) 屬於可能解集。所以求兄弟拆分可用兩種方法求出:1) 枚舉出運算結果相同的全部拆分,逐一判斷是否在可能解集內;2) 遍歷可能解集,篩選出運算結果相同的全部拆分。因爲判斷屬於可能解集的操做要使用查找操做,所以不管從實現複雜度仍是效率上來說都是方法2)較優。

排除的操做即對應於程序中的刪除,對於絕大多數數據結構,刪除中間元素都比添加到末尾麻煩一些。所以最高效的方法不是直接刪除排除掉的拆分,而是另存不被排除的拆分,最後替換原集。可是另存會產生重複元素,且會致使無序。所以在替換原集以前作排序和去重是必要的。

爲了不結構體操做,可以使用一個unsigned long類型的整數表示一個拆分,其中高16位和低16位分別表示拆分中的兩個數。

綜上所述,用C++語言實現,解集用stl庫中的vector<unsigned long>表示。推導函數可抽象爲:對於一個可能解集,用一種運算(乘或加)求出全部兄弟拆分,再用一種判斷(小於等於1或不等於1)來決定求出的每個兄弟拆分是否應該從解集中刪除。所以推導函數可用模板實現,運算操做和判斷操做可直接使用stl庫中的functional的相關仿函數實現。

更通常的,咱們能夠求出每一種取值範圍[1, n],n從2變化爲99。

 

3. C++代碼

#include <iostream>
#include <algorithm>
#include <functional>
#include <vector>

typedef unsigned long ulong;
typedef unsigned short ushort;
typedef std::vector<ulong> ULONGVEC;
typedef ULONGVEC::iterator ULONGVEC_I;

template<typename _Op, typename _Jd>
void Deduce(ULONGVEC &pairs, _Op op, _Jd jd)
{
	ULONGVEC bros;
	for (ULONGVEC_I i = pairs.begin(); i != pairs.end(); ++i) {
		ulong cnt = 0, res = op(*i >> 16, *i & 0xFFFF);
		//求出全部兄弟拆分,存入bros的末尾
		for (ULONGVEC_I j = pairs.begin(); j != pairs.end(); ++j) {
			if (op(*j >> 16, *j & 0xFFFF) == res) {
				bros.push_back(*j);
				++cnt;
			}
		}
		// 判斷兄弟拆分是否知足條件,如不知足則不保留該兄弟集合
		if (jd(cnt, 1)) bros.erase(bros.end() - cnt, bros.end());
	}
	//排序、去重、替換原集
	std::sort(bros.begin(), bros.end());
	ULONGVEC_I iEnd = std::unique(bros.begin(), bros.end());
	pairs.assign(bros.begin(), iEnd);
}

int main()
{
	for (ushort n = 2; n < 100; ++n) {
		ULONGVEC pairs;
		for (ushort i = 1; i < n; ++i)
			for (ushort j = i + 1; j <= n; ++j)
				pairs.push_back((i << 16) | j);
		// 四次推導過程
		Deduce(pairs, std::multiplies<ulong>(), std::less_equal<ulong>());
		Deduce(pairs, std::plus<ulong>(), std::less_equal<ulong>());
		Deduce(pairs, std::multiplies<ulong>(), std::not_equal_to<ulong>());
		Deduce(pairs, std::plus<ulong>(), std::not_equal_to<ulong>());
		std::cout << "1 to " << n << ": ";
		for (ULONGVEC_I i = pairs.begin(); i != pairs.end(); ++i)
			std::cout << '(' << (*i >> 16) << ',' << (*i & 0xFFFF) << "); ";
		std::cout << std::endl;
	}
	return 0;
}

 

4. 運行結果

1 to 2:
1 to 3:
1 to 4:
1 to 5:
1 to 6:
1 to 7:
1 to 8:
1 to 9: (1,8); (3,8);
1 to 10: (3,8); (5,8);
1 to 11: (3,8); (5,8);
1 to 12: (1,6); (4,9); (8,9);
1 to 13: (1,6); (4,9); (8,9);
1 to 14: (1,6); (7,12);
1 to 15: (1,6);
1 to 16: (1,6); (1,8);
1 to 17: (1,6); (1,8);
1 to 18: (1,6); (1,8); (9,12); (9,16);
1 to 19: (1,6); (1,8); (9,12); (9,16);
1 to 20: (1,6); (1,8);
1 to 21: (1,6); (1,8); (14,18);
1 to 22: (1,6); (1,8); (11,16); (14,18);
1 to 23: (1,6); (1,8); (11,16); (14,18);
1 to 24: (1,6); (1,8);
1 to 25: (1,6); (1,8); (16,18); (18,20);
1 to 26: (1,6); (1,8); (18,20);
1 to 27: (1,6); (1,8); (18,21);
1 to 28: (1,6); (1,8); (16,27);
1 to 29: (1,6); (1,8); (16,27);
1 to 30: (1,6); (1,8);
1 to 31: (1,6); (1,8);
1 to 32: (1,6); (1,8); (20,27);
1 to 33: (1,6); (1,8); (20,27);
1 to 34: (1,6); (1,8); (22,27);
1 to 35: (1,6); (1,8); (25,28);
1 to 36: (1,6); (1,8); (24,30); (27,32);
1 to 37: (1,6); (1,8); (24,30); (27,32);
1 to 38: (1,6); (1,8); (27,32);
1 to 39: (1,6); (1,8);
1 to 40: (1,6); (1,8);
1 to 41: (1,6); (1,8);
1 to 42: (1,6); (1,8); (25,42); (26,36);
1 to 43: (1,6); (1,8); (25,42); (26,36);
1 to 44: (1,6); (1,8);
1 to 45: (1,6); (1,8); (33,40);
1 to 46: (1,6); (1,8); (33,40);
1 to 47: (1,6); (1,8); (33,40);
1 to 48: (1,6); (1,8); (36,40);
1 to 49: (1,6); (1,8); (35,42);
1 to 50: (1,6); (1,8); (35,42);
1 to 51: (1,6); (1,8); (33,48);
1 to 52: (1,6); (1,8); (33,48); (40,45);
1 to 53: (1,6); (1,8); (33,48); (40,45);
1 to 54: (1,6); (1,8); (42,45);
1 to 55: (1,6); (1,8); (44,45);
1 to 56: (1,6); (1,8); (40,54);
1 to 57: (1,6); (1,8); (36,56);
1 to 58: (1,6); (1,8); (36,56);
1 to 59: (1,6); (1,8); (36,56);
1 to 60: (1,6); (1,8); (45,52); (48,50);
1 to 61: (1,6); (1,8); (45,52); (48,50);
1 to 62: (1,6); (1,8); (45,52); (48,50);
1 to 63: (1,6); (1,8); (48,50);
1 to 64: (1,6); (1,8); (48,56);
1 to 65: (1,6); (1,8); (48,56);
1 to 66: (1,6); (1,8); (48,63);
1 to 67: (1,6); (1,8); (48,63);
1 to 68: (1,6); (1,8); (48,65);
1 to 69: (1,6); (1,8);
1 to 70: (1,6); (1,8); (55,56); (56,60);
1 to 71: (1,6); (1,8); (55,56); (56,60);
1 to 72: (1,6); (1,8); (49,72); (54,64); (60,63);
1 to 73: (1,6); (1,8); (49,72); (54,64); (60,63);
1 to 74: (1,6); (1,8); (49,72); (54,64); (60,63);
1 to 75: (1,6); (1,8); (54,64); (60,63);
1 to 76: (1,6); (1,8); (54,64);
1 to 77: (1,6); (1,8); (54,64);
1 to 78: (1,6); (1,8); (65,66);
1 to 79: (1,6); (1,8); (65,66);
1 to 80: (1,6); (1,8); (65,66); (65,72);
1 to 81: (1,6); (1,8);
1 to 82: (1,6); (1,8);
1 to 83: (1,6); (1,8);
1 to 84: (1,6); (1,8);
1 to 85: (1,6); (1,8); (64,75); (72,77);
1 to 86: (1,6); (1,8); (64,75); (72,77);
1 to 87: (1,6); (1,8); (64,75); (68,75); (72,77);
1 to 88: (1,6); (1,8); (60,88); (64,75); (72,77);
1 to 89: (1,6); (1,8); (60,88); (64,75); (72,77);
1 to 90: (1,6); (1,8); (75,78);
1 to 91: (1,6); (1,8);
1 to 92: (1,6); (1,8); (72,80);
1 to 93: (1,6); (1,8); (72,80);
1 to 94: (1,6); (1,8); (72,80);
1 to 95: (1,6); (1,8); (72,80);
1 to 96: (1,6); (1,8); (72,92); (76,90);
1 to 97: (1,6); (1,8); (72,92); (76,90);
1 to 98: (1,6); (1,8); (72,92); (76,90);
1 to 99: (1,6); (1,8); (72,92); (75,96);

 

5. 複雜度分析

對於每一種n的取值進行分析。取值範圍爲[1,n],那麼全集的元素個數爲C(n, 2),即n(n-1)/2,故構造全集的複雜度爲O(n(n-1)/2)=O(n^2)。假設每次推導的複雜度均爲O(f(m)),那麼4次推導的複雜度爲O(4*f(m))=O(f(m)),所以4次推導的複雜度以其中最大的一次爲準。又由於推導函數的執行過程相同,算雜度只和輸入的集合元素個數相關,故甲的第一次推導起決定做用。設輸入的集合元素個數爲m,在推導過程當中並無刪除元素,兩種循環的執行次數相同,且在bros末尾添加元素的複雜度爲O(1),所以複雜度爲O(m^2);後面的排序去重操做的複雜度爲O(m*logm)。綜上,推導函數的複雜度爲O(m^2)。將m=n^2代入,獲得算法總體複雜度爲O(n^4)。

 

4、擴展

1. 進一步發散

對於兩個數不相同的設定,這道題只有在取值範圍是[1, 16]的前題下有惟一解:1和8。若是咱們更改推導的過程,是否能夠增長能推導出惟一解的取值範圍的數量呢?答案是確定的。顯然兩我的中只要有一我的推導成功,那麼這個遊戲將在本輪或下一輪結束(取決因而乙先推導出仍是甲先推導出),也就是說在某我的推導出一對肯定的拆分後,再讓他推導發言是沒有意義的。所以只可以給甲乙增長「不知道」的推導,才符合事實和邏輯。那咱們嘗試給甲增長一次「不知道」的推導,看看結果如何。這樣兩人對話就變成了:

甲:「我不知道這兩個數是什麼」,乙:「我也不知道」,甲:「我仍是不知道」,乙:「那我就知道了」,甲:「那我也知道了」。

對應於算法就是將甲的第二次推導斷定條件更改成:「std::less_equal<ulong>()」,這樣就獲得了一組解,其中具備惟一解的取值範圍列舉以下:

1 to 14: (5,14);
1 to 18: (7,18);
1 to 19: (7,18);
1 to 21: (12,20);
1 to 24: (10,24);
1 to 25: (14,24);
1 to 27: (15,24);
1 to 28: (15,28);
1 to 29: (15,28);
1 to 32: (15,32);
1 to 33: (16,33);
1 to 38: (24,35);
1 to 45: (28,45);
1 to 46: (28,45);
1 to 47: (28,45);
1 to 48: (28,48);
1 to 49: (32,45);

其中最小的惟一解取值範圍是[1,14],這兩個數是5和14。這裏要注意的是:[1, 49]的取值範圍內有惟一解32和45,不表明[1, 50]的範圍內有惟一解。事實上若取值範圍設定爲[1, 50]是無解的。

 

2. 練習

請讀者思考如下3道練習題,其中第3道題還沒有被解決。

  1. 若是兩個數能夠相同,那這道題是否有惟一解?若是有,解是什麼?請用程序實現。
  2. 若取值範圍的設定不超過[1, 100],即從[1,2]到[1,100],存在惟一解的推導過程最多有幾輪?
  3. 要算出1到50000取值範圍內的解,上述程序算法將遇到性能瓶頸,請問有什麼辦法解決?
相關文章
相關標籤/搜索