C语言编程快速解决爱因斯坦谜题

2012年7月15日

1      问题简介

爱因斯坦谜题(Einstein'sriddle)是很多人熟悉的问题。记得读高中时候就有同学就拿这个题目来考人,不过那时的解法主要是数学推理和逻辑判断,现在学习了编程技术后,自己再尝试使用计算机来协助解决这个问题,又是另一种不同的感受。

--------------------------------------------------------------------------------------------------------------------------------------

源码链接:https://einsteinriddle.googlecode.com/files/EinsteinRiddle.rar

SVN检出:svn checkout http://einsteinriddle.googlecode.com/svn/trunk/ einsteinriddle-read-only

-------------------------------------------------------------------------------------------------------------------------------------

1.1    谜题内容

爱因斯坦在20世纪初出的这个谜语。他说世界上有98%的人答不出来。

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香烟的人有一个喝水的邻居

以上是爱因斯坦在20世纪初出的这个谜语。他说世界上有98%的人答不出来

1.2     数学推理思路

这里顺便讲解一下运用数学推理解决此问题的方法,已经了解过的人可以跳过此部分,直接关注编程解决章节。

基本解决思路(类似数独游戏的填充过程):

1)        可以绘制一份5X5的表格来表示这25种属性。

2)        根据条件8、条件9、条件14可以确定出三个格子的内容。

属性

房子1

房子2

房子3

房子4

房子5

颜色

 

蓝色

     

国籍

挪威

       

饮料

   

牛奶

   

香烟

         

宠物

         

3)        根据条件5,绿色房子的主人喜欢喝咖啡,断定该条件只能使用房子1、4、5;根据条件4,绿色的房子在白色的房子紧邻的左边,那么可以推断出房子5是白色、 4是绿色;而红色房子住的英国人,该条件只能满足房子3;剩下的房子1必然是黄色的,而根据条件7黄色房子主人抽Dunhill香烟,可确定起香烟属性。

如图所示:

属性

房子1

房子2

房子3

房子4

房子5

颜色

黄色

蓝色

红色

绿色

白色

国籍

挪威

 

英国

   

饮料

   

牛奶

咖啡

 

香烟

Dunhill

       

宠物

         

4)        然后条件3丹麦人喜欢喝茶与条件12抽BlueMaster的人喜欢啤酒,这两个条件只能满足房子2和房子4,所以断定房子2和房子4的主人是和啤酒或茶的,由此推出房子1的主人是水的;根据条件15抽Blends香烟的人有个喝水的邻居,判断房子2的主人抽Blends香烟,所以也判断出房子2不抽BlueMaster香烟不喝啤酒,由此房子2是丹麦并喝茶,房子5是BlueMaster并喝啤酒;根据条件11养马的人住在Dunhill旁边,推断房子2的主人养马。

属性

房子1

房子2

房子3

房子4

房子5

颜色

黄色

蓝色

红色

绿色

白色

国籍

挪威

丹麦

英国

   

饮料

牛奶

咖啡

啤酒

香烟

Dunhill

Blends

   

BlueMaster

宠物

 

     

5)        根据条件13德国人抽Prince香烟,可以看到只有房子4满足;根据条件6抽PallMall香烟的人养鸟,只有房子3可以满足该条件(房子4的香烟已经刚刚被确定为Prince);根据条件2瑞典人养狗,只有房子5能够满足;根据条件条件10养猫人住在Blends香烟隔壁,房子3的宠物确定为猫,那么只能房子1养猫;

属性

房子1

房子2

房子3

房子4

房子5

颜色

黄色

蓝色

红色

绿色

白色

国籍

挪威

丹麦

英国

德国

瑞典

饮料

牛奶

咖啡

啤酒

香烟

Dunhill

Blends

PallMall

Prince

BlueMaster

宠物

 

6)        最后可以看到鱼应该放在哪个位置。答案当然就是德国人养鱼。

1.3    反思

使用数学推理方式找到其中判定技巧,当然可以很快地解决该问题;但是如果问题规模上升,条件更加庞大、关系更为复杂,仅靠单纯的推理可能难以解决问题。所以,通过计算机编程来解决该类问题是更为明智的选择。

2       程序设计

程序设计部分,尝试使用C语言编程来解决该问题。目前本人的编程环境是Windows下的MinGW,当然程序内容与平台及编译器相关并不大。

在实现该问题的过程,总共产生了三个不同版本的程序:

源码链接:http://download.csdn.net/download/aspirationflow/4429819

其他下载位置:https://einsteinriddle.googlecode.com/files/EinsteinRiddle.rar

SVN检出:svn checkout http://einsteinriddle.googlecode.com/svn/trunk/ einsteinriddle-read-only

EinsteinRiddleV1.c:原始暴力计算,无法实用。

EinsteinRiddleV2.c:优化后的程序,瞬间计算完毕。

EinsteinRiddleV3.c:整理完善的代码,优化程序结构。

2.1    暴力解法

因为想让程序实现“逻辑推理”的功能是比较复杂的,我们这里就选择最原始最简单暴力计算(brute-force calculation)。后续的编程实现证实该方法是可行的,而且优化处理后的执行速度也比较快。

2.1.1   定义属性枚举

定义不同行的取值的枚举类型。所以类型的值对应1~5,这样方便直接利用排列计算出来的不同情况。

例如,

enum Color {blue=1,green,red,white,yellow};
enum Nationality {Dane=1,Englishman,German,Swede,Norwegian};
enum Cigarette {Blend=1,BlueMaster,Dunhill,PallMall,Prince};
enum Drink {bier=1,coffee,milk,tea,water};
enum Pet {birds=1,cats,dogs,fish,horses};

2.1.2   预先计算排列

表格中每一行都有5种取值(比如房子的颜色有黄色、蓝色红色、绿色、白色),所以每一行都有5的阶乘:5!=120种可能的情况。为了避免在程序执行过程中重复计算这120种情况,我们先将所有的情况计算出来,存放在一份表格中。这是以空间换时间的思路。

static int genperm(char perm[PERM][COL],int loopcnt)
{static int rowcnt=0;  static char column[COL]={1,2,3,4,5};static char temp[COL];int i,j;//DBGTRACE("genperm loopcnt=%d rowcnt=%d\n",loopcnt,rowcnt);for(i=0;i<COL;i++){if(column[i]<0)continue;if(column[i]>0){temp[loopcnt]=column[i];column[i]=-1;}//End condition!if(loopcnt==4){for(j=0;j<COL;j++)perm[rowcnt][j]=temp[j];rowcnt++;if(rowcnt>120){printf("Error! 5! can be only 120 cases.");exit(2);}}else{genperm(perm,loopcnt+1);}column[i]=temp[loopcnt];}return 0;
}

2.1.3   循环遍历检查条件

接下来可以使用5重循环遍历所有可能的情况,然后逐一检查所有的15个条件是否满足。

循环大致结构如下:

 //Start to find correct cases.for(icolor=0;icolor<PERM;icolor++)for(ination=0;ination<PERM;ination++)for(icigar=0;icigar<PERM;icigar++)for(idrink=0;idrink<PERM;idrink++)for(ipet=0;ipet<PERM;ipet++){

粗略计算一下120^5,总共有240多亿种情况。

所以,如果使用此“原始的简陋的”设计方式,我们可能需要计算240多亿X15条件的检查。此程序是我们的第一个版本EinsteinRiddleV1.c,在我的机器上编译后执行了两个小时左右,才计算100万种情况,推测起来把240亿种情况遍历完毕大概需要1000多天时间。所以,显然该程序并没有实用价值。

2.1.4   思考

这个程序设计运行如此之慢,它的瓶颈又在那里呢?

我们可以通过什么方式优化一下这个设计呢?

思考一番后,发现我们循环计算的大多数的“情况”都是没有必要的。

2.2    优化的暴力解法

2.2.1   尽早地判断条件

刚刚实现第一版程序,无法及时地计算出结果来。而通过我们的分析和思考,发现其中大量的循环都是没有必要的。比如,在第一重循环(确定房子颜色)中,如果检测第三间房子已经不是蓝色,那么剩下的其他循环都没有必要再执行了。

优化原则:尽早地通过已知条件排除掉无效的循环

以下代码,将判断条件尽可能地搬动到上级循环,以便尽早地排除无用循环。

 //Start to find correct cases.//There are 5 attributes row(color,nation,drink,cigar,pet),//and each attribute row can has 5!=120 cases.//We have 5 loop for 5 attributes,and we try process the conditions as early as possible.for(icolor=0;icolor<PERM;icolor++){DBGTRACE("enter icolor loop\n");/*---------Basic Exculision Operation------*///C14if(perm[icolor][1]!=blue)continue;//C4if(!checkCondition(perm,icolor,green,icolor,white,left))continue;/*====End of Basic Exculision Operation====*/ for(ination=0;ination<PERM;ination++){DBGTRACE("enter ination loop\n");/*---------Basic Exculision Operation------*///C9if(perm[ination][0]!=Norwegian)continue;//C1if(!checkCondition(perm,ination,Englishman,icolor,red,same))continue;/*====End of Basic Exculision Operation====*/for(idrink=0;idrink<PERM;idrink++){DBGTRACE("enter idrink loop\n");/*---------Basic Exculision Operation------*///C8if(perm[idrink][2]!=milk)continue;//C5if(!checkCondition(perm,icolor,green,idrink,coffee,same))continue;//C3if(!checkCondition(perm,ination,Dane,idrink,tea,same))continue;/*====End of Basic Exculision Operation====*/for(icigar=0;icigar<PERM;icigar++){DBGTRACE("enter icigar loop\n");/*---------Basic Exculision Operation------*///C7if(!checkCondition(perm,icolor,yellow,icigar,Dunhill,same))continue;//C11if(!checkCondition(perm,idrink,bier,icigar,BlueMaster,same))continue;//C13if(!checkCondition(perm,ination,German,icigar,Prince,same))continue;/*====End of Basic Exculision Operation====*/for(ipet=0;ipet<PERM;ipet++){int index[5];loopcounter++;DBGTRACE("loopcounter:%d\n",loopcounter);//C6if(!checkCondition(perm,icigar,PallMall,ipet,birds,same))continue;//C2if(!checkCondition(perm,ination,Swede,ipet,dogs,same))continue;//C10if(!checkCondition(perm,icigar,Blend,ipet,cats,neighbor))continue;//C12if(!checkCondition(perm,ipet,horses,icigar,Dunhill,neighbor))continue;//C15if(!checkCondition(perm,icigar,Blend,idrink,water,neighbor))continue;/*=================================================*///If we reach here,the final results has been found.//Output the results and return.DBGTRACE("We have found the correct case using times:%d\n",loopcounter);index[0]=icolor;index[1]=ination;index[2]=idrink;index[3]=icigar;index[4]=ipet;for(i=0;i<COL;i++){for(j=0;j<COL;j++)array[i][j]=perm[index[i]][j];}//Call function to format the output.dispalyresult(array);return 0;}}} }}

由此产生了我们的第二版程序EinsteinRiddleV2.c。使用该程序编译后执行,非常令人欣慰:瞬间执行完毕。显然,我们的优化处理让该程序的性能提高成千上万倍。

2.2.2   抽象出条件检查

第二版程序中条件判定是依据题目陈述和实现的数据结构来编写粗糙的检查,存在大量地代码冗余与flag,既不方便维护也容易滋生其他的BUG,所以有必要对之进行抽象和优化。

这里我们抽象出一种条件检查的通用的函数:

条件陈述主体+条件陈述客体+两者关系

比如:已知条件1,英国人住红色房子

其中主体是英国人、客体是红色房子,关系是描述统一房子的情况。

又如:已知条件11,养马的人住抽Dunhill香烟的人隔壁

其中主体是养马的人,客体是Dunhill的人,关系是描述两者是邻居关系。

我们总共定义了四种关系:

enum Relation{same=1,neighbor,left,right};

其中,same表示描述的是同一所房子、neighbor表示描述的是相邻房子、left表示描述的主体在客体的左边,right表示描述主体在客体的右边。当然,如果还想描述其他类型的关系,还可以在这里添加合适类型,并在检查函数(checkCondition)中实现相应的检查功能。

/*
*checkCondition() function is used to check each condition from the original question.
*Each conditon consists of a subject/a object and their relation.
*Subject and object need key-value pairs to represent.
*Relation can be an integer only.
*parameters:
*0)char perm[][COL]:input the permutation table.
*1)int stype:subject type(including color,nation,cigar,drink,pet,position etc.)
*2)int svalue:subject corresponding value.
*3)int otype:object type(including color,nation,cigar,drink,pet,position etc.)
*4)int ovalue:object corresponding value.
*5)int relation:relation for subject and object: 1:on the same column.2:they are neighbors.3:subject is to the left of object.4:subject is to the right of objct.
enum Relation{same=1,neighbor,left,right}
*/static inline int checkCondition(char perm[][COL],int stype,int svalue,int otype,int ovalue,enum Relation relation)
{int i;//Process different relations.switch(relation){//Two attributes are on the same column.case same:for(i=0;i<COL;i++)if(perm[stype][i]==svalue&&perm[otype][i]==ovalue)return 1;return 0;break;//They are neighbors.case neighbor:for(i=0;i<COL;i++)if(perm[stype][i]==svalue){if(0==i&&perm[otype][1]==ovalue)return 1;else if(COL-1==i&&perm[otype][COL-2]==ovalue)return 1;else if(perm[otype][i-1]==ovalue||perm[otype][i+1]==ovalue)return 1;}return 0;break;//subject is to the left of object.case left:for(i=0;i<COL;i++)if(perm[stype][i]==svalue&&i<COL-1&&perm[otype][i+1]==ovalue)return 1;return 0;break;//subject is to the right of objct.case right:for(i=0;i<COL;i++)if(perm[stype][i]==svalue&&i>0&&perm[otype][i-1]==ovalue)return 1;return 0;break;default:printf("Error! Can't process this relation!");exit(1);}
}

由此产生了我们第三个版本的程序EinsteinRiddleV3.c,这个版本的代码看起来就相对清晰一点。

3      总结

虽然爱因斯坦谜题本身使用数学推理也不难解决,但是通过编程的方式来破解此谜题给我们的是另外一种不同思路。我们一时半会没有办法让程序具有“数学推理”的能力,但是借助于其高速的运算和可优化的潜力也可以很快地算出问题的答案。

当然,编程意义并不在于解决此数学问题,而在于创造出一种更加通用的工具,这样即使面对“成百上千”个新出现的爱因斯坦谜题,也可以运用工具快速解决。

C语言编程快速解决爱因斯坦谜题相关推荐

  1. P4语言编程快速开始 实践二

    参考:P4语言编程快速开始 上一篇系列博客:P4语言编程快速开始 实践二 Demo 2 本Demo所做的修改及实现的功能: 为simple_router添加一个计数器(counter),该计数器附加( ...

  2. 怎么运行c语言程序,如何使用C语言进行编程操作,教程在这里,C语言编程快速入门...

    原标题:如何使用C语言进行编程操作,教程在这里,C语言编程快速入门 C语言编程初级学习 编程语言有很多种,我们今天就介绍一种很瘦大家喜欢的C语言.该语言在大学中也是一门较为重要的学科,更是计算机学科里 ...

  3. 利用Python语言编程,解决鸡兔同笼问题,已知同一个笼子里鸡和兔总共有h只,鸡和兔的脚数总共有f只,其中h和f需要用户输入,求鸡和兔各有多少只。

    利用Python语言编程,解决鸡兔同笼问题,已知同一个笼子里鸡和兔总共有h只,鸡和兔的脚数总共有f只,其中h和f需要用户输入,求鸡和兔各有多少只. 提示:利用循环,采用枚举法. 要求:当输入的h和f没 ...

  4. 【爱因斯坦谜题】用python基础语法解决爱因斯坦谜题

    今天分享一个困难的谜题 据传是爱因斯坦还年轻时所想出来的 本人自学python已经有一段时间,偶然间了解到了爱因斯坦谜题,就想用已经学过的知识来解决这个谜题,下面我就将我的方法分享给大家. 题目: 有 ...

  5. C语言编程快速入门黎明,何用C语言模拟键盘输入?

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 static char szUnd[] ="_______ __ ____ _____ ______ ______ ___ _______ __ ...

  6. c语言编程快速收敛的圆周率计算,[原创]圆周率PI的计算(精确到几十万位)

    [原创]圆周率PI的计算(精确到几十万位) //环境:VC6.0,Console Application //原理:π=2+1/3*(2+2/5*(2+3/7*(2+... //特点:内嵌汇编提速并扩 ...

  7. C语言--爱因斯坦谜题

    尊重原创:http://blog.csdn.net/aspirationflow/article/details/7748364 C语言编程快速解决爱因斯坦谜题 2012年7月15日 1      问 ...

  8. linux+下c语言编程项目,精通UNIX下C语言编程与项目实践

    cc -I  //include 目录 -L //静态库目录?动态也可以 -l //小写L,接静态库名称?动态也可以 -DXXX='"XXFF"' //-D直接定义宏 -c 只编译 ...

  9. c语言写程序思路考研题,快速解题 | 在考场C语言编程题

    原标题:快速解题 | 在考场C语言编程题 对于考<C语言程序设计>的小伙伴们来说,程序设计题是很多同学觉得相对较难的一个版块.其题目虽然不算多,但在考研150分的试卷中,却占据了较大的比重 ...

最新文章

  1. ASP.Net 2.0 发送邮件的代码
  2. Linux下glibc内存管理
  3. svn命令行使用说明
  4. 记录flink和spark提交任务常用句
  5. python 线性回归 优化_python – 线性回归实现总是比sklearn更糟糕
  6. modprobe:FATAL: could not load /lib/modules/2.6.35-22-generic/modules.dep No such file or directory
  7. Datedifff返回两个日期之间的天数
  8. 鲲鹏凌云,并行科技Paramon通过华为云鲲鹏云服务兼容性认证
  9. python示例异常处理与程序调试_Python错误处理及代码调试方
  10. 从C#到Nodejs,从windowns到mac
  11. redis 删除key的命令_面试官问:Redis变慢了,你会怎么排查?
  12. VFP开眼看世界的第一眼,就是学会真正的BS开发,走错一步费三年
  13. SuperMap BIM+GIS技术白皮书
  14. 异速联:解决打印样式个性化设置
  15. 前几日共享HCIE面试战报兄弟的通过感言,看完之后希望你不要沉默
  16. ROS 2 Humble Hawksbill 环境基础
  17. 软件开发生命周期-2-每个阶段的利益相关方
  18. android FM的流程
  19. Jump Crypto:详解五大多链架构的特点与优劣势
  20. Unity插件之天气系统UniStorm

热门文章

  1. CSS3文字描边效果
  2. 2.16 这些都是我常用的修图工具【玩赚小红书】
  3. 创业未成功人士讲述未成功经验
  4. ldap over ssl php,ldapsearch over ssl/tls不起作用
  5. JVM 工作原理和流程
  6. 部分地区联通iphone4涨价300元
  7. background-color和border-color设置颜色相同,渲染出来不一致
  8. oracle soa 12c 教程,Oracle SOA Suite 12c Handbook
  9. 常见word,excel,ppt 转pdf
  10. one night in 北京-信乐团