这是一个很有趣的逻辑推理题,传说是爱因斯坦提出来的,他宣称世界上只有 2%的人能解出这个题目。传说不一定属实,但是这个推理题还是很有意思的。题目是这样的,据说有五个不同颜色的房间排成一排,每个房间里分别住着一个不同国籍的人,每个人都喝一种特定品牌的饮料,抽一种特定品牌的烟,养一种宠物,没有任意两个人抽相同品牌的香烟,或喝相同品牌的饮料,或养相同的宠物。问题是谁在养鱼作为宠物?为了寻找答案,爱因斯坦给出了以下15 条线索。
- 英国人住在红色的房子里;
- 瑞典人养狗作为宠物;
- 丹麦人喝茶;
- 绿房子紧挨着白房子,在白房子的左边;
- 绿房子的主人喝咖啡;
- 抽 Pall Mall 牌香烟的人养鸟;
- 黄色房子里的人抽 Dunhill 牌香烟;
- 住在中间那个房子里的人喝牛奶;
- 挪威人住在第一个房子里面;
- 抽 Blends 牌香烟的人和养猫的人相邻;
- 养马的人和抽 Dunhill 牌香烟的人相邻;
- 抽 BlueMaster 牌香烟的人喝啤酒;
- 德国人抽 Prince 牌香烟;
- 挪威人和住在蓝房子的人相邻;
- 抽 Blends 牌香烟的人和喝矿泉水的人相邻。
1 问题的答案
一般人很难同时记住这么多线索,所以解决这个问题需要用纸和笔画一些表格,一步一步慢慢推理,必要时需要一些假设进行尝试,如果假设错误就推倒重来。我缺乏耐心去做这个事情,所以我一直解不出这个问题。直到有一天,我的一个聪明的朋友告诉我一个答案。我对比了一下前面提到的 15 条线索,发现这是一个正确答案。答案是住在绿色房子里的德国人养鱼作为宠物,完整的推理结果如表 所示。
2 分析问题的数学模型
整个问题的描述分成两部分,一部分是对问题基本结构的描述,比如每个人住一种颜色的房子,抽一种牌子的香烟,喝一种饮料,等等。另一部分是对线索的描述,比如英国人住在红色的房子中。如果说基本数据结构只是定义了推理结果的一个框架,则线索就可以理解为不同属性之间的绑定关系,用来填充基本结构。因此,对本问题的建模也分成两个部分,一部分是基本模型定义,另一部分是线索模型定义。
2.1 基本模型定义
这个问题的描述比较复杂,总结起来共有 5 种颜色的房子、5 种国籍、5 种饮料、5 种宠物和5 种牌子的香烟,如何用一个数学模型同时表达这 25 个属性呢?这 25 个属性分成 5 种类别,仔细观察这些属性,会发现每个属性都可以用“类型+值”二元组来描述。举个例子,房子颜色是个类型,黄色就是值,组合成“黄色房子”就是一个属性。我们首先将属性的数据结构定义为:
typedef struct tagItem
{
ITEM_TYPE type;
int value;
}ITEM;
ITEM_TYPE 是个枚举类型的量,可以是房子颜色、国籍、饮料类型、宠物类型和香烟牌子五种类型之一,value 是 type 对应的值。value 的取值范围是 0~4,根据 type 的不同,0~4 代表的意义也不相同。如果 type 对应的是房子颜色,则 value 取值 0~4 分别代表蓝色、红色、绿色、黄色和白色,如果 type 对应的是饮料类型,则 value 取值 0~4 分别代表茶、水、咖啡、啤酒和牛奶。
如果任由这 25 个属性离散存在,会给设计算法带来困难,一般算法建模都会用各种数据结构将这些属性组织起来。观察一下表 8-1 给出的推理结果,我们发现这 25 个属性在两个维度上都存在关系,可以按照类型组织,也可以按照同一推理之间的关系组织,是一个矩阵式关系。根据题目描述,每个人住在一种颜色的房子中,喝一种饮料,养一种宠物,抽一种牌子的香烟,这些关系是固定的,一个人不会同时养两种宠物或喝两种饮料。我们将这种固定的关系称为组(group),一个组中包含一种颜色的房子、一个国籍的人、一种饮料、一种宠物和一种牌子的香烟,他们之间的关系是固定的。既然是这样,可以将 group 数据结构设计为:
typedef struct tagGroup
{
ITEM items[GROUPS_ITEMS];
}GROUP;
这样的设计中规中矩,但是会给算法实现带来麻烦,访问每种属性都要遍历 items,通过每个 items 的 type 属性确定要访问的类型。比如要查询或设置房子的颜色,需要遍历 items,找到items[i].type== type_house
的那个属性进行操作。
考虑到上面的麻烦,需要修改 GROUP 的设计,不妨将每种类型在 GROUP 中的位置固定,然后直接利用数据下标进行访问。比如将房子颜色类型固定为数组第一个元素,将国籍固定为数组第二个元素,以此类推,这样 GROUP 定义中可以不需要属性的类型信息(类型信息已经由数组下标表达),只需要一个值信息即可:
typedef struct tagGroup
{
int itemValue[GROUPS_ITEMS];
}GROUP;
与此同时,需要对 ITEM_TYPE 枚举类型做值绑定,以便和数组下标对应,绑定值如下:
typedef enum tagItemType
{
type_house = 0,
type_nation = 1,
type_drink = 2,
type_pet = 3,
type_cigaret = 4
}ITEM_TYPE;
使用这种定义数据结构的方式,不仅可以减少设计算法实现的麻烦,还可以提高算法执行效率。比如现在要查看一个 GROUP 绑定组中房子的颜色是否是蓝色,就可以这样编写代码:
if(group.itemValue[type_house] == COLOR_BLUE)
2.2 线索模型定义
接下来考虑一下如何对线索建立数学模型。线索模型的意义在于判断一个枚举结果是否正确,如果某个枚举结果能够符合全部 15 条线索,那这个结果就是最终的正确结果。因此,线索数据结构的定义非常关键,如果定义不好,不仅算法实现会遇到很大的麻烦,而且影响算法实现的效率。即使最后设计出了算法实现,也是到处都是长长的 if…else 分支,本书中多次强调,代码中长长的 if…else 分支结构意味着出现了不良设计。
先分析一下这 15 条线索,大致可以分成三类:第一类是描述某些属性之间具有固定绑定关系的线索,比如,“丹麦人喝茶”和“住绿房子的人喝咖啡”,等等,线索 1、2、3、5、6、7、12、13 可归为此类;第二类是描述某些属性类型所在的“组”所具有的相邻关系的线索,比如,“养马的人和抽 Dunhill 牌香烟的人相邻”和“抽 Blends 牌香烟的人和养猫的人相邻”,等等,线索 10、11、14、15 可归为此类;第三类就是不能描述属性之间固定关系或关系比较弱的线索,比如,“绿房子紧挨着白房子,在白房子的左边”和“住在中间那个房子里的人喝牛奶”,等等。
对于第一类具有绑定关系的线索,其数学模型可以这样定义:
typedef struct tagBind
{
ITEM_TYPE first_type;
int first_val;
ITEM_TYPE second_type;
int second_val;
}BIND;
first_type 和 first_val 是一个绑定关系中前一个属性的类型和值,second_type 和 second_val是绑定关系中后一个属性的类型和值。以线索 6:“绿房子的主人喝咖啡”为例,first_type 就是type_house,first_val 就是 COLOR_GREEN(COLOR_GREEN 是个整数型常量),second_type 就是type_drink,second_val 就是 DRINK_COFFEE(DRINK_COFFEE 是个整数型常量)。线索 1、2、3、5、6、 7、12、13 就可以存储在 binds 数组中:
const BIND binds[] =
{
{
type_house, COLOR_RED, type_nation, NATION_ENGLAND },
{
type_nation, NATION_SWEDEND, type_pet, PET_DOG },
{
type_nation, NATION_DANMARK, type_drink, DRINK_TEA },
{
type_house, COLOR_GREEN, type_drink, DRINK_COFFEE },
{
type_cigaret, CIGARET_PALLMALL, type_pet, PET_BIRD },
{
type_house, COLOR_YELLOW, type_cigaret, CIGARET_DUNHILL },
{
type_cigaret, CIGARET_BLUEMASTER, type_drink, DRINK_BEER },
{
type_nation, NATION_GERMANY, type_cigaret, CIGARET_PRINCE }
};
对于第二类描述元素所在的“组”具有相邻关系的线索,其数学模型可以这样定义&