{ "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 problem, record the response, and comment on it. I'll alternate between Gemini, Claude, and ChatGPT. So far the LLMs are doing great, but IMHO I prefer my code.\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: .0006 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: .1425 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": "bb0f6906-369e-4b3c-8840-d5648d713942", "metadata": {}, "source": [ "That's a 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 (in the appropriate direction): " ] }, { "cell_type": "code", "execution_count": 6, "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": 7, "id": "8969191e-bd50-4187-8e4b-64524bc8427a", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 1.2: .0009 seconds, answer 6907 ok" ] }, "execution_count": 7, "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": 8, "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": 9, "id": "d682f3f2-415e-4556-b6c8-43e331a38703", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1990936" ] }, "execution_count": 9, "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. By *first half* I mean by digits: the first half of a six-digit number is the first three digits.\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 case 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": 10, "id": "1345f93d-84c5-43f8-b6c2-9fc7b8f5ed90", "metadata": {}, "outputs": [], "source": [ "def invalids_in_range(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", "def invalids(id_ranges) -> List[int]:\n", " \"\"\"Invalid IDs, according to the list of invalid ID ranges.\"\"\"\n", " return [id for (lo, hi) in id_ranges\n", " for id in invalids_in_range(lo, hi)]\n", "\n", "def invalids(id_ranges) -> List[int]:\n", " \"\"\"Invalid IDs, according to the list of invalid ID ranges.\"\"\"\n", " return append(invalids_in_range(lo, hi)\n", " for (lo, hi) in id_ranges)\n", "\n", "assert invalids([(11, 22)]) == [11, 22]" ] }, { "cell_type": "code", "execution_count": 11, "id": "8b0d12b3-f184-4149-8b49-f9ff78663d46", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 2.1: .0027 seconds, answer 23560874270 ok" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(2.1, 23560874270, lambda:\n", " sum(invalids(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 consists of two *or more* repeats of a sequence of digits. So 111 (1 repeated three times), 12121212 (12 repeated four times), and 222222 (2 repeated six times) are all invalid. I'll rewrite `invalids_in_range` to take an optional argument saying how many repeats we're looking for, and introduce `all_invalids` to try all possible repeat lengths:" ] }, { "cell_type": "code", "execution_count": 12, "id": "6b8d6dad-c5b6-4ed8-8dfa-5acbb54c8001", "metadata": {}, "outputs": [], "source": [ "def invalids_in_range(lo: int, hi: int, repeat=2) -> Iterable[int]:\n", " \"\"\"Yield all the invalid IDs between lo and hi inclusive\n", " that are formed from exactly `repeat` repeated digit 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 all_invalids(id_ranges) -> Set[int]:\n", " \"\"\"All invalid IDs, according to the list of ranges, with any number of repeats.\"\"\"\n", " return union(invalids_in_range(lo, hi, repeat)\n", " for (lo, hi) in id_ranges\n", " for repeat in range(2, len(str(hi)) + 1))\n", "\n", "assert invalids([(11, 22)]) == [11, 22]\n", "assert list(invalids_in_range(2121212118, 2121212124, 5)) == [2121212121]\n", "assert 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": 13, "id": "8a7c6c25-4b5f-4178-8559-166ba1a9f924", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 2.1: .0031 seconds, answer 23560874270 ok" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(2.1, 23560874270, lambda:\n", " sum(invalids(id_ranges)))" ] }, { "cell_type": "code", "execution_count": 14, "id": "32fefd65-df2a-4ea3-9acd-7525ebd32380", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 2.2: .0040 seconds, answer 44143124633 ok" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(2.2, 44143124633, lambda:\n", " sum(all_invalids(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 `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": 15, "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 I chose to do the string to int conversion in `total_joltage`; it would also be fine to have `joltage` return an int." ] }, { "cell_type": "code", "execution_count": 16, "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", " Pick the first digit, then the biggest digit from the rest of the bank.\"\"\"\n", " digit = max(bank[:-1]) # The first digit can't be the last character\n", " rest = bank[bank.index(digit) + 1:]\n", " return digit + max(rest)\n", "\n", "def total_joltage(banks: List[str]) -> int:\n", " \"\"\"The maximum possible joltage from all the banks.\"\"\"\n", " return sum(int(joltage(bank)) for bank in banks)\n", "\n", "assert joltage(\"8647\") == \"87\"\n", "assert joltage(\"1119\") == \"19\"" ] }, { "cell_type": "code", "execution_count": 17, "id": "9cc62ae9-b313-4b82-b8fb-bab9c0ef5cb6", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 3.1: .0006 seconds, answer 17085 ok" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(3.1, 17085, lambda:\n", " total_joltage(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 two functions, passing in the number of digits to be chosen, *n*. The function `joltage` stops when we get to 1 digit remaining, and recurses when there is more than one digit remaining, choosing the first digit from the bank up to the last *n* - 1 characters, then recursively finding the biggest joltage from the rest, but with *n* - 1 choices. " ] }, { "cell_type": "code", "execution_count": 18, "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", " Pick the first digit, then the maximum joltage from the rest of the bank.\"\"\"\n", " if n == 1:\n", " return max(bank)\n", " else:\n", " digit = max(bank[:-(n - 1)]) # The first digit can't be the last n-1 characters\n", " rest = bank[bank.index(digit) + 1:]\n", " return digit + joltage(rest, n - 1)\n", "\n", "def total_joltage(banks: List[str], n=2) -> int:\n", " \"\"\"The maximum possible joltage from all the banks.\"\"\"\n", " return sum(int(joltage(bank, n)) for bank in banks)\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": 19, "id": "fe3cff78-81c0-4d4a-bb4d-f0f841067e0d", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 3.1: .0006 seconds, answer 17085 ok" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(3.1, 17085, lambda:\n", " total_joltage(banks))" ] }, { "cell_type": "code", "execution_count": 20, "id": "f971839e-81ea-49b4-a92f-a44884be645d", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 3.2: .0020 seconds, answer 169408143086082 ok" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(3.2, 169408143086082, lambda:\n", " total_joltage(banks, 12))" ] }, { "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. Some 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": 21, "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": [ "\"Gary\n", "\n", "\n", "### 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 (that's why I specified `directions=directions8` in defining `paper_grid`). 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": 22, "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 is_accessible(p, grid)]\n", "\n", "def is_accessible(p: Point, grid: Grid) -> bool:\n", " \"\"\"Is point p on the grid an accessible paper roll?\"\"\"\n", " return grid[p] == '@' and 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": 23, "id": "a5ef09cf-b204-41eb-80d8-de107d385dbb", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 4.1: .0531 seconds, answer 1569 ok" ] }, "execution_count": 23, "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, enabling them to remove more. How many rolls 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": 24, "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", " grid2 = grid.copy() # To avoid mutating the input grid\n", " points = accessible_rolls(grid2)\n", " while points:\n", " yield from points\n", " grid2.update({p: '.' for p in points})\n", " points = accessible_rolls(grid2)" ] }, { "cell_type": "code", "execution_count": 25, "id": "2fb17a51-05f7-42ec-8d6c-222121a026cf", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 4.2: 1.2392 seconds, answer 9280 ok" ] }, "execution_count": 25, "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. The main issue is that `accessible_rolls` has to look at the whole grid on every iteration. If the previous iteration only removed one roll, all we really need to look at on the next iteration is the neighbors of the removed roll. So I'll keep a queue of possibly removable points 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": 26, "id": "54f20b5e-6713-459c-8d40-e545ce6b8e42", "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", " grid2 = grid.copy() # To avoid mutating the original input grid\n", " Q = list(grid) # A queue of possibly removable positions in the grid\n", " while Q:\n", " p = Q.pop()\n", " if is_accessible(p, grid2):\n", " yield p\n", " grid2[p] = '.'\n", " Q.extend(grid2.neighbors(p))" ] }, { "cell_type": "code", "execution_count": 27, "id": "bcba970b-09aa-479b-9c6d-4f6a7ac49fed", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 4.2: .1394 seconds, answer 9280 ok" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(4.2, 9280, lambda:\n", " quantify(removable_rolls(paper_grid)))" ] }, { "cell_type": "markdown", "id": "1f9a1e40-192a-4386-8cfb-bc5f69c88a9b", "metadata": {}, "source": [ "# [Day 5](https://adventofcode.com/2025/day/5): Cafeteria\n", "\n", "Today we're in the cafeteria, and the elves need to figure out which of their ingredients are fresh or spoiled. The input file is in two paragraphs, the first consisting of ranges of fresh ingredient IDs, like \"3-5\" and the second consisting of the available ingredient IDs, like \"8\". I can parse the data like this:" ] }, { "cell_type": "code", "execution_count": 28, "id": "4b508db5-aeae-410e-a062-1b2d4ce41253", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "Puzzle input ➜ 1184 strs:\n", "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "292632986393425-296797126337251\n", "428261559408337-431275643240865\n", "197704206528056-198822557342819\n", "36791726875734-37049023408764\n", "134880223152389-139959748438608\n", "31870818340663-32138457068292\n", "443613579514078-447616030778273\n", "284142407577672-288117756872436\n", "...\n", "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "Parsed representation ➜ 2 tuples:\n", "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "((292632986393425, 296797126337251), (428261559408337, 431275643240865), (197704206528056, 19882 ...\n", "(92087202605588, 314304587960778, 19184152482180, 331502336245673, 104093299503920, 224082459481 ...\n" ] } ], "source": [ "def parse_ingredients(text: str) -> tuple:\n", " \"\"\"Parse either ranges like \"3-5\", or integers like \"8\".\"\"\"\n", " return mapt(positive_ints, lines(text)) if '-' in text else ints(text)\n", " \n", "fresh_ranges, available_ingredient_ids = parse(5, parse_ingredients, sections=paragraphs)" ] }, { "cell_type": "markdown", "id": "ebb61d5f-4683-4ff6-986c-69154d512e1b", "metadata": {}, "source": [ "### Part 1: How many of the available ingredient IDs are fresh?\n", "\n", "I saw that the input file is only a few pages long, so I don't need any fancy data structures (like binary trees); I can approach this in the simplest way: for each ingredient ID, check to see if it is contained in any of the ranges:" ] }, { "cell_type": "code", "execution_count": 29, "id": "ae5b33e4-92f5-4fe2-bd5a-6b22095724fa", "metadata": {}, "outputs": [], "source": [ "def count_fresh_ingredients(fresh_ranges, available_ingredient_ids) -> int:\n", " \"\"\"How many of the available ingredient IDs are in one of the fresh ranges?\"\"\"\n", " return quantify(any(lo <= id <= hi for (lo, hi) in fresh_ranges)\n", " for id in available_ingredient_ids)" ] }, { "cell_type": "code", "execution_count": 30, "id": "112a84f7-9bb8-45f0-9d7e-f600f37f0fdf", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 5.1: .0072 seconds, answer 635 ok" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(5.1, 635, lambda:\n", " count_fresh_ingredients(fresh_ranges, available_ingredient_ids))" ] }, { "cell_type": "markdown", "id": "01c2c322-e873-412f-9c47-59ffdf9adf79", "metadata": {}, "source": [ "### Part 2: How many ingredient IDs are fresh?\n", "\n", "In Part 2 we are asked how many of the possible ingredient IDs are fresh, regardless of whether the ID is available or not. I could convert a range like \"3-5\" into the set {3, 4, 5} and then union the sets together. How many elements would be in that set? Here's an estimate: " ] }, { "cell_type": "code", "execution_count": 31, "id": "47a660d2-746c-433f-b4ae-9d01f70bc504", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "476036797138761" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "sum(hi - lo for (lo, hi) in fresh_ranges)" ] }, { "cell_type": "markdown", "id": "1360869f-3ccb-4c71-8d00-35de1215389f", "metadata": {}, "source": [ "OK, switch to Plan B. Instead of explicitly building this set I can instead sort the fresh ID ranges (by their low number) and go through them, keeping track of the lowest ID that has not yet been explored for freshness, and a count of the fresh IDs found so far. For each range, the number of new fresh IDs is the length of the range that starts with either the start of the range or the first unexplored ID, and ends at the end of the range." ] }, { "cell_type": "code", "execution_count": 32, "id": "f52e8ecd-325e-4ed4-8928-de9365b5b7d4", "metadata": {}, "outputs": [], "source": [ "def count_fresh_ids(fresh_ranges) -> int:\n", " \"\"\"How many IDs are contained in the union of the ranges?\"\"\"\n", " fresh_count = 0\n", " unexplored = 0\n", " for (lo, hi) in sorted(fresh_ranges):\n", " start = max(unexplored, lo)\n", " fresh_count += len(range(start, hi + 1))\n", " unexplored = max(unexplored, hi + 1)\n", " return fresh_count" ] }, { "cell_type": "code", "execution_count": 33, "id": "fab6f1ed-543f-4f42-b1d5-2551742e3a4f", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 5.2: .0001 seconds, answer 369761800782619 ok" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(5.2, 369761800782619, lambda:\n", " count_fresh_ids(fresh_ranges))" ] }, { "cell_type": "markdown", "id": "6b83e353-3b77-4645-85b9-2f6ee6ff9e1d", "metadata": {}, "source": [ "# [Day 6](https://adventofcode.com/2025/day/5): Trash Conmpactor\n", "\n", "Trash Compactor? [I've got a bad feeling about this!](https://youtu.be/CZgeYSqUeTA?si=5UPS_HiCOmTKrEWX&t=32) We've fallen into a garbage smasher and have been asked to help some of the resident cephalopods with their math homework. We can parse the homework worksheet, but we were told that the exact alignment of columns matters, so I'll keep each line as a string rather than converting it to a list of ints." ] }, { "cell_type": "code", "execution_count": 34, "id": "bca852a9-e4c5-4706-abfc-afc6d4a4eeb5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "Puzzle input ➜ 5 strs:\n", "────────────────────────────────────────────────────────────────────────────────────────────────────\n", " 4 82 68 85 74 6 56 14 2 8669 66 13 927 3 235 44 52 16 37 61 82 1774 96 15 775 789 91 ...\n", " 6 87 39 72 56 12 69 79 58 4378 86 49 146 5 412 85 7751 577 69 813 55 9942 753 49 734 587 15 ...\n", " 827 446 82 72 76 21 31 32 96 3257 21 21 171 2 31 17 9178 977 11 469 58 712 162 4 1 132 91 ...\n", "9472 154 36 76 5 89 37 5 28 6 95 49 82 66 7 44 8183 524 38 819 11 7 825 9 8 397 48 ...\n", "+ + + * * * + * * + * + * * + + + + * + * + * + * * * ...\n" ] } ], "source": [ "worksheet = parse(6, str)" ] }, { "cell_type": "markdown", "id": "ae8a7f1d-07ec-46e4-880f-abbd82d46a67", "metadata": {}, "source": [ "### Part 1: What is the grand total of the answers to the individual problems?\n", "\n", "We humans are used to arithmetic problems written in left-to-right order, but cephalopods use top-to-bottom order, with a postfix operator in the last row. We need to compute the sum or product for each column and add all those results together. So the problem in the first column above would be \"4 + 6 + 827 + 9472\". Here's the code. (Note my utility function `T` is the matrix transpose function, which turns rows into columns, which is just what we need for this puzzle.)" ] }, { "cell_type": "code", "execution_count": 35, "id": "92f320f4-c7a3-4dfd-ae61-6b1b180bdc93", "metadata": {}, "outputs": [], "source": [ "def grand_total(worksheet):\n", " \"\"\"The sum of the individual sum/product problems, where each column is a problem.\"\"\"\n", " columns = T(map(str.split, worksheet))\n", " operations = {'*': prod, '+': sum}\n", " return sum(operations[op](mapt(int, numbers)) \n", " for (*numbers, op) in columns)" ] }, { "cell_type": "code", "execution_count": 36, "id": "92a47f3a-127f-41e8-bee1-6276885bd36b", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 6.1: .0015 seconds, answer 5877594983578 ok" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(6.1, 5877594983578, lambda:\n", " grand_total(worksheet))" ] }, { "cell_type": "markdown", "id": "226772ae-0d6e-43b8-9159-171ec1b36a5d", "metadata": {}, "source": [ "### Part 2: What is the grand total of the answers to the individual problems now?\n", "\n", "We learn that we did all the problems wrong. Cephalopodish number notation is different; numbers are read vertically rather than horizontally and the exact column alignment of each digit matters. Given the worksheet:\n", "\n", " 4 82 68 85 74 6 56 14 2 8669 66 13 927 3 235 44 \n", " 6 87 39 72 56 12 69 79 58 4378 86 49 146 5 412 85 \n", " 827 446 82 72 76 21 31 32 96 3257 21 21 171 2 31 17 \n", " 9472 154 36 76 5 89 37 5 28 6 95 49 82 66 7 44 \n", " + + + * * * + * * + * + * * + + \n", "\n", "The problem in the leftmost column is not \"`4 + 6 + 827 + 9472`\", rather it is \"`9 + 84 + 27 + 4672`\".\n", "\n", "That means I can't just split each line into numbers, I'll have to be careful to maintain the blank spaces, and I have to know the difference between a blank space that marks a column between problems versus a blank space that marks the lack of a digit. In `grand_total2` I first break each line into columns (which requires finding the columns that are all blank spaces), then I do the math." ] }, { "cell_type": "code", "execution_count": 37, "id": "f332aec9-dbfa-4bf9-b338-9a33c5f3bd89", "metadata": {}, "outputs": [], "source": [ "def grand_total2(worksheet: List[str]):\n", " \"\"\"Solve the problem in each column with cephalopodish math and return the sum.\"\"\"\n", " problems = T(break_into_columns(worksheet))\n", " return sum(map(cephalopodish_math, problems))\n", "\n", "def break_into_columns(worksheet: List[str]) -> List[List[str]]:\n", " \"\"\"Break a whole worksheet into columns by finding the `blanks` (the columns with\n", " only blank spaces) and then separating each line at these posisitons.\"\"\"\n", " worksheet = [' ' + line + ' ' for line in worksheet] # Blanks on each end of line\n", " *number_lines, operator_line = worksheet\n", " blanks = [i for i in range(len(worksheet[0]))\n", " if all(line[i] == ' ' for line in number_lines)]\n", " return [break_line_into_columns(line, blanks) for line in worksheet]\n", "\n", "def break_line_into_columns(line: str, blanks: List[int]) -> List[str]:\n", " \"\"\"Break one line into columns, as specified by the list of `blanks` positions.\"\"\"\n", " return [line[blanks[i]+1:blanks[i + 1]] \n", " for i in range(len(blanks) - 1)]\n", "\n", "def cephalopodish_math(problem) -> int:\n", " \"\"\"Return the sum or product of the vertically-arranged numbers.\"\"\"\n", " *numbers, op = problem\n", " sum_or_prod = {'+': sum, '*': prod}[op.strip()]\n", " return sum_or_prod(int(cat(digits)) for digits in T(numbers))" ] }, { "cell_type": "code", "execution_count": 38, "id": "39a8fa78-946f-45aa-8d36-76883f4aeaff", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 6.2: .0082 seconds, answer 11159825706149 ok" ] }, "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(6.2, 11159825706149, lambda:\n", " grand_total2(worksheet))" ] }, { "cell_type": "markdown", "id": "ca461ef3-50c3-4189-b724-5f2b3898f27d", "metadata": {}, "source": [ "I initially had an `IndexError` **bug** because the operator line is shorter then the numbers lines. Then I had an off-by-one **bug** that made me mess up the problem in the last column. To debug the problems I worked on the smaller example worksheet:" ] }, { "cell_type": "code", "execution_count": 39, "id": "a13dbe89-3178-433f-9a50-28ed9a2f8358", "metadata": {}, "outputs": [], "source": [ "worksheet2 = \"\"\"\\\n", "123 328 51 64 \n", " 45 64 387 23 \n", " 6 98 215 314\n", "* + * +\"\"\".splitlines()" ] }, { "cell_type": "code", "execution_count": 40, "id": "3aa0b661-367a-4316-905a-60f1d9e6df01", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[['123', '328', ' 51', '64 '],\n", " [' 45', '64 ', '387', '23 '],\n", " [' 6', '98 ', '215', '314'],\n", " ['* ', '+ ', '* ', '+ ']]" ] }, "execution_count": 40, "metadata": {}, "output_type": "execute_result" } ], "source": [ "break_into_columns(worksheet2)" ] }, { "cell_type": "code", "execution_count": 41, "id": "13392d9c-fef8-4946-b945-48ad6bc7eca9", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[('123', ' 45', ' 6', '* '),\n", " ('328', '64 ', '98 ', '+ '),\n", " (' 51', '387', '215', '* '),\n", " ('64 ', '23 ', '314', '+ ')]" ] }, "execution_count": 41, "metadata": {}, "output_type": "execute_result" } ], "source": [ "problems = T(break_into_columns(worksheet2))\n", "problems" ] }, { "cell_type": "code", "execution_count": 42, "id": "9a25ff50-f465-4de3-95ca-71b7a3d9569b", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(8544, 625, 3253600, 1058)" ] }, "execution_count": 42, "metadata": {}, "output_type": "execute_result" } ], "source": [ "mapt(cephalopodish_math, problems)" ] }, { "cell_type": "markdown", "id": "7f31ae9b-6606-40b0-9bb1-ed9b3fe3cbf0", "metadata": {}, "source": [ "# Summary" ] }, { "cell_type": "code", "execution_count": 43, "id": "ba36579c-d0b4-4fd3-939c-0026ecddd7e9", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Puzzle 1.1: .0006 seconds, answer 1182 ok\n", "Puzzle 1.2: .0009 seconds, answer 6907 ok\n", "Puzzle 2.1: .0031 seconds, answer 23560874270 ok\n", "Puzzle 2.2: .0040 seconds, answer 44143124633 ok\n", "Puzzle 3.1: .0006 seconds, answer 17085 ok\n", "Puzzle 3.2: .0020 seconds, answer 169408143086082 ok\n", "Puzzle 4.1: .0531 seconds, answer 1569 ok\n", "Puzzle 4.2: .1394 seconds, answer 9280 ok\n", "Puzzle 5.1: .0072 seconds, answer 635 ok\n", "Puzzle 5.2: .0001 seconds, answer 369761800782619 ok\n", "Puzzle 6.1: .0015 seconds, answer 5877594983578 ok\n", "Puzzle 6.2: .0082 seconds, answer 11159825706149 ok\n" ] }, { "data": { "text/plain": [ "0.22060370445251465" ] }, "execution_count": 43, "metadata": {}, "output_type": "execute_result" } ], "source": [ "for d in sorted(answers):\n", " print(answers[d])\n", "sum(a.secs for a in answers.values())" ] }, { "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 }