{ "cells": [ { "cell_type": "markdown", "id": "bd491a25-2d77-441b-8c64-e4871329b43c", "metadata": {}, "source": [ "
Peter Norvig, December 2025
\n", "\n", "# Advent of Code 2025: The AI 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", "\n", "In **this** notebook I show some solutions by various AI Large Language Models: Gemini, Claude, and ChatGPT.\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).\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. So far the LLMs are doing quite well. \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 aaccurate and thorough!)\n", "- The \"__main__\"` idiom for running code from the command line\n", "- Test examples to run.\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." ] }, { "cell_type": "code", "execution_count": 1, "id": "34563e0b-09c5-4600-a455-3ff0f31b81a0", "metadata": {}, "outputs": [], "source": [ "%run AdventUtils.ipynb\n", "current_year = 2025" ] }, { "cell_type": "markdown", "id": "39657fac-2a12-43d5-ab79-024ebceeadc4", "metadata": {}, "source": [ "# [Day 1](https://adventofcode.com/2025/day/1) Gemini 3 Pro\n", "\n", "I started with the Gemini 3 Pro Fast model, which produced this following code:" ] }, { "cell_type": "code", "execution_count": 2, "id": "a03354cc-951c-4609-80e1-a7e1a6d0c2bd", "metadata": {}, "outputs": [], "source": [ "def solve_safe_password(rotations):\n", " \"\"\"\n", " Simulates the safe dial rotations and counts how many times the dial\n", " lands on 0.\n", "\n", " Args:\n", " rotations: A list of strings, each representing a rotation (e.g., \"L68\").\n", "\n", " Returns:\n", " The total number of times the dial points at 0 after a rotation.\n", " \"\"\"\n", " # The dial starts at 50\n", " current_position = 50\n", " zero_count = 0\n", " \n", " # The dial has numbers 0 through 99, so all math is modulo 100\n", " MODULO = 100\n", "\n", " for rotation in rotations:\n", " # 1. Parse the instruction\n", " direction = rotation[0]\n", " # Distance is the rest of the string converted to an integer\n", " distance = int(rotation[1:])\n", "\n", " # 2. Determine the rotation value\n", " if direction == 'R':\n", " rotation_value = distance\n", " elif direction == 'L':\n", " # Left rotations subtract from the current position\n", " rotation_value = -distance\n", " else:\n", " # Should not happen based on problem description\n", " print(f\"Error: Unknown direction '{direction}' in rotation: {rotation}\")\n", " continue\n", "\n", " # 3. Calculate the new position and apply Modulo 100\n", " # The standard Python % operator handles negative numbers correctly\n", " # for a positive modulus (e.g., (-1 - 99) % 100 == 0).\n", " new_position = (current_position + rotation_value) % MODULO\n", "\n", " # 4. Check for Zero\n", " if new_position == 0:\n", " zero_count += 1\n", " \n", " # 5. Update Position\n", " current_position = new_position\n", " \n", " # Optional: Print step-by-step for debugging/verification\n", " # print(f\"Rotation {rotation}: new position = {new_position}, zero_count = {zero_count}\")\n", "\n", " return zero_count" ] }, { "cell_type": "markdown", "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:" ] }, { "cell_type": "code", "execution_count": 3, "id": "c3ff875a-6267-47b8-a032-115a40bfd246", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 1.1: .0013 seconds, answer 1182 ok" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "rotations = get_text(1).splitlines()\n", "\n", "answer(puzzle=1.1, solution=1182, code=lambda: \n", " solve_safe_password(rotations))" ] }, { "cell_type": "markdown", "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:" ] }, { "cell_type": "code", "execution_count": 4, "id": "a1cf0e58-4567-4713-9d4b-e57f5c958800", "metadata": {}, "outputs": [], "source": [ "import math\n", "\n", "def solve_new_safe_password(rotations):\n", " \"\"\"\n", " Simulates the safe dial rotations and counts every time the dial\n", " points at 0, including during the rotation movement.\n", "\n", " Args:\n", " rotations: A list of strings, each representing a rotation (e.g., \"L68\").\n", "\n", " Returns:\n", " The total number of times the dial points at 0.\n", " \"\"\"\n", " # The dial starts at 50\n", " current_position = 50\n", " total_zero_crossings = 0\n", " MODULO = 100\n", "\n", " for rotation in rotations:\n", " direction = rotation[0]\n", " distance = int(rotation[1:])\n", " \n", " S = current_position # Start Position\n", " D = distance # Distance\n", " \n", " crossings_for_this_rotation = 0\n", "\n", " if direction == 'L':\n", " # --- LEFT ROTATION ---\n", " # Dial moves from S -> S-1 -> ... -> 0 -> 99 -> ...\n", " \n", " # 1. Distance from S to the first 0: S clicks\n", " # If D is large enough to cover this distance, the first 0 is crossed.\n", " \n", " if D >= S:\n", " # The first crossing (at position 0) is achieved\n", " crossings_for_this_rotation += 1\n", " \n", " # 2. Remaining distance after first 0 crossing\n", " remaining_distance = D - S\n", " \n", " # 3. Subsequent crossings: The dial crosses 0 every 100 clicks\n", " # after the first one.\n", " crossings_for_this_rotation += remaining_distance // MODULO\n", " \n", " elif direction == 'R':\n", " # --- RIGHT ROTATION ---\n", " # Dial moves from S -> S+1 -> ... -> 99 -> 0 -> 1 -> ...\n", " \n", " # 1. Distance from S to the first 0: 100 - S clicks\n", " # (e.g., 50 to 0 is 50 clicks; 99 to 0 is 1 click)\n", " distance_to_first_zero = MODULO - S\n", " \n", " if D >= distance_to_first_zero:\n", " # The first crossing (at position 0) is achieved\n", " crossings_for_this_rotation += 1\n", " \n", " # 2. Remaining distance after first 0 crossing\n", " remaining_distance = D - distance_to_first_zero\n", " \n", " # 3. Subsequent crossings: The dial crosses 0 every 100 clicks\n", " crossings_for_this_rotation += remaining_distance // MODULO\n", " \n", " # Add the crossings for this rotation to the total\n", " total_zero_crossings += crossings_for_this_rotation\n", "\n", " # Calculate the final position to set up for the next rotation\n", " if direction == 'R':\n", " rotation_value = distance\n", " else: # 'L'\n", " rotation_value = -distance\n", " \n", " current_position = (current_position + rotation_value) % MODULO\n", "\n", " return total_zero_crossings" ] }, { "cell_type": "markdown", "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", "\n", "Here I check whether the code is correct:" ] }, { "cell_type": "code", "execution_count": 5, "id": "a0b3e0fc-e044-4c1e-b4b6-6bc735145f57", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 1.2: .0018 seconds, answer 7509 WRONG; expected answer is 6907" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(1.2, 6907, lambda: \n", " solve_new_safe_password(rotations))" ] }, { "cell_type": "markdown", "id": "d640d8e4-6940-416d-93af-b55d82482a35", "metadata": {}, "source": [ "Too bad. 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", "\n", "which is exactly the error I made on *my* first attempt. Gemini produced the following updated code:" ] }, { "cell_type": "code", "execution_count": 6, "id": "196a6a93-e318-4a45-a023-9997fa0fb0f5", "metadata": {}, "outputs": [], "source": [ "def solve_safe_password_method_2(rotations):\n", " \"\"\"\n", " Simulates the safe dial rotations and counts every time the dial\n", " points at 0 (Method 0x434C49434B).\n", " \n", " Correctly handles cases where the dial starts at 0.\n", " \"\"\"\n", " current_position = 50\n", " total_zero_crossings = 0\n", " MODULO = 100\n", "\n", " for rotation in rotations:\n", " direction = rotation[0]\n", " distance = int(rotation[1:])\n", " \n", " # Determine distance required to hit the *next* 0\n", " if direction == 'L':\n", " if current_position == 0:\n", " dist_to_first_zero = 100\n", " else:\n", " dist_to_first_zero = current_position\n", " \n", " elif direction == 'R':\n", " if current_position == 0:\n", " dist_to_first_zero = 100\n", " else:\n", " dist_to_first_zero = 100 - current_position\n", "\n", " # Check if the rotation distance is enough to reach the first zero\n", " if distance >= dist_to_first_zero:\n", " # We hit 0 at least once\n", " count = 1\n", " \n", " # Remove the distance traveled to the first zero\n", " remaining_distance = distance - dist_to_first_zero\n", " \n", " # Add any subsequent full rotations (every 100 clicks hits 0 again)\n", " count += remaining_distance // 100\n", " \n", " total_zero_crossings += count\n", "\n", " # Update position for next step\n", " if direction == 'R':\n", " current_position = (current_position + distance) % 100\n", " else:\n", " current_position = (current_position - distance) % 100\n", "\n", " return total_zero_crossings" ] }, { "cell_type": "markdown", "id": "e05db80b-c56d-4e1f-988c-a09be8ca5a3e", "metadata": {}, "source": [ "This second try is indeed correct:" ] }, { "cell_type": "code", "execution_count": 7, "id": "021043ed-2ac5-4aaf-8171-985f0f5911f2", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 1.2: .0024 seconds, answer 6907 ok" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(1.2, 6907, lambda: \n", " solve_safe_password_method_2(rotations))" ] }, { "cell_type": "markdown", "id": "82fb1dca-1619-4ad7-9155-52fb4804470e", "metadata": {}, "source": [ "# [Day 2](https://adventofcode.com/2025/day/2) Claude Opus 4.5\n", "\n", "I gave Claude the instructions for Day 2 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:" ] }, { "cell_type": "code", "execution_count": 8, "id": "8eac98f3-b884-4d95-b38b-ea4365ec3004", "metadata": {}, "outputs": [], "source": [ "def find_invalid_ids_in_range(start, end):\n", " invalid_ids = []\n", " min_digits = len(str(start))\n", " max_digits = len(str(end))\n", " \n", " for total_digits in range(min_digits, max_digits + 1):\n", " if total_digits % 2 != 0:\n", " continue\n", " \n", " half_digits = total_digits // 2\n", " pattern_start = 1 if half_digits == 1 else 10 ** (half_digits - 1)\n", " pattern_end = 10 ** half_digits - 1\n", " for pattern in range(pattern_start, pattern_end + 1):\n", " pattern_str = str(pattern)\n", " invalid_id = int(pattern_str + pattern_str)\n", " if start <= invalid_id <= end:\n", " invalid_ids.append(invalid_id)\n", " \n", " return invalid_ids\n", "\n", "\n", "def solve(input_str):\n", " input_str = input_str.strip().replace('\\n', '').replace(' ', '').rstrip(',')\n", " total = 0\n", " for r in input_str.split(','):\n", " if r:\n", " start, end = map(int, r.split('-'))\n", " total += sum(find_invalid_ids_in_range(start, end))\n", " return total" ] }, { "cell_type": "markdown", "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", "\n", "I verified that the code gives the correct answer:" ] }, { "cell_type": "code", "execution_count": 9, "id": "a91845ec-ace7-482e-b0b5-8a620ef3461f", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 2.1: .1263 seconds, answer 23560874270 ok" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "id_ranges = get_text(2)\n", "\n", "answer(2.1, 23560874270, lambda:\n", " solve(id_ranges))" ] }, { "cell_type": "markdown", "id": "a31d006f-8cf2-4e4c-92d3-d7b7def22227", "metadata": {}, "source": [ "When given the Part 2 instructions, Claude wrote the following code:" ] }, { "cell_type": "code", "execution_count": 10, "id": "f0dc176b-dd85-40a4-ac5c-dfa936a6a524", "metadata": {}, "outputs": [], "source": [ "def find_invalid_ids_in_range(start, end):\n", " invalid_ids = set()\n", " min_digits = len(str(start))\n", " max_digits = len(str(end))\n", " \n", " for total_digits in range(min_digits, max_digits + 1):\n", " # Pattern length can be 1 to total_digits//2 (need at least 2 repetitions)\n", " for pattern_len in range(1, total_digits // 2 + 1):\n", " if total_digits % pattern_len != 0:\n", " continue\n", " \n", " repetitions = total_digits // pattern_len\n", " if repetitions < 2:\n", " continue\n", " \n", " pattern_start = 1 if pattern_len == 1 else 10 ** (pattern_len - 1)\n", " pattern_end = 10 ** pattern_len - 1\n", " \n", " for pattern in range(pattern_start, pattern_end + 1):\n", " pattern_str = str(pattern)\n", " invalid_id = int(pattern_str * repetitions)\n", " if start <= invalid_id <= end:\n", " invalid_ids.add(invalid_id)\n", " \n", " return invalid_ids\n", "\n", "\n", "def solve(input_str):\n", " input_str = input_str.strip().replace('\\n', '').replace(' ', '').rstrip(',')\n", " total = 0\n", " for r in input_str.split(','):\n", " if r:\n", " start, end = map(int, r.split('-'))\n", " total += sum(find_invalid_ids_in_range(start, end))\n", " return total" ] }, { "cell_type": "markdown", "id": "287ed190-e8c3-4aa8-a01b-b4ecd97327b5", "metadata": {}, "source": [ "Claude's code produces the correct answer:" ] }, { "cell_type": "code", "execution_count": 11, "id": "9c0049e6-a992-4aa8-a2d7-3ea748e361a6", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 2.2: .1350 seconds, answer 44143124633 ok" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(2.2, 44143124633, lambda:\n", " solve(id_ranges))" ] }, { "cell_type": "markdown", "id": "d3533d6a-d12f-4dbf-b0e8-9d878c9bc283", "metadata": {}, "source": [ "# [Day 3](https://adventofcode.com/2025/day/3) ChatGPT 5.1 Auto\n", "\n", "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 (lacking comments or doc strings):" ] }, { "cell_type": "code", "execution_count": 12, "id": "3aa266f3-50d0-4d8d-a464-4c74c52daa69", "metadata": {}, "outputs": [], "source": [ "def total_output_joltage(input_text: str) -> int:\n", " def max_joltage_for_bank(bank: str) -> int:\n", " max_first = -1\n", " max_pair = 0\n", " for ch in bank:\n", " if not ch.isdigit():\n", " continue\n", " d = int(ch)\n", " if max_first != -1:\n", " pair = 10 * max_first + d\n", " if pair > max_pair:\n", " max_pair = pair\n", " if d > max_first:\n", " max_first = d\n", " return max_pair\n", "\n", " total = 0\n", " for line in input_text.strip().splitlines():\n", " line = line.strip()\n", " if line:\n", " total += max_joltage_for_bank(line)\n", " return total" ] }, { "cell_type": "markdown", "id": "6c2da1ae-d915-4f37-9494-bbe427ef87ad", "metadata": {}, "source": [ "I verified this is correct:" ] }, { "cell_type": "code", "execution_count": 13, "id": "09bf306b-8762-4346-aff9-bcff33639c71", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 3.1: .0044 seconds, answer 17085 ok" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "banks = get_text(3)\n", "\n", "answer(3.1, 17085, lambda:\n", " total_output_joltage(banks))" ] }, { "cell_type": "markdown", "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):" ] }, { "cell_type": "code", "execution_count": 14, "id": "bdb8b4e4-bed0-48dc-a045-47cd4c6002fd", "metadata": {}, "outputs": [], "source": [ "def total_output_joltage(input_text: str, k: int = 12) -> int:\n", " \"\"\"\n", " Given the full puzzle input as a string, return the total maximum joltage\n", " when, for each line (bank), you must choose exactly k digits in order to\n", " form the largest possible number.\n", " \"\"\"\n", "\n", " def max_joltage_for_bank(bank: str, k: int) -> int:\n", " # Extract digits from the line\n", " digits = [int(ch) for ch in bank if ch.isdigit()]\n", " n = len(digits)\n", " if k > n:\n", " raise ValueError(f\"Bank has only {n} digits, cannot pick {k}.\")\n", "\n", " # We will remove exactly n - k digits to maximize the resulting sequence\n", " removals = n - k\n", " stack = []\n", "\n", " for d in digits:\n", " # While we can still remove digits, and the last digit in the stack\n", " # is smaller than the current digit, pop it to make the number larger.\n", " while stack and removals > 0 and stack[-1] < d:\n", " stack.pop()\n", " removals -= 1\n", " stack.append(d)\n", "\n", " # If we still have removals left, remove from the end\n", " if removals > 0:\n", " stack = stack[:-removals]\n", "\n", " # Now stack should have length n - original_removals == k\n", " chosen = stack[:k]\n", " return int(\"\".join(map(str, chosen)))\n", "\n", " total = 0\n", " for line in input_text.strip().splitlines():\n", " line = line.strip()\n", " if not line:\n", " continue\n", " total += max_joltage_for_bank(line, k)\n", "\n", " return total" ] }, { "cell_type": "code", "execution_count": 15, "id": "70bde9b9-beb1-4e9d-bef6-0f20fb958891", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 3.2: .0076 seconds, answer 169408143086082 ok" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(3.2, 169408143086082, lambda:\n", " total_output_joltage(banks))" ] }, { "cell_type": "markdown", "id": "00625b83-f56f-4fff-8d87-1e9cdbc02847", "metadata": {}, "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`)." ] }, { "cell_type": "code", "execution_count": 16, "id": "35bf1f30-07c7-4842-a6e3-e33fb874e779", "metadata": {}, "outputs": [], "source": [ "def count_accessible_rolls(grid_str):\n", " \"\"\"\n", " Parses a grid string and counts paper rolls (@) that have\n", " fewer than 4 neighbors of the same type.\n", " \"\"\"\n", " # Parse input into a list of lists (2D grid)\n", " # .strip() removes leading/trailing whitespace to handle clean input\n", " lines = grid_str.strip().split('\\n')\n", " grid = [list(line) for line in lines]\n", " \n", " rows = len(grid)\n", " cols = len(grid[0])\n", " \n", " accessible_count = 0\n", " \n", " # Directions for 8 neighbors: (row_offset, col_offset)\n", " directions = [\n", " (-1, -1), (-1, 0), (-1, 1),\n", " (0, -1), (0, 1),\n", " (1, -1), (1, 0), (1, 1)\n", " ]\n", " \n", " for r in range(rows):\n", " for c in range(cols):\n", " # We only care if the current spot is a paper roll\n", " if grid[r][c] != '@':\n", " continue\n", " \n", " neighbor_rolls = 0\n", " \n", " # Check all 8 neighbors\n", " for dr, dc in directions:\n", " nr, nc = r + dr, c + dc\n", " \n", " # Check boundaries\n", " if 0 <= nr < rows and 0 <= nc < cols:\n", " if grid[nr][nc] == '@':\n", " neighbor_rolls += 1\n", " \n", " # The Condition: fewer than 4 adjacent rolls\n", " if neighbor_rolls < 4:\n", " accessible_count += 1\n", " \n", " return accessible_count" ] }, { "cell_type": "markdown", "id": "1e12bc4c-8cc8-4c01-b4ad-5392b49642e6", "metadata": {}, "source": [ "Here I verify that the code does produce the correct answer:" ] }, { "cell_type": "code", "execution_count": 17, "id": "5b54c152-ce26-4baf-8b51-d4a166c6c2e7", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 4.1: .0171 seconds, answer 1569 ok" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "grid = get_text(4)\n", "\n", "answer(4.1, 1569, lambda:\n", " count_accessible_rolls(grid))" ] }, { "cell_type": "markdown", "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." ] }, { "cell_type": "code", "execution_count": 18, "id": "16a1a0db-7501-41fd-a606-87fbb79273bd", "metadata": {}, "outputs": [], "source": [ "def count_total_removable_rolls(grid_str):\n", " \"\"\"\n", " Parses a grid string and iteratively removes paper rolls (@) \n", " that have fewer than 4 neighbors of the same type.\n", " Returns the total number of rolls removed.\n", " \"\"\"\n", " # Parse input into a list of lists (2D grid)\n", " lines = grid_str.strip().split('\\n')\n", " grid = [list(line) for line in lines]\n", " \n", " rows = len(grid)\n", " cols = len(grid[0])\n", " \n", " total_removed_count = 0\n", " \n", " # Directions for 8 neighbors\n", " directions = [\n", " (-1, -1), (-1, 0), (-1, 1),\n", " (0, -1), (0, 1),\n", " (1, -1), (1, 0), (1, 1)\n", " ]\n", " \n", " while True:\n", " rolls_to_remove = []\n", " \n", " # 1. Identify all rolls that can be removed in this pass\n", " for r in range(rows):\n", " for c in range(cols):\n", " if grid[r][c] != '@':\n", " continue\n", " \n", " neighbor_rolls = 0\n", " for dr, dc in directions:\n", " nr, nc = r + dr, c + dc\n", " if 0 <= nr < rows and 0 <= nc < cols:\n", " if grid[nr][nc] == '@':\n", " neighbor_rolls += 1\n", " \n", " # Condition: fewer than 4 adjacent rolls\n", " if neighbor_rolls < 4:\n", " rolls_to_remove.append((r, c))\n", " \n", " # 2. If nothing to remove, we are done\n", " if not rolls_to_remove:\n", " break\n", " \n", " # 3. Remove them (update grid and count)\n", " total_removed_count += len(rolls_to_remove)\n", " for r, c in rolls_to_remove:\n", " grid[r][c] = '.' # Mark as empty space\n", " \n", " return total_removed_count" ] }, { "cell_type": "code", "execution_count": 19, "id": "b47c2e05-978a-4b22-aafc-e31ee1825387", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 4.2: .3960 seconds, answer 9280 ok" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(4.2, 9280, lambda:\n", " count_total_removable_rolls(grid))" ] }, { "cell_type": "markdown", "id": "81d88b41-9dec-404d-9bca-9e3ba5caa14d", "metadata": {}, "source": [ "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", "\n", "This is the same efficiency gain that I realized in my solution." ] }, { "cell_type": "code", "execution_count": 20, "id": "71bfe887-fbd4-4378-b37f-d0b88f9fa3e7", "metadata": {}, "outputs": [], "source": [ "def count_total_removable_rolls(grid_str):\n", " \"\"\"\n", " Parses a grid string and iteratively removes paper rolls (@) \n", " that have fewer than 4 neighbors of the same type.\n", " Returns the total number of rolls removed.\n", " \n", " Optimized to use a worklist (queue) approach instead of full grid scans.\n", " \"\"\"\n", " # Parse input into a list of lists (2D grid)\n", " lines = grid_str.strip().split('\\n')\n", " grid = [list(line) for line in lines]\n", " \n", " rows = len(grid)\n", " cols = len(grid[0])\n", " \n", " total_removed_count = 0\n", " \n", " # Directions for 8 neighbors\n", " directions = [\n", " (-1, -1), (-1, 0), (-1, 1),\n", " (0, -1), (0, 1),\n", " (1, -1), (1, 0), (1, 1)\n", " ]\n", " \n", " def count_neighbors(r, c):\n", " count = 0\n", " for dr, dc in directions:\n", " nr, nc = r + dr, c + dc\n", " if 0 <= nr < rows and 0 <= nc < cols:\n", " if grid[nr][nc] == '@':\n", " count += 1\n", " return count\n", "\n", " # 1. Initial scan: Find all rolls that are immediately removable\n", " queue = []\n", " for r in range(rows):\n", " for c in range(cols):\n", " if grid[r][c] == '@':\n", " if count_neighbors(r, c) < 4:\n", " queue.append((r, c))\n", " \n", " # 2. Process the queue\n", " # Using a list as a queue. Duplicates are handled by checking grid state.\n", " head = 0\n", " while head < len(queue):\n", " r, c = queue[head]\n", " head += 1\n", " \n", " # If already removed (e.g., added to queue multiple times), skip\n", " if grid[r][c] != '@':\n", " continue\n", " \n", " # Remove the roll\n", " grid[r][c] = '.'\n", " total_removed_count += 1\n", " \n", " # Check its neighbors\n", " for dr, dc in directions:\n", " nr, nc = r + dr, c + dc\n", " if 0 <= nr < rows and 0 <= nc < cols:\n", " # If neighbor is a roll, check if it became removable\n", " if grid[nr][nc] == '@':\n", " if count_neighbors(nr, nc) < 4:\n", " queue.append((nr, nc))\n", " \n", " return total_removed_count" ] }, { "cell_type": "code", "execution_count": 21, "id": "f370ee38-67af-42a6-9ad3-cdeec2019ff3", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 4.2: .0937 seconds, answer 9280 ok" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(4.2, 9280, lambda:\n", " count_total_removable_rolls(grid))" ] }, { "cell_type": "markdown", "id": "78434cfe-d728-453c-8f45-fc6b5fea18c3", "metadata": {}, "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." ] }, { "cell_type": "code", "execution_count": 22, "id": "e7ab7dac-9686-4a76-b83f-6779275c3519", "metadata": {}, "outputs": [], "source": [ "\"\"\"\n", "Solution for the Fresh Ingredients puzzle.\n", "Parses a database with fresh ingredient ID ranges and available ingredient IDs,\n", "then counts how many available ingredients are fresh.\n", "\"\"\"\n", "\n", "def parse_input(input_text):\n", " \"\"\"Parse the input into ranges and ingredient IDs.\"\"\"\n", " parts = input_text.strip().split('\\n\\n')\n", " \n", " # Parse fresh ranges\n", " ranges = []\n", " for line in parts[0].strip().split('\\n'):\n", " start, end = map(int, line.split('-'))\n", " ranges.append((start, end))\n", " \n", " # Parse available ingredient IDs\n", " ingredient_ids = []\n", " for line in parts[1].strip().split('\\n'):\n", " ingredient_ids.append(int(line))\n", " \n", " return ranges, ingredient_ids\n", "\n", "\n", "def is_fresh(ingredient_id, ranges):\n", " \"\"\"Check if an ingredient ID falls within any fresh range.\"\"\"\n", " for start, end in ranges:\n", " if start <= ingredient_id <= end:\n", " return True\n", " return False\n", "\n", "\n", "def count_fresh_ingredients(ranges, ingredient_ids):\n", " \"\"\"Count how many ingredient IDs are fresh.\"\"\"\n", " count = 0\n", " for ingredient_id in ingredient_ids:\n", " if is_fresh(ingredient_id, ranges):\n", " count += 1\n", " return count\n", "\n", "\n", "def solve(input_text):\n", " \"\"\"Solve the puzzle and return the count of fresh ingredients.\"\"\"\n", " ranges, ingredient_ids = parse_input(input_text)\n", " return count_fresh_ingredients(ranges, ingredient_ids)" ] }, { "cell_type": "markdown", "id": "40fc662b-ecee-4abe-a23a-ca7786edd438", "metadata": {}, "source": [ "Here I feed the input to `solve` and verify that the answer is correct:" ] }, { "cell_type": "code", "execution_count": 23, "id": "49f0bb9c-00c0-4983-ab26-d1cec1e692ac", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 5.1: .0058 seconds, answer 635 ok" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "input_text = get_text(5)\n", "\n", "answer(5.1, 635, lambda:\n", " solve(input_text))" ] }, { "cell_type": "markdown", "id": "dabe3fbc-0fa6-46d0-adfb-f6413f3a63d5", "metadata": {}, "source": [ "For Part 2, Claude chose to sort ranges and then merge them. That's a reasonable approach." ] }, { "cell_type": "code", "execution_count": 24, "id": "b70269fb-3a0b-4728-9d60-421e3b35a877", "metadata": {}, "outputs": [], "source": [ "\"\"\"\n", "Solution for the Fresh Ingredients puzzle - Part 2.\n", "Count all unique ingredient IDs that are considered fresh by any range.\n", "\"\"\"\n", "\n", "def parse_input(input_text):\n", " \"\"\"Parse the input to get fresh ranges (ignore ingredient IDs section).\"\"\"\n", " parts = input_text.strip().split('\\n\\n')\n", " \n", " # Parse fresh ranges (only first section matters now)\n", " ranges = []\n", " for line in parts[0].strip().split('\\n'):\n", " start, end = map(int, line.split('-'))\n", " ranges.append((start, end))\n", " \n", " return ranges\n", "\n", "\n", "def merge_ranges(ranges):\n", " \"\"\"Merge overlapping ranges to count unique IDs efficiently.\"\"\"\n", " if not ranges:\n", " return []\n", " \n", " # Sort ranges by start value\n", " sorted_ranges = sorted(ranges)\n", " \n", " merged = [sorted_ranges[0]]\n", " \n", " for current_start, current_end in sorted_ranges[1:]:\n", " last_start, last_end = merged[-1]\n", " \n", " # Check if current range overlaps or is adjacent to the last merged range\n", " if current_start <= last_end + 1:\n", " # Merge by extending the end if needed\n", " merged[-1] = (last_start, max(last_end, current_end))\n", " else:\n", " # No overlap, add as new range\n", " merged.append((current_start, current_end))\n", " \n", " return merged\n", "\n", "\n", "def count_fresh_ids(ranges):\n", " \"\"\"Count total unique fresh ingredient IDs.\"\"\"\n", " merged = merge_ranges(ranges)\n", " \n", " total = 0\n", " for start, end in merged:\n", " # Count is inclusive: end - start + 1\n", " total += end - start + 1\n", " \n", " return total\n", "\n", "\n", "def solve(input_text):\n", " \"\"\"Solve part 2 and return the count of all fresh ingredient IDs.\"\"\"\n", " ranges = parse_input(input_text)\n", " return count_fresh_ids(ranges)" ] }, { "cell_type": "code", "execution_count": 25, "id": "bc525b8f-8c03-4ac1-a52d-1ba2d8c1e5bb", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 5.2: .0002 seconds, answer 369761800782619 ok" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(5.2, 369761800782619, lambda:\n", " solve(input_text))" ] }, { "cell_type": "markdown", "id": "b1503029-3a5f-4949-8502-75b051f78a23", "metadata": {}, "source": [ "# [Day 6](https://adventofcode.com/2025/day/6): ChatGPT 5.1 Auto\n", "\n", "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: the pattern \"`if some_boolean: True else: False`\" can always be replaced with \"`some_boolean`\".)" ] }, { "cell_type": "code", "execution_count": 26, "id": "fc4e8201-532f-47e8-aa95-e5dad13b8f4f", "metadata": {}, "outputs": [], "source": [ "import re\n", "from functools import reduce\n", "from operator import add, mul\n", "\n", "def solve(lines):\n", " if not lines:\n", " return 0\n", "\n", " # Preserve internal spaces; pad all lines to same width\n", " width = max(len(line) for line in lines)\n", " grid = [line.rstrip('\\n').ljust(width) for line in lines]\n", " h = len(grid)\n", "\n", " # Identify separator columns: columns that are all spaces\n", " sep = []\n", " for c in range(width):\n", " if all(grid[r][c] == ' ' for r in range(h)):\n", " sep.append(True)\n", " else:\n", " sep.append(False)\n", "\n", " # Find ranges of columns that form individual problems\n", " problem_ranges = []\n", " in_problem = False\n", " start = 0\n", " for c in range(width):\n", " if not sep[c]:\n", " if not in_problem:\n", " in_problem = True\n", " start = c\n", " else:\n", " if in_problem:\n", " in_problem = False\n", " problem_ranges.append((start, c - 1))\n", " if in_problem:\n", " problem_ranges.append((start, width - 1))\n", "\n", " grand_total = 0\n", "\n", " # Process each problem\n", " for c_start, c_end in problem_ranges:\n", " numbers = []\n", "\n", " # All rows except the last contain numbers (potentially)\n", " for r in range(h - 1):\n", " segment = grid[r][c_start:c_end + 1]\n", " for match in re.finditer(r'\\d+', segment):\n", " numbers.append(int(match.group(0)))\n", "\n", " # Last row contains the operator (+ or *)\n", " bottom_segment = grid[h - 1][c_start:c_end + 1]\n", " if '+' in bottom_segment:\n", " op = add\n", " elif '*' in bottom_segment:\n", " op = mul\n", " else:\n", " # No valid operator found; skip this malformed problem\n", " continue\n", "\n", " if not numbers:\n", " continue\n", "\n", " # Fold numbers using the chosen operator\n", " result = reduce(op, numbers)\n", " grand_total += result\n", "\n", " return grand_total" ] }, { "cell_type": "markdown", "id": "54e76b17-4372-4074-b7f9-9439c44bc3c4", "metadata": {}, "source": [ "Here I interface my input data to ChatGPT's function and verify it is correct:" ] }, { "cell_type": "code", "execution_count": 27, "id": "d6beae51-7205-4cae-b4d7-495da2953baf", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 6.1: .0078 seconds, answer 5877594983578 ok" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "lines = get_text(6).splitlines()\n", "\n", "answer(6.1, 5877594983578, lambda:\n", " solve(lines))" ] }, { "cell_type": "markdown", "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)`. However, the program is correct." ] }, { "cell_type": "code", "execution_count": 28, "id": "053c3971-9aa5-490a-a016-f303b391c5d8", "metadata": {}, "outputs": [], "source": [ "import re\n", "from functools import reduce\n", "from operator import add, mul\n", "\n", "def solve(lines):\n", " if not lines:\n", " return 0\n", "\n", " # Normalize lines to a rectangle, preserving internal spaces\n", " width = max(len(line) for line in lines)\n", " grid = [line.rstrip('\\n').ljust(width) for line in lines]\n", " h = len(grid)\n", "\n", " # A separator column is all spaces -> separates problems\n", " sep = []\n", " for c in range(width):\n", " if all(grid[r][c] == ' ' for r in range(h)):\n", " sep.append(True)\n", " else:\n", " sep.append(False)\n", "\n", " # Find contiguous ranges of non-separator columns (each is a problem)\n", " problem_ranges = []\n", " in_problem = False\n", " start = 0\n", " for c in range(width):\n", " if not sep[c]:\n", " if not in_problem:\n", " in_problem = True\n", " start = c\n", " else:\n", " if in_problem:\n", " in_problem = False\n", " problem_ranges.append((start, c - 1))\n", " if in_problem:\n", " problem_ranges.append((start, width - 1))\n", "\n", " grand_total = 0\n", "\n", " for c_start, c_end in problem_ranges:\n", " # Find the operator row for THIS problem slice:\n", " # bottom-most row with + or * in [c_start, c_end]\n", " op_row = None\n", " for r in range(h):\n", " segment = grid[r][c_start:c_end + 1]\n", " if '+' in segment or '*' in segment:\n", " op_row = r\n", "\n", " if op_row is None:\n", " # No operator for this vertical slice -> skip\n", " continue\n", "\n", " # Determine the operator\n", " op_segment = grid[op_row][c_start:c_end + 1]\n", " if '+' in op_segment and '*' in op_segment:\n", " # If both appear (shouldn't in valid input), choose the leftmost\n", " plus_idx = op_segment.find('+') if '+' in op_segment else float('inf')\n", " mul_idx = op_segment.find('*') if '*' in op_segment else float('inf')\n", " op = add if plus_idx < mul_idx else mul\n", " elif '+' in op_segment:\n", " op = add\n", " elif '*' in op_segment:\n", " op = mul\n", " else:\n", " # No operator actually present\n", " continue\n", "\n", " # Build numbers column-by-column (each column is one number),\n", " # reading digits from top to just above op_row\n", " numbers = []\n", " for c in range(c_start, c_end + 1):\n", " digits = []\n", " for r in range(op_row):\n", " ch = grid[r][c]\n", " if ch.isdigit():\n", " digits.append(ch)\n", " if digits:\n", " num = int(''.join(digits))\n", " numbers.append(num)\n", "\n", " if not numbers:\n", " continue\n", "\n", " result = reduce(op, numbers)\n", " grand_total += result\n", "\n", " return grand_total" ] }, { "cell_type": "code", "execution_count": 29, "id": "e37018e7-0c82-4501-830e-dfe55efc3ad6", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 6.2: .0064 seconds, answer 11159825706149 ok" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(6.2, 11159825706149, lambda:\n", " solve(lines))" ] }, { "cell_type": "markdown", "id": "8aa26008-a652-4860-9c84-5ba4344d32f3", "metadata": {}, "source": [ "# Summary\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." ] }, { "cell_type": "code", "execution_count": 30, "id": "2d03c39d-42f5-4f51-89b9-638d8d4a4a35", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Puzzle 1.1: .0013 seconds, answer 1182 ok\n", "Puzzle 1.2: .0024 seconds, answer 6907 ok\n", "Puzzle 2.1: .1263 seconds, answer 23560874270 ok\n", "Puzzle 2.2: .1350 seconds, answer 44143124633 ok\n", "Puzzle 3.1: .0044 seconds, answer 17085 ok\n", "Puzzle 3.2: .0076 seconds, answer 169408143086082 ok\n", "Puzzle 4.1: .0171 seconds, answer 1569 ok\n", "Puzzle 4.2: .0937 seconds, answer 9280 ok\n", "Puzzle 5.1: .0058 seconds, answer 635 ok\n", "Puzzle 5.2: .0002 seconds, answer 369761800782619 ok\n", "Puzzle 6.1: .0078 seconds, answer 5877594983578 ok\n", "Puzzle 6.2: .0064 seconds, answer 11159825706149 ok\n" ] }, { "data": { "text/plain": [ "0.40801501274108887" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "for d in sorted(answers):\n", " print(answers[d])\n", "sum(a.secs for a in answers.values())" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.15" } }, "nbformat": 4, "nbformat_minor": 5 }