`
saybody
  • 浏览: 902995 次
  • 性别: Icon_minigender_2
  • 来自: 西安
文章分类
社区版块
存档分类
最新评论

穷举和推理:用C++程序求解“谁养鱼”

阅读更多

穷举和推理:用C++程序求解“谁养鱼”

这期《程序员》提到“爱因斯坦的谜题”,我才注意到“谁养鱼”这个题目。问题如下:

1、在一条街上,有5座房子,喷了5种颜色
2、每个房里住着不同国籍的人
3、每个人喝不同的饮料,抽不同品牌的香烟,养不同的宠物
已知:
1、英国人住红色房子
2、瑞典人养狗
3、丹麦人喝茶
4、绿色房子在白色房子左面
5、绿色房子主人喝咖啡
6、抽Pall Mall 香烟的人养鸟
7、黄色房子主人抽Dunhill 香烟
8、住在中间房子的人喝牛奶
9、 挪威人住第一间房
10、抽Blends香烟的人住在养猫的人隔壁
11、养马的人住抽Dunhill 香烟的人隔壁
12、抽Blue Master的人喝啤酒
13、德国人抽Prince香烟
14、挪威人住蓝色房子隔壁
15、抽Blends香烟的人有一个喝水的邻居
问题是:谁养鱼?

在网上找一下,看到以下资料:一个手工表格求解、一个Lisp程序求解阿牛的穷举程序,用C#实现。本文分别用穷举法和推理法解答了这个问题。我先用C++完成了推理法求解。后来看到阿牛的穷举程序后,为了比较两者的效率,又用C++实现了穷举法(参照阿牛的程序)。在我的电脑上,推理法运行时间为:

求解1000次花费3828毫秒,平均每次需要3.8毫秒

在我的T7100笔记本上平均运行时间约为1.88毫秒。穷举法的运行时间与初始数据有很大关系。对比Lisp程序需要的时间(几百到几千毫秒),C++程序确实要快一点。

本文主要介绍算法,对具体实现感兴趣的朋友可以查看源代码。从我的主页(http://www.fmddlmyy.cn)可以下载推理法穷举法的完整源代码。

1 穷举

穷举法的思路很简单:遍历没有约束时所有可能的解(本文称作解空间),用所有约束条件逐一检查,直到找到符合要求的解。实现穷举法的关键是:怎样遍历解空间?换句话说,我们要按照一定顺序枚举出所有可能的解。

可以把“谁养鱼”的解看作按照房间位置排序的5组属性,例如:

1 挪威 黄色 猫 水 Dunhill
2 丹麦 蓝色 马 茶 Blends
3 英国 红色 鸟 牛奶 Pall Mall
4 德国 绿色 鱼 咖啡 Prince
5 瑞典 白色 狗 啤酒 Blue Master

每组属性是5个元素的排列(不重复),有120(5的阶乘)种可能性,如果没有约束条件,解空间的大小就是120的五次方。尽管解空间很大,由于约束条件可以一下排除很多解,所以需要检查的次数并不像“穷举法”字面上暗示的那么多。穷举法的主要逻辑如下:

foreach (属性1的所有排列) {
if (违反约束条件) {
提前进入下轮循环;
}
foreach (属性2的所有排列) {
if (违反约束条件) {
提前进入下轮循环;
}
foreach (属性3的所有排列) {
if (违反约束条件) {
提前进入下轮循环;
}
foreach (属性4的所有排列) {
if (违反约束条件) {
提前进入下轮循环;
}
foreach (属性5的所有排列) {
if (符合约束条件) {
得到正确解,返回;
}
}
}
}
}
}

进入内层循环前的判断很重要,通过这个判断可以跳过很多次检查。如果约束条件能在外层发挥作用,就可以跳过更多的检查。在外层循环检查约束时,因为还没有填写内层属性,无法使用与内层属性相关的约束条件。所以,将属性放在循环的哪个层次对总检查次数的影响很明显。例如:

属性排列(外->内) 找到正确解的检查次数 总检查次数(找到正确解后不返回) 执行时间(1000次平均)
国家-颜色-香烟-宠物-饮料 6167 11476 14.6毫秒
国家-颜色-宠物-饮料-香烟 28896 61290 84.2毫秒
国家-香烟-饮料-颜色-宠物 47460 86756 110.3毫秒

可见,根据约束条件仔细安排属性位置可以明显降低总检查次数。“找到正确解的检查次数”(或“执行时间”)也有很大的偶然性。假设我们把正确解作为初始数据,显然一下就完成了。

穷举法类似于武功中的“一力降十会”,让我们再试试“以巧破千斤”。

2 推理

我们可以让计算机按照人类的思维模式使用约束条件逐步推导,得到答案。为了更清晰地理解问题域,我们先约定一些名词。

2.1 探索问题域

2.1.1 对象、方案和方案集

在这个问题域中,我们用对象表示一个具有全部属性的人。对象有6个属性:国家、颜色、宠物、饮料、香烟、房间号。可以将所有属性都未定的对象称为空对象。两个空对象是不可区分的。只要对象具备一个及以上已知属性,它就成为唯一的、独特的。

每个方案包含5个对象。这5个对象的所有已知属性都是不同的。方案中的对象在逻辑上是无序的。

方案集是方案的集合。

如果把推理过程看作船的航行,Wizard会这样预言这次航行:

方案集是船,方案是乘客,
所有约束条件构成问题域的环境。
航行伊始,船上只有一个乘客。
它的所有对象的
所有属性都是未定的,
代表我们对环境的一无所知。
当船经过一个个约束条件时,
船上的乘客有时增加、有时减少。
无论乘客怎么变化,船总包含着所有可能。
无论乘客怎么变化,我们的知识都在增加。
当船通过所有的约束条件,
一个乘客会到达终点,
它就是答案。

在介绍详细的推理过程前,让我们再考察一下约束条件。

2.1.2 对象内约束和对象间约束

约束条件可以分为两类:对象内约束和对象间约束。对象内约束描述一个对象内两个属性的关系,例如“英国人住红色房子”。对象间约束描述两个对象的属性间的关系,例如“抽Blends香烟的人住在养猫的人隔壁”。

在“谁养鱼”的约束条件中,对象间约束有两种关系:

  1. 邻居关系,例如前面所说的“抽Blends香烟的人住在养猫的人隔壁”;
  2. 有序邻居关系。例如“绿色房子在白色房子左面”。

2.2 推理过程

2.2.1 初始状态

“我们构建一个空方案,它的所有对象的所有属性都是未定的,它是方案集中的唯一方案。未定代表可能,这个唯一的方案也包含了所有的可能。”

这个开头虽然很有“无中生有”的哲学意味,但是不实用。我实际采用的初始状态是这样的:

方案1
英国 红色 未定 未定 未定 未定
瑞典 未定 狗 未定 未定 未定
丹麦 未定 未定 茶 未定 未定
德国 未定 未定 未定 Prince 未定
挪威 未定 未定 未定 未定 房间1

我手工选择了5个“对象内约束”对方案作了初始化。我特意选择了不可能出现在同一个对象上的约束条件。这样,这个方案还是包含了所有的可能。初始状态的方案集中只有这么一个方案。

2.2.2 使用对象内约束

将当前方案集作为输入方案集,再准备一个空的输出方案集。将对象内约束应用到输入方案集的所有方案上。例如将“绿色房子主人喝咖啡”应用到方案1上,这时可能出现两种情况:

  1. 约束条件可以放到方案的多个对象上。例如“绿色房子主人喝咖啡”可以放到颜色和饮料属性未定的对象上。每次将约束条件放到一个不矛盾的位置,就会产生一个新的方案。将新方案放到输出方案集中。
  2. 约束条件可能与方案冲突。这时简单地忽略这个方案。

范例程序可以打印出推导过程,“使用对象内约束”的推导过程如下:

初始状态
-------------------- 一共有 1 个解 --------------------

解1
英国 红色 未定 未定 未定 未定
瑞典 未定 狗 未定 未定 未定
丹麦 未定 未定 茶 未定 未定
德国 未定 未定 未定 Prince 未定
挪威 未定 未定 未定 未定 房间1



增加约束: 5、绿色房子主人喝咖啡
-------------------- 一共有 3 个解 --------------------

解1
英国 红色 未定 未定 未定 未定
瑞典 绿色 狗 咖啡 未定 未定
丹麦 未定 未定 茶 未定 未定
德国 未定 未定 未定 Prince 未定
挪威 未定 未定 未定 未定 房间1

解2
英国 红色 未定 未定 未定 未定
瑞典 未定 狗 未定 未定 未定
丹麦 未定 未定 茶 未定 未定
德国 绿色 未定 咖啡 Prince 未定
挪威 未定 未定 未定 未定 房间1

解3
英国 红色 未定 未定 未定 未定
瑞典 未定 狗 未定 未定 未定
丹麦 未定 未定 茶 未定 未定
德国 未定 未定 未定 Prince 未定
挪威 绿色 未定 咖啡 未定 房间1



增加约束: 12、抽Blue Master的人喝啤酒
-------------------- 一共有 7 个解 --------------------
...


增加约束: 6、抽Pall Mall香烟的人养鸟
-------------------- 一共有 16 个解 --------------------
...


增加约束: 7、黄色房子主人抽Dunhill香烟
-------------------- 一共有 19 个解 --------------------
...


增加约束: 9+14、 挪威人住第一间房+挪威人住蓝色房子隔壁=第二间房是蓝色的
-------------------- 一共有 28 个解 --------------------
...


增加约束: 8、住在中间房子的人喝牛奶
-------------------- 一共有 30 个解 --------------------
...

对象内约束的指定属性被应用到方案的未定属性上,使方案的眉目逐渐清晰。每个输出方案集总是包含着在增加了当前所有约束后的所有可能。

2.2.3 使用对象间约束

对象间约束必须被应用到方案的已知属性上,然后判断两个方案的房间号属性是否符合指定关系。为此,我们在对方案使用对象间约束前,必须保证:

  1. 假设约束条件使用了某个属性值(例如颜色为绿色),方案中必须有对象具备这个属性值;
  2. 方案所有对象的房间号属性已知。

如果方案不满足上述条件,我们在应用约束条件前就必须先“扩展”方案,让其满足上述条件。“扩展”的过程还是读入一个方案集,输出一个方案集。读入方案如果已经具备指定属性值,就直接将其放入输出方案集。读入方案如果已经不具备指定属性值,就将增加该属性值后的所有可能方案放入输出方案集。

对房间号属性的扩展只需要在应用第一个“对象间约束”前做一次就可以了。

使用对象间约束对扩展过的方案集进行过滤,去掉不满足约束条件的方案,就得到了最后的答案。这部分的推导过程如下:

增加约束: 4、绿色房子在白色房子左面
-------------------- 一共有 4 个解 --------------------

解1
英国 红色 鸟 牛奶 Pall Mall 房间3
瑞典 蓝色 狗 啤酒 Blue Master 房间2
丹麦 白色 未定 茶 Blends 房间5
德国 绿色 未定 咖啡 Prince 房间4
挪威 黄色 未定 水 Dunhill 房间1

解2
英国 红色 鸟 牛奶 Pall Mall 房间3
瑞典 白色 狗 啤酒 Blue Master 房间5
丹麦 蓝色 未定 茶 Blends 房间2
德国 绿色 未定 咖啡 Prince 房间4
挪威 黄色 未定 水 Dunhill 房间1

解3
英国 红色 未定 牛奶 Blends 房间3
瑞典 蓝色 狗 啤酒 Blue Master 房间2
丹麦 白色 鸟 茶 Pall Mall 房间5
德国 绿色 未定 咖啡 Prince 房间4
挪威 黄色 未定 水 Dunhill 房间1

解4
英国 红色 未定 牛奶 Blends 房间3
瑞典 白色 狗 啤酒 Blue Master 房间5
丹麦 蓝色 鸟 茶 Pall Mall 房间2
德国 绿色 未定 咖啡 Prince 房间4
挪威 黄色 未定 水 Dunhill 房间1



增加约束: 10、抽Blends香烟的人住在养猫的人隔壁
-------------------- 一共有 4 个解 --------------------

解1
英国 红色 鸟 牛奶 Pall Mall 房间3
瑞典 蓝色 狗 啤酒 Blue Master 房间2
丹麦 白色 未定 茶 Blends 房间5
德国 绿色 猫 咖啡 Prince 房间4
挪威 黄色 未定 水 Dunhill 房间1

解2
英国 红色 鸟 牛奶 Pall Mall 房间3
瑞典 白色 狗 啤酒 Blue Master 房间5
丹麦 蓝色 未定 茶 Blends 房间2
德国 绿色 未定 咖啡 Prince 房间4
挪威 黄色 猫 水 Dunhill 房间1

解3
英国 红色 未定 牛奶 Blends 房间3
瑞典 蓝色 狗 啤酒 Blue Master 房间2
丹麦 白色 鸟 茶 Pall Mall 房间5
德国 绿色 猫 咖啡 Prince 房间4
挪威 黄色 未定 水 Dunhill 房间1

解4
英国 红色 未定 牛奶 Blends 房间3
瑞典 白色 狗 啤酒 Blue Master 房间5
丹麦 蓝色 鸟 茶 Pall Mall 房间2
德国 绿色 猫 咖啡 Prince 房间4
挪威 黄色 未定 水 Dunhill 房间1



增加约束: 15、抽Blends香烟的人有一个喝水的邻居
-------------------- 一共有 1 个解 --------------------

解1
英国 红色 鸟 牛奶 Pall Mall 房间3
瑞典 白色 狗 啤酒 Blue Master 房间5
丹麦 蓝色 未定 茶 Blends 房间2
德国 绿色 未定 咖啡 Prince 房间4
挪威 黄色 猫 水 Dunhill 房间1



增加约束: 11、养马的人住抽Dunhill香烟的人隔壁
-------------------- 一共有 1 个解 --------------------

解1
英国 红色 鸟 牛奶 Pall Mall 房间3
瑞典 白色 狗 啤酒 Blue Master 房间5
丹麦 蓝色 马 茶 Blends 房间2
德国 绿色 鱼 咖啡 Prince 房间4
挪威 黄色 猫 水 Dunhill 房间1

推理结束。

2.3 不足

目前的推理法程序还有一个地方可以改进:就是在扩展房间号时,其实没有必要先扩展方案集。因为,扩展房间号其实就是几个房间号在几个未定位置的排列。可以对每个方案,枚举所有房间号的排列后,直接用约束条件过滤。这样可以减少需要放入方案集中的方案。

这个推理法已经比较接近人类的思维模式了。但有一点不像:人在推理时喜欢假设和尝试,如果走不通就退回去再尝试其它可能。人类一般不会一板一眼地在每一步上列出所有可能。我的算法没有设计“尝试-回溯”机制。增加这个机制会使逻辑更复杂,严重违反KISS(Keep It Simple, Stupid)理念。其实对于“谁养鱼”这种规模的问题,穷举法是最合适的。

我本来想把“谁养鱼”的问题从5阶扩展到12阶,以测试穷举法和推理法的性能,还选择了一个武林大会的题材。但在准备了12组属性后,发现至少要写70多个hint才能玩起来,就不想玩了。有兴趣的朋友可以用推理法的程序出题。

分享到:
评论

相关推荐

    穷举法C/C++程序

    在编程领域,穷举法是一种常见的解决问题的策略,尤其在C和C++这两种语言中,开发者经常使用这种方法来实现特定的算法。穷举法,也称为全搜索或枚举,是指在解决一个问题时,尝试所有可能的情况,直到找到正确答案。...

    迷宫问题c++源程序

    在这个迷宫问题中,我们可能使用到的C++特性包括类(class)来封装数据和行为,以及文件输入输出(I/O)来读取和写入源程序。 接着,我们关注迷宫问题的核心算法。通常有多种方法来解决迷宫问题,如深度优先搜索...

    迷宫求解(穷举法)c++

    使用穷举法编写的C++迷宫解法。使用了数组操作模拟栈的操作。

    穷举法求解0-1整数规划的matlab程序.zip_TSP问题穷举法_穷举_穷举法求解0-1_穷举法;整数规划_背包问题MATL

    0-1整数规划有很广泛的应用背景,比如指派问题,背包问题等等,实际上TSP问题也是一个0-1问题,当然这些问题都是NP问题,对于规模较大的问题用穷举法是没有办法在可接受的时间内求得最优解的,本程序只不过是一个...

    编程(C++)解决逻辑推理问题

    为了解决这个问题,我们可以使用穷举法来穷举各种可能性,并使用 bool 型数组来存储六个人参加会议的情况,true 表示参加,false 表示不参加。下面是 C++ 代码: ```c #include #include void change(bool a[],int...

    穷举法求解0-1整数规划的matlab程序

    根据提供的文件信息,本文将详细解析“穷举法求解0-1整数规划的MATLAB程序”的核心知识点,包括穷举法的基本概念、在0-1整数规划中的应用以及MATLAB实现方法。 ### 一、穷举法概述 穷举法(Exhaustive Search ...

    旅行商问题求解(C++)

    在这个项目中,开发者使用C++编写了求解TSP的程序,并结合OpenCV库进行图形化展示。OpenCV是一个开源计算机视觉库,它包含了各种图像处理和计算机视觉的函数,可以用于绘制旅行商路径,使结果更直观易懂。 在源码中...

    穷举法解题:一筐鸡蛋,一个一个拿

    这个问题是经典的数学问题,涉及到的是整除和同余理论,同时也是一种穷举法的应用。穷举法,又称全搜索法,是一种在所有可能的解中寻找正确答案的算法。在这个"一筐鸡蛋"的问题中,我们可以理解为一个整数除以特定...

    C语言24点游戏穷举法

    在编程领域,C语言是一种广泛使用的底层编程语言,以其高效、灵活和强大的功能著称。本文将深入探讨如何使用C语言实现24点游戏的穷举法。24点游戏是一种数学智力游戏,玩家需要从四张1到13之间的扑克牌中,通过加、...

    c++实验代码汇总和实验题目

    本资源是C++实验代码的汇总,涵盖了C++编程的整个过程,包括简单C++程序的设计、编译、连接、运行、调试、函数的定义和使用、函数重载、嵌套调用和递归调用等。 实验1:简单C++程序设计 * 熟悉Visual C++ 6.0集成...

    C++算24点代码穷举实现

    在编程领域,"C++算24点代码穷举实现"是一个典型的数学和算法问题,它涉及到使用C++编程语言来解决24点游戏。24点游戏是一种流行的心算游戏,玩家需要从四张1到13的扑克牌中通过加、减、乘、除运算得出24这个结果。...

    算法与程序设计:第2章 穷举法与迭代法.ppt

    算法与程序设计:穷举法与迭代法 本章节主要介绍了穷举法和迭代法这两种基本的算法设计方法。穷举法是一种简单、基础的算法,它的特点是穷举所有可能的候选区间,找出...4. 穷举法和迭代法在程序设计中的应用非常重要

    用穷举法设计程序教学设计.doc

    - 知识与技能:理解穷举法的基本概念和特点,掌握穷举法设计算法的过程,能编写程序解决具体问题,并进行算法优化。 - 过程与方法:通过实践,体验穷举法解决问题的过程,学习如何分析问题并运用穷举法。 - 情感...

    穷举法求解0-1整数规划的matlab程序_matlab源码.rar

    0-1整数规划是一种特殊的线性规划问题...通过阅读和理解提供的MATLAB源码,我们可以深入学习0-1整数规划的求解过程,同时也可以了解如何用编程语言实现优化算法。这对于提升在优化领域的理论知识和实践能力都大有裨益。

    C++ 穷举法 冒泡法 打开线数据*.wal GDAL打开遥感影像

    本项目聚焦于使用C++解决特定问题,包括穷举法、冒泡法以及通过GDAL库处理线数据(*.wal)和遥感影像。 首先,让我们详细探讨穷举法。穷举法,又称全搜索法,是解决问题的一种策略,它尝试枚举所有可能的解以找到...

    用vc++穷举windows应用程序密码

    用vc++穷举windows应用程序密码

    C++程序设计及题集(含答案)..pdf

    《C++程序设计及题集》是一份针对C++初学者的练习材料,涵盖了C++的基础知识和实际编程技能的训练。以下是对题目的详细解释和相关知识点的介绍: 1. **异或运算加密解密**:这个题目涉及到C++中的位运算,特别是...

    TSP.rar_java TSP GA_tsp_穷举法_穷举法tsp_穷举法求解tsp

    本文将详细介绍如何使用Java编程语言实现TSP问题的穷举法和模拟退火算法。 **一、穷举法** 穷举法是一种简单直接的求解策略,它通过遍历所有可能的解决方案来找到最优解。对于TSP问题,如果城市数量为n,则有n!种...

    数独求解程序游戏设计报告书

    - **Sudoku Solver by Nick Smallbone**:这是一个用Python语言编写的数独求解程序,支持多种求解算法。 - **Sudoku Assistant v1.09 by Andrew Stuart**:提供了多种辅助功能,如提示、错误检查等,帮助用户逐步...

    SADirRead 穷举目录 穷举文件

    这个程序可能是用C++语言编写的,因为压缩包里包含的源代码文件如"SADirRead.cpp"、"DirDlg.cpp"等都是C++的源码文件。"VC"通常指的是Visual C++,一个微软提供的C++开发工具,这进一步确认了程序的编程语言。 描述...

Global site tag (gtag.js) - Google Analytics