From fad50d38633f0786925675af09346259fa0f722a Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Fri, 12 Dec 2025 22:48:22 -0800 Subject: [PATCH] Add files via upload --- ipynb/Advent-2025.ipynb | 437 ++++++++++++++++++++++------------------ 1 file changed, 243 insertions(+), 194 deletions(-) diff --git a/ipynb/Advent-2025.ipynb b/ipynb/Advent-2025.ipynb index b637e67..df06926 100644 --- a/ipynb/Advent-2025.ipynb +++ b/ipynb/Advent-2025.ipynb @@ -11,15 +11,11 @@ "\n", "I enjoy doing the [**Advent of Code**](https://adventofcode.com/) (AoC) programming puzzles, so here we go for 2025! \n", "\n", - "This year I will be doing something different: I will solve each problem myself here, and then [**in another notebook**](Advent-2025-AI.ipynb) I will ask an AI Large Language Model to solve the same problem. Check out the differences!\n", + "This year I will be doing something different: I will solve each problem myself here, and then [**in another notebook**](Advent-2025-AI.ipynb) I will ask an **AI Large Language Model** to solve the same problem. Check out the differences!\n", "\n", "# Day 0\n", "\n", - "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)." + "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, which also prints the first few lines of the input and the resulting parsed data objects), then solve Part 1 and Part 2 (recording the correct answer with my `answer` function)." ] }, { @@ -33,6 +29,16 @@ "current_year = 2025" ] }, + { + "cell_type": "markdown", + "id": "d0d7cba9-e8be-4eeb-9918-2a2aecb21738", + "metadata": {}, + "source": [ + "I'm thankful that [@GaryGrady](https://mastodon.social/@garygrady) is providing cartoons:\n", + "\n", + "\"Gary" + ] + }, { "cell_type": "markdown", "id": "37bc12b8-d0dc-4873-984c-6f09ae647229", @@ -40,7 +46,9 @@ "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 24 challenges along the way; the first one involves unlocking a safe. The safe has a dial with 100 numbers, and an arrow that currently points at 50. Our input for today is a sequence of left and right rotations; for example \"R20\" means move the dial right by 20 numbers and \"L13\" means move it left by 13 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:" + "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 24 challenges along the way; the first one involves unlocking a safe. The safe has a dial with 100 numbers, and an arrow that currently points at 50. Our input for today is a sequence of left and right rotations; for example \"R20\" means move the dial right by 20 numbers and \"L13\" means move it left by 13 numbers. \n", + "\n", + "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. " ] }, { @@ -184,7 +192,7 @@ { "data": { "text/plain": [ - "Puzzle 1.2: .1463 seconds, answer 6907 correct" + "Puzzle 1.2: .1357 seconds, answer 6907 correct" ] }, "execution_count": 6, @@ -262,7 +270,7 @@ "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 of items in stock. Each range is a pair of integers separated by a dash, and the ranges are separated by commas:" + "We're in the North Pole gift shop, where the elves are doing inventory control. Today's input is a list of ranges of product IDs of items in stock. Each range is a pair of integers separated by a dash, and the ranges are separated by commas:" ] }, { @@ -309,7 +317,7 @@ "\n", "An invalid ID is defined as 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?" + "We could look at every number in every 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?" ] }, { @@ -338,10 +346,10 @@ "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 don't mean divide by 2; I mean the first half of the digit string: the first half of \"123456\" is \"123\".\n", + "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 in the range, and automatically generate invalid IDs by appending a copy of the first half to itself. By *first half* I don't mean divide by 2; I mean the first half of the digit string: the first half of \"123456\" is \"123\".\n", "\n", "Suppose the range is 123456-223000. I enumerate from 123 to 223, and for each number 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 are in the range 123456-223000). 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.)" + "[123123, 124124, 125125, ... 223223]. I then yield the IDs that are within the range (in this case all but the first and the last are in the range 123456-223000). Altogether I only have to consider 101 half IDs rather than 100,001 full IDs. (The algorithm scales with the square root of the size of the range, not with the size of the range itself.)" ] }, { @@ -362,12 +370,12 @@ " elif id > hi:\n", " return\n", "\n", - "def invalids(id_ranges) -> List[int]:\n", + "def invalids(id_ranges) -> Set[int]:\n", " \"\"\"Invalid IDs, according to any one of the list of invalid ID ranges.\"\"\"\n", - " return append(invalids_in_range(lo, hi)\n", - " for (lo, hi) in id_ranges)\n", + " return union(invalids_in_range(lo, hi)\n", + " for (lo, hi) in id_ranges)\n", "\n", - "assert invalids([(11, 22)]) == [11, 22]" + "assert invalids([(11, 22)]) == {11, 22}" ] }, { @@ -379,7 +387,7 @@ { "data": { "text/plain": [ - "Puzzle 2.1: .0029 seconds, answer 23560874270 correct" + "Puzzle 2.1: .0027 seconds, answer 23560874270 correct" ] }, "execution_count": 12, @@ -399,7 +407,7 @@ "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:" + "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 100100 is still invalid, but so are 100100100 and 100100100100 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 across all ID ranges:" ] }, { @@ -463,7 +471,7 @@ { "data": { "text/plain": [ - "Puzzle 2.1: .0029 seconds, answer 23560874270 correct" + "Puzzle 2.1: .0027 seconds, answer 23560874270 correct" ] }, "execution_count": 15, @@ -503,7 +511,7 @@ "id": "872cf212-bfbf-4edd-b898-5f76ad122a85", "metadata": {}, "source": [ - "I initially had another **bug** here: I initially counted \"222222\" three times: once as 2 repeats of \"222\", once as 3 repeats of \"22\", and once as 6 repeats of \"2\". I changed the output of `all_invalids` to be a `set` rather than a `list` to fix that." + "I initially had another **bug** here: I initially counted \"222222\" three times: as 2 repeats of \"222\", or 3 repeats of \"22\", or 6 repeats of \"2\". I changed the output of `all_invalids` to be a `set` rather than a `list` to fix that." ] }, { @@ -513,7 +521,7 @@ "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. " + "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 digits representing the *joltage* level of a battery. " ] }, { @@ -552,7 +560,7 @@ "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; that's the maximum. 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 there would be no choices left for the second digit. (I chose to do the string-to-int conversion in `total_joltage`; it would also be fine to have `joltage` return an int.)" + "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; that's the maximum. We are asked what is the sum of the maximal possible joltage from each bank. The function `total_joltage` computes that; it calls `joltage` on each bank, and `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 there would be no choices left for the second digit. (I chose to do the string-to-int conversion in `total_joltage`; it would also be okay to have `joltage` return an int.)" ] }, { @@ -614,7 +622,7 @@ "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", + "The elf hits the ***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* (with default 2 for backwards compatibility). The function `joltage` 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. " ] @@ -671,7 +679,7 @@ { "data": { "text/plain": [ - "Puzzle 3.1: .0006 seconds, answer 17085 correct" + "Puzzle 3.1: .0007 seconds, answer 17085 correct" ] }, "execution_count": 23, @@ -693,7 +701,7 @@ { "data": { "text/plain": [ - "Puzzle 3.2: .0024 seconds, answer 169408143086082 correct" + "Puzzle 3.2: .0021 seconds, answer 169408143086082 correct" ] }, "execution_count": 24, @@ -792,7 +800,7 @@ { "data": { "text/plain": [ - "Puzzle 4.1: .0553 seconds, answer 1569 correct" + "Puzzle 4.1: .0572 seconds, answer 1569 correct" ] }, "execution_count": 27, @@ -812,7 +820,7 @@ "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 in turn make other rolls accessible, and hence removable. How many rolls in total can be removed?\n", + "If a paper roll is accessible, it can be removed by forklift. That may in turn make other rolls accessible, and hence removable. 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." ] @@ -843,7 +851,7 @@ { "data": { "text/plain": [ - "Puzzle 4.2: 1.2531 seconds, answer 9280 correct" + "Puzzle 4.2: 1.2546 seconds, answer 9280 correct" ] }, "execution_count": 29, @@ -861,7 +869,7 @@ "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. That's wasteful: 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. When the queue is empty, no more rolls can be removed." + "That's the right answer, but the run time is a bit slow. The main issue is that `accessible_rolls` has to look at the whole grid on every iteration. But 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. When the queue is empty, no more rolls can be removed." ] }, { @@ -873,8 +881,8 @@ "source": [ "def removable_rolls(grid: Grid) -> Iterable[Point]:\n", " \"\"\"The positions of paper rolls that can be removed, in any number 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", + " grid2 = grid.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 is_accessible(p, grid2):\n", @@ -892,7 +900,7 @@ { "data": { "text/plain": [ - "Puzzle 4.2: .1394 seconds, answer 9280 correct" + "Puzzle 4.2: .1461 seconds, answer 9280 correct" ] }, "execution_count": 31, @@ -910,7 +918,9 @@ "id": "4aae3157-9c06-40d1-b0cd-c6f5515b0064", "metadata": {}, "source": [ - "Let's visualize the paper rolls before after removal:" + "That's almost 10 times faster!\n", + "\n", + "Let's visualize the paper rolls before and after removal:" ] }, { @@ -1131,7 +1141,7 @@ "source": [ "# [Day 6](https://adventofcode.com/2025/day/5): Trash Compactor\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 children 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.\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 cephalopod children 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.\n", "\n", "\"Gary" ] @@ -1195,7 +1205,7 @@ { "data": { "text/plain": [ - "Puzzle 6.1: .0022 seconds, answer 5877594983578 correct" + "Puzzle 6.1: .0025 seconds, answer 5877594983578 correct" ] }, "execution_count": 41, @@ -1215,7 +1225,7 @@ "source": [ "### Part 2: What is the grand total of the answers to the individual problems with the new rules?\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", + "In Part 2 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", @@ -1223,11 +1233,11 @@ " 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", + "The problem in the leftmost column is not \"`4 + 6 + 827 + 9472`\"; rather it is \"`9 + 84 + 27 + 4672`\". But the numbers are not always right-justified; the problem in the sixth colun is \"`6128 * 219`\".\n", "\n", "That means I can't just split each line into numbers, I'll have to be careful to maintain the blank spaces to the right and left of the digits, and I have to know in what position each column starts and ends. That part was tricky, so here's an explanation:\n", "- I note from the worksheet above that each column starts at a position above a `+` or `*` sign.\n", - "- Each column ends one character befpre the next `+` or `*` sign (one character is a blank space to separate columns).\n", + "- Each column ends one character before the next `+` or `*` sign.\n", "- For the last column there is no terminator, so I'll add the string `' *'` to the operator line before computing the start positions.\n", "\n", "In `grand_total2` I first break each line into columns, take the transpose of that (to give a list of problems), do the math on each problem, and return the sum of the results. Within `cephalopodish_math` I call `vertically`, whioch again does a transpose and then puts the digits together into an integer." @@ -1265,7 +1275,7 @@ " return sum_or_prod(vertically(numbers))\n", "\n", "def vertically(numbers: List[str]) -> List[int]:\n", - " \"\"\"Return a list of integers found by read numbers vertically by column.\"\"\"\n", + " \"\"\"Return a list of integers found by reading numbers vertically by column.\"\"\"\n", " return [int(cat(digits)) for digits in T(numbers)]" ] }, @@ -1278,7 +1288,7 @@ { "data": { "text/plain": [ - "Puzzle 6.2: .0063 seconds, answer 11159825706149 correct" + "Puzzle 6.2: .0064 seconds, answer 11159825706149 correct" ] }, "execution_count": 43, @@ -1296,7 +1306,7 @@ "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 messed up the problem in the last column. To debug my errors I worked on the smaller example worksheet, doing things like this:" + "I initially had an `IndexError` **bug** because the operator line is shorter then the numbers lines; the `ljust` fixed that. Then I had an off-by-one **bug** that messed up the problem in the last column. To debug my errors I worked on the smaller example worksheet, doing things like this:" ] }, { @@ -1414,7 +1424,7 @@ "source": [ "# [Day 7](https://adventofcode.com/2025/day/7): Laboratories\n", "\n", - "In the teleporter lab is a tachyon manifold in need of repair. We find a diagram of the manifold, with `S` marking the start of the tachyon beams and `^` marking is a splitter. \n", + "In the lab is a tachyon manifold in need of repair. We find a diagram of the manifold, with `S` marking the start of the tachyon beams and `^` marking a splitter. \n", "\n", "I could parse this as a `Grid`, but I think I will just keep it as a list of strings. The main idea of a `Grid` is dealing with the 4 or 8 neighbors; a concept that this problem does not use." ] @@ -1457,7 +1467,7 @@ "\n", "Tachyon beams move downwards unless they enounter a `^` splitter, in which case they split into two beams, one immediately to the left of the splitter and one to the right. We're asked how many splits occur. If two beams end up in the same place they count as one beam, so if that beam is split again, that's just one more split, not two more.\n", "\n", - "That suggests I should keep a `set` of current beam positions, and update the set every time we go down one line. (It is a set rather than a list so that duplicate beam positions count as one, not as two. (Also, if I kept a list of positions then given the size of the input there would be quadrillions of beams by the end, so it would take a very long time to get the wrong answer.)) I use the trick of iterating with `for b in list(beams)` rather than `for b in beams` so that I can mutate the set `beams` during the iteration. I also need to keep track of the split count, and return that at the end:" + "That suggests I should keep a `set` of current horizontal beam positions, and update the set every time we go down one line. (It is a set rather than a list so that duplicate beam positions count as one, not as two. (Also, if I kept a list of positions then given the size of the input there would be trillions of beams by the end, so it would take a very long time to get the wrong answer.)) I use the idiom of iterating with `for b in list(beams)` rather than `for b in beams` so that I can mutate the set `beams` during the iteration. I also need to keep track of the split count, and return that at the end:" ] }, { @@ -1490,7 +1500,7 @@ { "data": { "text/plain": [ - "Puzzle 7.1: .0010 seconds, answer 1681 correct" + "Puzzle 7.1: .0007 seconds, answer 1681 correct" ] }, "execution_count": 51, @@ -1512,39 +1522,47 @@ "\n", "Now we're told this is a *quantum* tachyon manifold and we need to know how many different *timelines* a single tachyon appears in, or in other words, how many different paths can the tachyon beams take to get to the last line.\n", "\n", - "We can't just count the number of beams in the last line, because if a beam in position *b* takes two different paths to get there, that counts as two, not one. Instead, I'll replace the `set` from Part 1 with a `Counter` of `{beam_position: number_of_paths_to_get_here}`. If a beam at position *b* with *n* paths to get there encounters a splitter, then in the next step both positions *b* - 1 and *b* + 1 are incremented by *n*. In the end we return the sum of all the counts of paths." + "We can't just count the number of beams in the last line, because if a beam in position *b* takes two different paths to get there, that counts as two, not one. Instead, I'll replace the `set` from Part 1 with a `Counter` of `{beam_position: number_of_paths_to_get_here}`. If a beam at position *b* with *n* paths to get there encounters a splitter, then in the next step both positions *b* - 1 and *b* + 1 are incremented by *n*, and position *b* is decremented by *n*. In the end we return the sum of all the counts of paths." ] }, { "cell_type": "code", "execution_count": 52, - "id": "c231059d-edad-4f1c-b584-e811fa8fad46", + "id": "df96165c-4e55-447a-bc5c-fba2f0462ebf", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Puzzle 7.2: .0021 seconds, answer 422102272495018 correct" - ] - }, - "execution_count": 52, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "def count_timelines(manifold: List[str]) -> int:\n", " \"\"\"How many possible paths are there to the final line of the manifold?\"\"\"\n", " start = manifold[0].index('S')\n", " beams = Counter({start: 1})\n", " for line in manifold:\n", - " for b, n in list(beams.items()):\n", + " for (b, n) in list(beams.items()):\n", " if line[b] == '^':\n", - " beams[b] -= n\n", " beams[b - 1] += n\n", + " beams[b] -= n\n", " beams[b + 1] += n\n", - " return sum(beams.values())\n", - "\n", + " return sum(beams.values())" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "ee179d56-ce86-473d-9801-39f1b90d03ab", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 7.2: .0014 seconds, answer 422102272495018 correct" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ "answer(7.2, 422102272495018, lambda:\n", " count_timelines(manifold))" ] @@ -1556,12 +1574,12 @@ "source": [ "# [Day 8](https://adventofcode.com/2025/day/8): Playground\n", "\n", - "In a giant playground, some elves are setting up an ambitious Christmas light decoration project using small electrical junction boxes. They have a list of the junction box coordinates in 3D space:" + "In a playground, some elves are setting up an ambitious Christmas light decoration project using electrical junction boxes. They have a list of the junction box coordinates in 3D space:" ] }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 54, "id": "a5cc43b2-3c5d-4d28-8c94-112bbb5739e7", "metadata": {}, "outputs": [ @@ -1611,14 +1629,14 @@ "\n", "The goal is to start connecting junction boxes, starting with the two boxes that are closest to each other in 3D space, then the next two closest, and so on. (I assume that a box can connect to an unlimited number of other boxes.) After connecting the 1000 pairs that are closest together, what do you get if you multiply together the sizes of the three largest circuits?\n", "\n", - "I recognize this as a [**greedy algorithm**](https://en.wikipedia.org/wiki/Greedy_algorithm), consuming shortest links first, and I've [done that before](TSP.ipynb). I also recognize this as a [**Union-Find**](https://en.wikipedia.org/wiki/Disjoint-set_data_structure) problem, and I know there are efficient data structures for that problem. However, for this problem we don't make heavy use of the union-find functionality, so keeping it simple seems to be the best approach. (After finishing my code I feel vindicated: I see in my [other notebook](Advent2025-AI.ipynb) that Claude Opus 4.5 implemented a Union-Find data structure, and ended up with code that ran slower than my simpler approach.)\n", + "I recognize this as a [**greedy algorithm**](https://en.wikipedia.org/wiki/Greedy_algorithm), consuming shortest links first, and I've [done that before](TSP.ipynb). I also recognize this as a [**Union-Find**](https://en.wikipedia.org/wiki/Disjoint-set_data_structure) problem, and I know there are efficient data structures for that problem. However, for this problem we don't make heavy use of the union-find functionality, so I'll keep it simple. (After finishing my code I feel vindicated: I see in my [other notebook](Advent2025-AI.ipynb) that Claude Opus 4.5 implemented a Union-Find data structure, and ended up with code that ran slower than my simpler approach.)\n", "\n", "The function `greedy_connect` will keep a dict that maps each box to the circuit it is part of (initially just itself), and update that dict when two circuits are connected together. Then I'll go through the 1000 `closest_pairs` of boxes, updating the dict for each one, and return the dict at the end. Then the function `largest` will find the largest 3 circuits, and `prod` will multiply the sizes together." ] }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 55, "id": "fb7744c9-105b-439b-aa57-caf93b8117b8", "metadata": {}, "outputs": [], @@ -1626,13 +1644,13 @@ "Circuit = Tuple[Point, ...]\n", "\n", "def greedy_connect(boxes, n=1000) -> Dict[Point, Circuit]:\n", - " \"\"\"Go through the `n` closest pairs of boxes, shortest first. \n", + " \"\"\"Go through the `n` closest pairs of boxes, shortest first.\n", " If two boxes can be connected to form a new circuit, do it.\"\"\"\n", " circuits = {B: (B,) for B in boxes} # A dict of {box: circuit}\n", " for (A, B) in closest_pairs(boxes, n):\n", " if circuits[A] != circuits[B]:\n", " new_circuit = circuits[A] + circuits[B]\n", - " for C in new_circuit:\n", + " for C in new_circuit: # Keep the circuits table up to date\n", " circuits[C] = new_circuit\n", " return circuits\n", "\n", @@ -1653,17 +1671,17 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 56, "id": "337ab7d6-d142-4c56-a0a3-2b6cd06cd895", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Puzzle 8.1: .5834 seconds, answer 24360 correct" + "Puzzle 8.1: .6336 seconds, answer 24360 correct" ] }, - "execution_count": 55, + "execution_count": 56, "metadata": {}, "output_type": "execute_result" } @@ -1685,7 +1703,7 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 57, "id": "1096ded6-749e-4787-9b90-2de917b147c4", "metadata": {}, "outputs": [], @@ -1705,17 +1723,17 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": 58, "id": "58f06244-ca67-47c9-8ca5-3346a249e1fc", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Puzzle 8.2: .6182 seconds, answer 2185817796 correct" + "Puzzle 8.2: .6273 seconds, answer 2185817796 correct" ] }, - "execution_count": 57, + "execution_count": 58, "metadata": {}, "output_type": "execute_result" } @@ -1745,7 +1763,7 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": 59, "id": "2dba6fa4-a09b-4ab3-a163-0f94f98e82f2", "metadata": {}, "outputs": [ @@ -1789,6 +1807,8 @@ "id": "f8d3b123-234d-41b9-bb5b-090ad1b11e78", "metadata": {}, "source": [ + "\"Gary\n", + "\n", "### Part 1: What is the largest area of any rectangle you can make?\n", "\n", "The Elves would like to find the largest rectangle that uses red tiles for two of its opposite corners. That's easy; we can try all combinations of two corners and take the corners with the maximum area. The only tricky part is remembering that we have to add one to the delta-x and delta-y values before multiplying them; the area of a square with corners (0, 0) and (1, 1) is 4 tiles, not 1." @@ -1796,7 +1816,7 @@ }, { "cell_type": "code", - "execution_count": 59, + "execution_count": 60, "id": "75f2742e-9dbd-4005-a882-3d3931fb1b36", "metadata": {}, "outputs": [], @@ -1819,17 +1839,17 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 61, "id": "27f350c5-7866-4b7b-8d32-91512fcec5b9", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Puzzle 9.1: .0370 seconds, answer 4772103936 correct" + "Puzzle 9.1: .0272 seconds, answer 4772103936 correct" ] }, - "execution_count": 60, + "execution_count": 61, "metadata": {}, "output_type": "execute_result" } @@ -1857,7 +1877,7 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 62, "id": "b5967a65-338d-4198-9f59-986ff74132e6", "metadata": {}, "outputs": [ @@ -1893,25 +1913,29 @@ "- Therefore one of the corners of the maximal rectangle has to be one of the two points on the east end of the equator lines, and the other corner has to be somewhere on the left side of the circle.\n", "- The points are all roughly in a circle, so we're looking for a rectangle roughly inscribed in the circle.\n", "- A roughly correct way to check if a candidate rectangle is all red-and-green is to see if **the rectangle contains a red tile** in its interior.\n", - "- To be more precise, consider the diagram below, in which the two large red circles mark the corners of a rectangle depicted with small purple squares. I have filled in the green tiles that connect the red tiles to form a polygon, and used light green for the interior of the polygon. Is the purple rectangle valid? It is ok that there are two other red tiles on the bottom border; they don't let a white square in. It is ok that there are two red tiles at the top that extend one square into the rectangle; they still don't let a white square in. But the red tile near the bottom right corner is two squares in from the border, and any red tiles in that position let in white squares (here three of them in the bottom right of the rectangle. The only exception is when there are two adjacent red tiles that form a 180 degree turn (as in the left of the rectangle); they do intrude into the rectangle, but because they are adjacent there are no white squares between them. \n", - "- \n", + "- To be more precise, consider the diagram below, in which the two large red circles mark the corners of a rectangle depicted with small purple squares. I have filled in the green tiles that connect the red tiles to form a polygon, and used light green for the interior of the polygon. Is the purple rectangle valid? It is ok that there are two other red tiles on the bottom border; they don't let a white square in. It is ok that there are two red tiles at the top that extend one square into the rectangle; they still don't let a white square in. But the red tile near the bottom right corner is two squares in from the border, and any red tiles in that position let in white squares (here three of them in the bottom right of the rectangle. The only exception is when there are two adjacent red tiles that form a 180 degree turn (as in the left of the rectangle); they do intrude into the rectangle, but because they are adjacent there are no white squares between them.\n", + " \n", + "        \n", + "\n", "- To deal with the two-adjacent-red-tile problem, I will verify that there are no adjacent red tiles in *my* input. I bet Eric Wastl designed it so that nobody gets two adjacent tiles, but they aren't explicitly forbidden in the rules.\n", "- This red-tile-in-interior heuristic by itself isn't enough to stop a candidate rectangle from crossing over the long equator lines. But if we assume that any maximal-area rectangle has sides at least *d* units long, we can fix the problem by inserting an extra red tile every *d* steps along the path. Then, any rectangle with sides greater than *d* that crosses the equator (or any other long line) will contain a red tile in the interior.\n", - "- Normally in problems like this we have to check if the rectangle we are considering is inside the polygon or outside of it. But for polygons that are anything like miine, only very small rectangles can be on the outside. Any sufficiently large polygon must be on the inside, so I didn't bother checking this.\n", + "- If there were more red tiles we could put them into a data structure like a [quadtree](https://en.wikipedia.org/wiki/Quadtree).\n", + "- Normally in problems like this we have to check if the rectangle we are considering is inside the polygon or outside of it. But for polygons that are anything like mine (i.e., mostly convex), only very small rectangles can be on the outside. Any sufficiently large rectangle must be on the inside, so I didn't bother checking.\n", "\n", "I'm ready to start coding! I'll start with this:\n", - "- `find_possible_corners` will return a list of the two candidate corner points at the east end of the equator lines.\n", - "- `breadcrumbs` will leave \"breadcrumbs\" (that is, red tiles) along the trail at least every `d` spaces." + "- `find_2_corners` will return a list of the two candidate corner points at the east end of the equator lines.\n", + "- `breadcrumbs` will leave \"breadcrumbs\" (that is, red tiles) along the trail at least every `d` spaces.\n", + "- An assertion to show that no two of my red tiles are in adjacent squares." ] }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 63, "id": "e08d6c65-5b4e-456e-b4de-38339d209372", "metadata": {}, "outputs": [], "source": [ - "def find_possible_corners(red_tiles, d=10000) -> Optional[List[Point]]:\n", + "def find_2_corners(red_tiles, d=10000) -> Optional[List[Point]]:\n", " \"\"\"Find two adjacent corners, separated on each side by a gap of at least `d`.\"\"\"\n", " return first([B, C] for [A, B, C, D] in sliding_window(red_tiles, 4)\n", " if distance(A, B) > d and distance(C, D) > d)\n", @@ -1925,7 +1949,9 @@ " dx, dy = sign(X_(p) - X_(q)), sign(Y_(p) - Y_(q))\n", " trail.append((X_(q) + d * dx, Y_(q) + d * dy)) # Leave a breadcrumb\n", " trail.append(p)\n", - " return trail" + " return trail\n", + "\n", + "assert not any(distance(p, q) == 1 for (p, q) in sliding_window(red_tiles, 2))" ] }, { @@ -1933,12 +1959,12 @@ "id": "4425bb14-d9ca-4afb-8f1c-5787bf3e17a4", "metadata": {}, "source": [ - "Let's visualize that these two functions work as intended:" + "Let's visualize the output of these two functions:" ] }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 64, "id": "9dabf8bd-bdac-4b9a-941f-9de68480ca5d", "metadata": {}, "outputs": [ @@ -1955,7 +1981,7 @@ ], "source": [ "plot_tiles(breadcrumbs(red_tiles))\n", - "plt.plot(*T(find_possible_corners(red_tiles)), 'bD');" + "plt.plot(*T(find_2_corners(red_tiles)), 'bD');" ] }, { @@ -1963,28 +1989,25 @@ "id": "d8039a9d-1af7-4a74-80c9-18bba48aeb7b", "metadata": {}, "source": [ - "Now I'll define `biggest_rectangle` to find the maximal-area all-red-and-green rectangle. I'll do that by considering pairs of corner points, where the first corner can be any red tile, and for the second corner there are three cases: you can pass in a list of candidate corners, but if you don't, it will call `find_possible_corners` to try to find the equator-end points, and failing that, it will fall back to all the red tiles. We then sort the possible rectangles by area, biggest first, and go through them one at a time. When we find one that does not have `any_intrusions`, we return it; it must be the biggest. (Note that we verify that there are no adjacent red tiles.)\n", + "Now I'll define `biggest_rectangle` to find the maximal-area all-red-and-green rectangle. I'll do that by considering pairs of corner points, where the first corner can be any red tile, and by default the second corner can also be any red tile, but you can optionally pass in the result of `find_2_corners` to speed things up. We then sort the possible rectangles by area, biggest first, and go through them one at a time. When we find one that does not have `any_intrusions`, we return it; it must be the biggest. \n", "\n", - "The function `any_intrusions` checks to see if a red tile is completely inside the rectangle defined by the corners, by checking the x and y coordinates." + "The function `any_intrusions` checks to see if a red tile is completely inside the rectangle defined by the corners, by checking the x and y coordinates of each red tile against the bounds of the rectangle." ] }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 95, "id": "922d721e-5330-466b-8a54-8a273522c44d", "metadata": {}, "outputs": [], "source": [ - "def biggest_rectangle(red_tiles, second_corner_list=None, d=10000) -> Corners:\n", - " \"\"\"Find the biggest rectangle that stays within the interior-and-border of the tiles.\n", - " By default, tries`find_possible_corners(red_tiles)` to narrow down choices for second corner.\"\"\"\n", - " assert not any(distance(p, q) == 1 # This is only guaranteed if no red tiles are adjacent\n", - " for (p, q) in sliding_window(red_tiles, 2))\n", + "def biggest_rectangle(red_tiles, second_corners=None, d=10000) -> Corners:\n", + " \"\"\"Find the biggest rectangle that stays within the interior-and-border of the tiles.\"\"\"\n", + " if second_corners is None: # You can pass in a hint for second corners, or it will try them all\n", + " second_corners = red_tiles\n", " tiles = breadcrumbs(red_tiles, d)\n", - " # Pass in a list of possible second corners, or try to find 2 on equator, or use all the red tiles\n", - " second_corner_list = second_corner_list or find_possible_corners(red_tiles) or red_tiles\n", - " both_corners_list = cross_product(second_corner_list, red_tiles)\n", - " for corners in sorted(both_corners_list, key=tile_area, reverse=True):\n", + " corner_pairs = cross_product(second_corners, red_tiles)\n", + " for corners in sorted(corner_pairs, key=tile_area, reverse=True):\n", " if not any_intrusions(tiles, corners):\n", " return corners\n", " raise ValueError('No rectangle') # Shouldn't get here unless there are no corners\n", @@ -2008,24 +2031,24 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 66, "id": "7ff60bbf-9cb6-4102-8b1c-5ef72be03c92", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Puzzle 9.2: .0164 seconds, answer 1529675217 correct" + "Puzzle 9.2: .0156 seconds, answer 1529675217 correct" ] }, - "execution_count": 65, + "execution_count": 66, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(9.2, 1529675217, lambda:\n", - " tile_area(biggest_rectangle(red_tiles)))" + " tile_area(biggest_rectangle(red_tiles, find_2_corners(red_tiles))))" ] }, { @@ -2038,7 +2061,7 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 67, "id": "8ba7df82-cef0-4c94-8ffa-1822910d21f7", "metadata": {}, "outputs": [ @@ -2054,7 +2077,7 @@ } ], "source": [ - "((x1, y1), (x2, y2)) = biggest_rectangle(red_tiles)\n", + "((x1, y1), (x2, y2)) = biggest_rectangle(red_tiles, find_2_corners(red_tiles))\n", "\n", "plot_tiles(breadcrumbs(red_tiles), figsize=(12, 12))\n", "plt.plot([x1, x1, x2, x2, x1], [y1, y2, y2, y1, y1], 'b:');" @@ -2067,12 +2090,12 @@ "source": [ "We see that if the upper-left corner of the blue rectangle were any higher, then there would be red (and white) tiles in the upper-right corner of the rectangle. If the upper-left corner of the blue rectangle were any further southwest that would be ok, but would result in a slightly smaller area. You'll just have to take it for granted that all the possible rectangles formed below the equater lines are also a little bit smaller in area.\n", "\n", - "How long would the run time be if we didn't rely on the second corner being an equatorial point? Would the answer be the same? Let's check:" + "What if we didn't rely on the second corner being an equatorial point? Would the answer be the same? Would it be a lot slower? Let's check:" ] }, { "cell_type": "code", - "execution_count": 67, + "execution_count": 94, "id": "d3b44691-da52-4794-ab77-bc4326aa6ca2", "metadata": {}, "outputs": [ @@ -2080,7 +2103,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 2.33 s, sys: 90.5 ms, total: 2.42 s\n", + "CPU times: user 1.56 s, sys: 4.09 ms, total: 1.56 s\n", "Wall time: 1.56 s\n" ] }, @@ -2090,13 +2113,13 @@ "True" ] }, - "execution_count": 67, + "execution_count": 94, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "%time tile_area(biggest_rectangle(red_tiles, red_tiles)) == tile_area(biggest_rectangle(red_tiles))" + "%time tile_area(biggest_rectangle(red_tiles, red_tiles)) == tile_area(biggest_rectangle(red_tiles, find_2_corners(red_tiles)))" ] }, { @@ -2106,7 +2129,10 @@ "source": [ "Yes, it finds the same maximal-area rectangle, but it takes a lot longer to find it.\n", "\n", - "**Two final remarks**: One, this was the first puzzle of the year that was **difficult**. Two, my solution is **unsatisfying** in that it works for *my* input, and I strongly suspect that it would work for *your* input, because Eric Wastl probably created them all to be similar. But it may not work on every possible input allowed by the rules. " + "**Three final remarks**:\n", + "1) This was the first puzzle of the year that was **difficult**; we had to work to find an efficient solution.\n", + "2) My solution is **unsatisfying** in that it works for *my* input, and I strongly suspect that it would work for *your* input, because Eric Wastl probably created them all to be similar. But it does not work on every possible input allowed by the rules. \n", + "3) In retrospect, I could have done the standard edge-edge intersection algorithm for determining if the rectangle intersects the polygon formed by the red tiles. I was scared away from that because the geometry is tricky for general polygon intersection, when the edges can have any slope. But the geometry is actually easy when all lines are axis-aligned." ] }, { @@ -2125,7 +2151,7 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": 69, "id": "84cd63a7-4d50-4f14-9807-3cb439f80c88", "metadata": {}, "outputs": [ @@ -2174,6 +2200,8 @@ "id": "d5d91403-5dc4-4fb5-be36-58a1e45e9eab", "metadata": {}, "source": [ + "\n", + "\n", "### Part 1: What is the fewest button presses to configure the lights on all the machines?\n", "\n", "The lights are initially all off, and we want to get them to the goal configuration with the minimum number of button presses. It makes no sense to press any button twice; that just toggles lights on and off and we end up where we started. So we want to find the smallest subset of buttons that when pressed gives the goal light configuration. The function `powerset` (from the [itertools recipes](https://docs.python.org/3/library/itertools.html#itertools-recipes)) yields subsets in smallest first order, so just look for the first subset of the buttons that toggles every light the proper odd/even number of times. " @@ -2181,7 +2209,7 @@ }, { "cell_type": "code", - "execution_count": 69, + "execution_count": 70, "id": "179e62b1-e4cb-43e4-8dc7-b87a28d21e8d", "metadata": {}, "outputs": [], @@ -2202,17 +2230,17 @@ }, { "cell_type": "code", - "execution_count": 70, + "execution_count": 71, "id": "d1368c2f-d792-4353-82e7-b0161ece784f", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Puzzle 10.1: .0531 seconds, answer 441 correct" + "Puzzle 10.1: .0569 seconds, answer 441 correct" ] }, - "execution_count": 70, + "execution_count": 71, "metadata": {}, "output_type": "execute_result" } @@ -2238,7 +2266,7 @@ }, { "cell_type": "code", - "execution_count": 71, + "execution_count": 72, "id": "1d2683fa-5126-4d2a-9dad-6a0e155f3449", "metadata": {}, "outputs": [ @@ -2248,7 +2276,7 @@ "7.181818181818182" ] }, - "execution_count": 71, + "execution_count": 72, "metadata": {}, "output_type": "execute_result" } @@ -2267,7 +2295,7 @@ }, { "cell_type": "code", - "execution_count": 72, + "execution_count": 73, "id": "a714c2c2-d433-41d0-8d1e-01832b35a1a9", "metadata": {}, "outputs": [ @@ -2277,7 +2305,7 @@ "114.81498043610085" ] }, - "execution_count": 72, + "execution_count": 73, "metadata": {}, "output_type": "execute_result" } @@ -2324,7 +2352,7 @@ }, { "cell_type": "code", - "execution_count": 73, + "execution_count": 74, "id": "713a1503-ae14-445f-91ea-714fcd618ad0", "metadata": {}, "outputs": [], @@ -2354,17 +2382,17 @@ }, { "cell_type": "code", - "execution_count": 74, + "execution_count": 75, "id": "46d0d274-bd4c-44af-83e9-35c791a8e96b", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Puzzle 10.2: .1123 seconds, answer 18559 correct" + "Puzzle 10.2: .1128 seconds, answer 18559 correct" ] }, - "execution_count": 74, + "execution_count": 75, "metadata": {}, "output_type": "execute_result" } @@ -2391,7 +2419,7 @@ }, { "cell_type": "code", - "execution_count": 75, + "execution_count": 76, "id": "b9f82fef-612b-48a5-9de1-6ff0b137f7fb", "metadata": {}, "outputs": [ @@ -2401,7 +2429,7 @@ "Counter({-1: 68, 1: 65, 0: 32})" ] }, - "execution_count": 75, + "execution_count": 76, "metadata": {}, "output_type": "execute_result" } @@ -2420,7 +2448,7 @@ }, { "cell_type": "code", - "execution_count": 76, + "execution_count": 77, "id": "e536441d-64d4-410d-bb40-2343f6e01d88", "metadata": {}, "outputs": [ @@ -2430,7 +2458,7 @@ "Counter({-1: 32, -2: 31, 1: 34, 2: 31, 0: 32, -3: 5})" ] }, - "execution_count": 76, + "execution_count": 77, "metadata": {}, "output_type": "execute_result" } @@ -2457,7 +2485,7 @@ }, { "cell_type": "code", - "execution_count": 77, + "execution_count": 78, "id": "b72544c8-6069-4310-a6dc-b4acd77981b4", "metadata": {}, "outputs": [], @@ -2484,7 +2512,7 @@ }, { "cell_type": "code", - "execution_count": 78, + "execution_count": 79, "id": "11e17f6a-acba-44c4-b704-7e4ff7471e7e", "metadata": {}, "outputs": [ @@ -2537,7 +2565,7 @@ }, { "cell_type": "code", - "execution_count": 79, + "execution_count": 80, "id": "7540a982-988a-4822-af0d-6581f6f848c6", "metadata": {}, "outputs": [], @@ -2564,17 +2592,17 @@ }, { "cell_type": "code", - "execution_count": 80, + "execution_count": 81, "id": "0c2d68a5-843b-49d6-aff6-23045968207f", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Puzzle 11.1: .0009 seconds, answer 574 correct" + "Puzzle 11.1: .0003 seconds, answer 574 correct" ] }, - "execution_count": 80, + "execution_count": 81, "metadata": {}, "output_type": "execute_result" } @@ -2596,7 +2624,7 @@ }, { "cell_type": "code", - "execution_count": 81, + "execution_count": 82, "id": "0294e044-c7ef-418a-9c02-71615a453002", "metadata": {}, "outputs": [], @@ -2616,17 +2644,17 @@ }, { "cell_type": "code", - "execution_count": 82, + "execution_count": 83, "id": "677a97b3-183b-474e-87ba-7db0d6b763d8", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Puzzle 11.2: .0017 seconds, answer 306594217920240 correct" + "Puzzle 11.2: .0016 seconds, answer 306594217920240 correct" ] }, - "execution_count": 82, + "execution_count": 83, "metadata": {}, "output_type": "execute_result" } @@ -2643,12 +2671,12 @@ "source": [ "# [Day 12](https://adventofcode.com/2025/day/12): Christmas Tree Farm \n", "\n", - "On the twelfth day, we're in a cavern full of Christmas trees and the elves would like help arranging presents under the trees. The day's input is in two sections. The first section is a list of 6 shapes, each annotated with their shape number. The second section is a list of regions, which has a width and length, and a desired number of presents of each shape, in shape-number order. Each shape is a separate paragraph, but the regions are all in one paragraph, so I can parse them like this:" + "On the twelfth day, we're in a cavern full of Christmas trees and the elves would like help arranging presents under the trees. The day's input is in two sections. The first section is a list of 6 present shapes, each annotated with their shape ID. Every present shape fits in a 3x3 grid, but they are odd shapes so some shapes could be squished closer together. The second section is a list of regions, where each region has a width and length, and a desired number of presents of each shape to fit, in shape-ID order. Each shape is a separate paragraph, but the regions are all in on paragraph, so I can parse them like this:" ] }, { "cell_type": "code", - "execution_count": 83, + "execution_count": 84, "id": "a1b304e8-339e-4e32-a462-3ba88103c415", "metadata": {}, "outputs": [ @@ -2710,7 +2738,7 @@ "def parse_presents(text: str):\n", " \"\"\"Parse either a single present (e.g. \"5: ###...\") or list of regions (e.g. \"12x5: 1 0 1 0 2 2\\n...\").\"\"\"\n", " if 'x' in text:\n", - " return tuple((x, y, quantities) for (x, y, *quantities) in map(ints, text.splitlines()))\n", + " return tuple((W, L, quantities) for (W, L, *quantities) in map(ints, text.splitlines()))\n", " else:\n", " id, *shape = text.splitlines()\n", " return (int(id[:-1]), shape)\n", @@ -2730,7 +2758,7 @@ }, { "cell_type": "code", - "execution_count": 84, + "execution_count": 85, "id": "194cbece-0104-4934-b335-13a4e7c720e0", "metadata": {}, "outputs": [ @@ -2740,7 +2768,7 @@ "1000" ] }, - "execution_count": 84, + "execution_count": 85, "metadata": {}, "output_type": "execute_result" } @@ -2754,12 +2782,12 @@ "id": "f579c5f1-f44c-4f9e-b6a9-5c75dbf20cfa", "metadata": {}, "source": [ - "What's the average size of the regions?" + "What's the average area of the regions?" ] }, { "cell_type": "code", - "execution_count": 85, + "execution_count": 86, "id": "388d13ab-b5db-47e4-9f33-50f7062424b9", "metadata": {}, "outputs": [ @@ -2769,13 +2797,13 @@ "1822.223" ] }, - "execution_count": 85, + "execution_count": 86, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "mean(x * y for (x, y, _) in regions)" + "mean(W * L for (W, L, _) in regions)" ] }, { @@ -2788,7 +2816,7 @@ }, { "cell_type": "code", - "execution_count": 86, + "execution_count": 87, "id": "5d982737-cbca-4739-be1c-fa1dce319ef4", "metadata": {}, "outputs": [ @@ -2798,7 +2826,7 @@ "240.488" ] }, - "execution_count": 86, + "execution_count": 87, "metadata": {}, "output_type": "execute_result" } @@ -2812,12 +2840,12 @@ "id": "af3fe830-aa75-469b-9046-3f36ff3a03e2", "metadata": {}, "source": [ - "Next I want to get a feel for the variation in how tight the packing is. Each present can definitely fit into a 3x3 square, so it would be trivially easy if we just put down one present in each 3x3 square, without trying to make them overlap. The number of full 3x3 squares in a region is `(x // 3) * (y // 3)`, which discards any leftover 1 or 2 units of width or length. So we have:" + "Next I want to get a feel for the variation in how tight the packing is. Each present can definitely fit into a 3x3 square, so what's the ratio of the total quantity of presents to the number of 3x3 squares? I'll make a histogram of that ratio for each region:" ] }, { "cell_type": "code", - "execution_count": 87, + "execution_count": 88, "id": "b8cf45b6-5513-426a-86c2-425d0d74d781", "metadata": {}, "outputs": [ @@ -2833,10 +2861,12 @@ } ], "source": [ - "def squares(x, y) -> int: \"Number of full 3x3 squares in a region.\"; return (x // 3) * (y // 3)\n", + "def squares(width, length) -> int: \n", + " \"Number of full 3x3 squares in a region.\"; \n", + " return (width // 3) * (length // 3)\n", " \n", - "occupancy_ratios = [sum(quantities) / squares(x, y) \n", - " for (x, y, quantities) in regions]\n", + "occupancy_ratios = [sum(quantities) / squares(W, L) \n", + " for (W, L, quantities) in regions]\n", "\n", "plt.hist(occupancy_ratios, bins=100);" ] @@ -2846,24 +2876,28 @@ "id": "54f2eaa0-b8b6-4a60-bcde-28eeefa26e8c", "metadata": {}, "source": [ - "**Very interesting!** There's a real split. A lot of regions have an occupabncy ratio below 1.0 and thus are trivially easy to fit, and the rest of the regions with an occupancy ratio of around 1.35 or more look like they are impossible to fit. I can do triage on the regions to classify each one: " + "**Very interesting!** There's a real split. A lot of regions have an occupabncy ratio below 1.0 and thus are trivially easy to fit into the region, and the rest of the regions with occupancy ratios above 1.35 may well be impossible to fit. I say that because, just looking at the shapes, I estimate that the most you could overlap a shape onto another would be to save two \".\" squares; , so I could see getting to an occupancy rato of 1 + 2/9 = 1.22, but I don't think it is possible to get to 1.35. I can prove it is impossible to fit all the presents in a region if the total area of the solid parts of the presents (the '#' squares) is more than the area of the region (the width times length). \n", + "\n", + "I can do triage on the regions to classify each one as a trivial fit, an impossible fit, or uncertain:" ] }, { "cell_type": "code", - "execution_count": 88, + "execution_count": 89, "id": "58b879f2-19ad-4e3f-a804-8c22151e5865", "metadata": {}, "outputs": [], "source": [ - "def triage(region, shape_area=[cat(s).count('#') for s in shapes]) -> str:\n", + "shape_area = [cat(shape).count('#') for shape in shapes] # Total number of '#' in each shape\n", + "\n", + "def triage(region) -> str:\n", " \"\"\"Decide if a region's presents trivially fit, or are impossible to fit, or it is uncertain.\"\"\"\n", - " x, y, quantities = region\n", + " width, length, quantities = region\n", " presents_area = sum(q * shape_area[i] for (i, q) in enumerate(quantities))\n", - " if sum(quantities) <= squares(x, y):\n", + " if sum(quantities) <= squares(width, length):\n", " return 'fit' # The number of presents is no more than the number of 3x3 squares\n", - " elif presents_area > x * y:\n", - " return 'impossible' # The area of all the presents is greater than the area of the region\n", + " elif presents_area > width * length:\n", + " return 'impossible' # The '#' area of all the presents is greater than the area of the region\n", " else:\n", " return 'uncertain' # We would need to do a search to see if the presents fit" ] @@ -2878,7 +2912,7 @@ }, { "cell_type": "code", - "execution_count": 89, + "execution_count": 90, "id": "6768fa83-2af7-4aab-a930-9106da3859bd", "metadata": {}, "outputs": [ @@ -2888,7 +2922,7 @@ "Counter({'impossible': 546, 'fit': 454})" ] }, - "execution_count": 89, + "execution_count": 90, "metadata": {}, "output_type": "execute_result" } @@ -2902,29 +2936,29 @@ "id": "cbedea21-3bed-4c99-9b57-fc1f1e03f76b", "metadata": {}, "source": [ - "**There are no uncertain regions!** The problem is solved, and I didn't have to rotate a single present!" + "**There are no uncertain regions!** The problem is solved, and I didn't have to rotate a single present! Sometimes the real treasure is the code you don't have to write along the way." ] }, { "cell_type": "code", - "execution_count": 90, + "execution_count": 91, "id": "0ec553c4-85eb-40a6-8bed-7adddf75e512", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Puzzle 12.1: .0019 seconds, answer 454 correct" + "Puzzle 12.1: .0018 seconds, answer 454 correct" ] }, - "execution_count": 90, + "execution_count": 91, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(12.1, 454, lambda:\n", - " Counter(map(triage, regions))['fit'])" + " quantify(triage(region) == 'fit' for region in regions))" ] }, { @@ -2939,7 +2973,7 @@ }, { "cell_type": "code", - "execution_count": 91, + "execution_count": 92, "id": "4d512a50-c6ae-4803-a787-b8f6e0103e31", "metadata": {}, "outputs": [ @@ -2949,33 +2983,41 @@ "text": [ "Puzzle 1.1: .0005 seconds, answer 1182 correct\n", "Puzzle 1.2: .0010 seconds, answer 6907 correct\n", - "Puzzle 2.1: .0029 seconds, answer 23560874270 correct\n", + "Puzzle 2.1: .0027 seconds, answer 23560874270 correct\n", "Puzzle 2.2: .0038 seconds, answer 44143124633 correct\n", - "Puzzle 3.1: .0006 seconds, answer 17085 correct\n", - "Puzzle 3.2: .0024 seconds, answer 169408143086082 correct\n", - "Puzzle 4.1: .0553 seconds, answer 1569 correct\n", - "Puzzle 4.2: .1394 seconds, answer 9280 correct\n", + "Puzzle 3.1: .0007 seconds, answer 17085 correct\n", + "Puzzle 3.2: .0021 seconds, answer 169408143086082 correct\n", + "Puzzle 4.1: .0572 seconds, answer 1569 correct\n", + "Puzzle 4.2: .1461 seconds, answer 9280 correct\n", "Puzzle 5.1: .0123 seconds, answer 635 correct\n", "Puzzle 5.2: .0002 seconds, answer 369761800782619 correct\n", - "Puzzle 6.1: .0022 seconds, answer 5877594983578 correct\n", - "Puzzle 6.2: .0063 seconds, answer 11159825706149 correct\n", - "Puzzle 7.1: .0010 seconds, answer 1681 correct\n", - "Puzzle 7.2: .0021 seconds, answer 422102272495018 correct\n", - "Puzzle 8.1: .5834 seconds, answer 24360 correct\n", - "Puzzle 8.2: .6182 seconds, answer 2185817796 correct\n", - "Puzzle 9.1: .0370 seconds, answer 4772103936 correct\n", - "Puzzle 9.2: .0164 seconds, answer 1529675217 correct\n", - "Puzzle 10.1: .0531 seconds, answer 441 correct\n", - "Puzzle 10.2: .1123 seconds, answer 18559 correct\n", - "Puzzle 11.1: .0009 seconds, answer 574 correct\n", - "Puzzle 11.2: .0017 seconds, answer 306594217920240 correct\n", - "Puzzle 12.1: .0019 seconds, answer 454 correct\n", + "Puzzle 6.1: .0025 seconds, answer 5877594983578 correct\n", + "Puzzle 6.2: .0064 seconds, answer 11159825706149 correct\n", + "Puzzle 7.1: .0007 seconds, answer 1681 correct\n", + "Puzzle 7.2: .0014 seconds, answer 422102272495018 correct\n", + "Puzzle 8.1: .6336 seconds, answer 24360 correct\n", + "Puzzle 8.2: .6273 seconds, answer 2185817796 correct\n", + "Puzzle 9.1: .0272 seconds, answer 4772103936 correct\n", + "Puzzle 9.2: .0156 seconds, answer 1529675217 correct\n", + "Puzzle 10.1: .0569 seconds, answer 441 correct\n", + "Puzzle 10.2: .1128 seconds, answer 18559 correct\n", + "Puzzle 11.1: .0003 seconds, answer 574 correct\n", + "Puzzle 11.2: .0016 seconds, answer 306594217920240 correct\n", + "Puzzle 12.1: .0018 seconds, answer 454 correct\n", "\n", - "Time in seconds: sum = 1.655, mean = .072, median = .003, max = .618\n" + "Time in seconds: sum = 1.715, mean = .075, median = .003, max = .634\n" ] } ], "source": [ + "def summary(answers: dict):\n", + " \"\"\"Summary report on the answers.\"\"\"\n", + " for day in sorted(answers):\n", + " print(answers[day])\n", + " times = [answer.secs for answer in answers.values()]\n", + " def stat(fn, times): return f'{fn.__name__} = {fn(times):.3f}'\n", + " print('\\nTime in seconds:', ', '.join(_zap0(stat(fn, times)) for fn in (sum, mean, median, max)))\n", + " \n", "summary(answers)" ] }, @@ -2984,7 +3026,14 @@ "id": "1098c017-2746-403c-bfb0-1a08cacc835d", "metadata": {}, "source": [ - "I got them all done in under 2 seconds of run time. Happy Advent everyone, and thank you Eric for the interesting puzzles!" + "I solved all the puzzles and they run in under 2 seconds of total run time. Happy Advent everyone, and thank you Eric for the interesting puzzles!\n", + "\n", + "

\n", + "\n", + "

\n", + "\n", + "\"Gary\n", + "Gary Grady @GaryGrady\n" ] } ],