Compare commits

...

2 Commits

Author SHA1 Message Date
Peter Norvig
45562b6690
Add files via upload 2025-12-23 12:45:27 -08:00
Peter Norvig
02168d2451
Add files via upload 2025-12-23 11:13:27 -08:00
3 changed files with 518 additions and 436 deletions

View File

@ -9,7 +9,7 @@
"\n", "\n",
"# Advent of Code 2025: The AI LLM Edition\n", "# Advent of Code 2025: The AI LLM Edition\n",
"\n", "\n",
"*This notebook shows some solutions by Gemini, Claude, and ChatGPT, three AI Large Language Models (LLMs), for the 2025 [**Advent of Code**](https://adventofcode.com/) (AoC) programming puzzles. In order to understand each puzzle, you'll have to look at the problem descriptions at [**Advent of Code**](https://adventofcode.com/2025) for each [**Day**](https://adventofcode.com/2025/day/1), and you can also look at [**my solutions**](Advent2025.ipynb), which I did before turning to the LLMs.*\n", "*This notebook shows some solutions by Gemini, Claude, and ChatGPT, three AI Large Language Models (LLMs), for the 2025 [**Advent of Code**](https://adventofcode.com/) (AoC) programming puzzles. In order to understand each puzzle, you'll have to look at the problem descriptions at [**Advent of Code**](https://adventofcode.com/2025) for each [**Day**](https://adventofcode.com/2025/day/1), and you can also look at [**my solutions**](Advent2025.ipynb), which I did before asking the LLMs for theirs.*\n",
"\n", "\n",
"*All the code in this notebook is written by an LLM (except for the one line where I call the LLM's code for each puzzle). My comments (like this one) are in italics, and my prompts given to the LLMs are in **bold italics**. Sometimes I quote the LLM's responses; those are in* regular roman font.\n", "*All the code in this notebook is written by an LLM (except for the one line where I call the LLM's code for each puzzle). My comments (like this one) are in italics, and my prompts given to the LLMs are in **bold italics**. Sometimes I quote the LLM's responses; those are in* regular roman font.\n",
"\n", "\n",
@ -19,11 +19,13 @@
"\n", "\n",
"*Now that the 12 days are finished, here are my conclusions:*\n", "*Now that the 12 days are finished, here are my conclusions:*\n",
"\n", "\n",
"- *Overall, the LLMs did very well, producing code that gives the correct answer to every puzzle.*\n", "- *Overall, the LLMs did **very well**, producing code that gives the correct answer to every puzzle.*\n",
"- *The run time were reasonably fast, all under a second, except for 12.1, which took about 2 minutes.*\n", "- *I'm beginning to think I should use an LLM as an assistant for all my coding, not just as an experiment like this.*\n",
"- *This is a huge improvement over just one year ago, when LLMs could not perform anywhere near this level.*\n",
"- *The three LLMS seemed to be roughly equal in quality.*\n", "- *The three LLMS seemed to be roughly equal in quality.*\n",
"- *The LLMs knew the things you would want an experienced engineer to know, and applied them at the right time:*\n", "- *I neglected to track the time it took them to produce the code, but it was a lot faster than memaybe 20 times faster.*\n",
" - *How to see through the story about elves and christmas trees, etc. and getting to the real programming issues*\n", "- *The LLMs knew the things you would want an experienced software engineer to know:*\n",
" - *How to see through the story about elves and christmas trees, etc. and get to the real programming issues*\n",
" - *Standard Python syntax, builtin types, and basic modules (e.g. `collections`, `functools`, `typing`, `numpy`)*\n", " - *Standard Python syntax, builtin types, and basic modules (e.g. `collections`, `functools`, `typing`, `numpy`)*\n",
" - *using the `re` module and/or `str.split` to parse input, even when it is in a somewhat tricky format*\n", " - *using the `re` module and/or `str.split` to parse input, even when it is in a somewhat tricky format*\n",
" - *modular arithmetic*\n", " - *modular arithmetic*\n",
@ -34,19 +36,20 @@
" - *when to use sets versus lists*\n", " - *when to use sets versus lists*\n",
" - *handling a 2D grid of points with 4 or 8 directional neighbors*\n", " - *handling a 2D grid of points with 4 or 8 directional neighbors*\n",
" - *accumulating sums in a defaultdict or Counter*\n", " - *accumulating sums in a defaultdict or Counter*\n",
" - *advanced data structures such as Union-Find and dancing links*\n", " - *advanced esoteric data structures such as Union-Find and dancing links*\n",
" - *computational geometry algorithms including scantiness, flood fill, and ray-casting*\n", " - *computational geometry algorithms including scan lines, flood fill, and ray-casting*\n",
" - *recognizing an integer linear programming problem and knowing how to call a package*\n", " - *recognizing an integer linear programming problem and knowing how to call a package*\n",
" - *depth-first search, meet-in-the-middle search, and recognizing search properties such as commutativity of actions*\n", " - *depth-first search, meet-in-the-middle search, and recognizing search properties such as commutativity of actions*\n",
" - *data classes*\n", " - *data classes*\n",
" - *sometimes type annotations (but not always)*\n", " - *sometimes type annotations (on about 1/3 of the solutions)*\n",
" - *sometimes good doc strings and comments (but not always, and sometimes too many comments).*\n", " - *sometimes good doc strings and comments (but not always, and sometimes too many comments).*\n",
"- *Problems 9.2 and 12.1 had a \"trick\" that allowed for a simpler, faster solution. For these I gave ChatGPT my input file, so it would have a chance of finding the trick. It didn't, but perhaps it was trying to solve the general problem over all possible inputs, whereas I understood that if AoC persents a trick input to me, they will present the same trick input to everyone.*\n", "- *Problems 9.2 and 12.1 had a \"trick\" that allowed for a simpler, faster solution. For these I gave ChatGPT my input file, so it would have a chance of finding the trick. It didn't, but perhaps it was trying to solve the general problem over all possible inputs, whereas I understood that if AoC persents a trick input to me, they will present the same trick input to everyone.*\n",
"- *Much of the code (from all 3 LLMs) could be improved stylistically. In many cases the code was one long function that does the parsing of input, intermediate processing, and final results, clearly violating Robert Maartin's maxim of \"Functions should do one thing.\" But maybe if you're vibe coding and not even looking at the code produced by the LLM, this doesn't matter?*\n", "- *Much of the code (from all 3 LLMs) could be improved stylistically. In many cases the code was one long function that has the parsing of input, the intermediate processing, and the return of the final result all intertwined, clearly violating Robert Maartin's maxim of \"Functions should do one thing.\"*\n",
"- *The LLMs produced code that was a lot more verbose than mine; their lines-of-code count is about 5 times mine. There are a few reasons:*\n", "- *The run time were reasonably fast, all under half a second. However, this was about 3 times slower than my code. (I'm not counting 12.1, which had a run time of about 2 minutes, but missed the \"trick\" that no search is actually required. It would be unfair to compare it to my code, which ran much faster, but would fail if a search was required.)*\n",
" - *I benefited from a few key utility functions to do things like \"return a tuple of all the integers in a text string.\" For some problems I could parse the input in 2 lines of code, while the LLM would take 20.*\n", "- *The LLMs' code is about five times more verbose than mine. There are a few reasons:*\n",
" - *The LLMs were being extra robust in doing error checking, while I recognized that within the bounds of AoC the input will always follow the prescribed format exactly.*\n", " - *I benefited from a few key utility functions to do things like \"return a tuple of all the integers in a text string.\" For most problems I could parse the input in 1 or 2 lines of code, while the LLM would take 20.*\n",
" - *I use a functional style; the LLMs were imperative. So my code was `sum(int(joltage(bank)) for bank in banks)` whereas ChatGPT's was:*\n", " - *The LLMs were being extra robust in doing error checking, while I recognized that within the confines of an AoC puzzle the input will always follow the prescribed format exactly.*\n",
" - *I use a functional style; the LLMs were imperative. So I did `sum(int(max_joltage(bank)) for bank in banks)` whereas ChatGPT did:*\n",
"\n", "\n",
" total = 0\n", " total = 0\n",
" for line in input_text.strip().splitlines():\n", " for line in input_text.strip().splitlines():\n",
@ -56,18 +59,19 @@
" total += max_joltage_for_bank(line)\n", " total += max_joltage_for_bank(line)\n",
" return total\n", " return total\n",
"\n", "\n",
"\n",
"***Note:*** *For brevity, I have removed some of the LLM output, such as:*\n", "***Note:*** *For brevity, I have removed some of the LLM output, such as:*\n",
"- *Usage examples on how to run the program on the test input*.\n", "- *Usage examples on how to run the program on the test input*.\n",
"- *Prose analysis of the problem, descriptions of the programs, and chain of thought thinking.*\n", "- *Prose analysis of the problem, descriptions of the programs, and chain of thought thinking.*\n",
" - *In most cases these were accurate and thorough! But they were too long to include here.*\n", " - *In most cases these were accurate and thorough! Great job! But they were too long to include here.*\n",
"- *The \"`#!/usr/bin/env python3`\" and \"`__main__`\" idioms and `sys.stdin.readline()` for command line execution.*\n", "- *The \"`#!/usr/bin/env python3`\" and \"`__main__`\" idioms for command line execution.*\n",
" - *In retrospect, I should have specified in the prompt that \"**You can get the input text as a single string with `get_text()`***\n", " - *In retrospect, I should have specified in the prompt that \"**You can get the input text as a single string with get_text(day)\"***\n",
"\n", "\n",
"# Day 0: Human\n", "# Day 0: Human\n",
"\n", "\n",
"*On Day 0 I load my [**AdventUtils.ipynb**](AdventUtils.ipynb) so I can access two of my utilities:*\n", "*On Day 0 I load my [**AdventUtils.ipynb**](AdventUtils.ipynb) so I can access two of my utilities:*\n",
"- `get_text(day)` returns the complete text of the input file for `current_year` and `day`\n", "- *`get_text(day)` returns the complete text of the input file for `current_year` and `day`*\n",
"- `answer` verifies whether the LLM got the right answer to the problem, and records the run time.*" "- *`answer` verifies whether the LLM got the right answer to the problem, and records the run time.*"
] ]
}, },
{ {
@ -174,7 +178,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 1.1: .0007 seconds, correct answer: 1182 " "Puzzle 1.1: 0.7 msec, correct answer: 1182 "
] ]
}, },
"execution_count": 3, "execution_count": 3,
@ -304,7 +308,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 1.2: .0009 seconds, WRONG!! answer: 7509 EXPECTED: 6907" "Puzzle 1.2: 0.8 msec, WRONG!! answer: 7509 ; EXPECTED: 6907"
] ]
}, },
"execution_count": 5, "execution_count": 5,
@ -408,7 +412,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 1.2: .0008 seconds, correct answer: 6907 " "Puzzle 1.2: 0.8 msec, correct answer: 6907 "
] ]
}, },
"execution_count": 7, "execution_count": 7,
@ -474,7 +478,7 @@
"id": "2bd0db00-952b-47e5-b787-b3887b7539f1", "id": "2bd0db00-952b-47e5-b787-b3887b7539f1",
"metadata": {}, "metadata": {},
"source": [ "source": [
"*This code is overall rather nice, but conspicously lacks comments and doc strings. (If you can't tell, the goal is to count the number of invalid IDs, which are numbers that consist of the same digit string repeated twice, like \"100100\", within some ID ranges.) It uses the more efficient \"enumerate over the first half of the digit string\" strategy, but is not precise in narrowing down the range it enumerates over. For example, for the range \"999000-109000\", this code will enumerate the range (100, 1000), when it could enumerate just the range (999, 1000).*\n", "*This code is overall rather nice, but conspicously lacks comments and doc strings. (If you can't tell what the code is doing without comments, the goal is to count the number of invalid IDs, which are numbers that consist of the same digit string repeated twice, like \"100100\", within some ID ranges.) It uses the more efficient \"enumerate over the first half of the digit string\" strategy, but is not precise in narrowing down the range it enumerates over. For example, for the range \"999000-109000\", this code will enumerate the range (100, 1000), when it could enumerate just the range (999, 1000).*\n",
"\n", "\n",
"*I verified that the code gives the correct answer:*" "*I verified that the code gives the correct answer:*"
] ]
@ -488,7 +492,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 2.1: .0383 seconds, correct answer: 23560874270 " "Puzzle 2.1: 36.7 msec, correct answer: 23560874270 "
] ]
}, },
"execution_count": 9, "execution_count": 9,
@ -508,7 +512,7 @@
"id": "a31d006f-8cf2-4e4c-92d3-d7b7def22227", "id": "a31d006f-8cf2-4e4c-92d3-d7b7def22227",
"metadata": {}, "metadata": {},
"source": [ "source": [
"*When given the **Part 2** instructions, Claude wrote the following code (where invalid IDs now can be any number of repetitions, like \"100100100\"):*" "*When given the **Part 2** instructions (where invalid IDs now can be any number of repetitions, like \"100100100\"), Claude wrote:*"
] ]
}, },
{ {
@ -572,7 +576,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 2.2: .0383 seconds, correct answer: 44143124633 " "Puzzle 2.2: 38.2 msec, correct answer: 44143124633 "
] ]
}, },
"execution_count": 11, "execution_count": 11,
@ -645,7 +649,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 3.1: .0020 seconds, correct answer: 17085 " "Puzzle 3.1: 1.7 msec, correct answer: 17085 "
] ]
}, },
"execution_count": 13, "execution_count": 13,
@ -730,7 +734,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 3.2: .0027 seconds, correct answer: 169408143086082" "Puzzle 3.2: 2.6 msec, correct answer: 169408143086082"
] ]
}, },
"execution_count": 15, "execution_count": 15,
@ -752,7 +756,7 @@
"\n", "\n",
"*In [**Day 4**](https://adventofcode.com/2025/day/4) we are given a 2D map and asked how many squares have a \"@\" that is surrounded by fewer than 4 other \"@\" (out of the 8 orthogonal or diagonal neighbors).*\n", "*In [**Day 4**](https://adventofcode.com/2025/day/4) we are given a 2D map and asked how many squares have a \"@\" that is surrounded by fewer than 4 other \"@\" (out of the 8 orthogonal or diagonal neighbors).*\n",
"\n", "\n",
"*Gemini produced a solution to **Part 1** that is straightforward and efficient, although perhaps could use some abstraction (e.g. if they had a function to count neighbors, they wouldn't need the `continue` in the main loop).*" "*Gemini produced a solution to **Part 1** that is straightforward and efficient, although perhaps could use better modularization (e.g. if they had a function to count neighbors, they wouldn't need the `continue` in the main loop).*"
] ]
}, },
{ {
@ -827,7 +831,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 4.1: .0088 seconds, correct answer: 1569 " "Puzzle 4.1: 8.5 msec, correct answer: 1569 "
] ]
}, },
"execution_count": 17, "execution_count": 17,
@ -922,7 +926,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 4.2: .2023 seconds, correct answer: 9280 " "Puzzle 4.2: 200.3 msec, correct answer: 9280 "
] ]
}, },
"execution_count": 19, "execution_count": 19,
@ -1031,7 +1035,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 4.2: .0332 seconds, correct answer: 9280 " "Puzzle 4.2: 33.8 msec, correct answer: 9280 "
] ]
}, },
"execution_count": 21, "execution_count": 21,
@ -1053,7 +1057,7 @@
"\n", "\n",
"*In [**Day 5**](https://adventofcode.com/2025/day/5) we are asked how many ingredient IDs from a list of IDs are fresh, according to a list of fresh ID ranges.*\n", "*In [**Day 5**](https://adventofcode.com/2025/day/5) we are asked how many ingredient IDs from a list of IDs are fresh, according to a list of fresh ID ranges.*\n",
"\n", "\n",
"*Claude produces a straightforward program that solves **Part 1** just fine and demonstrates good use of abstraction. This time it has nice doc strings; for Day 2 it had none. Go figure.*" "*Claude produces a straightforward program that solves **Part 1** just fine and demonstrates good use of modularization. This time it has nice doc strings; for Day 2 it had none. Go figure.*"
] ]
}, },
{ {
@ -1127,7 +1131,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 5.1: .0029 seconds, correct answer: 635 " "Puzzle 5.1: 3.1 msec, correct answer: 635 "
] ]
}, },
"execution_count": 23, "execution_count": 23,
@ -1226,7 +1230,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 5.2: .0001 seconds, correct answer: 369761800782619" "Puzzle 5.2: 0.1 msec, correct answer: 369761800782619"
] ]
}, },
"execution_count": 25, "execution_count": 25,
@ -1248,7 +1252,7 @@
"\n", "\n",
"*For [**Day 6**](https://adventofcode.com/2025/day/6) we are asked to solve some math problems written in an unusal format (vertical instead of horizontal, with some special rules).*\n", "*For [**Day 6**](https://adventofcode.com/2025/day/6) we are asked to solve some math problems written in an unusal format (vertical instead of horizontal, with some special rules).*\n",
"\n", "\n",
"*For **Part 1** ChatGPT produced a program that is correct, but has poor abstraction, with one long 63-line function. (It also contains a pet peeve of mine: in lines 1720 the pattern \"`if some_boolean: True else: False`\" can always be replaced with \"`some_boolean`\".) And it would have been easier to replace the six lines with one: `sep = {c for c in range(width) if all(grid[r][c] == ' ' for r in range(h))}`." "*For **Part 1** ChatGPT produced a program that is correct, but has poor modularization, with one long 63-line function. (It also contains a pet peeve of mine: in lines 1720 the pattern \"`if some_boolean: True else: False`\" can always be replaced with \"`some_boolean`\".) And it would have been easier to replace the six lines with one: `sep = {c for c in range(width) if all(grid[r][c] == ' ' for r in range(h))}`."
] ]
}, },
{ {
@ -1344,7 +1348,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 6.1: .0031 seconds, correct answer: 5877594983578 " "Puzzle 6.1: 2.9 msec, correct answer: 5877594983578 "
] ]
}, },
"execution_count": 27, "execution_count": 27,
@ -1482,7 +1486,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 6.2: .0023 seconds, correct answer: 11159825706149 " "Puzzle 6.2: 2.3 msec, correct answer: 11159825706149 "
] ]
}, },
"execution_count": 29, "execution_count": 29,
@ -1596,7 +1600,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 7.1: .0004 seconds, correct answer: 1681 " "Puzzle 7.1: 0.4 msec, correct answer: 1681 "
] ]
}, },
"execution_count": 31, "execution_count": 31,
@ -1711,7 +1715,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 7.2: .0009 seconds, correct answer: 422102272495018" "Puzzle 7.2: 0.8 msec, correct answer: 422102272495018"
] ]
}, },
"execution_count": 33, "execution_count": 33,
@ -1739,7 +1743,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"def count_timelines(manifold: List[str]) -> int:\n", "def count_timelines(manifold: list[str]) -> int:\n",
" \"\"\"How many possible paths are there to the final line of the manifold?\"\"\"\n", " \"\"\"How many possible paths are there to the final line of the manifold?\"\"\"\n",
" start = manifold[0].index('S')\n", " start = manifold[0].index('S')\n",
" beams = Counter({start: 1})\n", " beams = Counter({start: 1})\n",
@ -1870,7 +1874,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 7.2: .0011 seconds, correct answer: 422102272495018" "Puzzle 7.2: 1.0 msec, correct answer: 422102272495018"
] ]
}, },
"execution_count": 36, "execution_count": 36,
@ -2011,7 +2015,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 8.1: .2977 seconds, correct answer: 24360 " "Puzzle 8.1: 293.3 msec, correct answer: 24360 "
] ]
}, },
"execution_count": 38, "execution_count": 38,
@ -2150,7 +2154,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 8.2: .3003 seconds, correct answer: 2185817796 " "Puzzle 8.2: 284.5 msec, correct answer: 2185817796 "
] ]
}, },
"execution_count": 40, "execution_count": 40,
@ -2170,7 +2174,7 @@
"source": [ "source": [
"# Day 9: ChatGPT 5.1 Auto\n", "# Day 9: ChatGPT 5.1 Auto\n",
"\n", "\n",
"*In [**Day 9**](https://adventofcode.com/2025/day/9) we are given the (x, y) coordsinates of a collection of red tiles on the floor, and asked what is the largest rectangle with two red tiles as corners.*\n", "*In [**Day 9**](https://adventofcode.com/2025/day/9) we are given the (x, y) coordinates of a collection of red tiles on the floor, and asked what is the largest rectangle with two red tiles as corners.*\n",
"\n", "\n",
"*For **Part 1**, I was getting tired of all the programs that have a `main` that reads from input and prints the answer, so I told ChatGPT: **Refactor to have a function that takes the points as input and returns the area** and got this:*" "*For **Part 1**, I was getting tired of all the programs that have a `main` that reads from input and prints the answer, so I told ChatGPT: **Refactor to have a function that takes the points as input and returns the area** and got this:*"
] ]
@ -2233,7 +2237,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 9.1: .0097 seconds, correct answer: 4772103936 " "Puzzle 9.1: 9.3 msec, correct answer: 4772103936 "
] ]
}, },
"execution_count": 42, "execution_count": 42,
@ -2441,7 +2445,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 9.2: .4522 seconds, correct answer: 1529675217 " "Puzzle 9.2: 447.0 msec, correct answer: 1529675217 "
] ]
}, },
"execution_count": 44, "execution_count": 44,
@ -2606,7 +2610,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 10.1: .0019 seconds, correct answer: 441 " "Puzzle 10.1: 1.9 msec, correct answer: 441 "
] ]
}, },
"execution_count": 46, "execution_count": 46,
@ -2833,7 +2837,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 10.2: 3.5274 seconds, correct answer: 18559 " "Puzzle 10.2: 3492.1 msec, correct answer: 18559 "
] ]
}, },
"execution_count": 48, "execution_count": 48,
@ -2953,7 +2957,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 10.2: .0480 seconds, correct answer: 18559 " "Puzzle 10.2: 45.3 msec, correct answer: 18559 "
] ]
}, },
"execution_count": 50, "execution_count": 50,
@ -3216,7 +3220,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 11.1: .0003 seconds, correct answer: 574 " "Puzzle 11.1: 0.3 msec, correct answer: 574 "
] ]
}, },
"execution_count": 53, "execution_count": 53,
@ -3332,7 +3336,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 11.2: .0011 seconds, correct answer: 306594217920240" "Puzzle 11.2: 0.8 msec, correct answer: 306594217920240"
] ]
}, },
"execution_count": 55, "execution_count": 55,
@ -3633,7 +3637,7 @@
"source": [ "source": [
"# AoC Utilities\n", "# AoC Utilities\n",
"\n", "\n",
"*I showed this notebook at a [Hacker Dojo](https://hackerdojo.org/) meetup and one comment was that the lines-of-code comparison was unfair, since I used my utilities module, which saved a lot of lines, particularly with parsing the input. I completely agree with this comment, so I asked each of the LLMs **If you were going to do the Advent of Code programming contest, what set of utility functions would you define ahead of time?***\n", "*I showed this notebook at a [Hacker Dojo](https://hackerdojo.org/) meetup and one comment was that the lines-of-code comparison was unfair, since I used my utilities module, which saved a lot of lines, particularly with parsing the input. I completely agree with this comment, so I asked each of the LLMs **If you were going to do the Advent of Code programming contest, what set of utility functions would you define ahead of time?** In retrospect, I should have done this right from the start.*\n",
"\n", "\n",
"*They all were quite familiar with AoC and had ideas for utilities. All three came up with very similar functions for these four areas:*\n", "*They all were quite familiar with AoC and had ideas for utilities. All three came up with very similar functions for these four areas:*\n",
"1) *Input parsing*\n", "1) *Input parsing*\n",
@ -3643,7 +3647,7 @@
"\n", "\n",
"*All three LLMs chose to implement grids as dicts rather than nested lists, perhaps because some AoC problems have grids of unbounded size. Since all three came up with similar code, I asked them **How did you come up with that?** and they each explained:*\n", "*All three LLMs chose to implement grids as dicts rather than nested lists, perhaps because some AoC problems have grids of unbounded size. Since all three came up with similar code, I asked them **How did you come up with that?** and they each explained:*\n",
"\n", "\n",
"***Gemini*** *gave credit to several people for inspirations: [betaveros](https://github.com/betaveros), [ecnerwala](https://github.com/ecnerwala/aoc-2019), [hyperneutrino](https://github.com/hyperneutrino/aoc-header), [Jonathan Paulson](https://github.com/jonathanpaulson/AdventOfCode), and some guy named [Peter Norvig](https://github.com/norvig/pytudes/tree/main) (for the `ints` function, about which Gemini says \"It is a brilliant \"lazy\" hack that saves 10 minutes of typing per day.\") Gemini also mentioned that some programmers at the top of the leaderboard use complex numbers for 2D coordinates, but that an `(x, y)` tuple is easier to read. Gemini also included a complicated function, `solve_cycle`, designed to simulate several generations of a process (like the Game of Life), find a repeated state, and then look up the state for a generation far in the future. Gemini said*: this is a lifesaver for problems that ask \"What will the grid look like after 1,000,000,000 steps?\"\n", "***Gemini*** *gave credit to [betaveros](https://github.com/betaveros), [ecnerwala](https://github.com/ecnerwala/aoc-2019), [hyperneutrino](https://github.com/hyperneutrino/aoc-header), [Jonathan Paulson](https://github.com/jonathanpaulson/AdventOfCode), and some guy named [Peter Norvig](https://github.com/norvig/pytudes/tree/main) (for the `ints` function, about which Gemini says* \"It is a brilliant \"lazy\" hack that saves 10 minutes of typing per day.\") *Gemini mentioned that some programmers at the top of the leaderboard use complex numbers for 2D coordinates, but that an `(x, y)` tuple is easier to read. Gemini also included a complicated function, `solve_cycle`, designed to simulate several generations of a process (like the Game of Life), find a repeated state, and then look up the state for a generation far in the future. Gemini said*: this is a lifesaver for problems that ask \"What will the grid look like after 1,000,000,000 steps?\"\n",
"\n", "\n",
"***Claude*** *said its inspiration came from* \"The AoC problem archetypes themselves\" *and* \"common frustrations: The `ints()` regex approach exists because so many inputs look like \"`Sensor at x=2, y=18: closest beacon is at x=-2, y=15`\" and manually parsing that is tedious.\"\n", "***Claude*** *said its inspiration came from* \"The AoC problem archetypes themselves\" *and* \"common frustrations: The `ints()` regex approach exists because so many inputs look like \"`Sensor at x=2, y=18: closest beacon is at x=-2, y=15`\" and manually parsing that is tedious.\"\n",
"\n", "\n",
@ -3656,7 +3660,7 @@
"\n", "\n",
"…you see the same shapes again and again, even when code differs. Examples:\n", "…you see the same shapes again and again, even when code differs. Examples:\n",
"- Everyones Dijkstra looks the same.\n", "- Everyones Dijkstra looks the same.\n",
"- Everyone eventually has a DIRS4.\n", "- Everyone eventually has a DIRS4. *(Note: mine is called `directions4`)*\n",
"- Everyone regrets not having ints() on Day 1.\n", "- Everyone regrets not having ints() on Day 1.\n",
"\n", "\n",
"A more “actionable” mapping (day number → utilities Id expect)\n", "A more “actionable” mapping (day number → utilities Id expect)\n",
@ -3675,7 +3679,7 @@
"\n", "\n",
"This is not a library. Its a memory prosthetic.\n", "This is not a library. Its a memory prosthetic.\n",
"\n", "\n",
"*Below I merge the three utility libraries into one for the four areas where they all wrote very similar code, and then I give the functions that were unique to each LLM:*" "*Below I merge the three utility libraries into one for the areas where they all wrote very similar code, and then I give the functions that were unique to each LLM:*"
] ]
}, },
{ {
@ -3879,87 +3883,114 @@
"id": "8aa26008-a652-4860-9c84-5ba4344d32f3", "id": "8aa26008-a652-4860-9c84-5ba4344d32f3",
"metadata": {}, "metadata": {},
"source": [ "source": [
"# Run Times, LOC, and Notes\n", "# Run Time Comparison\n",
"\n", "\n",
"\n", "\n",
"*The LLM run times are roughly double the human-written run times. (This is after throwing out 12.1, because the human interepreted it as \"solve my particular input\" and the LLM as \"solve any possible input.\")*\n", "*The human-written code is roughly **three times faster** than the LLM code (for both total and median times).*\n",
"\n", "\n",
"*The LLM lines-of-code count is about 5 times the human count.*\n", "*(This is after throwing out 12.1, because the human interepreted it as \"solve my particular input\" and the LLM as \"solve any possible input\" so it is not fair to compare run times.)*\n",
"\n" "\n",
"<table>\n",
"<tr><th>LLM<th>Human</tr>\n",
"<tr><td><pre>\n",
"Time in msecs: sum: 1215.3, mean: 55.2, median: 2.8\n",
"\n",
"Puzzle 1.1: 0.7 msec, answer: 1182 \n",
"Puzzle 1.2: 0.8 msec, answer: 6907 \n",
"Puzzle 2.1: 36.7 msec, answer: 23560874270 \n",
"Puzzle 2.2: 38.2 msec, answer: 44143124633 \n",
"Puzzle 3.1: 1.7 msec, answer: 17085 \n",
"Puzzle 3.2: 2.6 msec, answer: 169408143086082\n",
"Puzzle 4.1: 8.5 msec, answer: 1569 \n",
"Puzzle 4.2: 33.8 msec, answer: 9280 \n",
"Puzzle 5.1: 3.1 msec, answer: 635 \n",
"Puzzle 5.2: 0.1 msec, answer: 369761800782619\n",
"Puzzle 6.1: 2.9 msec, answer: 5877594983578 \n",
"Puzzle 6.2: 2.3 msec, answer: 11159825706149 \n",
"Puzzle 7.1: 0.4 msec, answer: 1681 \n",
"Puzzle 7.2: 1.0 msec, answer: 422102272495018\n",
"Puzzle 8.1: 293.3 msec, answer: 24360 \n",
"Puzzle 8.2: 284.5 msec, answer: 2185817796 \n",
"Puzzle 9.1: 9.3 msec, answer: 4772103936 \n",
"Puzzle 9.2: 447.0 msec, answer: 1529675217 \n",
"Puzzle 10.1: 1.9 msec, answer: 441 \n",
"Puzzle 10.2: 45.3 msec, answer: 18559 \n",
"Puzzle 11.1: 0.3 msec, answer: 574 \n",
"Puzzle 11.2: 0.8 msec, answer: 306594217920240\n",
"</pre>\n",
"<td><pre>\n",
"Time in msecs: sum: 366.2, mean: 15.9, median: 0.9\n",
"\n",
"Puzzle 1.1: 0.2 msec, answer: 1182 \n",
"Puzzle 1.2: 0.4 msec, answer: 6907 \n",
"Puzzle 2.1: 0.1 msec, answer: 23560874270 \n",
"Puzzle 2.2: 0.2 msec, answer: 44143124633 \n",
"Puzzle 3.1: 0.3 msec, answer: 17085 \n",
"Puzzle 3.2: 0.8 msec, answer: 169408143086082\n",
"Puzzle 4.1: 18.8 msec, answer: 1569 \n",
"Puzzle 4.2: 50.1 msec, answer: 9280 \n",
"Puzzle 5.1: 4.4 msec, answer: 635 \n",
"Puzzle 5.2: 0.0 msec, answer: 369761800782619\n",
"Puzzle 6.1: 0.9 msec, answer: 5877594983578 \n",
"Puzzle 6.2: 1.3 msec, answer: 11159825706149 \n",
"Puzzle 7.1: 0.4 msec, answer: 1681 \n",
"Puzzle 7.2: 0.7 msec, answer: 422102272495018\n",
"Puzzle 8.1: 93.0 msec, answer: 24360 \n",
"Puzzle 8.2: 113.5 msec, answer: 2185817796 \n",
"Puzzle 9.1: 11.7 msec, answer: 4772103936 \n",
"Puzzle 9.2: 2.0 msec, answer: 1529675217 \n",
"Puzzle 10.1: 21.5 msec, answer: 441 \n",
"Puzzle 10.2: 44.3 msec, answer: 18559 \n",
"Puzzle 11.1: 0.1 msec, answer: 574 \n",
"Puzzle 11.2: 0.9 msec, answer: 306594217920240\n",
"</pre>\n",
"</table>"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "a27a329a-01f5-4fbc-a3a6-5946bffe859f", "id": "ac68c9e7-5a96-45eb-bc16-d3a097f3afc4",
"metadata": {}, "metadata": {},
"source": [ "source": [
" | DAY | LLM<br>Name | LLM<br>Time | Human<br>Time | LLM<br>LOC | Human<br>LOC | Comments |\n", "# Lines of Code Comparison and Commentary\n",
" | --- | ------ | ---- | ----- | --- | ----- | ---|\n", "\n",
" | 1.1 | Gemini | .0007 | .0002 | 51 | 6 | Straightforward and easy for LLM and human. | \n", "*The human-written code is about **five times more concise** than the LLM code.*\n",
" | 1.2 | Gemini | .0008 | .0004 | 75 | 11 | Both LLM and human erred on the distance from 0 to 0. | \n", "\n",
" | 2.1 | Claude | .0355 | .0001 | 29 | 17 | | \n", "*The LOC numbers are total lines of code, including blank lines, comments, and doc strings.*"
" | 2.2 | Claude | .0403 | .0002 | 35 | 16 | Both LLM and human found the more efficient half-digits approach | \n",
" | 3.1 | ChatGPT | .0019 | .0003 | 22 | 11 | | \n",
" | 3.2 | ChatGPT | .0026 | .0008 | 42 | 14 | | \n",
" | 4.1 | Gemini | .0084 | .0194 | 44 | 9 | | \n",
" | 4.2 | Gemini | .0329 | .0495 | 52 | 8 | LLM chose the less efficient scan-whole-grid approach | \n",
" | 5.1 | Claude | .0029 | .0045 | 45 | 11 | | \n",
" | 5.2 | Claude | .0001 | .0000 | 58 | 9 | | \n",
" | 6.1 | ChatGPT | .0034 | .0008 | 67 | 7 | bad “if x: True else: False” idiom by LLM | \n",
" | 6.2 | ChatGPT | .0023 | .0013 | 87 | 27 | LLM overly verbose | \n",
" | 7.1 | Gemini | .0004 | .0003 | 63 | 13 | | \n",
" | 7.2 | Gemini | .0011 | .0007 | 70 | 11 | | \n",
" | 8.1 | Claude | .2886 | .1981 | 91 | 27 | | \n",
" | 8.2 | Claude | .2857 | .2034 | 82 | 11 | but LLMs Union-Find data type runs slower than mine. | \n",
" | 9.1 | ChatGPT | .0094 | .0187 | 33 | 7 | | \n",
" | 9.2 | ChatGPT | .4376 | .0046 | 157 | 36 | LLM code a bit complicated; human uses “2 point” trick for speedup | \n",
" | 10.1 | Gemini | .0019 | .0242 | 101 | 18 | | \n",
" | 10.2 | Gemini | .0461 | .0680 | 70 | 13 | milp solutions similar; LLM offers other solutions | \n",
" | 11.1 | Claude | .0003 | .0001 | 83 | 11 | LLM has a bit of vestigial code | \n",
" | 11.2 | Claude | .0009 | .0010 | 77 | 11 | | \n",
" | 12.1 | ChatGPT | ----- | .0006 | 238 | 20 | Human used shortcut to avoid search; LLM wrote slow search | \n",
" | **TOTAL** | |**1.204** | **.597** | **1672** | **324** | **Total time ignores 12.1. Overall, Human code is 5x briefer, 2x faster** | "
] ]
}, },
{ {
"cell_type": "code", "cell_type": "markdown",
"execution_count": 59, "id": "e72b00a0-cc8c-431e-95d2-151848777080",
"id": "f66db331-b68b-4588-908e-e561da114ecc",
"metadata": {}, "metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Puzzle 1.1: .0007 seconds, correct answer: 1182 \n",
"Puzzle 1.2: .0008 seconds, correct answer: 6907 \n",
"Puzzle 2.1: .0383 seconds, correct answer: 23560874270 \n",
"Puzzle 2.2: .0383 seconds, correct answer: 44143124633 \n",
"Puzzle 3.1: .0020 seconds, correct answer: 17085 \n",
"Puzzle 3.2: .0027 seconds, correct answer: 169408143086082\n",
"Puzzle 4.1: .0088 seconds, correct answer: 1569 \n",
"Puzzle 4.2: .0332 seconds, correct answer: 9280 \n",
"Puzzle 5.1: .0029 seconds, correct answer: 635 \n",
"Puzzle 5.2: .0001 seconds, correct answer: 369761800782619\n",
"Puzzle 6.1: .0031 seconds, correct answer: 5877594983578 \n",
"Puzzle 6.2: .0023 seconds, correct answer: 11159825706149 \n",
"Puzzle 7.1: .0004 seconds, correct answer: 1681 \n",
"Puzzle 7.2: .0011 seconds, correct answer: 422102272495018\n",
"Puzzle 8.1: .2977 seconds, correct answer: 24360 \n",
"Puzzle 8.2: .3003 seconds, correct answer: 2185817796 \n",
"Puzzle 9.1: .0097 seconds, correct answer: 4772103936 \n",
"Puzzle 9.2: .4522 seconds, correct answer: 1529675217 \n",
"Puzzle 10.1: .0019 seconds, correct answer: 441 \n",
"Puzzle 10.2: .0480 seconds, correct answer: 18559 \n",
"Puzzle 11.1: .0003 seconds, correct answer: 574 \n",
"Puzzle 11.2: .0011 seconds, correct answer: 306594217920240\n",
"\n",
"Time in seconds: sum = 1.246, mean = .057, median = .003, max = .452\n"
]
}
],
"source": [ "source": [
"summary(answers)" " | DAY | LLM<br>Name | LLM<br>LOC | Human<br>LOC | Commentary |\n",
" |---:|:------:|---:|-----:|---|\n",
" | 1.1 | Gemini | 57 | 5 | Nice code, a little over-commented. | \n",
" | 1.2 | Gemini | 80 | 12 | Both LLM and human erred on the distance from 0 to 0. | \n",
" | 2.1 | Claude | 31 | 19 | No comments by Claude today | \n",
" | 2.2 | Claude | 36 | 19 | Both LLM and human found the more efficient half-digits approach | \n",
" | 3.1 | ChatGPT | 24 | 12 | Nice use of type annotations by ChatGPT today | \n",
" | 3.2 | ChatGPT | 43 | 15 | | \n",
" | 4.1 | Gemini | 46 | 10 | | \n",
" | 4.2 | Gemini | 67| 7 | LLM chose the less efficient scan-whole-grid approach; when prompted, fixed it | \n",
" | 5.1 | Claude | 47 | 12 | Nice modularization and doc strings; they were missing on Day 2 | \n",
" | 5.2 | Claude | 59 | 10| | \n",
" | 6.1 | ChatGPT | 69 | 8 | Poor modularization; bad “if x: True else: False” idiom by ChatGPT | \n",
" | 6.2 | ChatGPT | 88 | 27 | LLM overly verbose | \n",
" | 7.1 | Gemini | 65 | 14 | When prompted, Gemini added type annotations and nice modularization | \n",
" | 7.2 | Gemini | 71 | 12 | | \n",
" | 8.1 | Claude | 92 | 27 | | \n",
" | 8.2 | Claude | 82 | 13| LLM's UnionFind class runs slower than my simpler code | \n",
" | 9.1 | ChatGPT | 34 | 8 | ChatGPT had no type annotations for 9.1, then added them for 9.2| \n",
" | 9.2 | ChatGPT | 158 | 48 | ChatGPT code a bit complicated; human uses “2 point” trick for speedup | \n",
" | 10.1 | Gemini | 103 | 20| Poor modularization; one 100-line function| \n",
" | 10.2 | Gemini | 71 | 14 | milp solutions similar; LLM offers other solutions | \n",
" | 11.1 | Claude | 85 | 12 | Good modularization, but a bit of vestigial code | \n",
" | 11.2 | Claude | 76 | 12 | Claude used type annotations today| \n",
" | 12.1 | ChatGPT | 248 | 20 | Human used shortcut to avoid search; LLM wrote slow search (but with type annotations) | \n",
" | **TOTAL** | | **1732** | **356** | | \n",
" | **MEAN** | | **75** | **14.5** | |"
] ]
} }
], ],

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"<div style=\"text-align: right\" align=\"right\"><i>Peter Norvig<br>Decembers 20162023</i></div>\n", "<div style=\"text-align: right\" align=\"right\"><i>Peter Norvig<br>Decembers 20162025</i></div>\n",
"\n", "\n",
"# Advent of Code Utilities\n", "# Advent of Code Utilities\n",
"\n", "\n",
@ -15,7 +15,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 2, "execution_count": 16,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@ -23,7 +23,7 @@
"from dataclasses import dataclass, field\n", "from dataclasses import dataclass, field\n",
"from itertools import permutations, combinations, cycle, chain, islice, accumulate\n", "from itertools import permutations, combinations, cycle, chain, islice, accumulate\n",
"from itertools import count as count_from, product as cross_product\n", "from itertools import count as count_from, product as cross_product\n",
"from typing import *\n", "from typing import Iterable, Iterator, Sequence, Collection, Callable\n",
"from statistics import mean, median\n", "from statistics import mean, median\n",
"from math import ceil, floor, factorial, gcd, log, log2, log10, sqrt, inf, atan2\n", "from math import ceil, floor, factorial, gcd, log, log2, log10, sqrt, inf, atan2\n",
"\n", "\n",
@ -67,7 +67,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 3, "execution_count": 17,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@ -77,7 +77,7 @@
"\n", "\n",
"def paragraphs(text): \"Split text into paragraphs\"; return text.split('\\n\\n')\n", "def paragraphs(text): \"Split text into paragraphs\"; return text.split('\\n\\n')\n",
"\n", "\n",
"def parse(day: Union[int, str], parser=str, sections=lines, show=8) -> tuple:\n", "def parse(day: int | str, parser=str, sections=lines, show=8) -> tuple:\n",
" \"\"\"Split the input text into `sections`, and apply `parser` to each.\n", " \"\"\"Split the input text into `sections`, and apply `parser` to each.\n",
" The first argument is either the text itself, or the day number of a text file.\"\"\"\n", " The first argument is either the text itself, or the day number of a text file.\"\"\"\n",
" if isinstance(day, str) and show == 8: \n", " if isinstance(day, str) and show == 8: \n",
@ -87,9 +87,11 @@
" records = mapt(parser, sections(text.rstrip()))\n", " records = mapt(parser, sections(text.rstrip()))\n",
" if parser != str or sections != lines:\n", " if parser != str or sections != lines:\n",
" show_items('Parsed representation', records, show)\n", " show_items('Parsed representation', records, show)\n",
" if show:\n",
" print(dash_line)\n",
" return records\n", " return records\n",
"\n", "\n",
"def get_text(day_or_text: Union[int, str]) -> str:\n", "def get_text(day_or_text: int | str) -> str:\n",
" \"\"\"The text used as input to the puzzle: either a string or the day number,\n", " \"\"\"The text used as input to the puzzle: either a string or the day number,\n",
" which denotes the file 'AOC/year/input{day}.txt'.\"\"\"\n", " which denotes the file 'AOC/year/input{day}.txt'.\"\"\"\n",
" if isinstance(day_or_text, str):\n", " if isinstance(day_or_text, str):\n",
@ -98,7 +100,9 @@
" filename = f'AOC/{current_year}/input{day_or_text}.txt'\n", " filename = f'AOC/{current_year}/input{day_or_text}.txt'\n",
" return pathlib.Path(filename).read_text()\n", " return pathlib.Path(filename).read_text()\n",
"\n", "\n",
"def show_items(source, items, show:int, hr=\"─\"*100):\n", "dash_line = \"─\" * 100\n",
"\n",
"def show_items(source, items, show:int):\n",
" \"\"\"Show the first few items, in a pretty format.\"\"\"\n", " \"\"\"Show the first few items, in a pretty format.\"\"\"\n",
" if show:\n", " if show:\n",
" types = Counter(map(type, items))\n", " types = Counter(map(type, items))\n",
@ -109,7 +113,7 @@
" size = f' in range {min(items)} to {max(items)}'\n", " size = f' in range {min(items)} to {max(items)}'\n",
" else:\n", " else:\n",
" size = ''\n", " size = ''\n",
" print(f'{hr}\\n{source} ➜ {counts}{size}:\\n{hr}')\n", " print(f'{dash_line}\\n{source} ➜ {counts}{size}:\\n{dash_line}')\n",
" for line in items[:show]:\n", " for line in items[:show]:\n",
" print(truncate(line))\n", " print(truncate(line))\n",
" if show < len(items):\n", " if show < len(items):\n",
@ -129,31 +133,31 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 4, "execution_count": 18,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"Char = str # Intended as the type of a one-character string\n", "Char = str # Intended as the type of a one-character string\n",
"Atom = Union[str, float, int] # The type of a string or number\n", "Atom = str | float | int # The type of a string or number\n",
"Ints = Sequence[int]\n", "Ints = Sequence[int]\n",
"\n", "\n",
"def ints(text: str) -> Tuple[int]:\n", "def ints(text: str) -> tuple[int]:\n",
" \"\"\"A tuple of all the integers in text, ignoring non-number characters.\"\"\"\n", " \"\"\"A tuple of all the integers in text, ignoring non-number characters.\"\"\"\n",
" return mapt(int, re.findall(r'-?[0-9]+', text))\n", " return mapt(int, re.findall(r'-?[0-9]+', text))\n",
"\n", "\n",
"def positive_ints(text: str) -> Tuple[int]:\n", "def positive_ints(text: str) -> tuple[int]:\n",
" \"\"\"A tuple of all the integers in text, ignoring non-number characters.\"\"\"\n", " \"\"\"A tuple of all the integers in text, ignoring non-number characters.\"\"\"\n",
" return mapt(int, re.findall(r'[0-9]+', text))\n", " return mapt(int, re.findall(r'[0-9]+', text))\n",
"\n", "\n",
"def digits(text: str) -> Tuple[int]:\n", "def digits(text: str) -> tuple[int]:\n",
" \"\"\"A tuple of all the digits in text (as ints 09), ignoring non-digit characters.\"\"\"\n", " \"\"\"A tuple of all the digits in text (as ints 09), ignoring non-digit characters.\"\"\"\n",
" return mapt(int, re.findall(r'[0-9]', text))\n", " return mapt(int, re.findall(r'[0-9]', text))\n",
"\n", "\n",
"def words(text: str) -> Tuple[str]:\n", "def words(text: str) -> tuple[str]:\n",
" \"\"\"A tuple of all the alphabetic words in text, ignoring non-letters.\"\"\"\n", " \"\"\"A tuple of all the alphabetic words in text, ignoring non-letters.\"\"\"\n",
" return tuple(re.findall(r'[a-zA-Z]+', text))\n", " return tuple(re.findall(r'[a-zA-Z]+', text))\n",
"\n", "\n",
"def atoms(text: str) -> Tuple[Atom]:\n", "def atoms(text: str) -> tuple[Atom]:\n",
" \"\"\"A tuple of all the atoms (numbers or identifiers) in text. Skip punctuation.\"\"\"\n", " \"\"\"A tuple of all the atoms (numbers or identifiers) in text. Skip punctuation.\"\"\"\n",
" return mapt(atom, re.findall(r'[+-]?\\d+\\.?\\d*|\\w+', text))\n", " return mapt(atom, re.findall(r'[+-]?\\d+\\.?\\d*|\\w+', text))\n",
"\n", "\n",
@ -177,7 +181,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 17, "execution_count": 19,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@ -197,26 +201,24 @@
" \"\"\"Check if the code computes the correct solution; record run time.\"\"\"\n", " \"\"\"Check if the code computes the correct solution; record run time.\"\"\"\n",
" start = time.time()\n", " start = time.time()\n",
" self.got = self.code()\n", " self.got = self.code()\n",
" self.secs = time.time() - start\n", " self.msecs = (time.time() - start) * 1000.\n",
" self.ok = (self.got == self.solution)\n", " self.ok = (self.got == self.solution)\n",
" return self.ok\n", " return self.ok\n",
" \n", " \n",
" def __repr__(self) -> str:\n", " def __repr__(self) -> str:\n",
" \"\"\"The repr of an answer shows what happened.\"\"\"\n", " \"\"\"The repr of an answer shows what happened.\"\"\"\n",
" secs = _zap0(f'{self.secs:7.4f}')\n",
" correct = 'correct' if self.ok else 'WRONG!!'\n", " correct = 'correct' if self.ok else 'WRONG!!'\n",
" expected = '' if self.ok else f'EXPECTED: {self.solution}'\n", " expected = '' if self.ok else f'; EXPECTED: {self.solution}'\n",
" return f'Puzzle {self.puzzle:4.1f}: {secs} seconds, {correct} answer: {self.got:<15}{expected}'\n", " return f'Puzzle {self.puzzle:4.1f}: {self.msecs:6.1f} msec, {correct} answer: {self.got:<15}{expected}'\n",
"\n",
"def _zap0(field: str) -> str: return field.replace(' 0.', ' .')\n",
"\n", "\n",
"def summary(answers: dict):\n", "def summary(answers: dict):\n",
" \"\"\"Summary report on the answers.\"\"\"\n", " \"\"\"Summary report on the answers.\"\"\"\n",
" times = [answer.msecs for answer in answers.values()]\n",
" def stat(fn, times): return f'{fn.__name__} = {fn(times):.1f}'\n",
" stats = [stat(fn, times) for fn in (sum, mean, median, max)]\n",
" print(f'Time in milliseconds: {\", \".join(stats)}\\n')\n",
" for day in sorted(answers):\n", " for day in sorted(answers):\n",
" print(answers[day])\n", " print(answers[day])"
" times = [answer.secs for answer in answers.values()]\n",
" def stat(fn, times): return f'{fn.__name__} = {fn(times):.3f}'\n",
" print('\\nTime in seconds:', ', '.join(_zap0(stat(fn, times)) for fn in (sum, mean, median, max)))"
] ]
}, },
{ {
@ -230,9 +232,20 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 6, "execution_count": 33,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [
{
"data": {
"text/plain": [
"'+20 -30'"
]
},
"execution_count": 33,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [ "source": [
"class multimap(defaultdict):\n", "class multimap(defaultdict):\n",
" \"\"\"A mapping of {key: [val1, val2, ...]}.\"\"\"\n", " \"\"\"A mapping of {key: [val1, val2, ...]}.\"\"\"\n",
@ -252,7 +265,7 @@
" result *= x\n", " result *= x\n",
" return result\n", " return result\n",
"\n", "\n",
"def T(matrix: Sequence[Sequence]) -> List[Tuple]:\n", "def T(matrix: Sequence[Sequence]) -> list[tuple]:\n",
" \"\"\"The transpose of a matrix: T([(1,2,3), (4,5,6)]) == [(1,4), (2,5), (3,6)]\"\"\"\n", " \"\"\"The transpose of a matrix: T([(1,2,3), (4,5,6)]) == [(1,4), (2,5), (3,6)]\"\"\"\n",
" return list(zip(*matrix))\n", " return list(zip(*matrix))\n",
"\n", "\n",
@ -260,7 +273,7 @@
" \"\"\"The sum of all the counts in a Counter.\"\"\"\n", " \"\"\"The sum of all the counts in a Counter.\"\"\"\n",
" return sum(counter.values())\n", " return sum(counter.values())\n",
"\n", "\n",
"def minmax(numbers) -> Tuple[int, int]:\n", "def minmax(numbers) -> tuple[int, int]:\n",
" \"\"\"A tuple of the (minimum, maximum) of numbers.\"\"\"\n", " \"\"\"A tuple of the (minimum, maximum) of numbers.\"\"\"\n",
" numbers = list(numbers)\n", " numbers = list(numbers)\n",
" return min(numbers), max(numbers)\n", " return min(numbers), max(numbers)\n",
@ -278,11 +291,11 @@
" if i > 1: raise ValueError(f'Expected exactly one item in the sequence.')\n", " if i > 1: raise ValueError(f'Expected exactly one item in the sequence.')\n",
" return item\n", " return item\n",
"\n", "\n",
"def split_at(sequence, i) -> Tuple[Sequence, Sequence]:\n", "def split_at(sequence, i) -> tuple[Sequence, Sequence]:\n",
" \"\"\"The sequence split into two pieces: (before position i, and i-and-after).\"\"\"\n", " \"\"\"The sequence split into two pieces: (before position i, and i-and-after).\"\"\"\n",
" return sequence[:i], sequence[i:]\n", " return sequence[:i], sequence[i:]\n",
"\n", "\n",
"def ignore(*args) -> None: \"Just return None.\"; return None\n", "def ignore(*args) -> None: \"Ignore arguments.\"\n",
"\n", "\n",
"def is_int(x) -> bool: \"Is x an int?\"; return isinstance(x, int) \n", "def is_int(x) -> bool: \"Is x an int?\"; return isinstance(x, int) \n",
"\n", "\n",
@ -292,12 +305,14 @@
"\n", "\n",
"def union(sets) -> set: \"Union of several sets\"; return set().union(*sets)\n", "def union(sets) -> set: \"Union of several sets\"; return set().union(*sets)\n",
"\n", "\n",
"unique = set # Find the unique elements in a collection\n",
"\n",
"def intersection(sets):\n", "def intersection(sets):\n",
" \"Intersection of several sets; error if no sets.\"\n", " \"Intersection of several sets; error if no sets.\"\n",
" first, *rest = sets\n", " first, *rest = sets\n",
" return set(first).intersection(*rest)\n", " return set(first).intersection(*rest)\n",
"\n", "\n",
"def accumulate_counts(item_count_pairs: Iterable[Tuple[object, int]]) -> Counter:\n", "def accumulate_counts(item_count_pairs: Iterable[tuple[object, int]]) -> Counter:\n",
" \"\"\"Add up all the (item, count) pairs into a Counter.\"\"\"\n", " \"\"\"Add up all the (item, count) pairs into a Counter.\"\"\"\n",
" counter = Counter()\n", " counter = Counter()\n",
" for (item, count) in item_count_pairs:\n", " for (item, count) in item_count_pairs:\n",
@ -326,17 +341,15 @@
" \"\"\"Invert a dict, e.g. {1: 'a', 2: 'b'} -> {'a': 1, 'b': 2}.\"\"\"\n", " \"\"\"Invert a dict, e.g. {1: 'a', 2: 'b'} -> {'a': 1, 'b': 2}.\"\"\"\n",
" return {dic[x]: x for x in dic}\n", " return {dic[x]: x for x in dic}\n",
"\n", "\n",
"def walrus(name, value):\n",
" \"\"\"If you're not in 3.8 or more, and you can't do `x := val`,\n",
" then you can use `walrus('x', val)`, if `x` is global.\"\"\"\n",
" globals()[name] = value\n",
" return value\n",
"\n",
"def truncate(object, width=100, ellipsis=' ...') -> str:\n", "def truncate(object, width=100, ellipsis=' ...') -> str:\n",
" \"\"\"Use elipsis to truncate `str(object)` to `width` characters, if necessary.\"\"\"\n", " \"\"\"Use elipsis to truncate `str(object)` to `width` characters, if necessary.\"\"\"\n",
" string = str(object)\n", " string = str(object)\n",
" return string if len(string) <= width else string[:width-len(ellipsis)] + ellipsis\n", " return string if len(string) <= width else string[:width-len(ellipsis)] + ellipsis\n",
"\n", "\n",
"def translate(text: str, *args) -> str:\n",
" \"\"\"Do a `text.translate`, using a `str.maketrans` of args.\"\"\"\n",
" return text.translate(str.maketrans(*args))\n",
"\n",
"def mapt(function: Callable, *sequences) -> tuple:\n", "def mapt(function: Callable, *sequences) -> tuple:\n",
" \"\"\"`map`, with the result as a tuple.\"\"\"\n", " \"\"\"`map`, with the result as a tuple.\"\"\"\n",
" return tuple(map(function, *sequences))\n", " return tuple(map(function, *sequences))\n",
@ -350,7 +363,9 @@
" return ''.join(map(str, things))\n", " return ''.join(map(str, things))\n",
" \n", " \n",
"cache = functools.lru_cache(None)\n", "cache = functools.lru_cache(None)\n",
"Ø = frozenset() # empty set" "Ø = frozenset() # empty set\n",
"\n",
"translate('R20 L30', 'RL', '+-')"
] ]
}, },
{ {
@ -364,7 +379,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 7, "execution_count": 21,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@ -400,11 +415,11 @@
" \"\"\"All length-n subsequences of sequence.\"\"\"\n", " \"\"\"All length-n subsequences of sequence.\"\"\"\n",
" return (sequence[i:i+n] for i in range(len(sequence) + 1 - n))\n", " return (sequence[i:i+n] for i in range(len(sequence) + 1 - n))\n",
"\n", "\n",
"def first(iterable, default=None) -> Optional[object]: \n", "def first(iterable, default=None) -> object: \n",
" \"\"\"The first element in an iterable, or the default if iterable is empty.\"\"\"\n", " \"\"\"The first element in an iterable, or the default if iterable is empty.\"\"\"\n",
" return next(iter(iterable), default)\n", " return next(iter(iterable), default)\n",
"\n", "\n",
"def last(iterable) -> Optional[object]: \n", "def last(iterable) -> object: \n",
" \"\"\"The last element in an iterable.\"\"\"\n", " \"\"\"The last element in an iterable.\"\"\"\n",
" for item in iterable:\n", " for item in iterable:\n",
" pass\n", " pass\n",
@ -424,18 +439,18 @@
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"# Points in Space\n", "# Points in 2D or 3D Space\n",
"\n", "\n",
"Many puzzles involve points; usually two-dimensional points on a plane. A few puzzles involve three-dimensional points, and perhaps one might involve non-integers, so I'll try to make my `Point` implementation flexible in a duck-typing way. A point can also be considered a `Vector`; that is, `(1, 0)` can be a `Point` that means \"this is location x=1, y=0 in the plane\" and it also can be a `Vector` that means \"move Eat (+1 in the along the x axis).\" First we'll define points/vectors:" "Many puzzles involve points; usually two-dimensional points on a plane. A few puzzles involve three-dimensional points, and perhaps one might involve non-integers, so I'll try to make my `Point` implementation flexible in a duck-typing way. A point can also be considered a `Vector`; that is, `(1, 0)` can be a `Point` that means \"this is location x=1, y=0 in the plane\" and it also can be a `Vector` that means \"move East (+1 along the x axis).\" "
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 8, "execution_count": 22,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"Point = Tuple[int, ...] # Type for points\n", "Point = tuple[int, ...] # Type for points (either 2D, 3D or more.)\n",
"Vector = Point # E.g., (1, 0) can be a point, or can be a direction, a Vector\n", "Vector = Point # E.g., (1, 0) can be a point, or can be a direction, a Vector\n",
"Zero = (0, 0)\n", "Zero = (0, 0)\n",
"\n", "\n",
@ -451,21 +466,28 @@
"def Y_(point) -> int: \"Y coordinate of a point\"; return point[1]\n", "def Y_(point) -> int: \"Y coordinate of a point\"; return point[1]\n",
"def Z_(point) -> int: \"Z coordinate of a point\"; return point[2]\n", "def Z_(point) -> int: \"Z coordinate of a point\"; return point[2]\n",
"\n", "\n",
"def Xs(points) -> Tuple[int]: \"X coordinates of a collection of points\"; return mapt(X_, points)\n", "def Xs(points) -> tuple[int]: \"X coordinates of a collection of points\"; return mapt(X_, points)\n",
"def Ys(points) -> Tuple[int]: \"Y coordinates of a collection of points\"; return mapt(Y_, points)\n", "def Ys(points) -> tuple[int]: \"Y coordinates of a collection of points\"; return mapt(Y_, points)\n",
"def Zs(points) -> Tuple[int]: \"X coordinates of a collection of points\"; return mapt(Z_, points)\n", "def Zs(points) -> tuple[int]: \"X coordinates of a collection of points\"; return mapt(Z_, points)\n",
"\n", "\n",
"# Basic arithmetic on points:\n",
"def add(p: Point, q: Point) -> Point: \"Add points\"; return mapt(operator.add, p, q)\n", "def add(p: Point, q: Point) -> Point: \"Add points\"; return mapt(operator.add, p, q)\n",
"def sub(p: Point, q: Point) -> Point: \"Subtract points\"; return mapt(operator.sub, p, q)\n", "def sub(p: Point, q: Point) -> Point: \"Subtract points\"; return mapt(operator.sub, p, q)\n",
"def neg(p: Point) -> Vector: \"Negate a point\"; return mapt(operator.neg, p)\n", "def neg(p: Point) -> Vector: \"Negate a point\"; return mapt(operator.neg, p)\n",
"def mul(p: Point, k: float) -> Vector: \"Scalar multiply\"; return tuple(k * c for c in p)\n", "def mul(p: Point, k: float) -> Vector: \"Scalar multiply\"; return Point(k * c for c in p)\n",
"\n", "\n",
"# Basic arithmetic on 2D points only; here for efficiency:\n",
"def add2(p: Point, q: Point) -> Point: \"Add points\"; return (p[0] + q[0], p[1] + q[1])\n",
"def sub2(p: Point, q: Point) -> Point: \"Subtract points\"; return (p[0] - q[0], p[1] - q[1])\n",
"def neg2(p: Point) -> Vector: \"Negate a point\"; return (-p[0], -p[1])\n",
"def mul2(p: Point, k: float) -> Vector: \"Scalar multiply\"; return (k * p[0], k * p[1])\n",
" \n",
"def distance(p: Point, q: Point) -> float:\n", "def distance(p: Point, q: Point) -> float:\n",
" \"\"\"Euclidean (L2) distance between two points.\"\"\"\n", " \"\"\"Euclidean (L2) distance between two points.\"\"\"\n",
" d = sum((pi - qi) ** 2 for pi, qi in zip(p, q)) ** 0.5\n", " d = sum((pi - qi) ** 2 for pi, qi in zip(p, q)) ** 0.5\n",
" return int(d) if d.is_integer() else d\n", " return int(d) if d.is_integer() else d\n",
"\n", "\n",
"def slide(points: Set[Point], delta: Vector) -> Set[Point]: \n", "def slide(points: set[Point], delta: Vector) -> set[Point]: \n",
" \"\"\"Slide all the points in the set of points by the amount delta.\"\"\"\n", " \"\"\"Slide all the points in the set of points by the amount delta.\"\"\"\n",
" return {add(p, delta) for p in points}\n", " return {add(p, delta) for p in points}\n",
"\n", "\n",
@ -477,14 +499,6 @@
"# Profiling found that `add` and `taxi_distance` were speed bottlenecks; \n", "# Profiling found that `add` and `taxi_distance` were speed bottlenecks; \n",
"# I define below versions that are specialized for 2D points only.\n", "# I define below versions that are specialized for 2D points only.\n",
"\n", "\n",
"def add2(p: Point, q: Point) -> Point: \n",
" \"\"\"Specialized version of point addition for 2D Points only. Faster.\"\"\"\n",
" return (p[0] + q[0], p[1] + q[1])\n",
"\n",
"def sub2(p: Point, q: Point) -> Point: \n",
" \"\"\"Specialized version of point subtraction for 2D Points only. Faster.\"\"\"\n",
" return (p[0] - q[0], p[1] - q[1])\n",
"\n",
"def taxi_distance(p: Point, q: Point) -> int:\n", "def taxi_distance(p: Point, q: Point) -> int:\n",
" \"\"\"Manhattan (L1) distance between two 2D Points.\"\"\"\n", " \"\"\"Manhattan (L1) distance between two 2D Points.\"\"\"\n",
" return abs(p[0] - q[0]) + abs(p[1] - q[1])" " return abs(p[0] - q[0]) + abs(p[1] - q[1])"
@ -510,7 +524,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 9, "execution_count": 23,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@ -557,7 +571,7 @@
" def copy(self): \n", " def copy(self): \n",
" return Grid(self, directions=self.directions, skip=self.skip, default=self.default)\n", " return Grid(self, directions=self.directions, skip=self.skip, default=self.default)\n",
" \n", " \n",
" def neighbors(self, point) -> List[Point]:\n", " def neighbors(self, point) -> list[Point]:\n",
" \"\"\"Points on the grid that neighbor `point`.\"\"\"\n", " \"\"\"Points on the grid that neighbor `point`.\"\"\"\n",
" return [add2(point, Δ) for Δ in self.directions \n", " return [add2(point, Δ) for Δ in self.directions \n",
" if add2(point, Δ) in self or self.default not in (KeyError, None)]\n", " if add2(point, Δ) in self or self.default not in (KeyError, None)]\n",
@ -566,11 +580,11 @@
" \"\"\"The contents of the neighboring points.\"\"\"\n", " \"\"\"The contents of the neighboring points.\"\"\"\n",
" return [self[p] for p in self.neighbors(point)]\n", " return [self[p] for p in self.neighbors(point)]\n",
"\n", "\n",
" def findall(self, contents: Collection) -> List[Point]:\n", " def findall(self, contents: Collection) -> list[Point]:\n",
" \"\"\"All points that contain one of the given contents, e.g. grid.findall('#').\"\"\"\n", " \"\"\"All points that contain one of the given contents, e.g. grid.findall('#').\"\"\"\n",
" return [p for p in self if self[p] in contents]\n", " return [p for p in self if self[p] in contents]\n",
" \n", " \n",
" def to_rows(self, xrange=None, yrange=None) -> List[List[object]]:\n", " def to_rows(self, xrange=None, yrange=None) -> list[list[object]]:\n",
" \"\"\"The contents of the grid, as a rectangular list of lists.\n", " \"\"\"The contents of the grid, as a rectangular list of lists.\n",
" You can define a window with an xrange and yrange; or they default to the whole grid.\"\"\"\n", " You can define a window with an xrange and yrange; or they default to the whole grid.\"\"\"\n",
" xrange = xrange or cover(Xs(self))\n", " xrange = xrange or cover(Xs(self))\n",
@ -591,7 +605,7 @@
" for m in markers:\n", " for m in markers:\n",
" plt.plot(*T(p for p in self if self[p] == m), markers[m], **kwds)\n", " plt.plot(*T(p for p in self if self[p] == m), markers[m], **kwds)\n",
" \n", " \n",
"def neighbors(point, directions=directions4) -> List[Point]:\n", "def neighbors(point, directions=directions4) -> list[Point]:\n",
" \"\"\"Neighbors of this point, in the given directions.\n", " \"\"\"Neighbors of this point, in the given directions.\n",
" (This function can be used outside of a Grid class.)\"\"\"\n", " (This function can be used outside of a Grid class.)\"\"\"\n",
" return [add(point, Δ) for Δ in directions]" " return [add(point, Δ) for Δ in directions]"
@ -614,7 +628,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 10, "execution_count": 24,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@ -642,7 +656,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 11, "execution_count": 25,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@ -719,7 +733,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 12, "execution_count": 26,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@ -748,7 +762,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 13, "execution_count": 27,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@ -763,7 +777,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 14, "execution_count": 28,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@ -778,7 +792,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 15, "execution_count": 29,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@ -799,7 +813,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 16, "execution_count": 30,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [