甲乙两人互猜数字(鬼谷子问题)的逻辑推理与算法建模

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取值范围内的解,上述程序算法将遇到性能瓶颈,请问有什么办法解决?
相关文章
相关标签/搜索