diff --git a/ipynb/Advent-2025-AI.ipynb b/ipynb/Advent-2025-AI.ipynb index df8c8ca..f5d0665 100644 --- a/ipynb/Advent-2025-AI.ipynb +++ b/ipynb/Advent-2025-AI.ipynb @@ -9,24 +9,27 @@ "\n", "# Advent of Code 2025: The AI LLM Edition\n", "\n", - "I enjoy doing the [**Advent of Code**](https://adventofcode.com/) (AoC) programming puzzles, and **my** solutions are [**over here**](Advent2025.ipynb). \n", + "*I enjoy doing the [**Advent of Code**](https://adventofcode.com/) (AoC) programming puzzles, and **my** solutions are [**over here**](Advent2025.ipynb).*\n", "\n", - "In **this** notebook I show some solutions by various AI Large Language Models: Gemini, Claude, and ChatGPT.\n", + "*In **this** notebook I show some solutions by various AI Large Language Models: Gemini, Claude, and ChatGPT. In order to tell the difference between what the LLM generates and what I say, my comments (like this one) are in italics, and my prompts given to the AI LLMs are in **bold italics**. The LLM responses are in* regular roman font.\n", "\n", - "In order to understand what's going on here, you'll have to look at the problem descriptions at [**Advent of Code**](https://adventofcode.com/2025) for each day.\n", + "*In order to really understand what's going on here, 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).*\n", "\n", - "Each day I'll choose a model and give it the prompt \"*Write code to solve the following problem:*\" followed by the full text of the AoC Part 1 problem description. Then I'll prompt again with \"*There is a change to the specification:*\" followed by the AoC Part 2 description. \n", + "*Each day I'll choose a model and give it the prompt* \"***Write code to solve the following problem:***\" *followed by the full text of the AoC Part 1 problem description. Then I'll prompt again with* \"***There is a change to the specification:***\" *followed by the AoC Part 2 description.*\n", + "\n", + "*For brevity, I have removed some of the models' output, such as:*\n", + "- *Prose analysis of the problem and descriptions of the programs. (In most cases these were accurate and thorough!)*\n", + "- *The \"`#!/usr/bin/env python3`\" at the top of the file*\n", + "- *The \"`__main__`\" idiom for running code from the command line*\n", + "- *Usage examples on how to run the program on test input*\n", "\n", - "For brevity, I have removed some of the models' output, such as:\n", - "- Prose descriptions of the programs. (In most cases these were accurate and thorough!)\n", - "- The \"`__main__`\" idiom for running code from the command line\n", - "- Usage examples on how to run the program (and in a few cases, unit tests)\n", "\n", - "Overall, the models did well, producing code that gives the correct answer in a reasonable run time. Some of the cosde could be improved stylistically. (But I guess if you're vibe coding and not looking at the code, maybe that doesn't matter.)\n", "\n", "# Day 0\n", "\n", - "I load my [**AdventUtils.ipynb**](AdventUtils.ipynb) and set the`current_year` so I can access my input files with `get_text(day_number)` and can use my `answer` function to verify whether the AI systems get the right answer." + "\n", + "\n", + "*On Day 0 I load my [**AdventUtils.ipynb**](AdventUtils.ipynb) and set the`current_year` so I can access my input files with `get_text(day_number)` and can use my `answer` function to verify whether the AI systems get the right answer.*" ] }, { @@ -47,7 +50,7 @@ "source": [ "# [Day 1](https://adventofcode.com/2025/day/1) Gemini 3 Pro\n", "\n", - "For **Part 1** I started with the Gemini 3 Pro Fast model, which produced this following code:" + "*For **Part 1** I started with the Gemini 3 Pro Fast model, which produced this following code:*" ] }, { @@ -115,7 +118,7 @@ "id": "c5219309-4f2f-49b3-8208-8c7a3526e328", "metadata": {}, "source": [ - "A bit verbose and over-commented, but pretty nice. Below I fetch the Day 1 data with `get_text(1)`, split it into lines (as expected by Gemini's `solve_safe_password` function), and verify that the answer is correct:" + "*A bit verbose and over-commented, but pretty nice. Below I fetch the Day 1 data with `get_text(1)`, split it into lines (as expected by Gemini's `solve_safe_password` function), and verify that the answer is correct:*" ] }, { @@ -127,7 +130,7 @@ { "data": { "text/plain": [ - "Puzzle 1.1: .0013 seconds, answer 1182 ok" + "Puzzle 1.1: .0013 seconds, answer 1182 correct" ] }, "execution_count": 3, @@ -147,7 +150,7 @@ "id": "47a04265-2dab-4a4c-9a24-e1c4c1b93c6b", "metadata": {}, "source": [ - "For **Part 2** I gave Gemini Fast the updated instructions, and again it had a nice analysis and the following code:" + "*For **Part 2** I gave Gemini Fast the updated instructions, and again it had a nice analysis and the following code:*" ] }, { @@ -239,9 +242,9 @@ "id": "f8e02f20-87b1-4bd1-9bdb-2bd187fc42cb", "metadata": {}, "source": [ - "Note that the code does `import math` but then never uses the math module. (I've done that, but I try to eventually clean things up. Maybe that's the different between Gemini \"Fast\" and Gemini \"Thinking\" models.)\n", + "*Note that the code does `import math` but then never uses the math module. (I've done that, but I try to eventually clean things up. Maybe that's the different between Gemini \"Fast\" and Gemini \"Thinking\" models.)*\n", "\n", - "Here I check whether the code is correct:" + "*Here I check whether the code is correct:*" ] }, { @@ -253,7 +256,7 @@ { "data": { "text/plain": [ - "Puzzle 1.2: .0017 seconds, answer 7509 WRONG; expected answer is 6907" + "Puzzle 1.2: .0019 seconds, answer 7509 WRONG; EXPECTED ANSWER IS 6907" ] }, "execution_count": 5, @@ -271,13 +274,12 @@ "id": "d640d8e4-6940-416d-93af-b55d82482a35", "metadata": {}, "source": [ - "Too bad; the code has a **bug**. I switched to the Gemini 3 Pro \"Thinking\" model and gave the prompt \"*That's not quite right.*\" It responded with\n", + "*Too bad; the code has a **bug**. I switched to the Gemini 3 Pro \"Thinking\" model and gave the prompt \"**That's not quite right.**\" It responded with*\n", "\n", - ">It seems I made a subtle logic error in my previous calculation for the Right (R) rotation.\n", - ">\n", - ">When the dial is at 0, the distance to the *next* zero is **100**, not 0.\n", + "It seems I made a subtle logic error in my previous calculation for the Right (R) rotation.\n", + "When the dial is at 0, the distance to the *next* zero is **100**, not 0.\n", "\n", - "which is exactly the error I made on *my* first attempt. Gemini produced the following updated code:" + "*which is exactly the error I made on *my* first attempt! Gemini produced the following updated code:*" ] }, { @@ -342,7 +344,7 @@ "id": "e05db80b-c56d-4e1f-988c-a09be8ca5a3e", "metadata": {}, "source": [ - "This second try is indeed correct:" + "*This second try is indeed correct:*" ] }, { @@ -354,7 +356,7 @@ { "data": { "text/plain": [ - "Puzzle 1.2: .0015 seconds, answer 6907 ok" + "Puzzle 1.2: .0016 seconds, answer 6907 correct" ] }, "execution_count": 7, @@ -374,7 +376,7 @@ "source": [ "# [Day 2](https://adventofcode.com/2025/day/2) Claude Opus 4.5\n", "\n", - "I gave Claude the instructions for **Part 1** and it wrote some code that produces the correct answer but prints a lot of unneccessary debugging output along the way. I prompted it to \"*Change the code to not print anything, just return the answer*\" and got the following:" + "*I gave Claude the instructions for **Part 1** and it wrote some code that produces the correct answer but prints a lot of unneccessary debugging output along the way. I prompted it to \"**Change the code to not print anything, just return the answer**\" and got the following:*" ] }, { @@ -420,9 +422,9 @@ "id": "2bd0db00-952b-47e5-b787-b3887b7539f1", "metadata": {}, "source": [ - "This code is overall rather nice, but conspicously lacks comments and doc strings. 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. 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", - "I verified that the code gives the correct answer:" + "*I verified that the code gives the correct answer:*" ] }, { @@ -434,7 +436,7 @@ { "data": { "text/plain": [ - "Puzzle 2.1: .1307 seconds, answer 23560874270 ok" + "Puzzle 2.1: .1247 seconds, answer 23560874270 correct" ] }, "execution_count": 9, @@ -454,7 +456,7 @@ "id": "a31d006f-8cf2-4e4c-92d3-d7b7def22227", "metadata": {}, "source": [ - "When given the **Part 2** instructions, Claude wrote the following code:" + "*When given the **Part 2** instructions, Claude wrote the following code:*" ] }, { @@ -506,7 +508,7 @@ "id": "287ed190-e8c3-4aa8-a01b-b4ecd97327b5", "metadata": {}, "source": [ - "Claude's code produces the correct answer:" + "*Claude's code produces the correct answer:*" ] }, { @@ -518,7 +520,7 @@ { "data": { "text/plain": [ - "Puzzle 2.2: .1311 seconds, answer 44143124633 ok" + "Puzzle 2.2: .1325 seconds, answer 44143124633 correct" ] }, "execution_count": 11, @@ -538,7 +540,7 @@ "source": [ "# [Day 3](https://adventofcode.com/2025/day/3) ChatGPT 5.1 Auto\n", "\n", - "Fpr **Part 1** ChatGPT gave a very brief analysis of the problem and then wrote a program that was designed to be called from the command line, using the `\"__main__\"` idiom. I told it \"*I don't want to run it like that, I want a function that I can pass the input text and have it return an int*\" and it produced this code (conspicuously lacking comments or doc strings):" + "*For **Part 1** ChatGPT gave a very brief analysis of the problem and then wrote a program that was designed to be called from the command line, using the `\"__main__\"` idiom. I told it \"**I don't want to run it like that, I want a function that I can pass the input text and have it return an int**\" and it produced this code (conspicuously lacking comments or doc strings):*" ] }, { @@ -589,7 +591,7 @@ { "data": { "text/plain": [ - "Puzzle 3.1: .0042 seconds, answer 17085 ok" + "Puzzle 3.1: .0042 seconds, answer 17085 correct" ] }, "execution_count": 13, @@ -609,7 +611,7 @@ "id": "4a07f37f-c5e3-4484-a7b1-2cae0ff5bd01", "metadata": {}, "source": [ - "For **Part 2** ChatGPT did well (and for some reason included comments and doc strings this time):" + "*For **Part 2** ChatGPT did well (and for some reason included comments and doc strings this time):*" ] }, { @@ -672,7 +674,7 @@ { "data": { "text/plain": [ - "Puzzle 3.2: .0073 seconds, answer 169408143086082 ok" + "Puzzle 3.2: .0078 seconds, answer 169408143086082 correct" ] }, "execution_count": 15, @@ -692,7 +694,7 @@ "source": [ "# [Day 4](https://adventofcode.com/2025/day/4): Gemini 3 Pro\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`)." + "*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`).*" ] }, { @@ -765,7 +767,7 @@ { "data": { "text/plain": [ - "Puzzle 4.1: .0173 seconds, answer 1569 ok" + "Puzzle 4.1: .0182 seconds, answer 1569 correct" ] }, "execution_count": 17, @@ -785,7 +787,7 @@ "id": "76845e18-6209-4995-a825-568782b79d0d", "metadata": {}, "source": [ - "For **Part 2**, Gemini chose the \"repeatedly remove acessible rolls\" approach rather than the more efficient (and arguable more elegant) \"remove a roll and check its neighbors\" approach." + "*For **Part 2**, Gemini chose the \"repeatedly remove acessible rolls\" approach rather than the more efficient (and arguable more elegant) \"remove a roll and check its neighbors\" approach.*" ] }, { @@ -858,7 +860,7 @@ { "data": { "text/plain": [ - "Puzzle 4.2: .3959 seconds, answer 9280 ok" + "Puzzle 4.2: .3946 seconds, answer 9280 correct" ] }, "execution_count": 19, @@ -876,11 +878,11 @@ "id": "81d88b41-9dec-404d-9bca-9e3ba5caa14d", "metadata": {}, "source": [ - "I asked Gemini \"*Can you do it more efficiently*\" and it replied\n", + "*I asked Gemini \"Can you do it more efficiently\" and it replied*\n", "\n", - ">The previous implementation scanned the entire grid repeatedly (`O(Rows * Cols * Iterations)`). I've replaced it with a Worklist/Queue algorithm that checks neighbors only when necessary. This reduces the complexity to be roughly proportional to the number of rolls, which is significantly faster for large grids.\n", + "The previous implementation scanned the entire grid repeatedly (`O(Rows * Cols * Iterations)`). I've replaced it with a Worklist/Queue algorithm that checks neighbors only when necessary. This reduces the complexity to be roughly proportional to the number of rolls, which is significantly faster for large grids.\n", "\n", - "This is the same efficiency gain that I realized in my solution." + "*This is the same approach that I used in my solution.*" ] }, { @@ -967,7 +969,7 @@ { "data": { "text/plain": [ - "Puzzle 4.2: .0900 seconds, answer 9280 ok" + "Puzzle 4.2: .0938 seconds, answer 9280 correct" ] }, "execution_count": 21, @@ -987,7 +989,7 @@ "source": [ "# [Day 5](https://adventofcode.com/2025/day/5): Claude Opus 4.5\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 abstraction. This time it has nice doc strings; for Day 2 it had none. Go figure.*" ] }, { @@ -1049,7 +1051,7 @@ "id": "40fc662b-ecee-4abe-a23a-ca7786edd438", "metadata": {}, "source": [ - "Here I feed the input to `solve` and verify that the answer is correct:" + "*Here I feed the input to `solve` and verify that the answer is correct:*" ] }, { @@ -1061,7 +1063,7 @@ { "data": { "text/plain": [ - "Puzzle 5.1: .0055 seconds, answer 635 ok" + "Puzzle 5.1: .0056 seconds, answer 635 correct" ] }, "execution_count": 23, @@ -1081,7 +1083,7 @@ "id": "dabe3fbc-0fa6-46d0-adfb-f6413f3a63d5", "metadata": {}, "source": [ - "For **Part 2**, Claude chose to sort ranges, then merge them, then count the total of the sizes of the ranges. That's a reasonable approach–it involves an extra step of merging ranges (instead of just counting on the fly) but it could be useful to have the merged ranges." + "*For **Part 2**, Claude chose to sort ranges, then merge them, then count the total of the sizes of the ranges. That's a reasonable approach–it involves an extra step of merging ranges (instead of just counting on the fly) but it could be useful to have the merged ranges.*" ] }, { @@ -1160,7 +1162,7 @@ { "data": { "text/plain": [ - "Puzzle 5.2: .0002 seconds, answer 369761800782619 ok" + "Puzzle 5.2: .0002 seconds, answer 369761800782619 correct" ] }, "execution_count": 25, @@ -1180,7 +1182,7 @@ "source": [ "# [Day 6](https://adventofcode.com/2025/day/6): ChatGPT 5.1 Auto\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 17–20 the pattern \"`if some_boolean: True else: False`\" can always be replaced with \"`some_boolean`\".)" + "*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 17–20 the pattern \"`if some_boolean: True else: False`\" can always be replaced with \"`some_boolean`\".)*" ] }, { @@ -1264,7 +1266,7 @@ "id": "54e76b17-4372-4074-b7f9-9439c44bc3c4", "metadata": {}, "source": [ - "Here I interface my input data to ChatGPT's function and verify it is correct:" + "*Here I interface my input data to ChatGPT's function and verify it is correct:*" ] }, { @@ -1276,7 +1278,7 @@ { "data": { "text/plain": [ - "Puzzle 6.1: .0085 seconds, answer 5877594983578 ok" + "Puzzle 6.1: .0078 seconds, answer 5877594983578 correct" ] }, "execution_count": 27, @@ -1296,7 +1298,7 @@ "id": "28de0612-03f3-4587-88cb-18ef9b10ee00", "metadata": {}, "source": [ - "For **Part 2** we get an even-longer function, with even more complications. For example in \"Determine the operator\"; the 14 lines could be replaced with `op = (add if '+' in segment else mul)`. I think ChatGPT ws trying to be robust against errors in the input. However, the program is correct." + "*For **Part 2** we get an even-longer function, with even more complications. For example in \"Determine the operator\"; the 14 lines could be replaced with `op = (add if '+' in segment else mul)`. I think ChatGPT ws trying to be robust against errors in the input.*" ] }, { @@ -1395,6 +1397,14 @@ " return grand_total" ] }, + { + "cell_type": "markdown", + "id": "799fdb8a-0564-42f6-9d6a-f6604666afad", + "metadata": {}, + "source": [ + "*The program is correct:*" + ] + }, { "cell_type": "code", "execution_count": 29, @@ -1404,7 +1414,7 @@ { "data": { "text/plain": [ - "Puzzle 6.2: .0060 seconds, answer 11159825706149 ok" + "Puzzle 6.2: .0070 seconds, answer 11159825706149 correct" ] }, "execution_count": 29, @@ -1424,7 +1434,7 @@ "source": [ "# [Day 7](https://adventofcode.com/2025/day/7): Gemini 3 Pro\n", "\n", - "Gemini's code for **Part 1** is a bit verbose, but gets the job done." + "*Gemini's code for **Part 1** is a bit verbose, but gets the job done.*" ] }, { @@ -1504,7 +1514,7 @@ "id": "f8de5310-3556-4150-a050-5303cd10702e", "metadata": {}, "source": [ - "The solution is correct:" + "*The solution is correct:*" ] }, { @@ -1516,7 +1526,7 @@ { "data": { "text/plain": [ - "Puzzle 7.1: .0009 seconds, answer 1681 ok" + "Puzzle 7.1: .0009 seconds, answer 1681 correct" ] }, "execution_count": 31, @@ -1536,7 +1546,7 @@ "id": "c56012a6-b9e7-4053-84c1-79fa457436b1", "metadata": {}, "source": [ - "For **Part 2**, Gemini gets the key ideas (although it is a bit old-fashioned to use a `defaultdict(int)` instead of a `Counter`)." + "*For **Part 2**, Gemini gets the key ideas (although it is a bit old-fashioned to use a `defaultdict(int)` instead of a `Counter`).*" ] }, { @@ -1617,7 +1627,7 @@ "id": "dc634316-8e9d-46d7-9380-f50345bf6149", "metadata": {}, "source": [ - "The answer is correct:" + "*The answer is correct:*" ] }, { @@ -1629,7 +1639,7 @@ { "data": { "text/plain": [ - "Puzzle 7.2: .0016 seconds, answer 422102272495018 ok" + "Puzzle 7.2: .0017 seconds, answer 422102272495018 correct" ] }, "execution_count": 33, @@ -1644,17 +1654,455 @@ }, { "cell_type": "markdown", - "id": "8aa26008-a652-4860-9c84-5ba4344d32f3", + "id": "7320594a-1284-4b9a-9a22-c6d2829b5e03", "metadata": {}, "source": [ - "# Summary\n", + "*My friend Barry Hayes suggested I ask Gemini whose code it preferred, its or mine, So I asked Gemini: \"**How would you compare your code to this:\"***" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "99d90004-938d-42ac-a5a5-86185129fe4f", + "metadata": {}, + "outputs": [], + "source": [ + "def count_timelines(manifold: List[str]) -> int:\n", + " \"\"\"How many possible paths are there to the final line of the manifold?\"\"\"\n", + " start = manifold[0].index('S')\n", + " beams = Counter({start: 1})\n", + " for line in manifold:\n", + " for b, n in list(beams.items()):\n", + " if line[b] == '^':\n", + " beams[b] -= n\n", + " beams[b - 1] += n\n", + " beams[b + 1] += n\n", + " return sum(beams.values())" + ] + }, + { + "cell_type": "markdown", + "id": "7a39ec7d-a27b-40ec-9d32-13f922cc764e", + "metadata": {}, + "source": [ + "*Here is Gemini's reply:*\n", "\n", - "The LLMs got all the problems right (with only a little extra prompting), and the programs are all reasonably efficient (the Day 2 programs could be faster). The readability of the code varies." + "This is a great comparison. Your code is much more concise and \"Pythonic,\" using collections.Counter and in-place updates to reduce verbosity. It strips away a lot of the boilerplate that my solution included for robustness.\n", + "\n", + "- **Your Code**: Assumes 'S' is always on the first line (manifold[0]). If 'S' is buried in the middle of the grid, your code crashes or misses it.\n", + "- **My Code**: Scans the whole grid for 'S', which handles cases where the entry point isn't at the very top.\n", + "- **Your Code**: Relies on Python's robust dictionary handling to avoid index errors (if b-1 is negative, it just adds a key to the dict). This is clever, but effectively allows particles to wrap around or exist in \"negative space\" if not careful (though typically harmless for this specific puzzle input).\n", + "\n", + "*It seems to me that the instructions make it clear that there must be an 'S' on the top line, although technically it isn't stated explicitly. (If there is no 'S' on the top line, my code will return 0, not crash.) Similarly, the puzzle implies that beams can't go outside the borders, but Gemini is right that if the input file did have a splitter in the first or last column then there could be a problem that I don't check for.*\n", + "\n", + "*Next I asked Gemini: **Do you think your code could be improved with some more modularity/abstraction, such as having a separate function to get input?** It replied:*\n", + "\n", + "Yes, absolutely. Breaking the code down makes it much easier to read, test, and debug. If the input format changes (e.g., reading from a file vs. a raw string) or the simulation rules change (e.g., different splitter types), you only have to modify one specific function.\n", + "\n", + "Here is a refactored version that separates Input Parsing, State Initialization, and Simulation Logic." ] }, { "cell_type": "code", "execution_count": 35, + "id": "624796aa-510a-4a6a-8c79-50395f654ee9", + "metadata": {}, + "outputs": [], + "source": [ + "from collections import Counter\n", + "from typing import List, Tuple\n", + "\n", + "def parse_manifold(input_text: str) -> List[str]:\n", + " \"\"\"\n", + " Cleans and converts raw input text into a grid of strings.\n", + " Handles stripping empty lines and padding if necessary.\n", + " \"\"\"\n", + " lines = [line for line in input_text.splitlines() if line]\n", + " if not lines:\n", + " return []\n", + " \n", + " # Optional: Pad lines to uniform width for safety\n", + " width = max(len(line) for line in lines)\n", + " return [line.ljust(width, '.') for line in lines]\n", + "\n", + "def find_start_position(grid: List[str]) -> Tuple[int, int]:\n", + " \"\"\"\n", + " Locates the 'S' character in the grid.\n", + " Returns (row_index, col_index).\n", + " \"\"\"\n", + " for r, row in enumerate(grid):\n", + " if 'S' in row:\n", + " return r, row.index('S')\n", + " raise ValueError(\"Start position 'S' not found in manifold.\")\n", + "\n", + "def process_row(beams: Counter, row_string: str) -> None:\n", + " \"\"\"\n", + " Updates the beam positions for a single row of the manifold.\n", + " Modifies the 'beams' Counter in-place using the subtraction logic.\n", + " \"\"\"\n", + " # Create a snapshot of items to iterate over, since we modify 'beams' in the loop\n", + " current_states = list(beams.items())\n", + " \n", + " for col, count in current_states:\n", + " # Boundary check ensures we don't access outside the string\n", + " if col < 0 or col >= len(row_string):\n", + " continue\n", + " \n", + " char = row_string[col]\n", + " \n", + " if char == '^':\n", + " # Splitter logic: Remove current stack, add to left and right\n", + " beams[col] -= count\n", + " beams[col - 1] += count\n", + " beams[col + 1] += count\n", + " # Else: beam passes through (no change needed for '.')\n", + "\n", + "def solve_quantum_manifold(input_text: str) -> int:\n", + " \"\"\"\n", + " Main orchestrator function.\n", + " \"\"\"\n", + " # 1. Get Input\n", + " grid = parse_manifold(input_text)\n", + " \n", + " # 2. Initialize State\n", + " try:\n", + " start_row, start_col = find_start_position(grid)\n", + " except ValueError:\n", + " return 0\n", + "\n", + " beams = Counter({start_col: 1})\n", + "\n", + " # 3. Run Simulation\n", + " # We slice the grid to start from the row containing 'S'\n", + " for row in grid[start_row:]:\n", + " process_row(beams, row)\n", + "\n", + " # 4. Output Result\n", + " return sum(beams.values())" + ] + }, + { + "cell_type": "markdown", + "id": "23e17286-9534-4da1-8ec9-26a4333b8c70", + "metadata": {}, + "source": [ + "*I like this refactored version better. It produces the correct answer:*" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "a4a0b5b5-00a0-4488-8588-cb6f137be239", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 7.2: .0022 seconds, answer 422102272495018 correct" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(7.2, 422102272495018, lambda:\n", + " solve_quantum_manifold(puzzle_input))" + ] + }, + { + "cell_type": "markdown", + "id": "518c135a-6b5e-4602-b37f-c79c0573a9a1", + "metadata": {}, + "source": [ + "# [Day 8](https://adventofcode.com/2025/day/8): Claude Opus 4.5\n", + "\n", + "*Here's Claude's code for **Part 1**:*" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "fce446d7-b72f-4ade-b783-b40f4212b43b", + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"\n", + "Solution for the Junction Box puzzle.\n", + "\n", + "Connect the 1000 pairs of junction boxes which are closest together.\n", + "Then multiply together the sizes of the three largest circuits.\n", + "\"\"\"\n", + "\n", + "import sys\n", + "from collections import defaultdict\n", + "\n", + "def parse_input(text):\n", + " \"\"\"Parse the input into a list of (x, y, z) coordinates.\"\"\"\n", + " boxes = []\n", + " for line in text.strip().split('\\n'):\n", + " if line.strip():\n", + " x, y, z = map(int, line.strip().split(','))\n", + " boxes.append((x, y, z))\n", + " return boxes\n", + "\n", + "def distance_squared(p1, p2):\n", + " \"\"\"Calculate squared Euclidean distance between two points.\"\"\"\n", + " return (p1[0] - p2[0])**2 + (p1[1] - p2[1])**2 + (p1[2] - p2[2])**2\n", + "\n", + "class UnionFind:\n", + " \"\"\"Union-Find data structure for tracking connected components.\"\"\"\n", + " def __init__(self, n):\n", + " self.parent = list(range(n))\n", + " self.rank = [0] * n\n", + " self.size = [1] * n\n", + " \n", + " def find(self, x):\n", + " if self.parent[x] != x:\n", + " self.parent[x] = self.find(self.parent[x])\n", + " return self.parent[x]\n", + " \n", + " def union(self, x, y):\n", + " px, py = self.find(x), self.find(y)\n", + " if px == py:\n", + " return False # Already in same circuit\n", + " if self.rank[px] < self.rank[py]:\n", + " px, py = py, px\n", + " self.parent[py] = px\n", + " self.size[px] += self.size[py]\n", + " if self.rank[px] == self.rank[py]:\n", + " self.rank[px] += 1\n", + " return True\n", + " \n", + " def get_component_sizes(self):\n", + " \"\"\"Get sizes of all connected components.\"\"\"\n", + " components = defaultdict(int)\n", + " for i in range(len(self.parent)):\n", + " root = self.find(i)\n", + " components[root] = self.size[root]\n", + " return list(components.values())\n", + "\n", + "def solve(boxes, num_connections=1000):\n", + " \"\"\"Connect the num_connections closest pairs and return product of 3 largest circuit sizes.\"\"\"\n", + " n = len(boxes)\n", + " \n", + " # Calculate all pairwise distances\n", + " distances = []\n", + " for i in range(n):\n", + " for j in range(i + 1, n):\n", + " dist_sq = distance_squared(boxes[i], boxes[j])\n", + " distances.append((dist_sq, i, j))\n", + " \n", + " # Sort by distance\n", + " distances.sort()\n", + " \n", + " # Use Union-Find to track circuits\n", + " uf = UnionFind(n)\n", + " \n", + " # Make the specified number of connections\n", + " connections_made = 0\n", + " for dist_sq, i, j in distances:\n", + " if connections_made >= num_connections:\n", + " break\n", + " # Attempt to connect (even if already in same circuit, it counts as a connection attempt)\n", + " uf.union(i, j)\n", + " connections_made += 1\n", + " \n", + " # Get component sizes and find the three largest\n", + " sizes = uf.get_component_sizes()\n", + " sizes.sort(reverse=True)\n", + " \n", + " # Multiply the three largest\n", + " result = 1\n", + " for i in range(min(3, len(sizes))):\n", + " result *= sizes[i]\n", + " \n", + " return result" + ] + }, + { + "cell_type": "markdown", + "id": "0a3f11c5-e193-41ad-b53c-4ca3aac11fc3", + "metadata": {}, + "source": [ + "*Here I parse the input and hand it to Claude's function, which computes the correct answer:*" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "dd99ffbf-3304-49f5-8cfa-d5428edf0db7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 8.1: .8993 seconds, answer 24360 correct" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "boxes = parse(8, ints, show=0)\n", + "\n", + "answer(8.1, 24360, lambda:\n", + " solve(boxes, 1000))" + ] + }, + { + "cell_type": "markdown", + "id": "2b1df990-73d9-4cb1-aeca-3bad52c86c8a", + "metadata": {}, + "source": [ + "*Here is Claude's **Part 2** code:*" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "9ed8312b-4455-4f2d-bef6-67097898aefd", + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"\n", + "Solution for the Junction Box puzzle - Part 2.\n", + "\n", + "Connect junction boxes until they're all in one circuit.\n", + "Find the last two junction boxes connected and multiply their X coordinates.\n", + "\"\"\"\n", + "\n", + "import sys\n", + "from collections import defaultdict\n", + "\n", + "def parse_input(text):\n", + " \"\"\"Parse the input into a list of (x, y, z) coordinates.\"\"\"\n", + " boxes = []\n", + " for line in text.strip().split('\\n'):\n", + " if line.strip():\n", + " x, y, z = map(int, line.strip().split(','))\n", + " boxes.append((x, y, z))\n", + " return boxes\n", + "\n", + "def distance_squared(p1, p2):\n", + " \"\"\"Calculate squared Euclidean distance between two points.\"\"\"\n", + " return (p1[0] - p2[0])**2 + (p1[1] - p2[1])**2 + (p1[2] - p2[2])**2\n", + "\n", + "class UnionFind:\n", + " \"\"\"Union-Find data structure for tracking connected components.\"\"\"\n", + " def __init__(self, n):\n", + " self.parent = list(range(n))\n", + " self.rank = [0] * n\n", + " self.size = [1] * n\n", + " self.num_components = n\n", + " \n", + " def find(self, x):\n", + " if self.parent[x] != x:\n", + " self.parent[x] = self.find(self.parent[x])\n", + " return self.parent[x]\n", + " \n", + " def union(self, x, y):\n", + " px, py = self.find(x), self.find(y)\n", + " if px == py:\n", + " return False # Already in same circuit\n", + " if self.rank[px] < self.rank[py]:\n", + " px, py = py, px\n", + " self.parent[py] = px\n", + " self.size[px] += self.size[py]\n", + " if self.rank[px] == self.rank[py]:\n", + " self.rank[px] += 1\n", + " self.num_components -= 1\n", + " return True\n", + "\n", + "def solve_part2(boxes):\n", + " \"\"\"Connect boxes until all in one circuit, return product of X coords of last connection.\"\"\"\n", + " n = len(boxes)\n", + " \n", + " # Calculate all pairwise distances\n", + " distances = []\n", + " for i in range(n):\n", + " for j in range(i + 1, n):\n", + " dist_sq = distance_squared(boxes[i], boxes[j])\n", + " distances.append((dist_sq, i, j))\n", + " \n", + " # Sort by distance\n", + " distances.sort()\n", + " \n", + " # Use Union-Find to track circuits\n", + " uf = UnionFind(n)\n", + " \n", + " # Keep connecting until all in one circuit\n", + " last_i, last_j = None, None\n", + " for dist_sq, i, j in distances:\n", + " if uf.union(i, j):\n", + " # This connection actually merged two circuits\n", + " last_i, last_j = i, j\n", + " if uf.num_components == 1:\n", + " # All boxes are now in one circuit!\n", + " break\n", + " \n", + " # Return product of X coordinates\n", + " x1 = boxes[last_i][0]\n", + " x2 = boxes[last_j][0]\n", + " print(f\"Last connection: {boxes[last_i]} and {boxes[last_j]}\")\n", + " print(f\"X coordinates: {x1} and {x2}\")\n", + " return x1 * x2" + ] + }, + { + "cell_type": "markdown", + "id": "fa637a95-9774-4d76-b2a4-c0f929f13b25", + "metadata": {}, + "source": [ + "*This code is correct. It does some extraneous printing, which is unneccessary, but not strictly incorrect.*" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "64309157-e1d3-48fc-ba06-e5e66ba23026", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Last connection: (53148, 96207, 9295) and (41127, 88410, 10248)\n", + "X coordinates: 53148 and 41127\n" + ] + }, + { + "data": { + "text/plain": [ + "Puzzle 8.2: .8707 seconds, answer 2185817796 correct" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(8.2, 2185817796, lambda:\n", + " solve_part2(boxes))" + ] + }, + { + "cell_type": "markdown", + "id": "8aa26008-a652-4860-9c84-5ba4344d32f3", + "metadata": {}, + "source": [ + "# Summary\n", + "\n", + "*Overall, the models did well, producing code that gives the correct answer in a reasonable run time. Some of the code could be improved stylistically. (But I guess if you're vibe coding and not looking at the code, maybe that doesn't matter.) Here are the answers and the run times:*" + ] + }, + { + "cell_type": "code", + "execution_count": 41, "id": "16ce8af4-a1da-4280-9078-116ced3e9941", "metadata": {}, "outputs": [ @@ -1662,22 +2110,24 @@ "name": "stdout", "output_type": "stream", "text": [ - "Puzzle 1.1: .0013 seconds, answer 1182 ok\n", - "Puzzle 1.2: .0015 seconds, answer 6907 ok\n", - "Puzzle 2.1: .1307 seconds, answer 23560874270 ok\n", - "Puzzle 2.2: .1311 seconds, answer 44143124633 ok\n", - "Puzzle 3.1: .0042 seconds, answer 17085 ok\n", - "Puzzle 3.2: .0073 seconds, answer 169408143086082 ok\n", - "Puzzle 4.1: .0173 seconds, answer 1569 ok\n", - "Puzzle 4.2: .0900 seconds, answer 9280 ok\n", - "Puzzle 5.1: .0055 seconds, answer 635 ok\n", - "Puzzle 5.2: .0002 seconds, answer 369761800782619 ok\n", - "Puzzle 6.1: .0085 seconds, answer 5877594983578 ok\n", - "Puzzle 6.2: .0060 seconds, answer 11159825706149 ok\n", - "Puzzle 7.1: .0009 seconds, answer 1681 ok\n", - "Puzzle 7.2: .0016 seconds, answer 422102272495018 ok\n", + "Puzzle 1.1: .0013 seconds, answer 1182 correct\n", + "Puzzle 1.2: .0016 seconds, answer 6907 correct\n", + "Puzzle 2.1: .1247 seconds, answer 23560874270 correct\n", + "Puzzle 2.2: .1325 seconds, answer 44143124633 correct\n", + "Puzzle 3.1: .0042 seconds, answer 17085 correct\n", + "Puzzle 3.2: .0078 seconds, answer 169408143086082 correct\n", + "Puzzle 4.1: .0182 seconds, answer 1569 correct\n", + "Puzzle 4.2: .0938 seconds, answer 9280 correct\n", + "Puzzle 5.1: .0056 seconds, answer 635 correct\n", + "Puzzle 5.2: .0002 seconds, answer 369761800782619 correct\n", + "Puzzle 6.1: .0078 seconds, answer 5877594983578 correct\n", + "Puzzle 6.2: .0070 seconds, answer 11159825706149 correct\n", + "Puzzle 7.1: .0009 seconds, answer 1681 correct\n", + "Puzzle 7.2: .0022 seconds, answer 422102272495018 correct\n", + "Puzzle 8.1: .8993 seconds, answer 24360 correct\n", + "Puzzle 8.2: .8707 seconds, answer 2185817796 correct\n", "\n", - "Time in seconds: total = 0.406, mean = 0.029, max = 0.131\n" + "Time in seconds: sum = 2.178, mean = .136, median = .007, max = .899\n" ] } ], diff --git a/ipynb/Advent-2025.ipynb b/ipynb/Advent-2025.ipynb index e541d30..6883196 100644 --- a/ipynb/Advent-2025.ipynb +++ b/ipynb/Advent-2025.ipynb @@ -9,7 +9,9 @@ "\n", "# Advent of Code 2025\n", "\n", - "I enjoy doing the [**Advent of Code**](https://adventofcode.com/) (AoC) programming puzzles, so here we go for 2025! This year I will be doing something different: I will solve each problem, and then [**in another notebook**](Advent-2025-AI.ipynb) I will ask an AI Large Language Model to solve the same problem, and I'll comment on it. So far the LLMs are doing great, but IMHO I prefer my code.\n", + "I enjoy doing the [**Advent of Code**](https://adventofcode.com/) (AoC) programming puzzles, so here we go for 2025! \n", + "\n", + "This year I will be doing something different: I will solve each problem myself here, and then [**in another notebook**](Advent-2025-AI.ipynb) I will ask an AI Large Language Model to solve the same problem. Check out the differences!\n", "\n", "# Day 0\n", "\n", @@ -52,7 +54,7 @@ "output_type": "stream", "text": [ "────────────────────────────────────────────────────────────────────────────────────────────────────\n", - "Puzzle input ➜ 4780 strs:\n", + "Puzzle input ➜ 4780 strs of size 2 to 4:\n", "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "L20\n", "L13\n", @@ -148,7 +150,7 @@ { "data": { "text/plain": [ - "Puzzle 1.1: .0005 seconds, answer 1182 ok" + "Puzzle 1.1: .0006 seconds, answer 1182 correct" ] }, "execution_count": 5, @@ -182,7 +184,7 @@ { "data": { "text/plain": [ - "Puzzle 1.2: .1389 seconds, answer 6907 ok" + "Puzzle 1.2: .1367 seconds, answer 6907 correct" ] }, "execution_count": 6, @@ -232,7 +234,7 @@ { "data": { "text/plain": [ - "Puzzle 1.2: .0009 seconds, answer 6907 ok" + "Puzzle 1.2: .0009 seconds, answer 6907 correct" ] }, "execution_count": 8, @@ -274,11 +276,11 @@ "output_type": "stream", "text": [ "────────────────────────────────────────────────────────────────────────────────────────────────────\n", - "Puzzle input ➜ 1 str:\n", + "Puzzle input ➜ 1 str of size 511:\n", "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "990244-1009337,5518069-5608946,34273134-34397466,3636295061-3636388848,8613701-8663602,573252-68 ...\n", "────────────────────────────────────────────────────────────────────────────────────────────────────\n", - "Parsed representation ➜ 35 tuples:\n", + "Parsed representation ➜ 35 tuples of size 2:\n", "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "(990244, 1009337)\n", "(5518069, 5608946)\n", @@ -377,7 +379,7 @@ { "data": { "text/plain": [ - "Puzzle 2.1: .0031 seconds, answer 23560874270 ok" + "Puzzle 2.1: .0029 seconds, answer 23560874270 correct" ] }, "execution_count": 12, @@ -461,7 +463,7 @@ { "data": { "text/plain": [ - "Puzzle 2.1: .0030 seconds, answer 23560874270 ok" + "Puzzle 2.1: .0032 seconds, answer 23560874270 correct" ] }, "execution_count": 15, @@ -483,7 +485,7 @@ { "data": { "text/plain": [ - "Puzzle 2.2: .0041 seconds, answer 44143124633 ok" + "Puzzle 2.2: .0037 seconds, answer 44143124633 correct" ] }, "execution_count": 16, @@ -525,7 +527,7 @@ "output_type": "stream", "text": [ "────────────────────────────────────────────────────────────────────────────────────────────────────\n", - "Puzzle input ➜ 200 strs:\n", + "Puzzle input ➜ 200 strs of size 100:\n", "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "5353323523322232362334333433323333353233331313222372133133353643423323233323333534414523333432223242\n", "6344544745655555456556556566665564538465555575558846455665837545764555554465564547547565544657585435\n", @@ -592,7 +594,7 @@ { "data": { "text/plain": [ - "Puzzle 3.1: .0006 seconds, answer 17085 ok" + "Puzzle 3.1: .0007 seconds, answer 17085 correct" ] }, "execution_count": 20, @@ -669,7 +671,7 @@ { "data": { "text/plain": [ - "Puzzle 3.1: .0006 seconds, answer 17085 ok" + "Puzzle 3.1: .0006 seconds, answer 17085 correct" ] }, "execution_count": 23, @@ -691,7 +693,7 @@ { "data": { "text/plain": [ - "Puzzle 3.2: .0020 seconds, answer 169408143086082 ok" + "Puzzle 3.2: .0020 seconds, answer 169408143086082 correct" ] }, "execution_count": 24, @@ -725,7 +727,7 @@ "output_type": "stream", "text": [ "────────────────────────────────────────────────────────────────────────────────────────────────────\n", - "Puzzle input ➜ 140 strs:\n", + "Puzzle input ➜ 140 strs of size 140:\n", "────────────────────────────────────────────────────────────────────────────────────────────────────\n", ".@@@@@...@.@@@@@@@@@@.@@@@@@@.@.@.@@@@@@@@@@@@@..@.@@@.@@@@@@..@.@..@.@@...@.@@@@..@@@@....@@@.@ ...\n", ".@@@@@.@....@.....@@@.@@.@@.@@@.@@@.@.@.@.@@@@@.@@.@@@@@.@@@@@@@@@@@..@@.@.@@.@@@.@@@@@@@@@@@..@ ...\n", @@ -790,7 +792,7 @@ { "data": { "text/plain": [ - "Puzzle 4.1: .0541 seconds, answer 1569 ok" + "Puzzle 4.1: .0527 seconds, answer 1569 correct" ] }, "execution_count": 27, @@ -841,7 +843,7 @@ { "data": { "text/plain": [ - "Puzzle 4.2: 1.2345 seconds, answer 9280 ok" + "Puzzle 4.2: 1.2384 seconds, answer 9280 correct" ] }, "execution_count": 29, @@ -890,7 +892,7 @@ { "data": { "text/plain": [ - "Puzzle 4.2: .1421 seconds, answer 9280 ok" + "Puzzle 4.2: .1424 seconds, answer 9280 correct" ] }, "execution_count": 31, @@ -924,7 +926,7 @@ "output_type": "stream", "text": [ "────────────────────────────────────────────────────────────────────────────────────────────────────\n", - "Puzzle input ➜ 1184 strs:\n", + "Puzzle input ➜ 1184 strs of size 0 to 31:\n", "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "292632986393425-296797126337251\n", "428261559408337-431275643240865\n", @@ -936,7 +938,7 @@ "284142407577672-288117756872436\n", "...\n", "────────────────────────────────────────────────────────────────────────────────────────────────────\n", - "Parsed representation ➜ 2 tuples:\n", + "Parsed representation ➜ 2 tuples of size 183 to 1000:\n", "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "((292632986393425, 296797126337251), (428261559408337, 431275643240865), (197704206528056, 19882 ...\n", "(92087202605588, 314304587960778, 19184152482180, 331502336245673, 104093299503920, 224082459481 ...\n" @@ -983,7 +985,7 @@ { "data": { "text/plain": [ - "Puzzle 5.1: .0076 seconds, answer 635 ok" + "Puzzle 5.1: .0079 seconds, answer 635 correct" ] }, "execution_count": 34, @@ -1064,7 +1066,7 @@ { "data": { "text/plain": [ - "Puzzle 5.2: .0001 seconds, answer 369761800782619 ok" + "Puzzle 5.2: .0001 seconds, answer 369761800782619 correct" ] }, "execution_count": 37, @@ -1100,7 +1102,7 @@ "output_type": "stream", "text": [ "────────────────────────────────────────────────────────────────────────────────────────────────────\n", - "Puzzle input ➜ 5 strs:\n", + "Puzzle input ➜ 5 strs of size 3732:\n", "────────────────────────────────────────────────────────────────────────────────────────────────────\n", " 4 82 68 85 74 6 56 14 2 8669 66 13 927 3 235 44 52 16 37 61 82 1774 96 15 775 789 91 ...\n", " 6 87 39 72 56 12 69 79 58 4378 86 49 146 5 412 85 7751 577 69 813 55 9942 753 49 734 587 15 ...\n", @@ -1148,7 +1150,7 @@ { "data": { "text/plain": [ - "Puzzle 6.1: .0014 seconds, answer 5877594983578 ok" + "Puzzle 6.1: .0015 seconds, answer 5877594983578 correct" ] }, "execution_count": 40, @@ -1231,7 +1233,7 @@ { "data": { "text/plain": [ - "Puzzle 6.2: .0039 seconds, answer 11159825706149 ok" + "Puzzle 6.2: .0039 seconds, answer 11159825706149 correct" ] }, "execution_count": 42, @@ -1367,7 +1369,7 @@ "source": [ "# [Day 7](https://adventofcode.com/2025/day/7): Laboratories\n", "\n", - "In the teleporter lab, we locate a diagram of a tachyon manifold, which is in need of repair. We obtain a diagram of the manifold, in which the `S` marks the start of the tachyon beams and a `^` is a splitter. \n", + "In the teleporter lab is a tachyon manifold in need of repair. We find a diagram of the manifold, with `S` marking the start of the tachyon beams and `^` marking is a splitter. \n", "\n", "I could parse this as a `Grid`, but I think I will just keep it as a list of strings. The main idea of a `Grid` is dealing with the 4 or 8 neighbors; a concept that this problem does not use." ] @@ -1383,7 +1385,7 @@ "output_type": "stream", "text": [ "────────────────────────────────────────────────────────────────────────────────────────────────────\n", - "Puzzle input ➜ 142 strs:\n", + "Puzzle input ➜ 142 strs of size 141:\n", "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "......................................................................S......................... ...\n", "................................................................................................ ...\n", @@ -1408,27 +1410,26 @@ "source": [ "### Part 1: How many times will the beam be split?\n", "\n", - "Tachyon beams move downwards unless they enounter a splitter, in which case they split into two beams, one immediately to the left of the splitter and one to the right, which each continue down from there. We're asked how many splits occur. If two beams end up in the same place they count as one beam, so if that beam is split again, that's just one more split, not two more.\n", + "Tachyon beams move downwards unless they enounter a `^` splitter, in which case they split into two beams, one immediately to the left of the splitter and one to the right. We're asked how many splits occur. If two beams end up in the same place they count as one beam, so if that beam is split again, that's just one more split, not two more.\n", "\n", - "That suggests I should keep a `set` of current beam positions, and update the set every time we go down one line. (It is a set rather than a list so that duplicate beam positins count as one, not as two.) I also need to keep track of a count of the number of splits, and return that at the end:" + "That suggests I should keep a `set` of current beam positions, and update the set every time we go down one line. (It is a set rather than a list so that duplicate beam positions count as one, not as two. (Also, if I kept a list of positions then given the size of the input there would be quadrillions of beams by the end, so it would take a very long time to get the wrong answer.)) I use the trick of iterating with `for b in list(beams)` rather than `for b in beams` so that I can mutate the set `beams` during the iteration. I also need to keep track of the split count, and return that at the end:" ] }, { "cell_type": "code", "execution_count": 49, - "id": "77852da2-0599-40e1-b949-f33ed69277c3", + "id": "d4d3bdbb-d8b2-4e22-adb4-c99a212a2ca7", "metadata": {}, "outputs": [], "source": [ - "### Part 1: How many times will the beam be split?\n", - "\n", "def count_splits(manifold: List[str]) -> int:\n", " \"\"\"How many beam split events occur as the beams make their way through the manifold?\"\"\"\n", - " beams = {manifold[0].index('S')}\n", + " start = manifold[0].index('S')\n", + " beams = {start}\n", " split_count = 0\n", " for line in manifold:\n", " for b in list(beams):\n", - " if line[b] == '^': # Replace the beam position with one on each side\n", + " if line[b] == '^': # Replace the beam position with one on each side and count a split\n", " split_count += 1\n", " beams.remove(b)\n", " beams.update({b - 1, b + 1})\n", @@ -1444,7 +1445,7 @@ { "data": { "text/plain": [ - "Puzzle 7.1: .0008 seconds, answer 1681 ok" + "Puzzle 7.1: .0008 seconds, answer 1681 correct" ] }, "execution_count": 50, @@ -1464,10 +1465,9 @@ "source": [ "### Part 2: In total, how many different timelines would a single tachyon particle end up on?\n", "\n", - "Now we're told this is a *quantum* tachyon manifold and we need to know how many different timelines a single tachyon appears in, or equivalently, how many different paths can the tachyon beams take. \n", + "Now we're told this is a *quantum* tachyon manifold and we need to know how many different *timelines* a single tachyon appears in, or in other words, how many different paths can the tachyon beams take to get to the last line.\n", "\n", - "We can't just count the number of beams in the last line, because if a beam in position *b* takes two diffferent paths to get there, that counts as two, not one. Instead, I'll replace the `set` from Part 1 with a `Counter` of `{beam_position: number_of_paths_to_get_here}`. If a beam at position *b* with *n* paths to get there encounters a splitter, then in the next step both positions *b* - 1 and *b* + 1 are incremented by *n*. In the end we return the sum of all the numbers of paths.\n", - "\n" + "We can't just count the number of beams in the last line, because if a beam in position *b* takes two different paths to get there, that counts as two, not one. Instead, I'll replace the `set` from Part 1 with a `Counter` of `{beam_position: number_of_paths_to_get_here}`. If a beam at position *b* with *n* paths to get there encounters a splitter, then in the next step both positions *b* - 1 and *b* + 1 are incremented by *n*. In the end we return the sum of all the counts of paths." ] }, { @@ -1479,7 +1479,7 @@ { "data": { "text/plain": [ - "Puzzle 7.2: .0013 seconds, answer 422102272495018 ok" + "Puzzle 7.2: .0014 seconds, answer 422102272495018 correct" ] }, "execution_count": 51, @@ -1490,11 +1490,12 @@ "source": [ "def count_timelines(manifold: List[str]) -> int:\n", " \"\"\"How many possible paths are there to the final line of the manifold?\"\"\"\n", - " beams = Counter({manifold[0].index('S'): 1})\n", + " start = manifold[0].index('S')\n", + " beams = Counter({start: 1})\n", " for line in manifold:\n", " for b, n in list(beams.items()):\n", " if line[b] == '^':\n", - " beams[b] = 0\n", + " beams[b] -= n\n", " beams[b - 1] += n\n", " beams[b + 1] += n\n", " return sum(beams.values())\n", @@ -1505,15 +1506,205 @@ }, { "cell_type": "markdown", - "id": "7f31ae9b-6606-40b0-9bb1-ed9b3fe3cbf0", + "id": "ed2369ae-cf37-4b12-9933-4bfe95fec3ac", "metadata": {}, "source": [ - "# Summary" + "\"Gary" + ] + }, + { + "cell_type": "markdown", + "id": "135b532d-d9ac-4400-9c6d-3b316d4596e1", + "metadata": {}, + "source": [ + "# [Day 8](https://adventofcode.com/2025/day/8): Playground\n", + "\n", + "In a giant playground, some elves are setting up an ambitious Christmas light decoration project using small electrical junction boxes. They have a list of the junction box coordinates in 3D space:" ] }, { "cell_type": "code", "execution_count": 52, + "id": "a5cc43b2-3c5d-4d28-8c94-112bbb5739e7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 1000 strs of size 13 to 17:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "83023,97914,59845\n", + "95221,6604,47587\n", + "15200,13848,533\n", + "91106,47235,95124\n", + "91396,58746,56994\n", + "72070,33359,10785\n", + "13733,43410,23707\n", + "84853,78378,68689\n", + "...\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Parsed representation ➜ 1000 tuples of size 3:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "(83023, 97914, 59845)\n", + "(95221, 6604, 47587)\n", + "(15200, 13848, 533)\n", + "(91106, 47235, 95124)\n", + "(91396, 58746, 56994)\n", + "(72070, 33359, 10785)\n", + "(13733, 43410, 23707)\n", + "(84853, 78378, 68689)\n", + "...\n" + ] + } + ], + "source": [ + "boxes = parse(8, ints)" + ] + }, + { + "cell_type": "markdown", + "id": "ee9f47ae-09f5-479e-9e34-34246d723952", + "metadata": {}, + "source": [ + "### Part 1: What is the product of the sizes of the three largest circuits?\n", + "\n", + "The goal is to start connecting junction boxes, starting with the two boxes that are closest to each other in 3D space, then the next two closest, and so on. (I assume that a box can connect to any number of other boxes.) After connecting the 1000 pairs that are closest together, what do you get if you multiply together the sizes of the three largest circuits?\n", + "\n", + "I recognize this as a [**greedy algorithm**](https://en.wikipedia.org/wiki/Greedy_algorithm), consuming shortest links first, and I've [done that before](TSP.ipynb). I also recognize this as a [**Union-Find**](https://en.wikipedia.org/wiki/Disjoint-set_data_structure) problem, and I know there are efficient data structures for that problem we don't make heavy use of the union-find functionality, so keeping it simple seems to be the best result. (After finishing my code I feel vindicated: I see in my [other notebook](Advent2025-AI.ipynb) that Claude Opus 4.5 also recognized the Union-Find problem, implemented a data structure for it, and ended up with code that ran slower than my simpler approach.)\n", + "\n", + "The function `greedy_connect` will keep a dict that maps each box to the circuit it is part of (initially just itself), and update that dict when two circuits are connected together. Then I'll go through the 1000 `closest_pairs` of boxes, updating the dict for each one, and return the dict at the end. Then the function `largest` will find the largest 3 circuits, and `prod` will multiply the sizes together." + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "fb7744c9-105b-439b-aa57-caf93b8117b8", + "metadata": {}, + "outputs": [], + "source": [ + "def greedy_connect(boxes, n=1000) -> Dict[Point, Tuple[Point, ...]]:\n", + " \"\"\"Go through the `n` closest pairs of boxes, shortest first. \n", + " If two boxes can be connected to form a new circuit, do it.\"\"\"\n", + " circuits = {B: (B,) for B in boxes} # A dict of {box: circuit}\n", + " for (A, B) in closest_pairs(boxes, n):\n", + " if circuits[A] != circuits[B]:\n", + " new_circuit = circuits[A] + circuits[B]\n", + " for C in new_circuit:\n", + " circuits[C] = new_circuit\n", + " return circuits\n", + "\n", + "def closest_pairs(points, n=1000) -> List[Tuple[Point, Point]]:\n", + " \"Return the `n` closest pairs of points, sorted shortest first.\"\n", + " pairs = combinations(points, 2)\n", + " return sorted(pairs, key=lambda link: distance_squared(*link))[:n]\n", + "\n", + "def largest(n, circuits) -> List[int]: \n", + " \"\"\"The lengths of the `n` largest circuits (ones with the most boxes).\"\"\"\n", + " return sorted(map(len, set(circuits.values())))[-n:]\n", + "\n", + "def distance_squared(P: Point, Q: Point) -> float:\n", + " \"\"\"The square of the distance between 3D points P and Q.\"\"\"\n", + " # Since we only care about sorting by distance, the distance squared is cheaper to compute\n", + " return (P[0] - Q[0]) ** 2 + (P[1] - Q[1]) ** 2 + (P[2] - Q[2]) ** 2" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "id": "337ab7d6-d142-4c56-a0a3-2b6cd06cd895", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 8.1: .6207 seconds, answer 24360 correct" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(8.1, 24360, lambda:\n", + " prod(largest(3, greedy_connect(boxes, 1000))))" + ] + }, + { + "cell_type": "markdown", + "id": "3291fa28-a8fa-4ef8-90b3-2b4b6fc9625e", + "metadata": {}, + "source": [ + "### Part 2: What do you get if you multiply together the X coordinates of the last two junction boxes you need to connect?\n", + "\n", + "Now we are asked for the product of the X coordinates of the final pair of boxes to be connected. Unfortunately I'll have to copy and paste most of `greedy_connect` to form `last_connected`, which checks to see if all the boxes are connected into one circuit, and if so returns the two boxes that made the final connection. " + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "1096ded6-749e-4787-9b90-2de917b147c4", + "metadata": {}, + "outputs": [], + "source": [ + "def last_connected(boxes) -> dict:\n", + " \"\"\"Go through tall the pairs of boxes, in closest first order. \n", + " Return the last two boxes that finally make it all one big circuit.\"\"\"\n", + " circuits = {B: (B,) for B in boxes} # A dict of {box: circuit}\n", + " for (A, B) in closest_pairs(boxes, -1):\n", + " if circuits[A] != circuits[B]:\n", + " new_circuit = circuits[A] + circuits[B]\n", + " if len(new_circuit) == len(boxes):\n", + " return (A, B)\n", + " for C in new_circuit:\n", + " circuits[C] = new_circuit" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "58f06244-ca67-47c9-8ca5-3346a249e1fc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 8.2: .6530 seconds, answer 2185817796 correct" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(8.2, 2185817796, lambda:\n", + " prod(Xs(last_connected(boxes))))" + ] + }, + { + "cell_type": "markdown", + "id": "fa988909-1a8b-4e8c-aca6-c53af99bc0b6", + "metadata": {}, + "source": [ + "Today's puzzles had the slowest run times yet. I could perhaps make them faster by mutating sets rather than forming a new tuple for each new circuit, but I think gains from that would be small, and since the run time is still under a second, I'll leave the code as is." + ] + }, + { + "cell_type": "markdown", + "id": "7f31ae9b-6606-40b0-9bb1-ed9b3fe3cbf0", + "metadata": {}, + "source": [ + "# Summary\n", + "\n", + "Here are the run times and the correct answers for each of the puzzles:" + ] + }, + { + "cell_type": "code", + "execution_count": 57, "id": "ba36579c-d0b4-4fd3-939c-0026ecddd7e9", "metadata": {}, "outputs": [ @@ -1521,22 +1712,24 @@ "name": "stdout", "output_type": "stream", "text": [ - "Puzzle 1.1: .0005 seconds, answer 1182 ok\n", - "Puzzle 1.2: .0009 seconds, answer 6907 ok\n", - "Puzzle 2.1: .0030 seconds, answer 23560874270 ok\n", - "Puzzle 2.2: .0041 seconds, answer 44143124633 ok\n", - "Puzzle 3.1: .0006 seconds, answer 17085 ok\n", - "Puzzle 3.2: .0020 seconds, answer 169408143086082 ok\n", - "Puzzle 4.1: .0541 seconds, answer 1569 ok\n", - "Puzzle 4.2: .1421 seconds, answer 9280 ok\n", - "Puzzle 5.1: .0076 seconds, answer 635 ok\n", - "Puzzle 5.2: .0001 seconds, answer 369761800782619 ok\n", - "Puzzle 6.1: .0014 seconds, answer 5877594983578 ok\n", - "Puzzle 6.2: .0039 seconds, answer 11159825706149 ok\n", - "Puzzle 7.1: .0008 seconds, answer 1681 ok\n", - "Puzzle 7.2: .0013 seconds, answer 422102272495018 ok\n", + "Puzzle 1.1: .0006 seconds, answer 1182 correct\n", + "Puzzle 1.2: .0009 seconds, answer 6907 correct\n", + "Puzzle 2.1: .0032 seconds, answer 23560874270 correct\n", + "Puzzle 2.2: .0037 seconds, answer 44143124633 correct\n", + "Puzzle 3.1: .0006 seconds, answer 17085 correct\n", + "Puzzle 3.2: .0020 seconds, answer 169408143086082 correct\n", + "Puzzle 4.1: .0527 seconds, answer 1569 correct\n", + "Puzzle 4.2: .1424 seconds, answer 9280 correct\n", + "Puzzle 5.1: .0079 seconds, answer 635 correct\n", + "Puzzle 5.2: .0001 seconds, answer 369761800782619 correct\n", + "Puzzle 6.1: .0015 seconds, answer 5877594983578 correct\n", + "Puzzle 6.2: .0039 seconds, answer 11159825706149 correct\n", + "Puzzle 7.1: .0008 seconds, answer 1681 correct\n", + "Puzzle 7.2: .0014 seconds, answer 422102272495018 correct\n", + "Puzzle 8.1: .6207 seconds, answer 24360 correct\n", + "Puzzle 8.2: .6530 seconds, answer 2185817796 correct\n", "\n", - "Time in seconds: total = 0.222, mean = 0.016, max = 0.142\n" + "Time in seconds: sum = 1.495, mean = .093, median = .003, max = .653\n" ] } ],