셰릴의 생일은 언제일까?

피터 노빅, 2015년 4월

이 논리 문제는 최근에 인터넷에서 크게 이슈가 (역주: 이 글은 2015년에 쓰여진 글로, 이슈가 된 것은 번역 시점인 2018년 2월 기준 4년 전 기준 최근이 된다) 되었는데:

  1. 알버트와 버나드가 셰릴과 친구가 되었는데, 생일을 알고 싶어한다. 셰릴은 다음 10가지 후보를 그들에게 주었는데:

        5월 15일     5월 16일     5월 19일
        6월 17일     6월 18일
        7월 14일     7월 16일
        8월 14일     8월 15일     8월 17일
    
  2. 그리고 나서 셰릴은 알버트와 버나드에게 각각 월과 일을 가르쳐주었다.

  3. 알버트: 셰릴의 생일이 언제인지 모르겠지만, 버나드도 모른다는건 확실해.

  4. 버나드: 처음엔 셰릴의 생일이 언제인지 몰랐는데, 이제는 확실해졌다.

  5. 알버트: 그렇다면, 나도 셰릴의 생일이 언제인지 알겠다.

  6. 그래서, 셰릴의 생일은 언제인가?

문제 해결을 위한 도구

셰릴의 문제는 수학 역사상 최고의 도구라고도 할 수 있는 종이와 연필을 (사람에 따라서 펜, 분필, 마커, 또는 모래를 선호하기도 한다만) 이용하여 풀 수 있도록 만들어져 있는데, 여기서는 다른 도구 - 프로그램 - 을 이용하여 풀어보려 한다. 이 도구를 선택한 이유는 다음과 같다.

  • 답을 찾아가기 위한 방법 중에서 상대적으로 직접적인 방법이다. 조건을 충실하게 "10개의 후보 날짜를 갖고, 알버트와 버나드 각각에가 월과 일을 알려준 다음 3에서 5의 조건이 참인지 확인하는" 와 같이 확인하는 프로그램을 만들면 된다. 의도한대로 종이와 연필로 풀이를 하려면, 문제의 이해를 해야할 뿐 아니라, 에 가까워지기 위해 한 단계씩 직접 풀어나가야 하는데; 아무래도 더 어려울 수 밖에 없다.
  • 검증된 코드를 이용하면 실수로 오답을 낼 여지가 적어진다.
  • 유사한 문제이나, 10개의 경우의 수가 아니라 수백만개의 경우의 수가 있어서 종이와 연필로 불가능한 문제도 풀 수 있는 바탕이 된다.
  • 문제를 풀이와 프로그래밍은 재미있는데, 두개를 결합하면 배로 재미있다.

상기 6개의 조건을 파이썬으로 번역하면 다음과 같다:

1. 셰릴은 다음 10가지 후보를 그들에게 주었는데:

In [21]:
DATES = ['5월 15일', '5월 16일', '5월 19일',
         '6월 17일', '6월 18일',
         '7월 14일', '7월 16일',
         '8월 14일', '8월 15일', '8월 17일']

월과 일에 대해서 쉽게 접근할 수 있는 함수를 만들어야 한다:

In [22]:
def Month(date): return date.split()[0]

def Day(date):   return date.split()[1]
In [23]:
Month('5월 15일')
Out[23]:
'5월'
In [24]:
Day('5월 15일')
Out[24]:
'15일'

2. 그리고 나서 셰릴은 알버트와 버나드에게 각각 월과 일을 가르쳐주었다.

가르쳐주는 행위를 여기서 정의하고, 하는 김에 아는 행위 또한 정의할 수 있는데:

In [47]:
def tell(part, possible_dates=DATES):
    "셰릴이 생일의 일부를 누군가에게 말해주면, 그 일부에 대하여 가능성 있는 후보를 반환한다."
    return [date for date in possible_dates if part in date]

def know(possible_dates):
    "후보 날짜가 1개로 좁혀진 경우에 대상은 생일은 안다고 할 수 있다."
    return len(list(possible_dates)) == 1

날짜의 목록을 이용하여 대상 인물이 알고 있는 후보 날짜를 표현하게 되며, 생일 자체를 아는 것은 후보 날짜가 1개로 좁혀진 경우가 된다. 예를 들면: 셰릴이 5월에 자신의 생일이 있다고 알버트에게 알려줄 경우, 알버트는 3개의 후보 날짜를 알고 있게 되는데:

In [26]:
tell('5월')
Out[26]:
['5월 15일', '5월 16일', '5월 19일']

버나드에게 생일이 15일이라고 알려주면, 버나드는 2개의 후보 날짜를 알고 있게 된다:

In [27]:
tell('15일')
Out[27]:
['5월 15일', '8월 15일']

두개의 후보를 갖고 있으니, 버나드는 생일을 알 수 없다:

In [28]:
know(tell('15일'))
Out[28]:
False

전반적인 전략

셰릴이 알버트에게 '5월' 이라고 하면 알버트는 3가지 후보 날짜가 있는 것을 알게 되는데, 문제를 푸는 입장에서는 셰릴이 무슨 말을 했는지 모르기 때문에 그 정보가 없다. 이때 어떻게 해야 할까? 모든 경우의 수를 고려하면 된다. 예를 들어, '5월 15일' 이라고 가정을 할 경우, 셰릴은 알버트에게 '5월', 버나드에게는 '15일'을 알려주게 되며, 각각에게는 위에 예시로 든 후보 날짜를 알고 있게 된다. 이 정보를 바탕으로, 3에서 5절의 제약들이 참인지 확인하면 된다. 확인 결과 그러할 경우, '5월 15일'은 문제의 해가 된다. 전체 후보 날짜에 대해서 한번씩 검증을 하면, 이상적으로 1개의 답으로 좁혀져야 한다.

아래에 있는 cheryls_birthday는 메인 함수에 해당된다:

In [58]:
# 역주: Python 3.x 에서 filter가 리스트 형을 반환하지 않게 되어,
# 기능 측면에서는 좋아졌으나 지금과 같은 문제 풀이를 하는 경우에는 확인이 오히려 불편해져,
# list_filter 라는 헬퍼 함수를 정의하여 사용한다.

list_filter = lambda predicate, items: list(filter(predicate, items))
In [59]:
def cheryls_birthday(possible_dates=DATES):
    "조건 3부터 5가 참인 날짜의 목록을 반환한다."
    return list_filter(statements3to5, possible_dates)

def statements3to5(date): return statement3(date) and statement4(date) and statement5(date)

## TO DO: define statement3, statement4, statement5

(파이썬 기능 설명: filter(predicate, items)predicate(item) 이 참인 items내 모든 item의 리스트를 반환한다.)

3. 알버트: 셰릴의 생일이 언제인지 모르겠지만, 버나드도 모른다는건 확실해.

함수 statement3 는 후보 날짜를 받아서 알버트가 말한 제약 조건이 해당 날짜에 대해서 참인지 거짓인지 반환한다. 알버트의 설명을 어떻게 파이썬 함수로 표현할 수 있을까? 이걸 조금 더 프로그램에 가깝게 풀어서 설명을 하면 다음과 같은데:

알버트: 셰릴이 생일의 월을 알려준 다음에도 정확한 생일은 알 수가 없었다. 셰릴이 버나드에게 무슨 일자를 알려줬는지 모르겠지만, 모든 경우의 수를 봤을때 버나드에게 그 날짜를 알려줬다면, 일자만으로는 정확한 생일을 알 수가 없다는건 확실하다.

이건 코드로 표현이 가능한데:

In [48]:
def statement3(date):
    "알버트: 셰릴의 생일이 언제인지 모르겠지만, 버나드도 모른다는건 확실해."
    possible_dates = tell(Month(date))
    return (not know(possible_dates) 
            and all(not know(tell(Day(d)))
                    for d in possible_dates))

함수가 동작하는지 확인을 해보면:

In [49]:
statement3('5월 15일')
Out[49]:
False

3번 제약 조건에 해당되는 모든 날짜를 다음과 같이 확인할수도 있다:

In [60]:
list_filter(statement3, DATES)
Out[60]:
['7월 14일', '7월 16일', '8월 14일', '8월 15일', '8월 17일']

4. 버나드: 처음엔 셰릴의 생일이 언제인지 몰랐는데, 이제는 확실해졌다.

말을 바꾸자면:

Bernard: 셰릴이 처음에 준 일자만으로는 알 수 가 없었다. 그런데 알버트가 말한 3번 조건에 해당되는 날짜로 좁히니 알겠다.

In [61]:
def statement4(date):
    "버나드: 처음엔 셰릴의 생일이 언제인지 몰랐는데, 이제는 확실해졌다."
    at_first = tell(Day(date))
    return (not know(at_first)
            and know(list_filter(statement3, at_first)))

3번과 4번을 모두 만족하는 날짜를 확인해보자:

In [62]:
list_filter(statement4, list_filter(statement3, DATES))
Out[62]:
['7월 16일', '8월 15일', '8월 17일']

여기서 잠시 - 버나드가 안다고 하지 않았던가? 왜 후보 날짜가 3개가 나오는 것일까? 실제로 버나드가 알고 있는 이유는 단순하다 - 푸는 입장에서는 모르는 정보를 가지고 있기 때문인데, 일자에 대한 정보를 갖고 있기 때문이다. 5번 절까지 확인하기 전에는 정확한 날짜를 알 수가 없다.

5. 알버트: 그렇다면, 나도 셰릴의 생일이 언제인지 알겠다.

알버트는 버나드의 4번 조건과 월이 언제인제 정보를 갖고, 날짜가 확실해진것 같다:

In [63]:
def statement5(date):
    "알버트: 그렇다면, 나도 셰릴의 생일이 언제인지 알겠다."
    return know(list_filter(statement4, tell(Month(date))))

6. 그래서 셰릴의 생일은 언제인가?

확인해보자:

In [64]:
cheryls_birthday()
Out[64]:
['7월 16일']

이렇게 셰릴의 생일은 7월 16일 이라는것을 도출해낼 수 있다. 이제는 셰릴의 생일을 알고 있다는 것이 True 라고도 할 수 있다:

In [17]:
know(cheryls_birthday())
Out[17]:
True