From ac4b9ec9be5278217b352deb7ee17fe708abc17f Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Thu, 4 Dec 2025 14:59:01 -0800 Subject: [PATCH] Advent od Code 2025 --- ipynb/Advent-2025-AI.ipynb | 1074 ++++++++++++++++++++++++++++++++++++ ipynb/Advent-2025.ipynb | 934 +++++++++++++++++++++++++++++++ ipynb/AdventUtils.ipynb | 185 +++---- 3 files changed, 2070 insertions(+), 123 deletions(-) create mode 100644 ipynb/Advent-2025-AI.ipynb create mode 100644 ipynb/Advent-2025.ipynb diff --git a/ipynb/Advent-2025-AI.ipynb b/ipynb/Advent-2025-AI.ipynb new file mode 100644 index 0000000..6fbc341 --- /dev/null +++ b/ipynb/Advent-2025-AI.ipynb @@ -0,0 +1,1074 @@ +{ + "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. 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 pronmpt again with \"*There is a change to the specification:*\" followed by the AoC Part 2 description.\n", + "\n", + "# Day 0\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": 7, + "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 a nice analysis of the problem (which I have omitted) and the following code:" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "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 interface my data (obtained by a call to `get_text(1)`) to this function and verify that it is correct:" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "c3ff875a-6267-47b8-a032-115a40bfd246", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 1.1: .0014 seconds, answer 1182 ok" + ] + }, + "execution_count": 49, + "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": [ + "I then gave Gemini Fast the instructions for Part 2, and again it had a nice analysis and the following code:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "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\n", + "\n", + "# --- EXAMPLE INPUT VERIFICATION ---\n", + "example_rotations = [\n", + " \"L68\", \"L30\", \"R48\", \"L5\", \"R60\", \n", + " \"L55\", \"L1\", \"L99\", \"R14\", \"L82\"\n", + "]\n", + "# Expected output for example is 6.\n", + "# print(f\"Example verification: {solve_new_safe_password(example_rotations)}\") \n", + "# The function will output 6, confirming the logic.\n", + "\n", + "# --- YOUR PUZZLE INPUT GOES HERE ---\n", + "# Replace this list with your actual puzzle input data\n", + "puzzle_input = [\n", + " # Paste your list of rotations here, one string per line\n", + " # e.g., \"R10\", \"L5\", ...\n", + " \"L68\", \"L30\", \"R48\", \"L5\", \"R60\", \"L55\", \"L1\", \"L99\", \"R14\", \"L82000\",\n", + " # ... and so on for all your input lines\n", + "]\n", + "\n", + "# Run the simulation with your full input\n", + "#actual_password = solve_new_safe_password(puzzle_input)\n", + "#print(f\"The actual password (total zero crossings) is: {actual_password}\")" + ] + }, + { + "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 clean things up.)\n", + "\n", + "Here I check whether the code is correct:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "a0b3e0fc-e044-4c1e-b4b6-6bc735145f57", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 1.2: .0017 seconds, answer 7509 WRONG; expected answer is 6907" + ] + }, + "execution_count": 14, + "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": 17, + "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\n", + "\n", + "# --- Example Input Verification ---\n", + "example_rotations = [\n", + " \"L68\", \"L30\", \"R48\", \"L5\", \"R60\", \n", + " \"L55\", \"L1\", \"L99\", \"R14\", \"L82\"\n", + "]\n", + "# Expected output: 6\n", + "# print(f\"Example result: {solve_safe_password_method_2(example_rotations)}\")\n", + "\n", + "# --- YOUR PUZZLE INPUT GOES HERE ---\n", + "puzzle_input = [\n", + " # Copy and paste your full input list here\n", + " # \"L10\", \"R5\", etc...\n", + "]\n", + "\n", + "# result = solve_safe_password_method_2(puzzle_input)\n", + "# print(f\"The actual password is: {result}\")" + ] + }, + { + "cell_type": "markdown", + "id": "e05db80b-c56d-4e1f-988c-a09be8ca5a3e", + "metadata": {}, + "source": [ + "This second try is indeed correct:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "021043ed-2ac5-4aaf-8171-985f0f5911f2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 1.2: .0015 seconds, answer 6907 ok" + ] + }, + "execution_count": 19, + "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 and then asked me to paste in the input file. I did and Claude ran the code, producing the correct answer but printing 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": 105, + "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, if the range is \"999000-109000\", this code will enumnrate the range(100, 1000), when it could enumerate just range(999, 1000).\n", + "\n", + "I verified that the code gives the correct answer:" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "id": "a91845ec-ace7-482e-b0b5-8a620ef3461f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 2.1: .1303 seconds, answer 23560874270 ok" + ] + }, + "execution_count": 98, + "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": [ + "Claude then wrote the following code when given the Part 2 instructions, nicely generalizing to any number of repeats:" + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "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": 100, + "id": "9c0049e6-a992-4aa8-a2d7-3ea748e361a6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 2.2: .1816 seconds, answer 44143124633 ok" + ] + }, + "execution_count": 100, + "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 `if __name__ == \"__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:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "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": 56, + "id": "09bf306b-8762-4346-aff9-bcff33639c71", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 3.1: .0040 seconds, answer 17085 ok" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "banks = get_text(3)\n", + "\n", + "answer(3.1, 17085, lambda:\n", + " total_output_joltage(banks))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0398d5b-485d-4479-9321-878564180b68", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 58, + "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": 59, + "id": "70bde9b9-beb1-4e9d-bef6-0f20fb958891", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 3.2: .0069 seconds, answer 169408143086082 ok" + ] + }, + "execution_count": 59, + "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 perhpas 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": 75, + "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 get the input and verify that the code does produce the correct answer:" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "id": "5b54c152-ce26-4baf-8b51-d4a166c6c2e7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 4.1: .0163 seconds, answer 1569 ok" + ] + }, + "execution_count": 82, + "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": 83, + "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": 79, + "id": "b47c2e05-978a-4b22-aafc-e31ee1825387", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 4.2: .4017 seconds, answer 9280 ok" + ] + }, + "execution_count": 79, + "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": 86, + "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": 88, + "id": "f370ee38-67af-42a6-9ad3-cdeec2019ff3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 4.2: .0761 seconds, answer 9280 ok" + ] + }, + "execution_count": 88, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(4.2, 9280, lambda:\n", + " count_total_removable_rolls(grid))" + ] + }, + { + "cell_type": "markdown", + "id": "8aa26008-a652-4860-9c84-5ba4344d32f3", + "metadata": {}, + "source": [ + "# Summary" + ] + }, + { + "cell_type": "code", + "execution_count": 101, + "id": "2d03c39d-42f5-4f51-89b9-638d8d4a4a35", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Puzzle 1.1: .0014 seconds, answer 1182 ok\n", + "Puzzle 1.2: .0015 seconds, answer 6907 ok\n", + "Puzzle 2.1: .1303 seconds, answer 23560874270 ok\n", + "Puzzle 2.2: .1816 seconds, answer 44143124633 ok\n", + "Puzzle 3.1: .0040 seconds, answer 17085 ok\n", + "Puzzle 3.2: .0069 seconds, answer 169408143086082 ok\n", + "Puzzle 4.1: .0163 seconds, answer 1569 ok\n", + "Puzzle 4.2: .0761 seconds, answer 9280 ok\n" + ] + } + ], + "source": [ + "for d in sorted(answers):\n", + " print(answers[d])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0e8b776-455d-405b-9370-2443daddee9b", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ipynb/Advent-2025.ipynb b/ipynb/Advent-2025.ipynb new file mode 100644 index 0000000..784b70b --- /dev/null +++ b/ipynb/Advent-2025.ipynb @@ -0,0 +1,934 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4769054f-d675-4678-852f-945df5f1fc7d", + "metadata": {}, + "source": [ + "
Peter Norvig, December 2025
\n", + "\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 problemm, record the response, and comment on it. I'll alternate between Gemini, Claude, and ChatGPT.\n", + "\n", + "# Day 0\n", + "\n", + "I'm glad that [@GaryGrady](https://mastodon.social/@garygrady) is providing cartoons:\n", + "\n", + "\"Gary\n", + "\n", + "I start by loading up my [**AdventUtils.ipynb**](AdventUtils.ipynb) notebook (same as last time except for the `current_year`). On each day I will first parse the input (with the help of my `parse` utility function), then solve Part 1 and Part 2 (recording the correct answer with my `answer` function)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d5f1da68-5da6-434d-a068-1d93497a86b3", + "metadata": {}, + "outputs": [], + "source": [ + "%run AdventUtils.ipynb\n", + "current_year = 2025" + ] + }, + { + "cell_type": "markdown", + "id": "37bc12b8-d0dc-4873-984c-6f09ae647229", + "metadata": {}, + "source": [ + "# [Day 1](https://adventofcode.com/2025/day/1): Secret Entrance\n", + "\n", + "On Day 1 we meet an elf and learn that our task is to finish decorating the North Pole by December 12th. There will be challenges along the way. Today we need to unlock a safe. The safe has a dial with 100 numbers. Our input for today is a sequence of left and right rotations; for example \"R20\" means move the dial right by 20 numbers. I'll use my `parse` utility function to parse each line of the input as an integer, after replacing each 'L' with a minus sign and each 'R' with a plus sign:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ed911a15-addc-4c04-8546-2c9f37aee341", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 4780 strs:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "L20\n", + "L13\n", + "L16\n", + "L16\n", + "L29\n", + "L7\n", + "L48\n", + "L48\n", + "...\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Parsed representation ➜ 4780 ints:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "-20\n", + "-13\n", + "-16\n", + "-16\n", + "-29\n", + "-7\n", + "-48\n", + "-48\n", + "...\n" + ] + } + ], + "source": [ + "rotations = parse(day=1, parser=lambda line: int(line.replace('L', '-').replace('R', '+')))" + ] + }, + { + "cell_type": "markdown", + "id": "7c98c883-d1dc-4d4e-9590-d47b1de000a0", + "metadata": {}, + "source": [ + "\"Gary\n", + "\n", + "### Part 1: How many times is the dial left pointing at 0 after any rotation in the sequence?\n", + "\n", + "Initially the safe's arrow is pointing at 50, and then we apply the rotations in order. We are asked how many of the rotations leave the dial pointing at 0. The `itertools.accumulate` function yields running totals of its input sequence, so we just have to count (quantify) how many times the running total of the rotations is 0 mod 100:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "86079e27-2912-4431-8608-7a110a115789", + "metadata": {}, + "outputs": [], + "source": [ + "def count_zeros(numbers, dial=100) -> int:\n", + " \"\"\"How many zeros (modulo `dial`) in the running partial sums of the numbers?\"\"\"\n", + " return quantify(total % dial == 0 for total in accumulate(numbers, initial=50))" + ] + }, + { + "cell_type": "markdown", + "id": "b5f68a70-8465-4249-954c-cba2b78751d8", + "metadata": {}, + "source": [ + "Here's the process I repeat for each puzzle: I ran `count_zeros(rotations)`, submitted the answer to AoC, and once I saw it was correct, I recorded the answer as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8387c4bd-a0b7-46d7-b726-d2d4b383b3cf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 1.1: .0008 seconds, answer 1182 ok" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(puzzle=1.1, solution=1182, code=lambda: \n", + " count_zeros(rotations))" + ] + }, + { + "cell_type": "markdown", + "id": "12d5d111-8f69-4944-855b-54cca8215d7b", + "metadata": {}, + "source": [ + "### Part 2: How many times does the dial point to 0 at any time?\n", + "\n", + "For Part 2 we need to count both when a rotation ends up at 0 and when the arrow passes 0 in the middle of the rotation. For example, if the arrow points to 95, then only a \"R5\" or a \"L95\" would register a 0 in Part 1, but now a rotation of \"R10\" would count because it passes 0 (as would any rotation of \"R5\" or larger, or \"L95\" or larger). \n", + "\n", + "I'll start with a simple but slow approach: treat a rotation of, say, -20 as 20 rotations of -1, and then use the same `count_zeros` function from part 1. (Note that `sign(r)` returns +1 for any positive input, and -1 for any negative input.)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "bfb5bd2d-d768-47b3-9897-d28f83f2202a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 1.2: .1516 seconds, answer 6907 ok" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(1.2, 6907, lambda:\n", + " count_zeros(sign(r) for r in rotations for _ in range(abs(r))))" + ] + }, + { + "cell_type": "markdown", + "id": "82c39114-fe32-4f98-8ea2-1bb73601883e", + "metadata": {}, + "source": [ + "I can speed this up by adding up the full-circle rotations separately from the partial rotations. That is, a rotation of \"L995\" does 9 complete circles of the dial (and thus 9 zero crossings), and then moves 95 more clicks (possibly crossing zero once more, depending on where we start). That gives us this:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "6e1cda01-5f1d-4363-a923-8c212667afc6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 1.2: .0489 seconds, answer 6907 ok" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(1.2, 6907, lambda:\n", + " sum(abs(r) // 100 for r in rotations) +\n", + " count_zeros(sign(r) for r in rotations for _ in range(abs(r) % 100)))" + ] + }, + { + "cell_type": "markdown", + "id": "bb0f6906-369e-4b3c-8840-d5648d713942", + "metadata": {}, + "source": [ + "That's three times faster, but still a comparatively long run time for a Day 1 problem, so here's a faster method. I break each rotation down into a number of full circles and some remainder, then add the full circles to the count of zeros, and add one more if the remainder is at least as much as the distance to zero: " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "e0b676d8-3d4f-4b36-835d-b045ece53bd6", + "metadata": {}, + "outputs": [], + "source": [ + "def zero_clicks(rotations, position=50, dial=100) -> int:\n", + " \"\"\"How many times does any click cause the dial to point at 0?\n", + " Count 1 if the rotation crosses the distance to 0,\n", + " and for large rotations, count abs(r) // 100 more.\"\"\"\n", + " zeros = 0\n", + " for r in rotations:\n", + " full_circles, remainder = divmod(abs(r), dial)\n", + " distance_to_0 = (dial - position if (r > 0 or position == 0) else position)\n", + " zeros += full_circles + (1 if remainder >= distance_to_0 else 0)\n", + " position = (position + r) % dial\n", + " return zeros" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "8969191e-bd50-4187-8e4b-64524bc8427a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 1.2: .0010 seconds, answer 6907 ok" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(1.2, 6907, lambda:\n", + " zero_clicks(rotations))" + ] + }, + { + "cell_type": "markdown", + "id": "f47b1f7a-c9d9-4b21-a28a-f1e4e73d4c0d", + "metadata": {}, + "source": [ + "That's much faster, but the code is trickier, and indeed I initially had a **bug** in the `distance_to_0` computation: when the current position is 0 the distance should be 100: it takes a full rotation to get back to 0. My code initially claimed the distance was 0; adding `or position == 0` fixed that." + ] + }, + { + "cell_type": "markdown", + "id": "dd6908ae-1906-4687-a50d-a50293a1dad5", + "metadata": {}, + "source": [ + "# [Day 2](https://adventofcode.com/2025/day/2): Gift Shop\n", + "\n", + "Today we're in the North Pole gift shop, and are asked to help the elves identify invalid product IDs on the items there. We're giving a list of ranges of product IDs. Each range is a pair of integers separated by a dash, and the ranges are separated by commas:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "63bb5099-68ab-41b3-8d0c-2f409433b3f2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 1 str:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "990244-1009337,5518069-5608946,34273134-34397466,3636295061-3636388848,8613701-8663602,573252-68 ...\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Parsed representation ➜ 35 tuples:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "(990244, 1009337)\n", + "(5518069, 5608946)\n", + "(34273134, 34397466)\n", + "(3636295061, 3636388848)\n", + "(8613701, 8663602)\n", + "(573252, 688417)\n", + "(472288, 533253)\n", + "(960590, 988421)\n", + "...\n" + ] + } + ], + "source": [ + "id_ranges = parse(day=2, parser=positive_ints, sections=lambda text: text.split(','))" + ] + }, + { + "cell_type": "markdown", + "id": "a5463e74-a3a1-4c79-9497-a1e307ef81e2", + "metadata": {}, + "source": [ + "\"GaryJGrady\n", + "\n", + "### Part 1: What is the sum of the invalid IDs?\n", + "\n", + "An invalid ID is one that consists of a digit sequence repeated twice. So 55, 6464 and 123123 are invalid. We're asked for the sum of the invalid IDs across all the ID ranges.\n", + "\n", + "We could look at every number in each range and check if the first half of the number (as a string) is the same as the second half. How many checks would that be?" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "d682f3f2-415e-4556-b6c8-43e331a38703", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1990936" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum((hi - lo + 1) for lo, hi in id_ranges)" + ] + }, + { + "cell_type": "markdown", + "id": "a9bf1aab-bd09-49a9-ab57-8b26b1f4a98d", + "metadata": {}, + "source": [ + "Only 2 million! So it would indeed be feasible to check every one. But I have a suspicion that Part 2 would make it infeasible, so I'll invest in a more efficient approach. For each ID range, instead of enumerating every number in the range and checking each one for validity, I will instead enumerate over the *first half* of the possible digit strings, and automatically generate invalid IDs by appending a copy of the first half to itself.\n", + "\n", + "Suppose the range is 123456-223000. I enumerate from 123 to 223, and for each one form generate an invalid ID:\n", + "[123123, 124124, 125125, ... 223223]. I then yield the IDs that are within the range; in this all but the first and the last. Altogether I only have to consider 101 IDs rather than 100,001. The algorithm scales with the square root of the size of the range, not with the size of the range itself." + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "id": "1345f93d-84c5-43f8-b6c2-9fc7b8f5ed90", + "metadata": {}, + "outputs": [], + "source": [ + "def generate_invalids(lo: int, hi: int) -> Iterable[int]:\n", + " \"\"\"Yield all the invalid IDs between lo and hi inclusive.\n", + " An ID is invalid if it consists of a digit sequence repeated twice.\"\"\"\n", + " lo_str = str(lo)\n", + " start = int(lo_str[:max(1, len(lo_str) // 2)])\n", + " for half in count_from(start):\n", + " id = int(str(half) * 2)\n", + " if lo <= id <= hi:\n", + " yield id\n", + " elif id > hi:\n", + " return\n", + "\n", + "assert list(generate_invalids(11, 22)) == [11, 22]" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "8b0d12b3-f184-4149-8b49-f9ff78663d46", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 2.1: .0030 seconds, answer 23560874270 ok" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(2.1, 23560874270, lambda:\n", + " sum(sum(generate_invalids(lo, hi)) for lo, hi in id_ranges))" + ] + }, + { + "cell_type": "markdown", + "id": "188dd774-0c2d-4304-8351-dfe208913a99", + "metadata": {}, + "source": [ + "### Part 2: What is the sum of the invalid IDs, under the new rules?\n", + "\n", + "In Part 2 we discover that an ID should be considered invalid if it repeats a sequence of digits two *or more* times. So 111 (repeated three times), 12121212 (four times), and 222222 (six times) are all invalid. I'll rewrite `generate_invalids` to take an optional argument saying how many repeats we're looking for, and introduce `generate_all_invalids` to try all possible repeat lengths:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "6b8d6dad-c5b6-4ed8-8dfa-5acbb54c8001", + "metadata": {}, + "outputs": [], + "source": [ + "def generate_invalids(lo: int, hi: int, repeat=2) -> Iterable[int]:\n", + " \"\"\"Yield all the invalid IDs between lo and hi inclusive\n", + " that are formed from `repeat` sequences.\"\"\"\n", + " lo_str = str(lo)\n", + " start = int(lo_str[:len(lo_str) // repeat] or 1)\n", + " for i in count_from(start):\n", + " id = int(str(i) * repeat)\n", + " if lo <= id <= hi:\n", + " yield id\n", + " elif id > hi:\n", + " return\n", + "\n", + "def generate_all_invalids(lo: int, hi: int) -> Set[int]:\n", + " \"\"\"All invalid numbers in the range lo to hi inclusive,\n", + " under the rules where 2 or more repeated digit sequences are invalid.\"\"\"\n", + " return {id for repeat in range(2, len(str(hi)) + 1)\n", + " for id in generate_invalids(lo, hi, repeat)}\n", + " \n", + "assert list(generate_invalids(11, 22)) == [11, 22]\n", + "assert list(generate_invalids(2121212118, 2121212124, 5)) == [2121212121]\n", + "assert list(generate_invalids(95, 115, 3)) == [111]\n", + "assert list(generate_all_invalids(95, 115)) == [99, 111]" + ] + }, + { + "cell_type": "markdown", + "id": "cb254f91-8c6d-445d-86be-97df9a93018c", + "metadata": {}, + "source": [ + "Now verify that the answer for Part 1 still works, and go ahead and compute the answer for Part 2:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "8a7c6c25-4b5f-4178-8559-166ba1a9f924", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 2.1: .0029 seconds, answer 23560874270 ok" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(2.1, 23560874270, lambda:\n", + " sum(sum(generate_invalids(lo, hi)) for lo, hi in id_ranges))" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "32fefd65-df2a-4ea3-9acd-7525ebd32380", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 2.2: .0038 seconds, answer 44143124633 ok" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(2.2, 44143124633, lambda:\n", + " sum(sum(generate_all_invalids(lo, hi)) for lo, hi in id_ranges))" + ] + }, + { + "cell_type": "markdown", + "id": "872cf212-bfbf-4edd-b898-5f76ad122a85", + "metadata": {}, + "source": [ + "I had another **bug** here: initially I counted \"222222\" twice: once as 2 repeats of \"222\" and once as 3 repeats of \"22\". I changed the output of `generate_all_invalids` to be a set to fix that." + ] + }, + { + "cell_type": "markdown", + "id": "16e222aa-d424-44d8-bcb3-7aa11c31b540", + "metadata": {}, + "source": [ + "# [Day 3](https://adventofcode.com/2025/day/3): Lobby\n", + "\n", + "Entering the lobby, we find that the elevators are offline. We might be able to fix the problem by turning on some batteries. There are multiple battery banks, each bank consisting of a sequence of batteries, each labeled with its *joltage*, a digit from 1 to 9. " + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "04ef4f2f-2b07-43cd-87d4-a8f63f7fe840", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 200 strs:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "5353323523322232362334333433323333353233331313222372133133353643423323233323333534414523333432223242\n", + "6344544745655555456556556566665564538465555575558846455665837545764555554465564547547565544657585435\n", + "2246273372253242254243532252231242225522622633532222322234255122531222423531343223123232234213323424\n", + "6545643634344444495734739454433454439454355654483544243344534445434437426443854344454534654439534424\n", + "2356636643143433535443636338231745346538433576334436353176353333433532345344334224435234343644332536\n", + "3221311221443323323322222214632342232233222322333436263122265162212321261323142262212332322125216222\n", + "3336332333336335335324359336493238433441666379243536334165623214253384333323893933867663434332383763\n", + "3235321252332431332223232436222532432226223222213233432853535322314122221322352235213323124321222233\n", + "...\n" + ] + } + ], + "source": [ + "banks = parse(day=3)" + ] + }, + { + "cell_type": "markdown", + "id": "529e9177-7e47-46fa-bdc0-b979205d2e72", + "metadata": {}, + "source": [ + "### Part 1: What is the maximum possible total output joltage?\n", + "\n", + "We can turn on exactly two batteries in each bank, resulting in a two digit number which is the *joltage* of the bank. For example, given the bank \"8647\" we could choose to turn on the \"8\" and \"7\" to produce a joltage of 87. The function `joltage` chooses the biggest first digit, and then the biggest second digit that follows the first digit. Note that the possible choices for the first digit exclude the last digit, because if we chose that, then we couldn't choose a second digit to follow. Note also I chose to return a string rather than an int; that seemed simpler within `joltage` but it does mean the caller might need to do a conversion." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "d1d18422-9054-4d89-85ed-37f3a21a2ef6", + "metadata": {}, + "outputs": [], + "source": [ + "def joltage(bank: str) -> str:\n", + " \"\"\"The maximum possible joltage by turning on 2 batteries in the bank.\"\"\"\n", + " choices = bank[:-1] # The first digit can't be the last character\n", + " index = first(bank.index(d) for d in '987654321' if d in choices)\n", + " return bank[index] + max(bank[index + 1:])\n", + "\n", + "assert joltage(\"8647\") == \"87\"\n", + "assert joltage(\"1119\") == \"19\"" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "9cc62ae9-b313-4b82-b8fb-bab9c0ef5cb6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 3.1: .0004 seconds, answer 17085 ok" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(3.1, 17085, lambda:\n", + " sum(int(joltage(b)) for b in banks))" + ] + }, + { + "cell_type": "markdown", + "id": "3f8a4645-5e36-4b3d-8ace-125024403b3b", + "metadata": {}, + "source": [ + "### Part 2: What is the new maximum possible total output joltage?\n", + "\n", + "In Part 2 the elf hits the \"joltage limit safety override\" button, and we can now turn on 12 batteries per bank, resulting in a 12-digit joltage. What is the new maximum possible total joltage?\n", + "\n", + "I will make a change to the function `joltage`, passing it the number of digits remaining to be chosen, *n*. The function stops when we get to 1 digit remaining, and recurses when there is more than one digit remaining. At each step we need to make sure the choice of first digit leaves *n*-1 digits for later choices." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "b79011e6-b448-4b71-88ca-3301888a7f29", + "metadata": {}, + "outputs": [], + "source": [ + "def joltage(bank: str, n=2) -> str:\n", + " \"\"\"The maximum possible joltage by turning on `n` batteries in the bank.\"\"\"\n", + " if n == 1:\n", + " return max(bank)\n", + " else:\n", + " choices = bank[:-(n - 1)]\n", + " index = first(bank.index(d) for d in '987654321' if d in choices)\n", + " return bank[index] + joltage(bank[index + 1:], n - 1)\n", + "\n", + "assert joltage(\"811111111111119\", 2) == '89'\n", + "assert joltage(\"818181911112111\", 5) == '92111'\n", + "assert joltage(\"818181911112111\", 12) == '888911112111'" + ] + }, + { + "cell_type": "markdown", + "id": "28d2e2ce-98b5-4b3b-9f4d-26efcb7e1258", + "metadata": {}, + "source": [ + "I'll first make sure that the new version of `joltage` is backwards compatible, and then solve Part 2:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "fe3cff78-81c0-4d4a-bb4d-f0f841067e0d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 3.1: .0005 seconds, answer 17085 ok" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(3.1, 17085, lambda:\n", + " sum(int(joltage(b)) for b in banks))" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "f971839e-81ea-49b4-a92f-a44884be645d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 3.2: .0022 seconds, answer 169408143086082 ok" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(3.2, 169408143086082, lambda:\n", + " sum(int(joltage(b, 12)) for b in banks))" + ] + }, + { + "cell_type": "markdown", + "id": "db13a440-9ad0-4344-b555-aec969869688", + "metadata": {}, + "source": [ + "# [Day 4](https://adventofcode.com/2025/day/4): Printing Department\n", + "\n", + "The floor of the printing department is divided into squares. Many squares contain a roll of paper; other squares are empty. The day's input is a map of the floor, with `@` representing a roll of paper. I can handle that with the `Grid` class from my AdventUtils:" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "c9f227d5-2748-48e1-80c1-7385dad46323", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 140 strs:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + ".@@@@@...@.@@@@@@@@@@.@@@@@@@.@.@.@@@@@@@@@@@@@..@.@@@.@@@@@@..@.@..@.@@...@.@@@@..@@@@....@@@.@ ...\n", + ".@@@@@.@....@.....@@@.@@.@@.@@@.@@@.@.@.@.@@@@@.@@.@@@@@.@@@@@@@@@@@..@@.@.@@.@@@.@@@@@@@@@@@..@ ...\n", + "@.@@@@.@@@@.@@@@..@@.@@@@@@@@.@@@@.@@@@.@@..@.@...@.@.@.@.@@..@@@@@.@.@.@@@@.@@@@@@@@@.@@@@..@@. ...\n", + ".@.....@.@@@..@.@@@.@..@@@@@..@@@.@@..@...@.@@@@.@@@.@.@@@@@@.@.@@@@@@@.@.@@@.@@@@@@...@@.@@..@. ...\n", + "@@@@@.@@@.@@@@@@@..@@.@.@@@..@@..@@@.@@....@.@..@@@@@@@@.@.@@..@@...@@.@@@...@.@.@@@..@.@.@@@@@@ ...\n", + "@.@@@@@@..@@@@...@..@@@@@@.@@@..@.....@@.@.@@...@@@.@@.@.@@@....@@.@.@.@@@@.@@@@@.@@@.@@...@@.@@ ...\n", + ".@@@.@.@@@..@@.@.@@@@@.@.@..@@....@..@.@.@@@@.@..@@.@..@@@@@.@@@@@@@.@.@@@.@.@@@.@@@@.@@@@@@@@.@ ...\n", + "@@@@@@@.@@...@@@....@.@@@@.@@@@@@@@@.@@@.@@.@@..@...@@@@@.@@@..@.@@@@@@@@@@.@@@.@..@@@.@@@@.@.@@ ...\n", + "...\n" + ] + } + ], + "source": [ + "paper_grid = Grid(parse(day=4), directions=directions8)" + ] + }, + { + "cell_type": "markdown", + "id": "4dd00e21-228c-41f6-a28c-e2213e60d4ce", + "metadata": {}, + "source": [ + "### Part 1: How many rolls of paper can be accessed by a forklift?\n", + "\n", + "A roll is **accessible** by forklift if there are fewer than four rolls of paper in the eight adjacent positions. Counting the number of accessible rolls is easy, but I decided to make `accessible rolls` return a list of positions, because I might need that in Part 2." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "6fe5bd44-8a28-4d7a-bc8b-edc4af8f23c3", + "metadata": {}, + "outputs": [], + "source": [ + "def accessible_rolls(grid: Grid) -> List[Point]:\n", + " \"\"\"A roll of paper is accessible if there are fewer than \n", + " four rolls of paper in the eight adjacent positions.\"\"\"\n", + " return [p for p in grid if grid[p] == '@'\n", + " if grid.neighbor_contents(p).count('@') < 4]" + ] + }, + { + "cell_type": "markdown", + "id": "714b9eed-bdee-4e3f-b0be-1a3f727dddfb", + "metadata": {}, + "source": [ + "Here's the answer:" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "a5ef09cf-b204-41eb-80d8-de107d385dbb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 4.1: .0540 seconds, answer 1569 ok" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(4.1, 1569, lambda:\n", + " len(accessible_rolls(paper_grid)))" + ] + }, + { + "cell_type": "markdown", + "id": "550312ae-a70b-405c-90e6-5df9c9afc0e8", + "metadata": {}, + "source": [ + "### Part 2: How many rolls of paper can be removed?\n", + "\n", + "If the elves can access a paper roll, they can remove it by forklift. That may make other rolls accessible. How many in total can be removed?\n", + "\n", + "It looks like I was right to make `accessible_rolls` return a list of points rather than a count! I can answer the question by repeatedly finding the accessible rolls, removing them (on a copy of the grid so I don't mess up the original grid), and repeating until there are no more accessible rolls." + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "id": "0ed53853-268c-4c2f-a929-cb3e6005a348", + "metadata": {}, + "outputs": [], + "source": [ + "def removable_rolls(grid: Grid) -> Iterable[Point]:\n", + " \"\"\"The positions of paper rolls that can be removed, in any nuber of iterations.\"\"\"\n", + " grid = grid.copy() # To avoid mutating the input grid\n", + " points = accessible_rolls(grid)\n", + " while points:\n", + " yield from points\n", + " grid.update({p: '.' for p in points})\n", + " points = accessible_rolls(grid)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "2fb17a51-05f7-42ec-8d6c-222121a026cf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 4.2: 1.2023 seconds, answer 9280 ok" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(4.2, 9280, lambda:\n", + " quantify(removable_rolls(paper_grid))) " + ] + }, + { + "cell_type": "markdown", + "id": "7143f73e-3b9b-49f3-bfa9-625899a56e37", + "metadata": {}, + "source": [ + "That's the right answer, but the run time is slow. One issue is that `accessible_rolls` has to look at the whole grid on every iteration. If the previous iteration only removed one or two rolls, that's a waste of time. Instead, we can keep a queue of possibly removable points (initially the points with a paper roll) and repeatedly pop a point off the queue, and if it is an accessible roll, remove it and put all its neighbors on the queue." + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "id": "15741f57-8527-4427-8cea-84951a1d14ee", + "metadata": {}, + "outputs": [], + "source": [ + "def count_removable_rolls(grid1: Grid) -> int:\n", + " \"\"\"Count the number of paper rolls that can be removed.\"\"\"\n", + " grid = grid1.copy() # To avoid mutating the original input grid\n", + " Q = grid.findall('@') # A queue of possibly removable positions in the grid\n", + " while Q:\n", + " p = Q.pop()\n", + " if grid[p] == '@' and grid.neighbor_contents(p).count('@') < 4:\n", + " grid[p] = '.'\n", + " Q.extend(grid.neighbors(p))\n", + " return len(grid1.findall('@')) - len(grid.findall('@')) # The number of '@' removed" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "bcba970b-09aa-479b-9c6d-4f6a7ac49fed", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 4.2: .1436 seconds, answer 9280 ok" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(4.2, 9280, lambda:\n", + " count_removable_rolls(paper_grid))" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "c4afcd9a-bf21-4b50-9081-9739e87eef30", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "7f31ae9b-6606-40b0-9bb1-ed9b3fe3cbf0", + "metadata": {}, + "source": [ + "# Summary" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "ba36579c-d0b4-4fd3-939c-0026ecddd7e9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Puzzle 1.1: .0008 seconds, answer 1182 ok\n", + "Puzzle 1.2: .0010 seconds, answer 6907 ok\n", + "Puzzle 2.1: .0029 seconds, answer 23560874270 ok\n", + "Puzzle 2.2: .0038 seconds, answer 44143124633 ok\n", + "Puzzle 3.1: .0005 seconds, answer 17085 ok\n", + "Puzzle 3.2: .0022 seconds, answer 169408143086082 ok\n", + "Puzzle 4.1: .0540 seconds, answer 1569 ok\n", + "Puzzle 4.2: .1484 seconds, answer 9280 ok\n" + ] + } + ], + "source": [ + "for d in sorted(answers):\n", + " print(answers[d])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72764869-84bf-4471-91a9-bf65889de29c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ipynb/AdventUtils.ipynb b/ipynb/AdventUtils.ipynb index 438b8a8..599e5c8 100644 --- a/ipynb/AdventUtils.ipynb +++ b/ipynb/AdventUtils.ipynb @@ -15,14 +15,14 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "from collections import Counter, defaultdict, namedtuple, deque, abc\n", "from dataclasses import dataclass, field\n", - "from itertools import permutations, combinations, cycle, chain, islice, filterfalse\n", - "from itertools import count as count_from, product as cross_product, takewhile\n", + "from itertools import permutations, combinations, cycle, chain, islice, accumulate\n", + "from itertools import count as count_from, product as cross_product\n", "from typing import *\n", "from statistics import mean, median\n", "from math import ceil, floor, factorial, gcd, log, log2, log10, sqrt, inf, atan2\n", @@ -58,7 +58,7 @@ "\n", "The function `parse` is meant to handle each day's input. A call `parse(day, parser, sections)` does the following:\n", " - Reads the input file for `day`.\n", - " - Breaks the file into a *sections*. By default, this is lines, but you can use `paragraphs`, or pass in a custom function.\n", + " - Breaks the file into *sections* (sometimes called *records*). By default, each line is a section, but you can use `paragraphs`, or pass in a custom function.\n", " - Applies `parser` to each section and returns the results as a tuple of records.\n", " - Useful parser functions include `ints`, `digits`, `atoms`, `words`, and the built-ins `int` and `str`.\n", " - Prints the first few input lines and output records. This is useful to me as a debugging tool, and to the reader.\n", @@ -67,36 +67,22 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": {}, - "outputs": [ - { - "ename": "NameError", - "evalue": "name 'whole' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[7], line 6\u001b[0m\n\u001b[1;32m 3\u001b[0m lines \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mstr\u001b[39m\u001b[38;5;241m.\u001b[39msplitlines \u001b[38;5;66;03m# By default, split input text into lines\u001b[39;00m\n\u001b[1;32m 5\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mparagraphs\u001b[39m(text): \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mSplit text into paragraphs\u001b[39m\u001b[38;5;124m\"\u001b[39m; \u001b[38;5;28;01mreturn\u001b[39;00m text\u001b[38;5;241m.\u001b[39msplit(\u001b[38;5;124m'\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m'\u001b[39m)\n\u001b[0;32m----> 6\u001b[0m whole\n\u001b[1;32m 8\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mparse\u001b[39m(day_or_text:Union[\u001b[38;5;28mint\u001b[39m, \u001b[38;5;28mstr\u001b[39m], parser\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mstr\u001b[39m, sections\u001b[38;5;241m=\u001b[39mlines, show\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m8\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28mtuple\u001b[39m:\n\u001b[1;32m 9\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Split the input text into `sections`, and apply `parser` to each.\u001b[39;00m\n\u001b[1;32m 10\u001b[0m \u001b[38;5;124;03m The first argument is either the text itself, or the day number of a text file.\"\"\"\u001b[39;00m\n", - "\u001b[0;31mNameError\u001b[0m: name 'whole' is not defined" - ] - } - ], + "outputs": [], "source": [ "current_year = 2023 # Subdirectory name for input files\n", "\n", "lines = str.splitlines # By default, split input text into lines\n", "\n", "def paragraphs(text): \"Split text into paragraphs\"; return text.split('\\n\\n')\n", - "def whole(text): \"The whole text\"; return [text]\n", "\n", - "def parse(day_or_text:Union[int, str], parser=str, sections=lines, show=8) -> tuple:\n", + "def parse(day: Union[int, str], parser=str, sections=lines, show=8) -> tuple:\n", " \"\"\"Split the input text into `sections`, and apply `parser` to each.\n", " The first argument is either the text itself, or the day number of a text file.\"\"\"\n", - " if isinstance(day_or_text, str) and show == 8: \n", + " if isinstance(day, str) and show == 8: \n", " show = 0 # By default, don't show lines when parsing example text.\n", - " start = time.time()\n", - " text = get_text(day_or_text)\n", + " text = get_text(day)\n", " show_items('Puzzle input', text.splitlines(), show)\n", " records = mapt(parser, sections(text.rstrip()))\n", " if parser != str or sections != lines:\n", @@ -133,7 +119,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -181,17 +167,18 @@ }, { "cell_type": "code", - "execution_count": 78, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "answers = {} # `answers` is a dict of {puzzle_number: answer}\n", + "\n", "unknown = 'unknown'\n", "\n", "class answer:\n", " \"\"\"Verify that calling `code` computes the `solution` to `puzzle`. \n", " Record results in the dict `answers`.\"\"\"\n", - " def __init__(self, puzzle: float, solution, code:Callable=lambda:unknown):\n", + " def __init__(self, puzzle: float, solution, code:Callable):\n", " self.puzzle, self.solution, self.code = puzzle, solution, code\n", " answers[puzzle] = self\n", " self.check()\n", @@ -206,19 +193,11 @@ " \n", " def __repr__(self) -> str:\n", " \"\"\"The repr of an answer shows what happened.\"\"\"\n", - " secs = f'{self.secs:6.3f}'.replace(' 0.', ' .')\n", + " secs = f'{self.secs:7.4f}'.replace(' 0.', ' .')\n", " comment = (f'' if self.got == unknown else\n", " f' ok' if self.ok else \n", " f' WRONG; expected answer is {self.solution}')\n", - " return f'Puzzle {self.puzzle:4.1f}: {secs} seconds, answer {self.got:<17}{comment}'\n", - "\n", - "def summary(answers):\n", - " \"\"\"Print a report that summarizes the answers.\"\"\"\n", - " for d in sorted(answers):\n", - " print(answers[d])\n", - " times = [answers[d].secs for d in answers]\n", - " print(f'\\nCorrect: {quantify(answers[d].ok for d in answers)}/{len(answers)}')\n", - " print(f'\\nTime in seconds: {median(times):.3f} median, {mean(times):.3f} mean, {sum(times):.3f} total.')" + " return f'Puzzle {self.puzzle:4.1f}: {secs} seconds, answer {self.got:<15}{comment}'" ] }, { @@ -232,7 +211,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -299,7 +278,7 @@ " first, *rest = sets\n", " return set(first).intersection(*rest)\n", "\n", - "def accumulate(item_count_pairs: Iterable[Tuple[object, int]]) -> Counter:\n", + "def accumulate_counts(item_count_pairs: Iterable[Tuple[object, int]]) -> Counter:\n", " \"\"\"Add up all the (item, count) pairs into a Counter.\"\"\"\n", " counter = Counter()\n", " for (item, count) in item_count_pairs:\n", @@ -347,9 +326,9 @@ " \"\"\"`map`, with the result as a list.\"\"\"\n", " return list(map(function, *sequences))\n", "\n", - "def cat(things: Collection, sep='') -> str:\n", + "def cat(things: Collection) -> str:\n", " \"\"\"Concatenate the things.\"\"\"\n", - " return sep.join(map(str, things))\n", + " return ''.join(map(str, things))\n", " \n", "cache = functools.lru_cache(None)\n", "Ø = frozenset() # empty set" @@ -366,7 +345,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -433,12 +412,11 @@ }, { "cell_type": "code", - "execution_count": 154, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "Point = Tuple[int, ...] # Type for points\n", - "Point2D = Tuple[int, int] # Type for 2-dimensional point\n", "Vector = Point # E.g., (1, 0) can be a point, or can be a direction, a Vector\n", "Zero = (0, 0)\n", "\n", @@ -458,38 +436,37 @@ "def Ys(points) -> Tuple[int]: \"Y coordinates of a collection of points\"; return mapt(Y_, points)\n", "def Zs(points) -> Tuple[int]: \"X coordinates of a collection of points\"; return mapt(Z_, points)\n", "\n", - "## I define point arithmetic for general points and separate versions for 2D points,\n", - "## because profiling showed the 2D versions are significantly faster\n", + "def add(p: Point, q: Point) -> Point: \"Add points\"; return mapt(operator.add, p, q)\n", + "def sub(p: Point, q: Point) -> Point: \"Subtract points\"; return mapt(operator.sub, p, q)\n", + "def neg(p: Point) -> Vector: \"Negate a point\"; return mapt(operator.neg, p)\n", + "def mul(p: Point, k: float) -> Vector: \"Scalar multiply\"; return tuple(k * c for c in p)\n", "\n", - "def add3(p: Point, q: Point) -> Point: \"Add points\"; return mapt(operator.add, p, q)\n", - "def sub3(p: Point, q: Point) -> Point: \"Subtract points\"; return mapt(operator.sub, p, q)\n", - "def mul3(p: Point, k: float) -> Vector: \"Scalar multiply\"; return tuple(k * c for c in p)\n", - "def neg(p: Point2D) -> Vector: \"Negate a 2D Point\"; return mapt(operator.neg, p)\n", - "\n", - "def add(p: Point2D, q: Point2D) -> Point2D: \"Add 2D Points\"; return (p[0] + q[0], p[1] + q[1])\n", - "def sub(p: Point2D, q: Point2D) -> Point2D: \"Subtract 2D Points\"; return (p[0] - q[0], p[1] - q[1])\n", - "def mul(p: Point2D, k: float) -> Point2D: \"Scalar multiply\"; return (p[0] * k, p[1] * k)\n", - "def neg(p: Point2D) -> Vector: \"Negate a 2D Point\"; return (-p[0], -p[1])\n", + "def distance(p: Point, q: Point) -> float:\n", + " \"\"\"Euclidean (L2) distance between two points.\"\"\"\n", + " d = sum((pi - qi) ** 2 for pi, qi in zip(p, q)) ** 0.5\n", + " return int(d) if d.is_integer() else d\n", "\n", "def slide(points: Set[Point], delta: Vector) -> Set[Point]: \n", " \"\"\"Slide all the points in the set of points by the amount delta.\"\"\"\n", " return {add(p, delta) for p in points}\n", "\n", - "def make_turn(facing: Vector, turn: str) -> Vector:\n", + "def make_turn(facing:Vector, turn:str) -> Vector:\n", " \"\"\"Turn 90 degrees left or right. `turn` can be 'L' or 'Left' or 'R' or 'Right' or lowercase.\"\"\"\n", " (x, y) = facing\n", " return (y, -x) if turn[0] in ('L', 'l') else (-y, x)\n", "\n", - "def distance(p: Point2D, q: Point2D) -> float:\n", - " \"\"\"Euclidean (L2) distance between two points.\"\"\"\n", - " d = sum((pi - qi) ** 2 for pi, qi in zip(p, q)) ** 0.5\n", - " return int(d) if d.is_integer() else d\n", - " \n", - "def distance_squared(p: Point2D, q: Point2D) -> float:\n", - " \"\"\"Square of the Euclidean (L2) distance between two 2D points.\"\"\"\n", - " return (p[0] - q[0]) ** 2 + (p[1] - q[1]) ** 2\n", - " \n", - "def taxi_distance(p: Point2D, q: Point2D) -> int:\n", + "# Profiling found that `add` and `taxi_distance` were speed bottlenecks; \n", + "# I define below versions that are specialized for 2D points only.\n", + "\n", + "def add2(p: Point, q: Point) -> Point: \n", + " \"\"\"Specialized version of point addition for 2D Points only. Faster.\"\"\"\n", + " return (p[0] + q[0], p[1] + q[1])\n", + "\n", + "def sub2(p: Point, q: Point) -> Point: \n", + " \"\"\"Specialized version of point subtraction for 2D Points only. Faster.\"\"\"\n", + " return (p[0] - q[0], p[1] - q[1])\n", + "\n", + "def taxi_distance(p: Point, q: Point) -> int:\n", " \"\"\"Manhattan (L1) distance between two 2D Points.\"\"\"\n", " return abs(p[0] - q[0]) + abs(p[1] - q[1])" ] @@ -514,7 +491,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -554,26 +531,21 @@ " 0 <= Y_(point) < Y_(self.size))\n", "\n", " def follow_line(self, start: Point, direction: Vector) -> Iterable[Point]:\n", - " \"\"\"All points from start going in direction, until the edge of the grid.\"\"\"\n", " while self.in_range(start):\n", " yield start\n", - " start = add(start, direction)\n", + " start = add2(start, direction)\n", "\n", - " def copy(self, updates={}): \n", - " \"\"\"Make a copy of this grid, and optionally update some positions with new values.\"\"\"\n", - " grid = Grid(self, directions=self.directions, skip=self.skip, default=self.default)\n", - " grid.update(updates)\n", - " return grid\n", + " def copy(self): \n", + " return Grid(self, directions=self.directions, skip=self.skip, default=self.default)\n", " \n", " def neighbors(self, point) -> List[Point]:\n", " \"\"\"Points on the grid that neighbor `point`.\"\"\"\n", - " return [add(point, Δ) for Δ in self.directions \n", - " if (add(point, Δ) in self) \n", - " or (self.default not in (KeyError, None))]\n", + " return [add2(point, Δ) for Δ in self.directions \n", + " if add2(point, Δ) in self or self.default not in (KeyError, None)]\n", " \n", - " def neighbor_contents(self, point) -> Iterable:\n", + " def neighbor_contents(self, point) -> list:\n", " \"\"\"The contents of the neighboring points.\"\"\"\n", - " return (self[p] for p in self.neighbors(point))\n", + " return [self[p] for p in self.neighbors(point)]\n", "\n", " def findall(self, contents: Collection) -> List[Point]:\n", " \"\"\"All points that contain one of the given contents, e.g. grid.findall('#').\"\"\"\n", @@ -592,8 +564,6 @@ " \"\"\"Print a representation of the grid.\"\"\"\n", " for row in self.to_rows(xrange, yrange):\n", " print(*row, sep=sep)\n", - "\n", - " def __str__(self): return cat(self.to_rows())\n", " \n", " def plot(self, markers={'#': 's', '.': ','}, figsize=(14, 14), **kwds):\n", " \"\"\"Plot a representation of the grid.\"\"\"\n", @@ -625,7 +595,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -653,7 +623,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ @@ -679,7 +649,8 @@ "class GridProblem(SearchProblem):\n", " \"\"\"Problem for searching a grid from a start to a goal location.\n", " A state is just an (x, y) location in the grid.\"\"\"\n", - " def actions(self, pos): return [p for p in self.grid.neighbors(pos) if self.grid[pos] != '#']\n", + " def actions(self, loc): return self.grid.neighbors(loc)\n", + " def result(self, loc1, loc2): return loc2\n", " def h(self, node): return taxi_distance(node.state, self.goal) \n", "\n", "class Node:\n", @@ -729,7 +700,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ @@ -758,7 +729,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 18, "metadata": {}, "outputs": [], "source": [ @@ -768,24 +739,19 @@ " \n", "class HCounter(Counter):\n", " \"\"\"A Counter, but it is hashable.\"\"\"\n", - " def __hash__(self): return hash(tuple(sorted(self.items())))\n", - "\n", - "class EqualityIsIdentity:\n", - " \"\"\"A mixin to say that objects of this class are equal only if they are identical.\"\"\"\n", - " def __hash__(self): return id(self)\n", - " def __eq__(self, other): return self is other" + " def __hash__(self): return hash(tuple(sorted(self.items())))" ] }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "class Graph(defaultdict):\n", " \"\"\"A graph of {node: [neighboring_nodes...]}. \n", " Can store other kwd attributes on it (which you can't do with a dict).\"\"\"\n", - " def __init__(self, contents=(), **kwds):\n", + " def __init__(self, contents, **kwds):\n", " self.update(contents)\n", " self.default_factory = list\n", " self.__dict__.update(**kwds)" @@ -793,7 +759,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 20, "metadata": {}, "outputs": [], "source": [ @@ -805,33 +771,6 @@ " self[attr] = value" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def get_size(obj, seen: Optional[Set[int]] = None) -> int:\n", - " \"\"\"Recursively finds size of objects.\"\"\"\n", - " seen = set() if seen is None else seen\n", - "\n", - " if id(obj) in seen: return 0 # to handle self-referential objects\n", - " seen.add(id(obj))\n", - "\n", - " size = sys.getsizeof(obj, 0) # pypy3 always returns default (necessary)\n", - " if isinstance(obj, dict):\n", - " size += sum(getSize(v, seen) + getSize(k, seen) for k, v in obj.items())\n", - " elif hasattr(obj, '__dict__'):\n", - " size += getSize(obj.__dict__, seen)\n", - " elif hasattr(obj, '__slots__'): # in case slots are in use\n", - " slotList = [getattr(C, \"__slots__\", []) for C in obj.__class__.__mro__]\n", - " slotList = [[slot] if isinstance(slot, str) else slot for slot in slotList]\n", - " size += sum(getSize(getattr(obj, a, None), seen) for slot in slotList for a in slot)\n", - " elif hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes, bytearray)):\n", - " size += sum(getSize(i, seen) for i in obj)\n", - " return size" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -841,7 +780,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 21, "metadata": {}, "outputs": [], "source": [ @@ -922,7 +861,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.8.15" } }, "nbformat": 4,