This commit is contained in:
Peter Norvig 2025-05-30 14:07:45 -07:00 committed by GitHub
parent 7f591a2f44
commit 95f9253d30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 1309 additions and 185 deletions

View File

@ -8,10 +8,8 @@
"\n",
"# When Cheryl Met Eve: A Birthday Story\n",
"\n",
"The ***Cheryl's Birthday*** logic puzzle [made the rounds](https://www.google.com/webhp?#q=cheryl%27s+birthday),\n",
"and I wrote [code](Cheryl.ipynb) that solves it. In that notebook I said that one reason for solving the puzzle with code rather than pencil and paper is that you can do more with code. \n",
"\n",
"**[Gabe Gaster](http://www.gabegaster.com/)** proved me right when he [tweeted](https://twitter.com/gabegaster/status/593976413314777089/photo/1) that he had used my code to generate a new list of dates that satisfies the constraints of the puzzle:\n",
"The ***Cheryl's Birthday*** logic puzzle [made the rounds](https://www.google.com/search?q=cheryl%27s+birthday),\n",
"and I wrote [code](Cheryl.ipynb) that solves it. In that notebook I said that one reason for solving the puzzle with code rather than pencil and paper is that you can do more with code. **[Gabe Gaster](http://www.gabegaster.com/)** proved me right when he [tweeted](https://twitter.com/gabegaster/status/593976413314777089/photo/1) that he had used my code to generate a new list of dates that satisfies the constraints of the puzzle:\n",
"\n",
" January 15, January 4,\n",
" July 13, July 24, July 30,\n",
@ -56,10 +54,10 @@
"- The global variable `DATES` is a set of all possible dates (each date is a string).\n",
"- `know(possible_dates)` is a function that returns `True` when there is only one possible date.\n",
"- `told(part)` is a function that returns the set of possible dates that remain after Cheryl tells a part (month or day).\n",
"- A statement is a function; *statement*`(date)` that returns true if the statement is true given that `date` is Cheryl's birthday.\n",
"- A statement is a function, *statement*`(date)`, that returns true if the statement is true given that `date` is Cheryl's birthday.\n",
"- `satisfy(possible_dates, statement,...)` returns a subset of possible_dates for which all the statements are true.\n",
"\n",
"In the [previous code](Cheryl.ipynb) I treated `DATES` as a constant, but in this version the whole point is exploring different sets of possible dates. The easiest way to refactor the code was to make `DATES` a global variable, and provide the function `set_dates` to set the value of the global variable. (It would be cleaner to package the dates into a non-global object, but it would be a big change to the code to inject this all the way down to the function `told`, where it is needed.)"
"In the [previous code](Cheryl.ipynb) I treated `DATES` as a constant, but in this version the whole point is exploring different sets of possible dates. The easiest way to refactor the code was to keep `DATES` as a global variable, and provide the function `set_dates` to set the value of the global variable. (It would be cleaner to package the dates into a non-global object, but it would be a big change to the code to inject this all the way down to the function `told`, where it is needed.)"
]
},
{
@ -68,7 +66,7 @@
"metadata": {},
"outputs": [],
"source": [
"BeliefState = set # A set of possible values\n",
"BeliefState = set # A set of possible values that a person believes might be the birthday\n",
"\n",
"DATES = BeliefState({'May 15', 'May 16', 'May 19', 'June 17', 'June 18', \n",
" 'July 14', 'July 16', 'August 14', 'August 15', 'August 17'})\n",
@ -121,7 +119,7 @@
"metadata": {},
"outputs": [],
"source": [
"# Some tests\n",
"# Some simple tests\n",
"\n",
"assert month('May 19') == 'May'\n",
"assert day('May 19') == '19'\n",
@ -132,7 +130,7 @@
"assert not know(told('15')) # Bernard does not know\n",
"\n",
"assert told('June') == {'June 17', 'June 18'} # Albert's belief state for June 18\n",
"assert told('18') == {'June 18'} # Bernard's belief state\n",
"assert told('18') == {'June 18'} # Bernard's belief state for June 18\n",
"assert not know(told('June')) # Albert does not know\n",
"assert know(told('18')) # Bernard DOES know"
]
@ -240,7 +238,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Gabe tweeted these ten dates:"
"Gabe Gaster tweeted these ten dates:"
]
},
{
@ -269,8 +267,8 @@
"metadata": {},
"outputs": [],
"source": [
"assert cheryls_birthday(gabe_dates) == {'July 30'}\n",
"assert know(cheryls_birthday(gabe_dates))"
"assert know(cheryls_birthday(gabe_dates))\n",
"assert cheryls_birthday(gabe_dates) == {'July 30'}"
]
},
{
@ -284,7 +282,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"If Gabe can do it, we can do it! Our strategy will be to repeatedly pick a random sample of dates, and check if they solve the puzzle. We'll limit ourselves to a subset of dates (not all 366) to make it more likely that a random selection will have multiple dates with the same month and day (otherwise Albert and/or Bernard would know right away):"
"If Gabe can do it, so can we! Our strategy will be to repeatedly pick a random sample of dates, and check if they solve the puzzle. We'll limit ourselves to a subset of dates (not all 366) to make it more likely that a random selection will have multiple dates with the same month and day (otherwise Albert and/or Bernard would know right away):"
]
},
{
@ -316,7 +314,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Now we need to cycle through random samples of these possible dates until we hit one that works. I anticipate wanting to solve other puzzles besides the original `cheryls_birthday`, so I'll define the function `pick_dates` to take a parameter, `puzzle`, and keep trying `puzzle(random_dates)` until the result is something we `know`:"
"Now we need to cycle through random samples of these possible dates until we hit one that works. I anticipate wanting to solve other puzzles besides the original `cheryls_birthday`, so I'll define the function `good_dates` to take a parameter, `puzzle`, and keep trying a set of `n` random dates until we `know(puzzle(random_dates))`:"
]
},
{
@ -327,12 +325,11 @@
"source": [
"import random\n",
"\n",
"def pick_dates(puzzle, k=10) -> BeliefState:\n",
" \"\"\"Pick a set of `k` dates for which the `puzzle` has a unique solution.\"\"\"\n",
"def good_dates(puzzle, n=10) -> BeliefState:\n",
" \"\"\"Pick a set of `n` dates for which the `puzzle` has a unique solution.\"\"\"\n",
" while True:\n",
" random_dates = set(random.sample(many_dates, k))\n",
" solutions = puzzle(random_dates)\n",
" if know(solutions):\n",
" random_dates = set(random.sample(many_dates, n))\n",
" if know(puzzle(random_dates)):\n",
" return random_dates"
]
},
@ -344,16 +341,16 @@
{
"data": {
"text/plain": [
"{'April 17',\n",
" 'April 18',\n",
" 'August 20',\n",
" 'June 13',\n",
" 'June 16',\n",
" 'June 26',\n",
"{'April 13',\n",
" 'April 17',\n",
" 'April 29',\n",
" 'August 23',\n",
" 'June 10',\n",
" 'June 23',\n",
" 'June 28',\n",
" 'March 17',\n",
" 'March 18',\n",
" 'March 20'}"
" 'March 10',\n",
" 'March 28',\n",
" 'May 27'}"
]
},
"execution_count": 11,
@ -362,7 +359,7 @@
}
],
"source": [
"pick_dates(cheryls_birthday)"
"good_dates(cheryls_birthday)"
]
},
{
@ -373,7 +370,7 @@
{
"data": {
"text/plain": [
"{'July 13', 'March 13', 'March 16', 'March 19', 'May 16', 'May 19'}"
"{'August 27', 'August 29', 'July 18', 'July 27', 'July 29', 'March 18'}"
]
},
"execution_count": 12,
@ -382,7 +379,7 @@
}
],
"source": [
"pick_dates(cheryls_birthday, k=6)"
"good_dates(cheryls_birthday, n=6)"
]
},
{
@ -400,11 +397,11 @@
"source": [
"from collections import Counter\n",
"\n",
"def solution_lengths(puzzle, N=10000, k=10, many_dates=many_dates):\n",
"def solution_lengths(puzzle, N=10000, n=10, many_dates=many_dates):\n",
" \"\"\"Try N random samples of k dates and count how often each possible \n",
" length-of-puzzle-solution appears.\"\"\"\n",
" return Counter(len(puzzle(random.sample(many_dates, k)))\n",
" for _ in range(N))"
" solutions = (puzzle(random.sample(many_dates, n)) for _ in range(N))\n",
" return Counter(sorted(map(len, solutions)))"
]
},
{
@ -415,7 +412,7 @@
{
"data": {
"text/plain": [
"Counter({0: 9572, 1: 153, 2: 275})"
"Counter({0: 9595, 1: 146, 2: 259})"
]
},
"execution_count": 14,
@ -433,7 +430,7 @@
"source": [
"This says that a bit over 1% of the time we get a unique solution (a set of length 1). More often than that we get an ambiguous solution (with 2 or more possible birth dates), and about 95% of the time the sample of dates has no solution (a set of length 0).\n",
"\n",
"What happens if Cheryl changes the number of possible dates?"
"What happens if Cheryl changes the number of possible dates? Let's try 6 possible dates:"
]
},
{
@ -444,7 +441,7 @@
{
"data": {
"text/plain": [
"Counter({0: 99926, 2: 54, 1: 20})"
"Counter({0: 99914, 1: 18, 2: 68})"
]
},
"execution_count": 15,
@ -453,7 +450,14 @@
}
],
"source": [
"solution_lengths(cheryls_birthday, k=6, N=100_000)"
"solution_lengths(cheryls_birthday, n=6, N=100_000)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"With 6 dates, we get no solution 99.9% of the time, but there are a few good solutions (of length 1). Let's try a whole range of values for `n`:"
]
},
{
@ -464,7 +468,18 @@
{
"data": {
"text/plain": [
"Counter({0: 9201, 1: 352, 2: 441, 3: 6})"
"{4: Counter({0: 10000}),\n",
" 5: Counter({0: 10000}),\n",
" 6: Counter({0: 9996, 1: 1, 2: 3}),\n",
" 7: Counter({0: 9972, 1: 5, 2: 23}),\n",
" 8: Counter({0: 9881, 1: 40, 2: 79}),\n",
" 9: Counter({0: 9782, 1: 65, 2: 153}),\n",
" 10: Counter({0: 9617, 1: 135, 2: 248}),\n",
" 11: Counter({0: 9403, 1: 229, 2: 366, 3: 2}),\n",
" 12: Counter({0: 9173, 1: 317, 2: 506, 3: 4}),\n",
" 13: Counter({0: 8888, 1: 537, 2: 562, 3: 13}),\n",
" 14: Counter({0: 8633, 1: 685, 2: 645, 3: 36, 4: 1}),\n",
" 15: Counter({0: 8377, 1: 870, 2: 711, 3: 42})}"
]
},
"execution_count": 16,
@ -473,28 +488,28 @@
}
],
"source": [
"solution_lengths(cheryls_birthday, k=12)"
"{n: solution_lengths(cheryls_birthday, n=n) for n in range(4, 16)}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"With 6 dates, we get no solution 99.9% of the time, but with 12 dates, finding a set of dates with a unique solution is easy."
"We found no solutions with 4 or 5 dates (although we haven't proven it is impossible). In general, the more dates, the more good solutions."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# A New Puzzle: All About Eve"
"# A New Birthday Puzzle: All About Eve"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now let's see if we can create a more complicated puzzle. We'll introduce a new character, Eve, and keep the same puzzle as before, except that after Albert's second statement, Eve makes this statement:\n",
"Let's create a more complicated puzzle. We'll introduce a new character, Eve, and keep the same puzzle as before, except that after Albert's second statement, Eve says:\n",
"\n",
"- **Eve**: \"Hi, Everybody. My name is Eve and I'm an evesdropper. It's what I do! I peeked and saw the first letter of the month and the first digit of the day. When I peeked, I didn't know Cheryl's birthday, but after listening to Albert and Bernard I do. And it's a good thing I peeked, because otherwise I couldn't have\n",
"figured it out.\"\n",
@ -528,7 +543,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"*Note*: I admit I \"cheated\" a bit here. Remember that the function `told` tests for `(part in date)`. For that to work for Eve, we have to make sure that the first letter is distinct from any other character in the date (it is—because only the first letter is uppercase) and that the first digit is distinct from any other character (it is—because in `many_dates` I carefully made sure that the first digit is always 1 or 2, and the second digit is never 1 or 2). \n",
"*Note*: I admit I \"cheated\" a bit here. Remember that the function `told` tests for `(part in date)`. For that to work for Eve, we have to make sure that the first letter is distinct from any other character in the date (it is—because only the first letter is uppercase) and that the first digit is distinct from any other character (it is—because in `many_dates` I made sure that the first digit is always 1 or 2, and the second digit is never 1 or 2). \n",
"\n",
"I have no idea if it is possible to find a set of dates that works for this puzzle. But I can try:"
]
@ -542,15 +557,15 @@
"data": {
"text/plain": [
"{'April 16',\n",
" 'April 23',\n",
" 'August 24',\n",
" 'August 25',\n",
" 'June 18',\n",
" 'June 23',\n",
" 'June 28',\n",
" 'March 15',\n",
" 'April 25',\n",
" 'August 10',\n",
" 'June 10',\n",
" 'June 19',\n",
" 'March 14',\n",
" 'March 17',\n",
" 'March 25',\n",
" 'May 16',\n",
" 'May 24'}"
" 'May 17'}"
]
},
"execution_count": 18,
@ -559,7 +574,7 @@
}
],
"source": [
"pick_dates(cheryls_birthday_with_eve)"
"good_dates(cheryls_birthday_with_eve)"
]
},
{
@ -570,7 +585,7 @@
{
"data": {
"text/plain": [
"{'April 23'}"
"{'May 17'}"
]
},
"execution_count": 19,
@ -597,7 +612,7 @@
{
"data": {
"text/plain": [
"Counter({0: 9773, 2: 119, 1: 108})"
"Counter({0: 9765, 1: 103, 2: 132})"
]
},
"execution_count": 20,
@ -620,7 +635,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"# An Even More Complex Puzzle"
"# An Even More Complex Birthday Puzzle"
]
},
{
@ -685,16 +700,16 @@
{
"data": {
"text/plain": [
"{'April 28',\n",
" 'August 16',\n",
" 'August 19',\n",
" 'August 28',\n",
" 'July 10',\n",
" 'July 16',\n",
" 'July 20',\n",
" 'June 14',\n",
" 'June 19',\n",
" 'March 18'}"
"{'April 15',\n",
" 'August 27',\n",
" 'July 17',\n",
" 'March 15',\n",
" 'March 16',\n",
" 'March 20',\n",
" 'March 26',\n",
" 'May 16',\n",
" 'May 17',\n",
" 'May 20'}"
]
},
"execution_count": 22,
@ -703,7 +718,7 @@
}
],
"source": [
"pick_dates(cheryls_birthday_complex)"
"good_dates(cheryls_birthday_complex)"
]
},
{
@ -714,7 +729,7 @@
{
"data": {
"text/plain": [
"{'August 28'}"
"{'May 20'}"
]
},
"execution_count": 23,
@ -741,7 +756,7 @@
{
"data": {
"text/plain": [
"Counter({0: 9386, 1: 613, 2: 1})"
"Counter({0: 9363, 1: 636, 2: 1})"
]
},
"execution_count": 24,
@ -757,7 +772,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Interesting. It was actually easier to find dates that work for this story than for any of the other stories."
"Interesting. It was actually easier to find good dates for this story than for the simpler versions."
]
},
{
@ -1067,7 +1082,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"_______\n",
"\n",
"\n",
"# New Puzzle: Steve's Bus\n",
"\n",
@ -1111,10 +1126,10 @@
"source": [
"Steve took the 8:32 bus.\n",
"\n",
"_________\n",
"\n",
"# Another New Puzzle: Evil Mad Scientist Cheryl\n",
"\n",
"I saw this posting:\n",
"\n",
"![](https://norvig.com/images/cheryl-trolley.png)\n",
"\n",
"Again, we can solve this problem just by passing in a different set of \"dates\":"
@ -1148,12 +1163,13 @@
"\n",
"(But may I point out that this Cheryl is not actually a mad scientist, just a [mad engineer](https://www.evilmadscientist.com/2015/evil-mad-engineers/). A true mad scientist would kill 25 people and use the other 25 as a control group.)\n",
"\n",
"_______\n",
"\n",
"# Yet Another Puzzle: Sum and Product\n",
"\n",
"Consider this puzzle:\n",
"\n",
"- A pair of numbers are chosen at random. They are both positive integers smaller than 100.\n",
"- Sam is told the sum of the numbers, while Pat is told the product of the numbers.\n",
"- **S**am is told the **S**um of the numbers, while **P**at is told the **P**roduct of the numbers.\n",
"- Then, this dialog occurs:\n",
"- 1) Pat: I don't know the numbers.\n",
"- 1) Sam: I don't know the numbers.\n",
@ -1170,10 +1186,11 @@
"- 7) Pat: I don't know the numbers.\n",
"- 7) Sam: I don't know the numbers.\n",
"- 8) Pat: Finally, I know the numbers.\n",
"- What are the two numbers?\n",
"\n",
"What are the two numbers? In this version of the puzzle, each person has 7 iterations of \"I don't know.\" What could the two numbers be if there are anywhere from 2 to 10 iterations? \n",
"In this version of the puzzle, each person has 7 iterations of \"I don't know.\" What could the two numbers be if instead of 7 iterations there are anywhere from 2 to 8 iterations? \n",
"\n",
"I could solve this using our existing `told` and `know` functions, but that would require encoding the pair of numbers, their sum, and their product into a string. So I'll introduce new functions instead:"
"I could solve this using our existing `told` and `know` functions, but that would require encoding the pair of numbers, their sum, and their product into a string. So I'll introduce new functions instead. At first I thought I would need two belief states, one for Pat and one for Sam. It is true that their belief states are different as the story progresses. But because we are only interested in the final answer, and because none of their statements refers to the other one's belief state (e.g. Sam never says \"I know that Pat does not know.\") we can get by with just one overall belief state over the possible pairs of integers."
]
},
{
@ -1204,13 +1221,13 @@
"from typing import Dict\n",
"from math import prod\n",
"\n",
"def sum_and_product(numbers=range(1, 100), N=8) -> Dict[int, BeliefState]:\n",
"def sum_and_product(integers=range(1, 100), N=9) -> Dict[int, BeliefState]:\n",
" \"\"\"Solves the Sum and Product puzzle for various numbers of repetitions of Pat and Sam saying \n",
" \"I don't know\" (up to `N` times) and for a given range of integers.\n",
" \"I don't know\" (up to `N` times) and for integers within a given range.\n",
" Returns a dictionary of {repetitions: {(x, y), ...}}.\"\"\"\n",
" pairs = set(combinations(numbers, 2))\n",
" pairs = set(combinations(integers, 2))\n",
" solutions = {}\n",
" for i in range(2, N + 1):\n",
" for i in range(2, N):\n",
" pairs = unknown_pairs(prod, pairs) # Pat doesn't know\n",
" pairs = unknown_pairs(sum, pairs) # Sam doesn't know\n",
" solutions[i] = known_pairs(prod, pairs) # Pat knows\n",
@ -1230,36 +1247,17 @@
]
},
{
"cell_type": "code",
"execution_count": 39,
"cell_type": "markdown",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{2: {(72, 92), (75, 96)},\n",
" 3: {(81, 88)},\n",
" 4: {(77, 90)},\n",
" 5: {(76, 90)},\n",
" 6: {(80, 84)},\n",
" 7: {(77, 84)},\n",
" 8: set()}"
]
},
"execution_count": 39,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sum_and_product(range(2, 100))"
"With 2 iterations there are 4 possible answers, so Pat wouldn't know for sure. But with anywhere from 3 to 7 iterations, there is a unique solution. For 8 iterations (and beyond) there are no solutions."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"_______\n",
"\n",
"\n",
"# What Next?\n",
"\n",

View File

@ -58,8 +58,8 @@
"metadata": {},
"outputs": [],
"source": [
"def month(date) -> str: return date.split()[0]\n",
"def day(date) -> str: return date.split()[1]\n",
"def month(date: str) -> str: return date.split()[0]\n",
"def day(date: str) -> str: return date.split()[1]\n",
"\n",
"assert month('May 15') == 'May'\n",
"assert day('May 15') == '15'"
@ -110,7 +110,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Now consider `'June 18'`; in this case, Bernard would know."
"Now consider `'June 18'`; in this case, Bernard would *know*."
]
},
{
@ -131,17 +131,17 @@
"source": [
"# Overall Strategy\n",
"\n",
"Now we have to think really carefully about what we're doing. The puzzle is tricky because we're dealing with two types of uncertainty:\n",
"The puzzle is tricky because we're dealing with two types of uncertainty:\n",
"\n",
"1. Albert and Bernard are initially uncertain about the birthdate. *(Cheryl knows something they don't know.)*\n",
"2. We (the puzzle solvers) don't know what Albert and Bernard were told. *(They know something we don't know.)*\n",
"2. We (the puzzle solvers) don't know what Albert and Bernard were told by Cheryl. *(They know something we don't know.)*\n",
"\n",
"If Cheryl tells Albert that the month is \"May\", his belief state becomes {May 15, May 16, May 19}. But we the puzzle solvers don't know what month he was told. To deal with this we will consider each of the ten dates one at a time and reason as follows: \n",
"- We know what Albert and Bernard were told; we have eliminated the second type of uncertainty. \n",
"- For each date, we know what Albert and Bernard were told; we have eliminated the second type of uncertainty. \n",
"- We can thus figure out if Albert and Bernard's statements are true (given this date). \n",
"- There should be only one date out of the ten that makes all the statements true; that's Cheryl's birthday.\n",
"\n",
"Here is the main function, `cheryls_birthday`, which computes the subset of possible dates that satisfy Albert and Bernard's statements. We will implement a **statement** as a boolean function that takes a single date as input and returns true if the statement would be true under the condition that the given date is Cheryl's actual birthday. (I will define the statements `albert1`, `bernard1`, and `albert2` a bit later)."
"Here is the main function, `cheryls_birthday`, which computes the subset of possible dates that satisfy Albert and Bernard's statements. We will implement a **statement** as a boolean function that takes a single date as input and returns true if the statement would be true under the condition that the given date is Cheryl's actual birthday. "
]
},
{
@ -152,14 +152,16 @@
"source": [
"def cheryls_birthday() -> BeliefState:\n",
" \"\"\"Return a subset of the global `DATES` for which all three statements are true.\"\"\"\n",
" return satisfy(DATES, albert1, bernard1, albert2)"
" return satisfy(DATES, albert1, bernard1, albert2)\n",
"\n",
"## TODO: Implement statements albert1, bernard1, albert2"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The function `satisfy` takes a belief state (a set of dates) and some statements and returns the subset that satisfies all the statements:"
"The function `satisfy` takes a belief state and some statements and returns the subset that satisfies all the statements:"
]
},
{
@ -287,7 +289,7 @@
"source": [
"Wait a minuteI thought that Bernard **knew**?! Why are there **three** possible dates? \n",
"\n",
"Bernard does indeed know; it is just that we, the puzzle solvers, don't know yet. That's because Bernard knows something we don't know: the day. If Bernard was told `'15'` then he would know `'August 15'`; if he was told `'17'` he would know `'August 17'`, and if he was told `'16'` he would know `'July 16'`. We'll need more information (coming in statement `albert2`) before *we* know.\n",
"According to the puzzle, Bernard does indeed know; it is just that we, the puzzle solvers, don't know yet. That's because Bernard was told something we don't know: the day. If Bernard was told `'15'` then he would know `'August 15'`; if he was told `'17'` he would know `'August 17'`, and if he was told `'16'` he would know `'July 16'`. We'll need more information (coming in statement `albert2`) before *we* know.\n",
"\n"
]
},
@ -348,6 +350,15 @@
"source": [
"**Success!** We have deduced that Cheryl's birthday is **July 16**. We know Cheryl's birthday:"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [],
"source": [
"assert know(cheryls_birthday())"
]
}
],
"metadata": {

View File

@ -2,46 +2,149 @@
"cells": [
{
"cell_type": "markdown",
"id": "e21c9af1-0087-440b-8dfe-758e0361f6e9",
"id": "af842d66-6b62-45e0-8ff3-3222494fbaed",
"metadata": {},
"source": [
"<div align=\"right\"><i>Peter Norvig<br>Sept 25, 2024<br>Update May 21, 2025</i></div>\n",
"\n",
"# LLMs, Theory of Mind, and Cheryl's Birthday\n",
"\n",
"There has been [much](https://spectrum.ieee.org/theory-of-mind-ai) [debate](https://aclanthology.org/2023.conll-1.25/) [on](https://www.gsb.stanford.edu/faculty-research/working-papers/theory-mind-may-have-spontaneously-emerged-large-language-models) [the](https://arxiv.org/abs/2302.02083) [degree](https://www.nature.com/articles/s41562-024-01882-z) to which Large Language Models (LLMs) have a theory of mind: a way of understanding what other people know and don't know. In this notebook I explore one small part of the issue by asking nine LLM chatbots to solve the [Cheryl's Birthday Problem](https://en.wikipedia.org/wiki/Cheryl%27s_Birthday), a well-known logic puzzle in which different characters have different states of knowledge at different times. I gave the candidate solvers two tasks:\n",
"1. **Write a program** to solve the problem, allowing for any possible set of dates.\n",
"2. **Solve** re-worded variants of the problem with different dates (so that they can't just retrieve a memorized answer).\n",
"There has been [much](https://spectrum.ieee.org/theory-of-mind-ai) [debate](https://aclanthology.org/2023.conll-1.25/) [on](https://www.gsb.stanford.edu/faculty-research/working-papers/theory-mind-may-have-spontaneously-emerged-large-language-models) [the](https://arxiv.org/abs/2302.02083) [degree](https://www.nature.com/articles/s41562-024-01882-z) to which Large Language Models (LLMs) have a [theory of mind](https://en.wikipedia.org/wiki/Theory_of_mind): a way of understanding what other people know and don't know. In this notebook I explore one small part of the issue by asking nine LLMs to solve the [Cheryl's Birthday Problem](https://en.wikipedia.org/wiki/Cheryl%27s_Birthday), a well-known logic puzzle in which different characters have different states of knowledge at different times. The LLMs were all familiar with the puzzle, so if I just asked them to solve it, they all would just recall the answer they have memorized, \"July 16.\" Instead I test the LLMs with the following two prompts, one a novel formulation that alters the dates and wording, and one that requires writing a general program that works for any set of dates:\n",
"\n",
"I did this originally in September 2024, and **all** of the LLMs failed.\n",
"But in 2025, all the models had updated versions, and Claude 3.7 Sonnet, Google Gemini 2.5 Pro and You.com Compute passed the test.\n",
"## Reasoning Test\n",
"\n",
"|September 2024|May 2025|\n",
"|----|----|\n",
"|✅ [A human programmer](https://github.com/norvig/)|✅ A human programmer|\n",
"|❌ [Anthropic Claude 3.5 Sonnet](https://claude.ai/new)|✅ Claude 3.7 Sonnet\n",
"|❌ [Gemini Advanced](https://gemini.google.com/app)|✅ Gemini 2.5 Pro|\n",
"|❌ [You.com](https://you.com/)|✅ You.com Compute|\n",
"|❌ [ChatGPT 4o](https://chatgpt.com/)|❌ ChatGPT o4-mini-high|\n",
"|❌ [Microsoft Copilot](https://copilot.microsoft.com|❌ Microsoft Copilot|\n",
"|❌ [Meta AI Llama 405B](https://www.meta.ai/)|❌ Meta AI|\n",
"|❌ [Perplexity](https://www.perplexity.ai/)|❌ Perplexity|\n",
"|❌ [Cohere Chat](https://cohere.com/chat)|❌ Cohere Command A|\n",
"|❌ [HuggingFace Chat](https://huggingface.co/chat/)|❌ HuggingChat v0.9.4|\n",
"1. **Ali and Bo are friends with Cam. Cam told them that her anniversary is one of 10 possible dates:**\n",
" - **April 17, April 18, April 28, July 16, July 17, July 19, June 16, June 29, March 18, March 19**\n",
"3. **Cam then privately tells Ali the month and Bo the day number of the anniversary.**\n",
"4. **Ali: \"I don't know when Cams anniversary is, and I know that Bo does not know it either.\"**\n",
"5. **Bo: \"At first I didn't know when Cams anniversary was, but I know now, after Ali's statement.\"**\n",
"6. **Ali: \"Then I also know when Cams anniversary is.\"**\n",
"7. **When is Cams anniversary?**\n",
"\n",
"The LLMs were all familiar with the problem, so I didn't have to describe it in the prompt, just name it. Most of them correctly recalled the answer to the original problem: July 16. But all of them in 2024, and the majority of them in 2025 failed to distinguish the different knowledge states of Alfred and Bernard. At least with respect to this problem, they had a poorly developed theory of mind. (Perhaps that is in part due to the fact that very few of the Python programs they were trained on deal with problems like this.)\n",
"## Programming Test\n",
"\n",
"- **What is the answer to the \"Cheryl's Birthday\" problem? Write a Python program to solve it. Make sure that the program will still work if the list of possible dates is changed.**\n",
"\n",
"\n",
"# 2024: Writing a Program\n",
"## Results\n",
"\n",
"Here is the prompt I used:\n",
"The correct answer for the reasoning test is **March 18**, and for the programming test, **July 16**.\n",
"\n",
"___\n",
"***What is the answer to the \"Cheryl's Birthday\" problem? Write a Python program to solve it. Make sure that the program will still work if the list of possible dates is changed.***\n",
"___\n",
"In **September 2024**, all of the LLMs failed both tests. They couldn't distinguish the different knowledge states of Alfred and Bernard. At least with respect to this problem, they had a poorly developed theory of mind. \n",
"\n",
"But in **May 2025**, all the LLMs had new versions, and three passed the programming test while four passed the reasoning test:\n",
"\n",
"|2024<br>Model/version|2025<br> version|2024<br> Program|2024<br> Reasoning|2025<br> Program|2025<br> Reasoning|\n",
"|--|--|:--:|:--:|:-:|:--:|\n",
"|[A human](https://github.com/norvig/)|older human|✅|✅|✅|✅|\n",
"|[Gemini Advanced](https://gemini.google.com/app)|2.5 Pro|❌ |❌|✅ |✅|\n",
"|[You.com](https://you.com/)|Compute|❌ |❌|✅ |✅|\n",
"|[Anthropic Claude 3.5 Sonnet](https://claude.ai/new)|Sonnet 4|❌ |❌|✅ |❌ |\n",
"|[ChatGPT 4o](https://chatgpt.com/)|o4-mini-high|❌ |❌|❌ |✅|\n",
"|[Perplexity](https://www.perplexity.ai/)|Pro|❌ |❌|❌ |✅|\n",
"|[Microsoft Copilot](https://copilot.microsoft.com)|Copilot|❌|❌ |❌|❌|\n",
"|[Meta AI Llama 405B](https://www.meta.ai/)|AI|❌ |❌|❌ |❌|\n",
"|[Cohere Chat](https://cohere.com/chat)|Command A|❌ |❌|❌ |❌|\n",
"|[HuggingFace Chat](https://huggingface.co/chat/)|v0.9.4|❌|❌ |❌|❌|"
]
},
{
"cell_type": "markdown",
"id": "cc9df830-5dcb-4f67-99a8-870f30653c64",
"metadata": {},
"source": [
"# 2024 and 2025: Reasoning\n",
"\n",
"As mentioned above, I created a reworded version of the puzzle and used it as a prompt:\n",
"\n",
"1. **Ali and Bo are friends with Cam. Cam told them that her anniversary is one of 10 possible dates:**\n",
" - **April 17, April 18, April 28, July 16, July 17, July 19, June 16, June 29, March 18, March 19**\n",
"3. **Cam then privately tells Ali the month and Bo the day number of the anniversary.**\n",
"4. **Ali: \"I don't know when Cams anniversary is, and I know that Bo does not know it either.\"**\n",
"5. **Bo: \"At first I didn't know when Cams anniversary was, but I know now, after Ali's statement.\"**\n",
"6. **Ali: \"Then I also know when Cams anniversary is.\"**\n",
"7. **When is Cams anniversary?**\n",
"\n",
"(The correct answer is **March 18**.)\n",
"\n",
"In **2024**, all the LLMs were generally headed in the right direction in their reasoning, but all made mistakes. For example, Claude says \"*Bo hears the day and realizes after Ali's statement. Since Bo did not initially know the date, the day number Bo heard must appear in more than one month. Therefore, the days 16, 18, and 19 must be eliminated since they have corresponding unique months.*\" But that's just not right; they don't have unique months. As it turns out, [http://you.com](you.com) did get the right answer on the first try. But some of the reasoning steps were wrong, so I re-ran it, and it failed the second time. Thus, all the LLMs failed in 2024. \n",
"\n",
"In **2025**, Gemini, You.com, ChatGPT, and Perplexity got the right answer, with well-reasoned steps. The others made mistakes similar to the 2024 versions. I'll only show one example of reasoning, by Gemini 2.5 Pro:\n"
]
},
{
"cell_type": "markdown",
"id": "55db0973-3189-44bb-8503-d61a1e01db68",
"metadata": {},
"source": [
"## Gemini Pro 2.5 \n",
"\n",
"\n",
"Each LLM provided explanatory output along with a program; for brevity I only show the explanatory output from the first LLM, ChatGPT 4o. My comments are in *[bracketed italics]*. \n"
"This is a classic logic puzzle that can be solved by systematically eliminating possibilities based on the statements made by Ali and Bo.\n",
"\n",
"1. The Initial Possibilities:\n",
"\n",
"Let's list the dates by month:\n",
"\n",
"- March: 18, 19\n",
"- April: 17, 18, 28\n",
"- June: 16, 29\n",
"- July: 16, 17, 19\n",
"\n",
"2. Ali's First Statement: \"I don't know when Cams anniversary is, and I know that Bo does not know it either.\"\n",
"\n",
"\"I don't know...\": This tells us that the month Ali was told must have more than one possible date. All four months (March, April, June, July) have multiple possibilities, so this part doesn't eliminate anything yet.\n",
"\"...and I know that Bo does not know it either.\": This is the crucial part. If Ali knows Bo doesn't know, it means that no matter which day is possible in her month, that day number must also appear in another month. If there was a day number unique to her month, she couldn't be sure Bo didn't know.\n",
"Let's look for unique day numbers in the original list:\n",
"\n",
"- 28 appears only in April.\n",
"- 29 appears only in June.\n",
"\n",
"If Ali was told April, she couldn't be sure Bo didn't know, because if the date was April 28, Bo (having '28') would know immediately.\n",
"If Ali was told June, she couldn't be sure Bo didn't know, because if the date was June 29, Bo (having '29') would know immediately.\n",
"Therefore, Ali must have been told a month where all possible days also appear in other months. This eliminates April and June.\n",
"\n",
"3. The Remaining Possibilities (after Ali's first statement):\n",
"\n",
"- March: 18, 19\n",
"- July: 16, 17, 19\n",
"\n",
"4. Bo's Statement: \"At first I didn't know when Cams anniversary was, but I know now, after Ali's statement.\"\n",
"\n",
"Bo now knows the date. This means that after eliminating April and June, his day number must now point to only one possible date among the remaining options (March 18, 19; July 16, 17, 19).\n",
"Let's check the day numbers in the remaining list:\n",
"- 16: Only appears in July 16. If Bo had 16, he would know.\n",
"- 17: Only appears in July 17. If Bo had 17, he would know.\n",
"- 18: Only appears in March 18. If Bo had 18, he would know.\n",
"- 19: Appears in March 19 and July 19. If Bo had 19, he would still not know.\n",
"\n",
"Since Bo now knows, his day number cannot be 19. This eliminates March 19 and July 19.\n",
"\n",
"5. The Remaining Possibilities (after Bo's statement):\n",
"\n",
"- March: 18\n",
"- July: 16, 17\n",
"\n",
"6. Ali's Second Statement: \"Then I also know when Cams anniversary is.\"\n",
"\n",
"Ali knows the month. She now knows the only possibilities are March 18, July 16, and July 17.\n",
"For her to know the exact date, her month must only have one option left in this list.\n",
"If her month was July, the options would be July 16 and July 17 she wouldn't know.\n",
"If her month was March, the only option is March 18 she would know.\n",
"Since Ali now knows, her month must be March.\n",
"Conclusion:\n",
"\n",
"The only remaining possibility that fits all the statements is March 18. Therefore, Cam's anniversary is March 18."
]
},
{
"cell_type": "markdown",
"id": "3028427a-7368-4992-8b48-9d70dd42959f",
"metadata": {},
"source": [
"# 2024: Programming\n",
"\n",
"None of the LLMs produced a correct program in 2024. Each LLM provided explanatory output along with a program; for brevity I only show the explanatory output from the first LLM, ChatGPT 4o, but I'll show the code for all hte LLMs. My comments in *[bracketed italics]* explain where they went wrong."
]
},
{
@ -49,7 +152,7 @@
"id": "c3e532bf-67b0-4976-aa5f-730401a4ef07",
"metadata": {},
"source": [
"# Human\n",
"## Human\n",
"\n",
"An actual human programmer (me) was able to write a program, shown in [**another notebook**](https://github.com/norvig/pytudes/blob/main/ipynb/Cheryl-and-Eve.ipynb), that correctly solves the original problem and also handles different sets of dates. I introduced the idea of a *BeliefState*, a set of possible dates that a person believes might possibly be the birthday, and I modeled a character's *statement* as a function that takes a particular date as input, and returns true if the date is consistent with the statement."
]
@ -59,7 +162,7 @@
"id": "55f6744e-356c-4fd8-af26-e7559618dd8d",
"metadata": {},
"source": [
"# ChatGPT 4o\n",
"## ChatGPT 4o\n",
"\n",
"*[ChatGPT 4o gives the following response, which starts with an excellent summary of the problem:]*\n",
"\n",
@ -181,7 +284,7 @@
"id": "93852dab-d167-4d14-8cc6-02979427eb01",
"metadata": {},
"source": [
"# Microsoft Copilot\n",
"## Microsoft Copilot\n",
"\n",
"*[Copilot makes a similar mistake.*]"
]
@ -235,7 +338,7 @@
"id": "d981531c-31f6-442e-b210-d0bff93d49bc",
"metadata": {},
"source": [
"# Gemini Advanced\n",
"## Gemini Advanced\n",
"\n",
"*[Gemini makes a similar mistake.]*\n"
]
@ -321,7 +424,7 @@
"id": "3932858f-170b-4979-a565-c3b41a74e7fe",
"metadata": {},
"source": [
"# Meta Llama 405B\n",
"## Meta Llama 405B\n",
"\n",
"*[Llama also fails to find a solution (and thus has no output). This program is quite flawed. I do like that it defines functions for the various pieces, but note that the function \"bernard_deduce\" is defined but never called.]*"
]
@ -373,7 +476,7 @@
"id": "02b1f7fd-918c-432d-8784-95dff3d4f86e",
"metadata": {},
"source": [
"# Claude 3.5 Sonnet\n",
"## Claude 3.5 Sonnet\n",
"\n",
"*[Claude makes a similar mistake. It deserves credit for explicitly trying an example with different dates, and it correctly reports that the second example has no solution, but that was by accident.]*"
]
@ -442,7 +545,7 @@
"id": "52003af0-0ea1-4ed4-9b5e-5b9b8862a068",
"metadata": {},
"source": [
"# Perplexity\n",
"## Perplexity\n",
"\n",
"*[Once again, this program has a similar mistake.]*"
]
@ -501,7 +604,7 @@
"id": "4b4e47b5-f5a2-491d-ad34-f8c08aceca59",
"metadata": {},
"source": [
"# Cohere Chat Playground\n",
"## Cohere Chat Playground\n",
"\n",
"*[Cohere too has the same mistake.]*"
]
@ -573,7 +676,7 @@
"id": "8379b2f8-9591-4f7e-be42-e5917ce0263c",
"metadata": {},
"source": [
"# HuggingFace Chat\n",
"## HuggingFace Chat\n",
"\n",
"*[HuggingFace is unique in suggesting a class definition, but still makes a similar mistake. Also, while it accepts a list of possible dates in the initializer, it then hard-codes certain months and days from the original problem.]*"
]
@ -628,7 +731,8 @@
" return self.possible_dates\n",
"\n",
"possible_dates = [(5, 14), (5, 15), (5, 16), (6, 15), (6, 16), (6, 17), (6, 18), (6, 19), \n",
" (7, 14), (7, 15), (7, 16), (7, 17), (7, 18), (8, 14), (8, 15), (8, 16), (8, 17), (8, 18), (8, 19)]\n",
" (7, 14), (7, 15), (7, 16), (7, 17), (7, 18), (8, 14), (8, 15), (8, 16), \n",
" (8, 17), (8, 18), (8, 19)]\n",
"problem = CherylBirthdayProblem(possible_dates)\n",
"print(problem.solve())"
]
@ -638,7 +742,7 @@
"id": "39904aa9-d97f-4b0c-a2d0-4731c2d4aff0",
"metadata": {},
"source": [
"# You.com Genius Mode\n",
"## You.com Genius Mode\n",
"\n",
"*[You.com makes a similar mistake, although the end result is that it selects the wrong date rather than no date.]*"
]
@ -681,51 +785,645 @@
},
{
"cell_type": "markdown",
"id": "dad267ee-36c9-4133-ae22-a436c2024e47",
"id": "3187709b-e6ff-45f7-9e06-a09420fdd79e",
"metadata": {},
"source": [
"# 2024: Solving a Re-worded Variant\n",
"# 2025: Programming\n",
"\n",
"I used [my program](https://github.com/norvig/pytudes/blob/main/ipynb/Cheryl-and-Eve.ipynb) to generate a new set of 10 dates that work. Then I changed the names of the chara cters and the wording of the puzzle, and used this as the prompt:\n",
"\n",
"___\n",
"1. **Ali and Bo are friends with Cam. Cam told them that her anniversary is one of 10 possible dates:**\n",
" - **April 17, April 18, April 28, July 16, July 17, July 19, June 16, June 29, March 18, March 19**\n",
"3. **Cam then privately tells Ali the month and Bo the day number of the anniversary.**\n",
"4. **Ali: \"I don't know when Cams anniversary is, and I know that Bo does not know it either.\"**\n",
"5. **Bo: \"At first I didn't know when Cams anniversary was, but I know now, after Ali's statement.\"**\n",
"6. **Ali: \"Then I also know when Cams anniversary is.\"**\n",
"7. **When is Cams anniversary?**\n",
"___\n",
"\n",
"\n",
"All the LLMs were generally headed in the right direction in their reasoning, but all made mistakes. For example, Claude says \"*Bo hears the day and realizes after Ali's statement. Since Bo did not initially know the date, the day number Bo heard must appear in more than one month. Therefore, the days 16, 18, and 19 must be eliminated since they have corresponding unique months.*\" But that's just not right; they don't have unique months. \n",
"\n",
"As it turns out, [http://you.com](you.com) did get the right answer, March 18! But some of the reasoning steps were wrong, so I tested it on another set of 10 dates, and it failed on that. Thus I declare that all the LLMs fail on this problem.\n",
"\n",
"Here are the responses: \n",
"\n",
"|LLM|Answer|\n",
"|---|------|\n",
"|[A human programmer](https://github.com/norvig/)|**March 18**|\n",
"|[ChatGPT 4o](https://chatgpt.com/)|July 17|\n",
"|[Microsoft Copilot](https://copilot.microsoft.com/)|June 17|\n",
"|[Gemini Advanced](https://gemini.google.com/app)|July 16|\n",
"|[Meta AI Llama 405B](https://www.meta.ai/)|July 19|\n",
"|[Anthropic Claude 3.5 Sonnet](https://claude.ai/new)|July 17|\n",
"|[Perplexity](https://www.perplexity.ai/)|April 17|\n",
"|[Cohere Chat](https://cohere.com/chat)|July 17|\n",
"|[HuggingFace Chat](https://huggingface.co/chat/)|July 17|\n",
"|[You.com](https://you.com)|**March 18** (but wrong answer on follow-up problem)|\n"
"In 2025, three of the LLMs solved the programming test. I'll show Gemini and Claude. I'll test them on the original dates (which each program defines) and on the following three sets of alternative dates, two that have a unique solution, and one with no solution."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7a069aeb-6fc1-4265-9f8a-28bd3eb0a520",
"execution_count": 10,
"id": "5f2927a6-edf7-42a7-afdf-37a8f272de37",
"metadata": {},
"outputs": [],
"source": []
"source": [
"alt_dates = [('January', 15), ('January', 4), ('July', 13), ('July', 24), ('July', 30), \n",
" ('March', 13), ('March', 24), ('May', 11), ('May', 17), ('May', 30)] # Answer: July 30\n",
"\n",
"alt_dates2 = [('April', 17), ('April', 18), ('April', 28), ('July', 16), ('July', 17), \n",
" ('July', 19), ('June', 16), ('June', 29), ('March', 18), ('March', 19)] # Answer: March 18\n",
"\n",
"bad_dates = [('January', 15), ('January', 4), ('July', 13), ('July', 24), ('July', 30), \n",
" ('March', 13), ('March', 24), ('May', 11), ('May', 17), ('May', 13)] # Answer: None"
]
},
{
"cell_type": "markdown",
"id": "ea5f5d9e-2cca-44f9-881d-3ae1b5097fa0",
"metadata": {},
"source": [
"## Gemini Pro 2.5\n",
"\n",
"Here's the program from **Gemini Pro 2.5** in 2025. It passes all the tests. However, there is a serious flaw: some of the explanatory output that is produces consists of static strings hard-coded to the original set of dates. The program still gets the right solution when given an alternative set of dates as input, but the explanatory output is wrong. (Also, I would have provided a keyword argument to make the printing optional.) I have to say that overall I like the style of [my program](Cheryl.ipynb) better, but this program certainly does the job (other than the issue with the explanatory output). Gemini's \"Show Thinking\" description of the program (not shown here) was also cogent, helpful, and correct."
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "e753ed64-7ac6-48ab-b729-ba838caf0987",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"--- Starting Cheryl's Birthday Puzzle Solver ---\n",
"\n",
"## Initial Possible Dates\n",
"Possible dates:\n",
"- August: 14, 15, 17\n",
"- July: 14, 16\n",
"- June: 17, 18\n",
"- May: 15, 16, 19\n",
"(10 possibilities)\n",
"\n",
"## After Albert knows Bernard doesn't know\n",
"Possible dates:\n",
"- August: 14, 15, 17\n",
"- July: 14, 16\n",
"(5 possibilities)\n",
" *Logic: We removed months (like May and June) that contained 'unique' days (18 or 19).\n",
" If Albert had May or June, he couldn't be certain Bernard didn't know.*\n",
"\n",
"## After Albert says 'I don't know'\n",
"Possible dates:\n",
"- August: 14, 15, 17\n",
"- July: 14, 16\n",
"(5 possibilities)\n",
" *Logic: Albert's month must still have multiple options. (In this case, no dates were removed)*\n",
"\n",
"## After Bernard says 'I know now'\n",
"Possible dates:\n",
"- August: 15, 17\n",
"- July: 16\n",
"(3 possibilities)\n",
" *Logic: We looked at the current list (July & August dates). We kept only those dates\n",
" whose day appears exactly once in this list (16, 15, 17). We removed 14 because\n",
" it appeared twice, meaning Bernard wouldn't know if his day was 14.*\n",
"\n",
"## After Albert says 'Then I also know'\n",
"Possible dates:\n",
"- July: 16\n",
"(1 possibilities)\n",
" *Logic: We looked at the current list (July 16, Aug 15, Aug 17). We kept only those dates\n",
" whose month appears exactly once. August appears twice, so if Albert had August,\n",
" he wouldn't know. Since he knows, he must have July.*\n",
"\n",
"========================================\n",
"🎉 The solution is: July 16 🎉\n",
"========================================\n"
]
}
],
"source": [
"from collections import Counter\n",
"\n",
"def print_dates(dates, step_message):\n",
" \"\"\"Helper function to print the current set of possible dates.\"\"\"\n",
" print(f\"\\n## {step_message}\")\n",
" if not dates:\n",
" print(\"No possible dates remain.\")\n",
" return\n",
" months = sorted(list(set(d[0] for d in dates)))\n",
" print(\"Possible dates:\")\n",
" for month in months:\n",
" days = sorted([d[1] for d in dates if d[0] == month])\n",
" print(f\"- {month}: {', '.join(map(str, days))}\")\n",
" print(f\"({len(dates)} possibilities)\")\n",
"\n",
"def solve_cheryl_birthday(possible_dates):\n",
" \"\"\"\n",
" Solves Cheryl's Birthday puzzle based on the statements.\n",
"\n",
" Args:\n",
" possible_dates: A list of tuples, where each tuple is (month, day).\n",
" \"\"\"\n",
"\n",
" print(\"--- Starting Cheryl's Birthday Puzzle Solver ---\")\n",
" print_dates(possible_dates, \"Initial Possible Dates\")\n",
"\n",
" # --- Statement 1: Albert: \"I don't know when Cheryl's birthday is,\n",
" # and I know that Bernard does not know.\" ---\n",
"\n",
" # Part 1.1: Bernard does not know.\n",
" # If a day is unique in the initial list, Bernard *would* know if he\n",
" # was told that day. Albert *knows* Bernard doesn't know. This means\n",
" # Albert's month *cannot* contain any day that is initially unique.\n",
" # So, we first find the days that appear only once (unique days).\n",
" initial_day_counts = Counter(d[1] for d in possible_dates)\n",
" unique_days = {day for day, count in initial_day_counts.items() if count == 1}\n",
"\n",
" # Now, we filter out any date whose day is unique. If a month contains\n",
" # *any* unique day, Albert couldn't be sure Bernard didn't know.\n",
" # So, we must eliminate any month that has a date with a unique day.\n",
" months_with_unique_days = {d[0] for d in possible_dates if d[1] in unique_days}\n",
" dates_after_albert1a = [d for d in possible_dates if d[0] not in months_with_unique_days]\n",
"\n",
" print_dates(dates_after_albert1a, \"After Albert knows Bernard doesn't know\")\n",
" print(\" *Logic: We removed months (like May and June) that contained 'unique' days (18 or 19).\")\n",
" print(\" If Albert had May or June, he couldn't be certain Bernard didn't know.*\")\n",
"\n",
" # Part 1.2: Albert does not know.\n",
" # This means that *after* the previous filtering (or even before),\n",
" # Albert's month must still have more than one possible date.\n",
" # We check the months remaining in 'dates_after_albert1a'. If any of\n",
" # these months now has only one date, it couldn't be Albert's month.\n",
" # However, in this specific problem, this step doesn't eliminate more dates,\n",
" # as both July and August still have multiple options. We'll keep the dates\n",
" # from the previous step.\n",
" dates_after_albert1 = dates_after_albert1a\n",
" # Optional: Add a check here if needed for other datasets.\n",
" # month_counts_a1 = Counter(d[0] for d in dates_after_albert1a)\n",
" # dates_after_albert1 = [d for d in dates_after_albert1a if month_counts_a1[d[0]] > 1]\n",
" # In this case, it yields the same result.\n",
"\n",
" print_dates(dates_after_albert1, \"After Albert says 'I don't know'\")\n",
" print(\" *Logic: Albert's month must still have multiple options. (In this case, no dates were removed)*\")\n",
"\n",
"\n",
" # --- Statement 2: Bernard: \"At first I didn't know..., but I know now.\" ---\n",
" # Bernard heard Albert's statement, so he knows the list is now\n",
" # 'dates_after_albert1'. Since he *now* knows, it means his day\n",
" # must be unique within *this new list*.\n",
" day_counts_b1 = Counter(d[1] for d in dates_after_albert1)\n",
" dates_after_bernard1 = [d for d in dates_after_albert1 if day_counts_b1[d[1]] == 1]\n",
"\n",
" print_dates(dates_after_bernard1, \"After Bernard says 'I know now'\")\n",
" print(\" *Logic: We looked at the current list (July & August dates). We kept only those dates\")\n",
" print(\" whose day appears exactly once in this list (16, 15, 17). We removed 14 because\")\n",
" print(\" it appeared twice, meaning Bernard wouldn't know if his day was 14.*\")\n",
"\n",
" # --- Statement 3: Albert: \"Then I also know when Cheryl's birthday is.\" ---\n",
" # Albert heard Bernard's statement, so he knows the list is now\n",
" # 'dates_after_bernard1'. Since Albert *now* knows, it means his\n",
" # month must be unique within *this new list*.\n",
" month_counts_a2 = Counter(d[0] for d in dates_after_bernard1)\n",
" final_dates = [d for d in dates_after_bernard1 if month_counts_a2[d[0]] == 1]\n",
"\n",
" print_dates(final_dates, \"After Albert says 'Then I also know'\")\n",
" print(\" *Logic: We looked at the current list (July 16, Aug 15, Aug 17). We kept only those dates\")\n",
" print(\" whose month appears exactly once. August appears twice, so if Albert had August,\")\n",
" print(\" he wouldn't know. Since he knows, he must have July.*\")\n",
"\n",
" # --- The Solution ---\n",
" print(\"\\n\" + \"=\"*40)\n",
" if len(final_dates) == 1:\n",
" print(f\"🎉 The solution is: {final_dates[0][0]} {final_dates[0][1]} 🎉\")\n",
" elif len(final_dates) > 1:\n",
" print(\"🤔 The statements lead to multiple possibilities.\")\n",
" else:\n",
" print(\"😥 No solution found based on the statements.\")\n",
" print(\"=\"*40)\n",
"\n",
" return final_dates\n",
"\n",
"# --- Run the puzzle with the original dates ---\n",
"if __name__ == \"__main__\":\n",
" cheryl_dates = [\n",
" ('May', 15), ('May', 16), ('May', 19),\n",
" ('June', 17), ('June', 18),\n",
" ('July', 14), ('July', 16),\n",
" ('August', 14), ('August', 15), ('August', 17)\n",
" ]\n",
"\n",
" solve_cheryl_birthday(cheryl_dates)"
]
},
{
"cell_type": "markdown",
"id": "e7739c8c-742b-4ccc-b22a-76981271ce31",
"metadata": {},
"source": [
"Let's verify that it computes the correct solution with the alternative sets of dates:"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "279abec2-552b-47f8-a53f-e34c2998f196",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"--- Starting Cheryl's Birthday Puzzle Solver ---\n",
"\n",
"## Initial Possible Dates\n",
"Possible dates:\n",
"- January: 4, 15\n",
"- July: 13, 24, 30\n",
"- March: 13, 24\n",
"- May: 11, 17, 30\n",
"(10 possibilities)\n",
"\n",
"## After Albert knows Bernard doesn't know\n",
"Possible dates:\n",
"- July: 13, 24, 30\n",
"- March: 13, 24\n",
"(5 possibilities)\n",
" *Logic: We removed months (like May and June) that contained 'unique' days (18 or 19).\n",
" If Albert had May or June, he couldn't be certain Bernard didn't know.*\n",
"\n",
"## After Albert says 'I don't know'\n",
"Possible dates:\n",
"- July: 13, 24, 30\n",
"- March: 13, 24\n",
"(5 possibilities)\n",
" *Logic: Albert's month must still have multiple options. (In this case, no dates were removed)*\n",
"\n",
"## After Bernard says 'I know now'\n",
"Possible dates:\n",
"- July: 30\n",
"(1 possibilities)\n",
" *Logic: We looked at the current list (July & August dates). We kept only those dates\n",
" whose day appears exactly once in this list (16, 15, 17). We removed 14 because\n",
" it appeared twice, meaning Bernard wouldn't know if his day was 14.*\n",
"\n",
"## After Albert says 'Then I also know'\n",
"Possible dates:\n",
"- July: 30\n",
"(1 possibilities)\n",
" *Logic: We looked at the current list (July 16, Aug 15, Aug 17). We kept only those dates\n",
" whose month appears exactly once. August appears twice, so if Albert had August,\n",
" he wouldn't know. Since he knows, he must have July.*\n",
"\n",
"========================================\n",
"🎉 The solution is: July 30 🎉\n",
"========================================\n"
]
}
],
"source": [
"assert solve_cheryl_birthday(alt_dates) == [('July', 30)]"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "01ce560e-716b-4809-94fe-9a9704b9bf29",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"--- Starting Cheryl's Birthday Puzzle Solver ---\n",
"\n",
"## Initial Possible Dates\n",
"Possible dates:\n",
"- April: 17, 18, 28\n",
"- July: 16, 17, 19\n",
"- June: 16, 29\n",
"- March: 18, 19\n",
"(10 possibilities)\n",
"\n",
"## After Albert knows Bernard doesn't know\n",
"Possible dates:\n",
"- July: 16, 17, 19\n",
"- March: 18, 19\n",
"(5 possibilities)\n",
" *Logic: We removed months (like May and June) that contained 'unique' days (18 or 19).\n",
" If Albert had May or June, he couldn't be certain Bernard didn't know.*\n",
"\n",
"## After Albert says 'I don't know'\n",
"Possible dates:\n",
"- July: 16, 17, 19\n",
"- March: 18, 19\n",
"(5 possibilities)\n",
" *Logic: Albert's month must still have multiple options. (In this case, no dates were removed)*\n",
"\n",
"## After Bernard says 'I know now'\n",
"Possible dates:\n",
"- July: 16, 17\n",
"- March: 18\n",
"(3 possibilities)\n",
" *Logic: We looked at the current list (July & August dates). We kept only those dates\n",
" whose day appears exactly once in this list (16, 15, 17). We removed 14 because\n",
" it appeared twice, meaning Bernard wouldn't know if his day was 14.*\n",
"\n",
"## After Albert says 'Then I also know'\n",
"Possible dates:\n",
"- March: 18\n",
"(1 possibilities)\n",
" *Logic: We looked at the current list (July 16, Aug 15, Aug 17). We kept only those dates\n",
" whose month appears exactly once. August appears twice, so if Albert had August,\n",
" he wouldn't know. Since he knows, he must have July.*\n",
"\n",
"========================================\n",
"🎉 The solution is: March 18 🎉\n",
"========================================\n"
]
}
],
"source": [
"assert solve_cheryl_birthday(alt_dates2) == [('March', 18)]"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "4a836fd6-c908-4667-857c-0da26cba394d",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"--- Starting Cheryl's Birthday Puzzle Solver ---\n",
"\n",
"## Initial Possible Dates\n",
"Possible dates:\n",
"- January: 4, 15\n",
"- July: 13, 24, 30\n",
"- March: 13, 24\n",
"- May: 11, 13, 17\n",
"(10 possibilities)\n",
"\n",
"## After Albert knows Bernard doesn't know\n",
"Possible dates:\n",
"- March: 13, 24\n",
"(2 possibilities)\n",
" *Logic: We removed months (like May and June) that contained 'unique' days (18 or 19).\n",
" If Albert had May or June, he couldn't be certain Bernard didn't know.*\n",
"\n",
"## After Albert says 'I don't know'\n",
"Possible dates:\n",
"- March: 13, 24\n",
"(2 possibilities)\n",
" *Logic: Albert's month must still have multiple options. (In this case, no dates were removed)*\n",
"\n",
"## After Bernard says 'I know now'\n",
"Possible dates:\n",
"- March: 13, 24\n",
"(2 possibilities)\n",
" *Logic: We looked at the current list (July & August dates). We kept only those dates\n",
" whose day appears exactly once in this list (16, 15, 17). We removed 14 because\n",
" it appeared twice, meaning Bernard wouldn't know if his day was 14.*\n",
"\n",
"## After Albert says 'Then I also know'\n",
"No possible dates remain.\n",
" *Logic: We looked at the current list (July 16, Aug 15, Aug 17). We kept only those dates\n",
" whose month appears exactly once. August appears twice, so if Albert had August,\n",
" he wouldn't know. Since he knows, he must have July.*\n",
"\n",
"========================================\n",
"😥 No solution found based on the statements.\n",
"========================================\n"
]
}
],
"source": [
"assert solve_cheryl_birthday(bad_dates) == []"
]
},
{
"cell_type": "markdown",
"id": "c1962ed7-21ba-4a0d-a251-ef1ba91f2584",
"metadata": {},
"source": [
"## Claude Sonnet 4\n",
"\n",
"Now for the program from **Anthropic Claude Sonnet 4** in 2025. It is similar to the Gemini program. It too produces explanatory output (that can't be turned off). With Claude, the explanatory output is not hard-coded to the original dates, so overall it is a better program."
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "6f53ddf4-7e3d-41d8-9bc8-fce73bb92d45",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"=== SOLVING ORIGINAL CHERYL'S BIRTHDAY PROBLEM ===\n",
"Possible dates: [('May', 15), ('May', 16), ('May', 19), ('June', 17), ('June', 18), ('July', 14), ('July', 16), ('August', 14), ('August', 15), ('August', 17)]\n",
"\n",
"Unique days (appear only once): [18, 19]\n",
"Months containing unique days: ['June', 'May']\n",
"Valid months for Albert (don't contain unique days): ['August', 'July']\n",
"Remaining dates after Albert's statement: [('July', 14), ('July', 16), ('August', 14), ('August', 15), ('August', 17)]\n",
"\n",
"Days that appear only once in remaining dates: [15, 16, 17]\n",
"Remaining dates after Bernard's statement: [('July', 16), ('August', 15), ('August', 17)]\n",
"\n",
"Months that appear only once in remaining dates: ['July']\n",
"Final answer: [('July', 16)]\n",
"\n",
"🎉 Cheryl's birthday is: July 16\n"
]
}
],
"source": [
"def solve_cheryls_birthday(possible_dates):\n",
" \"\"\"\n",
" Solve Cheryl's Birthday problem for any list of possible dates.\n",
" \n",
" Args:\n",
" possible_dates: List of tuples (month, day) representing possible birthdays\n",
" \n",
" Returns:\n",
" The unique solution as a tuple (month, day), or None if no unique solution\n",
" \"\"\"\n",
" \n",
" print(f\"Possible dates: {possible_dates}\")\n",
" print()\n",
" \n",
" # Step 1: Albert says \"I don't know when Cheryl's birthday is, \n",
" # but I know that Bernard doesn't know either.\"\n",
" \n",
" # For Albert to know that Bernard doesn't know, Albert's month must not \n",
" # contain any unique days (days that appear only once across all dates)\n",
" \n",
" # Find all unique days (days that appear only once)\n",
" day_counts = {}\n",
" for month, day in possible_dates:\n",
" day_counts[day] = day_counts.get(day, 0) + 1\n",
" \n",
" unique_days = {day for day, count in day_counts.items() if count == 1}\n",
" print(f\"Unique days (appear only once): {sorted(unique_days)}\")\n",
" \n",
" # Find months that contain unique days\n",
" months_with_unique_days = set()\n",
" for month, day in possible_dates:\n",
" if day in unique_days:\n",
" months_with_unique_days.add(month)\n",
" \n",
" print(f\"Months containing unique days: {sorted(months_with_unique_days)}\")\n",
" \n",
" # Albert's month cannot be one that contains unique days\n",
" # (otherwise he couldn't be sure Bernard doesn't know)\n",
" valid_months = set()\n",
" for month, day in possible_dates:\n",
" if month not in months_with_unique_days:\n",
" valid_months.add(month)\n",
" \n",
" print(f\"Valid months for Albert (don't contain unique days): {sorted(valid_months)}\")\n",
" \n",
" # Filter possible dates to only those in valid months\n",
" after_step1 = [(month, day) for month, day in possible_dates \n",
" if month in valid_months]\n",
" \n",
" print(f\"Remaining dates after Albert's statement: {after_step1}\")\n",
" print()\n",
" \n",
" # Step 2: Bernard says \"At first I didn't know when Cheryl's birthday is, \n",
" # but I know now.\"\n",
" \n",
" # For Bernard to know now, his day must appear only once in the remaining dates\n",
" remaining_day_counts = {}\n",
" for month, day in after_step1:\n",
" remaining_day_counts[day] = remaining_day_counts.get(day, 0) + 1\n",
" \n",
" unique_remaining_days = {day for day, count in remaining_day_counts.items() \n",
" if count == 1}\n",
" \n",
" print(f\"Days that appear only once in remaining dates: {sorted(unique_remaining_days)}\")\n",
" \n",
" # Filter to dates where the day is unique in remaining set\n",
" after_step2 = [(month, day) for month, day in after_step1 \n",
" if day in unique_remaining_days]\n",
" \n",
" print(f\"Remaining dates after Bernard's statement: {after_step2}\")\n",
" print()\n",
" \n",
" # Step 3: Albert says \"Then I also know when Cheryl's birthday is.\"\n",
" \n",
" # For Albert to know, there must be only one date left with his month\n",
" month_counts = {}\n",
" for month, day in after_step2:\n",
" month_counts[month] = month_counts.get(month, 0) + 1\n",
" \n",
" # Find months that appear only once in remaining dates\n",
" unique_remaining_months = {month for month, count in month_counts.items() if count == 1}\n",
" \n",
" print(f\"Months that appear only once in remaining dates: {sorted(unique_remaining_months)}\")\n",
" \n",
" # Final answer: dates where the month is unique in remaining set\n",
" final_dates = [(month, day) for month, day in after_step2 \n",
" if month in unique_remaining_months]\n",
" \n",
" print(f\"Final answer: {final_dates}\")\n",
" \n",
" if len(final_dates) == 1:\n",
" return final_dates[0]\n",
" else:\n",
" return None\n",
"\n",
"# Original Cheryl's Birthday dates\n",
"original_dates = [\n",
" (\"May\", 15), (\"May\", 16), (\"May\", 19),\n",
" (\"June\", 17), (\"June\", 18),\n",
" (\"July\", 14), (\"July\", 16),\n",
" (\"August\", 14), (\"August\", 15), (\"August\", 17)\n",
"]\n",
"\n",
"print(\"=== SOLVING ORIGINAL CHERYL'S BIRTHDAY PROBLEM ===\")\n",
"solution = solve_cheryls_birthday(original_dates)\n",
"if solution:\n",
" print(f\"\\n🎉 Cheryl's birthday is: {solution[0]} {solution[1]}\")\n",
"else:\n",
" print(\"\\n❌ No unique solution found\")"
]
},
{
"cell_type": "markdown",
"id": "74b594df-5ca2-4cce-b880-6c7a42c7cb4e",
"metadata": {},
"source": [
"Here we verify that it works on the alternative dates:"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "943483b6-1e50-4d9a-a530-26d207541cfb",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Possible dates: [('January', 15), ('January', 4), ('July', 13), ('July', 24), ('July', 30), ('March', 13), ('March', 24), ('May', 11), ('May', 17), ('May', 30)]\n",
"\n",
"Unique days (appear only once): [4, 11, 15, 17]\n",
"Months containing unique days: ['January', 'May']\n",
"Valid months for Albert (don't contain unique days): ['July', 'March']\n",
"Remaining dates after Albert's statement: [('July', 13), ('July', 24), ('July', 30), ('March', 13), ('March', 24)]\n",
"\n",
"Days that appear only once in remaining dates: [30]\n",
"Remaining dates after Bernard's statement: [('July', 30)]\n",
"\n",
"Months that appear only once in remaining dates: ['July']\n",
"Final answer: [('July', 30)]\n"
]
}
],
"source": [
"assert solve_cheryls_birthday(alt_dates) == ('July', 30)"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "6a714ed0-94a4-4fcf-a125-fbed4c1e7499",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Possible dates: [('April', 17), ('April', 18), ('April', 28), ('July', 16), ('July', 17), ('July', 19), ('June', 16), ('June', 29), ('March', 18), ('March', 19)]\n",
"\n",
"Unique days (appear only once): [28, 29]\n",
"Months containing unique days: ['April', 'June']\n",
"Valid months for Albert (don't contain unique days): ['July', 'March']\n",
"Remaining dates after Albert's statement: [('July', 16), ('July', 17), ('July', 19), ('March', 18), ('March', 19)]\n",
"\n",
"Days that appear only once in remaining dates: [16, 17, 18]\n",
"Remaining dates after Bernard's statement: [('July', 16), ('July', 17), ('March', 18)]\n",
"\n",
"Months that appear only once in remaining dates: ['March']\n",
"Final answer: [('March', 18)]\n"
]
}
],
"source": [
"assert solve_cheryls_birthday(alt_dates2) == ('March', 18)"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "e1d81eb7-053f-4c32-97b4-8a3642a2a5b9",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Possible dates: [('January', 15), ('January', 4), ('July', 13), ('July', 24), ('July', 30), ('March', 13), ('March', 24), ('May', 11), ('May', 17), ('May', 13)]\n",
"\n",
"Unique days (appear only once): [4, 11, 15, 17, 30]\n",
"Months containing unique days: ['January', 'July', 'May']\n",
"Valid months for Albert (don't contain unique days): ['March']\n",
"Remaining dates after Albert's statement: [('March', 13), ('March', 24)]\n",
"\n",
"Days that appear only once in remaining dates: [13, 24]\n",
"Remaining dates after Bernard's statement: [('March', 13), ('March', 24)]\n",
"\n",
"Months that appear only once in remaining dates: []\n",
"Final answer: []\n"
]
}
],
"source": [
"assert solve_cheryls_birthday(bad_dates) == None"
]
}
],
"metadata": {

417
ipynb/Untitled31.ipynb Normal file
View File

@ -0,0 +1,417 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 51,
"id": "1f1c566e-412c-471c-8ee4-b1ab749cfbc4",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{1: [(1, 6), (1, 8), (9, 12), (9, 16)], 2: [(9, 14)]}"
]
},
"execution_count": 51,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import collections\n",
"import itertools\n",
"\n",
"\n",
"def apply_pat_dont_know(current_pairs_set):\n",
" \"\"\"\n",
" Filters the current set of possible pairs based on Pat's statement\n",
" \"I don't know the numbers.\"\n",
" Pat knows the product P. If P can be formed by multiple pairs in\n",
" current_pairs_set, he doesn't know.\n",
" Returns a new set of pairs that are still possible.\n",
" \"\"\"\n",
" if not current_pairs_set:\n",
" return set()\n",
" \n",
" product_to_candidate_pairs = collections.defaultdict(list)\n",
" for x, y in current_pairs_set:\n",
" product_to_candidate_pairs[x * y].append((x, y))\n",
" \n",
" next_possible_pairs = set()\n",
" for product, candidates in product_to_candidate_pairs.items():\n",
" # If there's more than one pair for this product, Pat doesn't know.\n",
" # All such candidate pairs remain possible.\n",
" if len(candidates) > 1:\n",
" for pair in candidates:\n",
" next_possible_pairs.add(pair)\n",
" return next_possible_pairs\n",
"\n",
"def apply_sam_dont_know(current_pairs_set):\n",
" \"\"\"\n",
" Filters the current set of possible pairs based on Sam's statement\n",
" \"I don't know the numbers.\"\n",
" Sam knows the sum S. If S can be formed by multiple pairs in\n",
" current_pairs_set, she doesn't know.\n",
" Returns a new set of pairs that are still possible.\n",
" \"\"\"\n",
" if not current_pairs_set:\n",
" return set()\n",
" \n",
" sum_to_candidate_pairs = collections.defaultdict(list)\n",
" for x, y in current_pairs_set:\n",
" sum_to_candidate_pairs[x + y].append((x, y))\n",
" \n",
" next_possible_pairs = set()\n",
" for s, candidates in sum_to_candidate_pairs.items():\n",
" # If there's more than one pair for this sum, Sam doesn't know.\n",
" # All such candidate pairs remain possible.\n",
" if len(candidates) > 1:\n",
" for pair in candidates:\n",
" next_possible_pairs.add(pair)\n",
" return next_possible_pairs\n",
"\n",
"def find_solutions_pat_knows(current_pairs_set):\n",
" \"\"\"\n",
" Finds solutions after Pat's final statement \"I do know the numbers.\"\n",
" Pat knows the product P. If P uniquely identifies a pair in\n",
" current_pairs_set, that pair is a solution.\n",
" Returns a list of solution pairs.\n",
" \"\"\"\n",
" if not current_pairs_set:\n",
" return []\n",
" \n",
" product_to_candidate_pairs = collections.defaultdict(list)\n",
" for x, y in current_pairs_set:\n",
" product_to_candidate_pairs[x * y].append((x, y))\n",
" \n",
" solutions = []\n",
" for product, candidates in product_to_candidate_pairs.items():\n",
" # If there's exactly one pair for this product, Pat now knows.\n",
" if len(candidates) == 1:\n",
" solutions.append(candidates[0])\n",
" \n",
" solutions.sort() # For consistent output ordering\n",
" return solutions\n",
"\n",
"def solve_puzzle(numbers=range(1, 100), iterations=range(1, 10)):\n",
" \"\"\"\n",
" Solves the Sum and Product puzzle for various numbers of repetitions (n).\n",
" The dialogue is:\n",
" Pat: I don't know.\n",
" Sam: I don't know.\n",
" ... (repeated n times) ...\n",
" Pat: I do know the numbers.\n",
" Returns a dictionary where keys are n (number of repetitions) and\n",
" values are lists of solution pairs (x,y).\n",
" \"\"\"\n",
" all_solutions_by_n_repetitions = {}\n",
" \n",
" initial_candidate_pairs = list(itertools.combinations(numbers, 2))\n",
" \n",
" # Set a maximum for n repetitions to check.\n",
" # This can be adjusted; higher values take longer.\n",
" max_n_to_check = 12 # Adjusted for potentially faster feedback\n",
"\n",
"\n",
" for n_rep in range(1, max_n_to_check + 1):\n",
" current_possible_pairs = initial_candidate_pairs.copy()\n",
" \n",
" possible_to_reach_final_stage = True\n",
" for i_round in range(n_rep):\n",
" # Pat's turn: \"I don't know\"\n",
" current_possible_pairs = apply_pat_dont_know(current_possible_pairs)\n",
" # print(f\" Round {i_round+1} (P): Pairs remaining = {len(current_possible_pairs)}\")\n",
" if not current_possible_pairs:\n",
" possible_to_reach_final_stage = False\n",
" break\n",
" \n",
" # Sam's turn: \"I don't know\"\n",
" current_possible_pairs = apply_sam_dont_know(current_possible_pairs)\n",
" # print(f\" Round {i_round+1} (S): Pairs remaining = {len(current_possible_pairs)}\")\n",
" if not current_possible_pairs:\n",
" possible_to_reach_final_stage = False\n",
" break\n",
" \n",
" if not possible_to_reach_final_stage:\n",
" #print(f\" Set of possible pairs became empty before Pat's final statement for n = {n_rep}.\")\n",
" continue \n",
"\n",
" if not current_possible_pairs:\n",
" #print(f\" Set of possible pairs is empty before Pat's final statement for n = {n_rep}.\")\n",
" continue\n",
"\n",
" #print(f\" Pairs remaining before Pat's final statement for n = {n_rep}: {len(current_possible_pairs)}\")\n",
" \n",
" solutions_for_this_n = find_solutions_pat_knows(current_possible_pairs)\n",
" \n",
" if solutions_for_this_n:\n",
" all_solutions_by_n_repetitions[n_rep] = solutions_for_this_n\n",
" \n",
" return all_solutions_by_n_repetitions\n",
"\n",
"solve_puzzle(numbers=range(1, 20))\n"
]
},
{
"cell_type": "code",
"execution_count": 70,
"id": "d7e40017-85d8-49b2-a36d-c557d0b3d10c",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{2: {(1, 6), (1, 8), (72, 92), (75, 96)},\n",
" 3: {(81, 88)},\n",
" 4: {(77, 90)},\n",
" 5: {(76, 90)},\n",
" 6: {(80, 84)},\n",
" 7: {(77, 84)},\n",
" 8: set(),\n",
" 9: set(),\n",
" 10: set(),\n",
" 11: set()}"
]
},
"execution_count": 70,
"metadata": {},
"output_type": "execute_result"
}
],
"source": []
},
{
"cell_type": "code",
"execution_count": 63,
"id": "63a43e89-550e-48f4-8582-e7bcc5d062e4",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{1: set(),\n",
" 2: set(),\n",
" 3: set(),\n",
" 4: set(),\n",
" 5: set(),\n",
" 6: set(),\n",
" 7: set(),\n",
" 8: set(),\n",
" 9: set()}"
]
},
"execution_count": 63,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"solve({(x, y) for x in range(2, 100) for y in range(x + 1, 100) if x + y <= 100})"
]
},
{
"cell_type": "code",
"execution_count": 48,
"id": "66e2c711-9a46-41b1-abcc-b1d53b50657b",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{(1, 6),\n",
" (1, 8),\n",
" (2, 3),\n",
" (2, 4),\n",
" (2, 6),\n",
" (2, 9),\n",
" (3, 4),\n",
" (3, 6),\n",
" (3, 8),\n",
" (4, 6)}"
]
},
"execution_count": 48,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"pairs = set(combinations(range(1, 10), 2))\n",
"apply_pat_dont_know(pairs)"
]
},
{
"cell_type": "code",
"execution_count": 49,
"id": "31725942-75a5-4613-9d8d-f40127903b87",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{(1, 6),\n",
" (1, 8),\n",
" (2, 3),\n",
" (2, 4),\n",
" (2, 6),\n",
" (2, 9),\n",
" (3, 4),\n",
" (3, 6),\n",
" (3, 8),\n",
" (4, 6)}"
]
},
"execution_count": 49,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"unknown_pairs(prod, pairs)"
]
},
{
"cell_type": "code",
"execution_count": 56,
"id": "79cdfef6-c87f-4456-814b-1cd8e74e18df",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{1: [(1, 6), (1, 8), (72, 92), (75, 96)],\n",
" 2: [(81, 88)],\n",
" 3: [(77, 90)],\n",
" 4: [(76, 90)],\n",
" 5: [(80, 84)],\n",
" 6: [(77, 84)]}"
]
},
"execution_count": 56,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"solve_puzzle(numbers=range(1, 100))"
]
},
{
"cell_type": "code",
"execution_count": 57,
"id": "5092bc62-7c82-4831-9038-83be1c25344c",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{1: {(1, 6), (1, 8), (72, 92), (75, 96)},\n",
" 2: {(81, 88)},\n",
" 3: {(77, 90)},\n",
" 4: {(76, 90)},\n",
" 5: {(80, 84)},\n",
" 6: {(77, 84)},\n",
" 7: set(),\n",
" 8: set(),\n",
" 9: set()}"
]
},
"execution_count": 57,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"solve(numbers=range(1, 100))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "51bc1771-ed13-4c97-8dee-5de1d1e51bdf",
"metadata": {},
"outputs": [],
"source": [
"#solve_puzzle(numbers=range(1, 10))\n",
"numbers=range(1, 10)\n",
"pairs = sum_pairs = prod_pairs = set(combinations(numbers, 2))\n",
"sum_pairs = unknown_pairs(prod, sum_pairs)\n",
"prod_pairs = unknown_pairs(sum, prod_pairs)\n",
"sum_pairs, prod_pairs"
]
},
{
"cell_type": "code",
"execution_count": 26,
"id": "eb6b40ec-9d62-4d66-8eb9-5d6c95f4822d",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{(1, 2), (5, 2)}"
]
},
"execution_count": 26,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"known_pairs(prod, {(1, 2), (2, 3), (6, 1), (3, 4), (6, 2), (5, 2)})"
]
},
{
"cell_type": "code",
"execution_count": 31,
"id": "78df4550-e213-4b36-aa57-ee993502cd9f",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{(1, 2), (5, 2)}"
]
},
"execution_count": 31,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"\n",
"\n",
"known_pairs(prod, {(1, 2), (2, 3), (6, 1), (3, 4), (6, 2), (5, 2)})"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b61e2589-bad6-48be-9b83-f0f1b28c8c60",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.15"
}
},
"nbformat": 4,
"nbformat_minor": 5
}