谁在撒谎
在lltong网友的博客 重拾C,一天一点点_7 中看到了一道小题,觉得挺有趣。这个题目是这样的:
有一个人在一个森林里迷路了,他想看一下时间,可是又发现自己没带表。恰好他看到前面有两个小女孩在玩耍,于是他决定过去打听一下。更不幸的是这两个小女孩有一个毛病,姐姐上午说真话,下午就说假话,而妹妹与姐姐恰好相反。但他还是走近去他问她们:“你们谁是姐姐?”胖的说:“我是。”瘦的也说:“我是。”他又问:现在是什么时候?胖的说:“上午。”“不对”,瘦的说:“应该是下午。” 这下他迷糊了,到底他们说的话是真是假?
这个小题之所以引起了我的兴趣,是因为一种直觉:这个题目虽小,但似乎却并不容易写。
有时候我们轻而易举能解决的小问题,反而不容易写出代码。因为问题太简单可能反而会让我们找不到算法。例如,几乎每个人都知道123是一个三位数,但对于初学者来说,写出求123是几位数的代码,却可能难于登天。因为这个问题太容易了,所以反而想不到用不断地除以10的办法来解决这个问题。
因此写小问题的代码,有时可能很有意义。因为这可以帮助我们发现自己的盲点,清晰自己的思路。
下面就来试解这个问题。
这个问题的一个特点是头绪较多,有姐姐、妹妹,胖子、瘦子,真话、假话,上午、下午,而且这些头绪复杂地交织在一起。这就是这个题目虽然小得可以直接猜到答案但却很不容易用代码解决的原因之一。多个因素,越是微缩化地结合在一起就越不容易解开。通常情况下,解开细绳结比解开粗绳节要难得多。道理是一样的。
编程首先要将问题数值化,也就是把姐姐、妹妹等等都用0,1这样的数值来表示或进行在代码层面的映射。但是由于这种数值表示离我们的思维较远,我们并不习惯甚至根本不会把一切都抽象为数值来进行思考。所以为了更符合人类的思维习惯,为了代码的可读性,多数语言都提供了返璞归真重新回人类思考轨道的手段,比如用宏名替代常量。
另一个常用手段就是枚举(enum)。枚举可以让我们更自然地思考而不是更“机器”地思考。因此,在还没想如何解决问题之前,我就先写下了这样的声明:
因为问题中有上午和下午,所以
typedef enum { 上午 , 下午 , } 时间;
因为问题中有姐姐和妹妹,所以
typedef enum { 姐姐 , 妹妹 , } 姐妹;
姐妹俩的回答都有两部分,所以
typedef struct { 时间 上午还是下午; 姐妹 姐姐还是妹妹; } 回答;
回答可能是真可能是假,因此
typedef enum { false , true , } bool;
在这里我随了一下俗,用了英文单词作为标识符。但我并没有使用C99提供的bool类型(_Bool),因为_Bool这种类型不适合写循环语句。
这样,就可以用下面的方式完整地描述一个女孩:
typedef struct { 回答 答案 ; 姐妹 身份 ; bool 说的话是真是假 ; } 女孩;
女孩一共有两名,因此定义两个变量
女孩 胖子 = { { 姐姐 , 上午 } } , 瘦子 = { { 姐姐 , !上午 } } ;
此外当时的时间也是待定之数,因此还需要定义一个记录时间的变量:
时间 现在 ;
现在就可以考虑问题的求解了。
这个问题可以从多个方向入手。一个较为简单的切入点是,任何一个女孩只有说真话和说假话两种可能,所以列举出这两种可能是非常容易的:
for ( 胖子.说的话是真是假 = false ; 胖子.说的话是真是假 <= true ; 胖子.说的话是真是假 ++ ) { }
根据胖女孩说的话是真是假,可以简单地求出她是姐姐还是妹妹以及当时的时间:
时间 求时间并确认身份( 女孩 * ) ; for ( 胖子.说的话是真是假 = false ; 胖子.说的话是真是假 <= true ; 胖子.说的话是真是假 ++ ) { 现在 = 求时间并确认身份( & 胖子 ); } 时间 求时间并确认身份( 女孩 * 她 ) { if ( 她 -> 说的话是真是假 == true ) { 她 -> 身份 = 她 -> 答案.姐姐还是妹妹 ; return 她 -> 答案.上午还是下午 ; } else { 她 -> 身份 = ! 她 -> 答案.姐姐还是妹妹 ; return ! 她 -> 答案.上午还是下午 ; } }
这样得到的结果可能是自相矛盾的。比如,假设胖女孩说的是假话,得到的结果是,现在是下午,她是妹妹。然而妹妹在下午说的却是真话,这就形成了一种悖论,就如同罗素提出的那个著名的理发师悖论一样。
理发师悖论是由伯特兰·罗素在1901年提出的,说的是村子里的理发师声称他只帮村子里所有不为自己理发的人理发。问题在于他是否给自己理发?如果给自己理发,那么就违反了他自己说的话,如果他不给自己理发,则同样与他自己的说法相悖。这个悖论导致了数学史上的第三次危机,后来的数学家又忙活了好多年,才在数学基础中避免了这种自相矛盾。
在我们这个问题中同样应该避免出现悖论,否则可能会导致很荒谬的结果。所以在进一步求解之前,需要排除这种情况。
bool 没有矛盾( 时间 , 女孩 * ) ; if ( 没有矛盾( 现在 , & 胖子 ) == true ) { //继续求解 } bool 没有矛盾( 时间 现在 , 女孩 * 她 ) { switch ( 现在 ) { case 上午: switch ( 她 -> 说的话是真是假 ) { case true : return 她 -> 身份 == 姐姐 ; case false : return 她 -> 身份 == 妹妹 ; } case 下午: switch ( 她 -> 说的话是真是假 ) { case true : return 她 -> 身份 == 妹妹 ; case false : return 她 -> 身份 == 姐姐 ; } } }
排除了自相矛盾的情况,就可以搜寻瘦女孩的各种可能性了。
由于一个是姐姐,另一个是妹妹,所以:
瘦子.身份 = !胖子.身份 ;
无论是上午还是下午,一个说的若是真话,另一个说的必然是假话:
瘦子.说的话是真是假 = ! 胖子.说的话是真是假 ;
再排除瘦女孩解中自相矛盾的可能,就可以直接输出结果了:
if ( 没有矛盾( 现在 , & 瘦子 ) == true ) { printf( "胖子说的是%s话\n" , 胖子.说的话是真是假?"真":"假" ); printf( "瘦子说的是%s话\n" , 瘦子.说的话是真是假?"真":"假" ); }
至此,代码完成。
时间 求时间并确认身份( 女孩 * ) ; bool 没有矛盾( 时间 , 女孩 * ) ; int main( void ) { 女孩 胖子 = { { 姐姐 , 上午 } } , 瘦子 = { { 姐姐 , !上午 } } ; 时间 现在 ; for ( 胖子.说的话是真是假 = false ; 胖子.说的话是真是假 <= true ; 胖子.说的话是真是假 ++ ) { 现在 = 求时间并确认身份( & 胖子 ); if ( 没有矛盾( 现在 , & 胖子 ) == true ) { 瘦子.身份 = !胖子.身份 ; 瘦子.说的话是真是假 = ! 胖子.说的话是真是假 ; if ( 没有矛盾( 现在 , & 瘦子 ) == true ) { printf( "胖子说的是%s话\n" , 胖子.说的话是真是假?"真":"假" ); printf( "瘦子说的是%s话\n" , 瘦子.说的话是真是假?"真":"假" ); } } } system("PAUSE"); return 0; } bool 没有矛盾( 时间 现在 , 女孩 * 她 ) { switch ( 现在 ) { case 上午: switch ( 她 -> 说的话是真是假 ) { case true : return 她 -> 身份 == 姐姐 ; case false : return 她 -> 身份 == 妹妹 ; } case 下午: switch ( 她 -> 说的话是真是假 ) { case true : return 她 -> 身份 == 妹妹 ; case false : return 她 -> 身份 == 姐姐 ; } } } 时间 求时间并确认身份( 女孩 * 她 ) { if ( 她 -> 说的话是真是假 == true ) { 她 -> 身份 = 她 -> 答案.姐姐还是妹妹 ; return 她 -> 答案.上午还是下午 ; } else { 她 -> 身份 = ! 她 -> 答案.姐姐还是妹妹 ; return ! 她 -> 答案.上午还是下午 ; } }
有人可能觉得这不是C代码,实际上这是C代码。C99之后C语言允许使用汉字作为标识符,尽管我还没有看见过这样的编译器。微软的VS声然号称不支持C99,但是在允许使用汉字作为标识符这点上却是对C99支持最好的。
可惜我手头没有VS,所以我还是把汉字标识符换成拉丁字符吧。
/* 有一个人在一个森林里迷路了,他想看一下时间,可是又发现自己没带表。 恰好他看到前面有两个小女孩在玩耍,于是他决定过去打听一下。 更不幸的是这两个小女孩有一个毛病, 姐姐上午说真话,下午就说假话,而妹妹与姐姐恰好相反。 但他还是走近去他问她们:“你们谁是姐姐?”胖的说:“我是。”瘦的也说:“我是。” 他又问:现在是什么时候?胖的说:“上午。”“不对”,瘦的说:“应该是下午。” 这下他迷糊了,到底他们说的话是真是假? */ #include <stdio.h> #include <stdlib.h> typedef enum { SW , /* 上午 ,*/ XW , /* 下午 ,*/ } SJ ; /* 时间 ;*/ typedef enum { JJ , /* 姐姐 ,*/ MM , /* 妹妹 ,*/ } JM ; /* 姐妹 ;*/ typedef struct { SJ SWHSXW ; // 时间 上午还是下午; JM JJHSMM ; // 姐妹 姐姐还是妹妹; } HD ; // 回答; typedef enum { false , true , } bool; typedef struct { HD DA ; // 回答 答案; JM SF ; // 姐妹 身份; bool SDHSZSJ ; // bool 说的话是真是假; } NH ; //女孩; SJ QSJBQRSF( NH * ) ; //时间 求时间并确认身份(女孩 * ) ; bool MYMD( SJ , NH * ) ; //bool 没有矛盾( 时间 , 女孩 * ); int main( void ) { NH PZ = { { JJ, SW } } , //女孩 胖子 = { { 姐姐 , 上午 } } , SZ = { { JJ , ! SW } } ; // 瘦子 = { { 姐姐 , !上午 } } ; SJ XZ ; //时间 现在 ; for ( PZ.SDHSZSJ = false ; //for ( 胖子.说的话是真是假 = false ; PZ.SDHSZSJ <= true ; // 胖子.说的话是真是假 <= true ; PZ.SDHSZSJ ++ ) // 胖子.说的话是真是假 ++ ) { XZ = QSJBQRSF( & PZ ); //现在 = 求时间并确认身份( & 胖子 ); if ( MYMD( XZ , & PZ ) == true )// if ( 没有矛盾( 现在 , & 胖子 ) == true ) { SZ.SF = !PZ.SF ; // 瘦子.身份 = !胖子.身份 ; SZ.SDHSZSJ = ! PZ.SDHSZSJ ; // 瘦子.说的话是真是假 = ! 胖子.说的话是真是假 ; if ( MYMD( XZ , & SZ ) == true )// if ( 没有矛盾( 现在 , & 瘦子 ) == true ) { printf( "胖子说的是%s话\n" , PZ.SDHSZSJ?"真":"假" );//胖子.说的话是真是假 printf( "瘦子说的是%s话\n" , SZ.SDHSZSJ?"真":"假" );//瘦子.说的话是真是假 } } } system("PAUSE"); return 0; } bool MYMD( SJ XZ , NH * Ta ) // bool 没有矛盾( 时间 现在 , 女孩 * 她 ) { switch ( XZ /* 现在*/ ) { case SW: /* 身份*/ switch ( Ta -> SDHSZSJ /* 她 -> 说的话是真是假*/ ) { case true : return Ta -> SF == JJ ; // 她 -> 身份 == 姐姐 ; case false : return Ta -> SF == MM ; // 她 -> 身份 == 妹妹 ; } case XW: /* 下午:*/ switch ( Ta -> SDHSZSJ /*她 -> 说的话是真是假*/ ) { case true : return Ta -> SF == MM ; //她 -> 身份 == 妹妹 ; case false : return Ta -> SF == JJ ; //她 -> 身份 == 姐姐 ; } } } SJ QSJBQRSF( NH * Ta ) //时间 求时间并确认身份( 女孩 * 她 ) { if ( Ta -> SDHSZSJ == true ) //( 她 -> 说的话是真是假 == true ) { Ta -> SF = Ta -> DA.JJHSMM ; //她 -> 身份 = 她 -> 答案.姐姐还是妹妹 ; return Ta -> DA.SWHSXW ; //她 -> 答案.上午还是下午 ; } else { Ta -> SF = ! Ta -> DA.JJHSMM ;// 她 -> 身份 = ! 她 -> 答案.姐姐还是妹妹 ; return ! Ta -> DA.SWHSXW ; // ! 她 -> 答案.上午还是下午 ; } }