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