在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 ; // ! 她 -> 答案.上午還是下午 ; } }