尊重原创:http://blog.youkuaiyun.com/aspirationflow/article/details/7748364
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 | ||||
宠物 |
属性 | 房子1 | 房子2 | 房子3 | 房子4 | 房子5 |
颜色 | 黄色 | 蓝色 | 红色 | 绿色 | 白色 |
国籍 | 挪威 | 丹麦 | 英国 | ||
饮料 | 水 | 茶 | 牛奶 | 咖啡 | 啤酒 |
香烟 | Dunhill | Blends | BlueMaster | ||
宠物 | 马 |
属性 | 房子1 | 房子2 | 房子3 | 房子4 | 房子5 |
颜色 | 黄色 | 蓝色 | 红色 | 绿色 | 白色 |
国籍 | 挪威 | 丹麦 | 英国 | 德国 | 瑞典 |
饮料 | 水 | 茶 | 牛奶 | 咖啡 | 啤酒 |
香烟 | Dunhill | Blends | PallMall | Prince | BlueMaster |
宠物 | 猫 | 马 | 鸟 | 狗 |
1.3 反思
使用数学推理方式找到其中判定技巧,当然可以很快地解决该问题;但是如果问题规模上升,条件更加庞大、关系更为复杂,仅靠单纯的推理可能难以解决问题。所以,通过计算机编程来解决该类问题是更为明智的选择。
2 程序设计
程序设计部分,尝试使用C语言编程来解决该问题。目前本人的编程环境是Windows下的MinGW,当然程序内容与平台及编译器相关并不大。
在实现该问题的过程,总共产生了三个不同版本的程序:
源码链接:http://download.youkuaiyun.com/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------*/
- //C14
- if(perm[icolor][1]!=blue)
- continue;
- //C4
- if(!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------*/
- //C9
- if(perm[ination][0]!=Norwegian)
- continue;
- //C1
- if(!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------*/
- //C8
- if(perm[idrink][2]!=milk)
- continue;
- //C5
- if(!checkCondition(perm,icolor,green,idrink,coffee,same))
- continue;
- //C3
- if(!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------*/
- //C7
- if(!checkCondition(perm,icolor,yellow,icigar,Dunhill,same))
- continue;
- //C11
- if(!checkCondition(perm,idrink,bier,icigar,BlueMaster,same))
- continue;
- //C13
- if(!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);
- //C6
- if(!checkCondition(perm,icigar,PallMall,ipet,birds,same))
- continue;
- //C2
- if(!checkCondition(perm,ination,Swede,ipet,dogs,same))
- continue;
- //C10
- if(!checkCondition(perm,icigar,Blend,ipet,cats,neighbor))
- continue;
- //C12
- if(!checkCondition(perm,ipet,horses,icigar,Dunhill,neighbor))
- continue;
- //C15
- if(!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 总结
虽然爱因斯坦谜题本身使用数学推理也不难解决,但是通过编程的方式来破解此谜题给我们的是另外一种不同思路。我们一时半会没有办法让程序具有“数学推理”的能力,但是借助于其高速的运算和可优化的潜力也可以很快地算出问题的答案。
当然,编程意义并不在于解决此数学问题,而在于创造出一种更加通用的工具,这样即使面对“成百上千”个新出现的爱因斯坦谜题,也可以运用工具快速解决。