\n",
"\n",
"# Advent of Code 2024\n",
"\n",
"I enjoy doing the [**Advent of Code**](https://adventofcode.com/) (AoC) programming puzzles, so here we go for 2024! Our old friend [@GaryJGrady](https://x.com/garyjgrady) is here to provide illustrations:\n",
"\n",
"\n",
"\n",
"I traditionally start by loading up my [**AdventUtils.ipynb**](AdventUtils.ipynb) notebook (same as last time except for the `current_year`):"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "ed82ed5b-a42d-468b-8f6e-288d3c2de20b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"3.12.7 | packaged by Anaconda, Inc. | (main, Oct 4 2024, 08:22:19) [Clang 14.0.6 ]\n"
]
}
],
"source": [
"%run AdventUtils.ipynb\n",
"current_year = 2024\n",
"\n",
"print(sys.version)"
]
},
{
"cell_type": "markdown",
"id": "dfecffd7-6955-45ba-9dc2-1ec805baba85",
"metadata": {},
"source": [
"Each day's solution consists of three parts, making use of my `parse` and `answer` utilities:\n",
"- **Reading the day's input**. E.g. `pairs = parse(1, ints)`. \n",
"- **Solving Part One**. Find the solution and record it with, e.g., `answer(1.1, 4, lambda: 2 + 2)`.\n",
"- **Solving Part Two**. Find the solution and record it with, e.g., `answer(1.2, 9, lambda: 3 * 3)`.\n",
"\n",
"The function `parse` assumes that the input is a sequence of records (default one per line), each of which should be parsed in some way (default just left as a string, but the argument `ints` says to treat each record as a tuple of integers). The function `answer` records the correct answer (for regression testing), and records the run time (that's why a `lambda:` is used).\n",
"\n",
"To fully understand each day's puzzle, and to follow along the drama involving Santa, the elves, the reindeer, the elephants, the Chief Historian, and all the rest, you need to read the descriptions on the [**AoC**](https://adventofcode.com/) site, as linked in the header for each of my day's solutions, e.g. [**Day 1**](https://adventofcode.com/2023/day/1) below. I can't copy the content of AoC here, nor show my input files; you need to go to the site for that."
]
},
{
"cell_type": "markdown",
"id": "4c6120a1-3129-44ff-935c-30c1d81ae028",
"metadata": {},
"source": [
"# [Day 1](https://adventofcode.com/2024/day/1) Historian Hysteria\n",
"\n",
"According to the narrative, North Pole Historians created two lists of **location IDs**. We can parse them as a sequence of pairs of integers, and then use the transpose function, `T`, from my [AdventUtils](AdventUtils.ipynb) to get two lists of ID numbers:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "22e5d621-a152-4712-866f-f8b962b5dd14",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 1000 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"38665 13337\n",
"84587 21418\n",
"93374 50722\n",
"68298 57474\n",
"54771 18244\n",
"49242 83955\n",
"66490 44116\n",
"65908 51323\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 1000 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"(38665, 13337)\n",
"(84587, 21418)\n",
"(93374, 50722)\n",
"(68298, 57474)\n",
"(54771, 18244)\n",
"(49242, 83955)\n",
"(66490, 44116)\n",
"(65908, 51323)\n",
"...\n"
]
}
],
"source": [
"left, right = location_ids = T(parse(1, ints))"
]
},
{
"cell_type": "markdown",
"id": "63cf2940-e251-49e4-8bc9-f1bcd599f8f4",
"metadata": {},
"source": [
"\n",
"\n",
"### Part 1: What is the total distance between your lists?\n",
"\n",
"The **distance** between two numbers is the absolute value of their difference, and the **total distance** between two lists is the sum of the distances between respective pairs, where \"respective\" means to sort each list and then take the distance between the first element of each list, plus the distance between the second element of each list, and so on. (I use the transpose utility function, `T`, to turn the input sequence of 1000 pairs into two lists, each of 1000 integers.)"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "8d6bc9f5-5fa1-4dad-bd43-d957833d8ea9",
"metadata": {},
"outputs": [],
"source": [
"def total_distance(left: Ints, right: Ints) -> int:\n",
" \"\"\"Total distance between respective list elements, after sorting.\"\"\"\n",
" return sum(abs(a - b) for a, b in zip(sorted(left), sorted(right)))"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "6ada5e5b-2fb7-4198-a5bb-7b2af4b9270a",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 1.1: .000 seconds, answer 1830467 ok"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(1.1, 1830467, lambda:\n",
" total_distance(left, right))"
]
},
{
"cell_type": "markdown",
"id": "88e26234-f1d2-4a62-86b0-2ad9251215eb",
"metadata": {},
"source": [
"### Part 2: What is their similarity score?\n",
"\n",
"The **similarity score** is the sum of each element of the left list times the number of times that value appears in the right list."
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "0131e096-38d1-4c13-9e9c-b0d09839a5cf",
"metadata": {},
"outputs": [],
"source": [
"def similarity_score(left: Ints, right: Ints) -> int:\n",
" \"\"\"The sum of each x in `left` times the number of times x appears in `right`.\"\"\"\n",
" counts = Counter(right)\n",
" return sum(x * counts[x] for x in left)"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "6f6c298a-53a1-4d80-8747-7dd713d4d4f0",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 1.2: .000 seconds, answer 26674158 ok"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(1.2, 26674158, lambda:\n",
" similarity_score(left, right))"
]
},
{
"cell_type": "markdown",
"id": "b9fa4fe0-4194-47d7-b815-b571af98caee",
"metadata": {},
"source": [
"# [Day 2](https://adventofcode.com/2024/day/2): Red-Nosed Reports\n",
"\n",
"Today's input is a sequence of **reports**, each of which is a sequence of integers:"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "10e1ab83-a6ec-4143-ad9a-eaae220adcde",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 1000 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"74 76 78 79 76\n",
"38 40 43 44 44\n",
"1 2 4 6 8 9 13\n",
"65 68 70 72 75 76 81\n",
"89 91 92 95 93 94\n",
"15 17 16 18 19 17\n",
"46 47 45 48 51 52 52\n",
"77 78 79 82 79 83\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 1000 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"(74, 76, 78, 79, 76)\n",
"(38, 40, 43, 44, 44)\n",
"(1, 2, 4, 6, 8, 9, 13)\n",
"(65, 68, 70, 72, 75, 76, 81)\n",
"(89, 91, 92, 95, 93, 94)\n",
"(15, 17, 16, 18, 19, 17)\n",
"(46, 47, 45, 48, 51, 52, 52)\n",
"(77, 78, 79, 82, 79, 83)\n",
"...\n"
]
}
],
"source": [
"reports = parse(2, ints)"
]
},
{
"cell_type": "markdown",
"id": "5dfd72c2-06c6-4c71-ae37-0c2c84074091",
"metadata": {},
"source": [
"### Part 1: How many reports are safe?\n",
"\n",
"A report is **safe** if it is monotonically strictly increasing or strictly decreasing, and if no difference between adjacent numbers is greater than 3 in absolute value."
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "368cbe1c-b6b6-4a82-bef9-599ee9725899",
"metadata": {},
"outputs": [],
"source": [
"def is_safe(report: Ints) -> bool:\n",
" \"\"\"A report is safe if all differences are either in {1, 2, 3} or in {-1, -2, -3}.\"\"\"\n",
" deltas = diffs(report)\n",
" return deltas.issubset({1, 2, 3}) or deltas.issubset({-1, -2, -3})\n",
" \n",
"def diffs(report: Ints) -> set:\n",
" \"\"\"The set of differences between adjacent numbers in the report.\"\"\"\n",
" return {report[i] - report[i - 1] for i in range(1, len(report))}\n",
"\n",
"assert diffs((7, 6, 4, 2, 1)) == {-1, -2}\n",
"assert is_safe((7, 6, 4, 2, 1)) == True"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "e662bf10-4d6a-40f1-95ce-dfc39f5b3fc2",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 2.1: .000 seconds, answer 257 ok"
]
},
"execution_count": 16,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(2.1, 257, lambda:\n",
" quantify(reports, is_safe))"
]
},
{
"cell_type": "markdown",
"id": "ee48bf63-8a67-407b-9a73-df097811eabc",
"metadata": {},
"source": [
"### Part 2: How many reports are safe using the Problem Dampener?\n",
"\n",
"The **problem dampener** says that a report is safe if you can drop one element and get a safe report."
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "67ba1d53-95b7-4811-b225-2ff15d6bdc5c",
"metadata": {},
"outputs": [],
"source": [
"def is_safe_with_dampener(report: Ints) -> bool:\n",
" \"\"\"Is there any way to drop one element of `report` to get a safe report?\"\"\"\n",
" return any(map(is_safe, drop_one(report)))\n",
"\n",
"def drop_one(report) -> Iterable:\n",
" \"\"\"All ways of dropping one element of the input report.\"\"\"\n",
" return (report[:i] + report[i + 1:] for i in range(len(report)))\n",
"\n",
"assert set(drop_one('1234')) == {'234', '134', '124', '123'}"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "d1b9ffb5-af7a-465f-a063-c31df2d0605c",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 2.2: .002 seconds, answer 328 ok"
]
},
"execution_count": 19,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(2.2, 328, lambda:\n",
" quantify(reports, is_safe_with_dampener))"
]
},
{
"cell_type": "markdown",
"id": "54d6a0c2-a8ed-404d-abc0-72aa28a49f5d",
"metadata": {},
"source": [
"# [Day 3](https://adventofcode.com/2024/day/3): Mull It Over\n",
"\n",
"Today's input is a computer program with some corrupted characters. The program has multiple lines, but lines don't matter, so I will concatenate them into one big string:"
]
},
{
"cell_type": "code",
"execution_count": 21,
"id": "78080200-0f9f-4492-9bee-c936737ee96f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 6 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"where(536,162)~'what()what()how(220,399){ mul(5,253);mul(757,101)$where()@why()who()&when()from( ...\n",
"}?~who()select()-mul(316,505)&%*how()mul(363,589)>?%-:)where()~{{mul(38,452)select()%>[{]%>%mul( ...\n",
"?>where(911,272)'mul(894,309)~+%@#}@#why()mul(330,296)what()mul(707,884)mul;&}<{>where()$why()]m ...\n",
"> (when()[where()/#!/usr/bin/perl,@;mul(794,217)select():'])select()mul(801,192)why()&]why()/:]* ...\n",
",+who():mul(327,845)/ >@[>@}}mul(86,371)!~&&~how(79,334)mul(637,103)why()mul(358,845)-#~?why(243 ...\n",
"where()#{*,!?:$mul(204,279)what()!{ what()mul(117,94)!select()>:mul(665,432)#don't()!!\n",
"\n",
"\n",
"### Part 1: What do you get if you add up all of the results of the multiplications?\n",
"\n",
"For Part 1, just look for instructions of the form \"mul(*digits*,*digits*)\", using a regular expression and `re.findall`. Perform each of these multiplications and add them up, and ignore all other characters/instructions:"
]
},
{
"cell_type": "code",
"execution_count": 23,
"id": "bf6366b1-6952-47d8-8b3c-09f8d05ec093",
"metadata": {},
"outputs": [],
"source": [
"def execute(program: str) -> int:\n",
" \"\"\"The sum of the results of the multiply instructions.\"\"\"\n",
" return sum(prod(ints(m)) for m in re.findall(multiplications, program))\n",
"\n",
"multiplications = r'mul\\(\\d+,\\d+\\)'"
]
},
{
"cell_type": "code",
"execution_count": 24,
"id": "2032c903-5f23-4c16-ba68-410b6c1750e1",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 3.1: .001 seconds, answer 156388521 ok"
]
},
"execution_count": 24,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(3.1, 156388521, lambda: \n",
" execute(program))"
]
},
{
"cell_type": "markdown",
"id": "622d7010-145e-422a-a592-d4b446afcc0f",
"metadata": {},
"source": [
"### Part 2: What do you get if you add up all of the results of just the enabled multiplications?\n",
"\n",
"For Part 2, the instruction \"don't()\" says to disable (ignore) following multiply instructions until a \"do()\" instruction enables them again. I will define the function `enabled`, which returns the part of the program that is enabled, by susbstituting a space for the \"don't()...do()\" sequence."
]
},
{
"cell_type": "code",
"execution_count": 26,
"id": "4525d01a-bac0-41c2-92b8-baf0fd395e88",
"metadata": {},
"outputs": [],
"source": [
"def enabled(program: str) -> str:\n",
" \"\"\"Just the part of the program that is enabled; remove \"don't()...do()\" text.\"\"\"\n",
" return re.sub(r\"don't\\(\\).*?(do\\(\\)|$)\", \" \", program)"
]
},
{
"cell_type": "code",
"execution_count": 27,
"id": "ce40f258-ca76-48c3-9965-27a6979a4243",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 3.2: .000 seconds, answer 75920122 ok"
]
},
"execution_count": 27,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(3.2, 75920122, lambda:\n",
" execute(enabled(program)))"
]
},
{
"cell_type": "markdown",
"id": "e1448343-6488-45ad-b03d-d7928feb75cd",
"metadata": {},
"source": [
"# [Day 4](https://adventofcode.com/2024/day/4): Ceres Search\n",
"\n",
"Today's puzzle is a 2D word-search puzzle:"
]
},
{
"cell_type": "code",
"execution_count": 29,
"id": "a0d903b9-018e-4861-9314-cafed59055fd",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 140 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"MASAMXMSSXXMAMXXMXMASXMASXMMSMSMMMAXMASASMMSSMSXAXMASMMSMMMSSMSASMSSSSMSMSMXXMXMAXAMXMSMSSXSAMXM ...\n",
"MASMMXMASAXASMSMMMSAMXSMSAMXAAAAAXAMXASXAMAAAAMMSMMMMMASXAAAAMMAMAMMASAAAAXMXMSSSSSSMMSAMAXAXXSM ...\n",
"MMXAXMMMSXMAMAAXAAXAAAXXSMMSMSMSMXAXMXSMMMMSSMXAMXAAXMAMMMMSSMMAMAMMAMMMMMXSAAXAAMMAXXSAMXMSMAXM ...\n",
"SXSAMASASMSXMSMSMSSMMMMMMXAMXMMXMASMMMMAXXAAAMMMSSSSSMASXXAAXASMSXXMXSXSXSASMSMMSMSAMMMAMXAAMASX ...\n",
"AAAXXXMASASXMXMAXXMMASAASMXSASASXAAAAMSSMMMSXMAAMMMMMXAXMMMMSAMXAMASAMXSAMASXXAXAAMAMXSAMXSXSMMA ...\n",
"MSMMXXMMMAMAMMMMMMXSAXXAMMMMXSAXMMXXAMXAAMMXMASXMAAASMMXAAMXAXAMMMAMAMAMAMXMASXMMXMAAXMAXMAMXMSA ...\n",
"MXAXAMXXMMMMSAMAASMMMSMMASASAMAMAXMSXMSMMXAMXAXMMSSXSASXSSSMAMSMXMXSAMSSSMAMXMXAMAXXMMSAXAXMMXMA ...\n",
"ASXMMXSAMXAASXXMXSAAAXASAMMMASMSSSMAAMMXMMSSMASAMAMMMAMMAXMAXMASXMAXMSAAASASAMXSSMXSAAXSSMXAAXXA ...\n",
"...\n"
]
}
],
"source": [
"xmas_grid = Grid(parse(4))"
]
},
{
"cell_type": "markdown",
"id": "16d56872-be2c-4e9d-8821-a0fe9f66970b",
"metadata": {},
"source": [
"### Part 1: How many times does XMAS appear?\n",
"\n",
"We just have to find how many times the word \"XMAS\" appears in the grid, horizontally, vertically, or diagonally, forwards or backwards. The variable `directions8` contains those eight directions (as (delta-x, delta-y) pairs). So examine each square of the grid and if it contains \"X\", see in how many of the directions it spells \"XMAS\". (Note that locations in the grid are denoted by `(x, y)` coordinates, as are directions (e.g., `(1, 0)` is the `East` direction. The functions `add` and `mul` do addition and scalar multiplication on these vectors.)"
]
},
{
"cell_type": "code",
"execution_count": 31,
"id": "72d48abb-7a82-452f-b91d-838b3836a90f",
"metadata": {},
"outputs": [],
"source": [
"def word_search(grid: Grid, word='XMAS') -> int:\n",
" \"\"\"How many times does the given word appear in the grid?\"\"\"\n",
" return quantify(grid_can_spell(grid, start, dir, word) \n",
" for start in grid \n",
" if grid[start] == word[0]\n",
" for dir in directions8)\n",
"\n",
"def grid_can_spell(grid, start, dir, word):\n",
" \"\"\"Does `word` appear in grid starting at `start` and going in direction `dir`?\"\"\"\n",
" return all(grid[add2(start, mul(dir, i))] == word[i] for i in range(len(word)))"
]
},
{
"cell_type": "code",
"execution_count": 32,
"id": "6175362b-d8b4-45d1-b70c-d8575a0fe188",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 4.1: .033 seconds, answer 2401 ok"
]
},
"execution_count": 32,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(4.1, 2401, lambda:\n",
" word_search(xmas_grid))"
]
},
{
"cell_type": "markdown",
"id": "eabe90c4-b668-4d9e-a345-b09f4b8ee42b",
"metadata": {},
"source": [
"### Part 1: How many times does an X-MAS appear?\n",
"\n",
"Upon further review, the goal is not to find \"XMAS\" byt rather X-\"MAS\"; that is, two \"MAS\" words in an X pattern. The pattern can be any of these four:\n",
"\n",
" M.S S.M M.M S.S\n",
" .A. .A. .A. .A.\n",
" M.S S.M S.S M.M\n",
"\n",
"I decided to find these by first looking for each the middle letter (\"A\") in the grid, and then, for each \"A\" and for each of the four pairs of diagonal directions, see if the target word (\"MAS\") can be spelled in both directions:"
]
},
{
"cell_type": "code",
"execution_count": 34,
"id": "3d8a051f-cf7b-4e8c-b0fb-78c3f089989d",
"metadata": {},
"outputs": [],
"source": [
"diagonal_pairs = ([SE, NE], [SW, NW], [SE, SW], [NE, NW])\n",
"\n",
"def x_search(grid: Grid, word='MAS') -> int:\n",
" \"\"\"How many times does an X-MAS appear in the grid?\"\"\"\n",
" return quantify((grid_can_spell(grid, sub(mid_pos, dir1), dir1, word) and\n",
" grid_can_spell(grid, sub(mid_pos, dir2), dir2, word))\n",
" for mid_pos in grid if grid[mid_pos] == word[1]\n",
" for dir1, dir2 in diagonal_pairs)"
]
},
{
"cell_type": "code",
"execution_count": 35,
"id": "64cde1d9-f58c-4633-b5da-87908a02f76d",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 4.2: .027 seconds, answer 1822 ok"
]
},
"execution_count": 35,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(4.2, 1822, lambda:\n",
" x_search(xmas_grid))"
]
},
{
"cell_type": "markdown",
"id": "0249ce80-e649-44b3-8c02-613fc7652110",
"metadata": {},
"source": [
"# [Day 5](https://adventofcode.com/2024/day/5): Print Queue\n",
"\n",
"Today's puzzle involves a **sleigh launch safety manual** that needs to be updated. The day's input is in two sections: the first a set of **rules** such as \"47|53\", which means that page 47 must be printed before page 53; and the second a list of **updates** of the form \"75,47,61,53,29\", meaning that those pages are to be printed in that order.\n",
"\n",
"\n",
"\n",
"I mostly like my `parse` function, but I admit it is not ideal when an input file has two sections like this. I'll parse the two sections as paragraphs, and then call `parse` again on each paragraph:"
]
},
{
"cell_type": "code",
"execution_count": 37,
"id": "b77a5a1f-a43b-4ce8-a60c-94d69a595505",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 1366 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"48|39\n",
"39|84\n",
"39|23\n",
"95|51\n",
"95|76\n",
"95|61\n",
"14|52\n",
"14|49\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 2 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"48|39\n",
"39|84\n",
"39|23\n",
"95|51\n",
"95|76\n",
"95|61\n",
"14|52\n",
"14|49\n",
"14|39\n",
"14|53\n",
"85|19\n",
"85|25\n",
"85|61\n",
"85|35\n",
"85|58\n",
"74|86\n",
" ...\n",
"61,58,51,32,12,14,71\n",
"58,25,54,14,12,94,32,76,39\n",
"35,53,26,77,14,71,25,76,85,55,51,49,95\n",
"32,91,76, ...\n"
]
}
],
"source": [
"manual = parse(5, sections=paragraphs)\n",
"rules = set(parse(manual[0], ints))\n",
"updates = parse(manual[1], ints)"
]
},
{
"cell_type": "code",
"execution_count": 38,
"id": "4c85a23e-686a-4129-a14c-ff6f6a88b9ac",
"metadata": {},
"outputs": [],
"source": [
"assert (48, 39) in rules # `rules` is a set of (earlier, later) page number pairs\n",
"assert updates[0] == (61, 58, 51, 32, 12, 14, 71) # `updates` is a sequence of page number tuples"
]
},
{
"cell_type": "markdown",
"id": "d6b6d374-cbe9-4b84-a1dd-d9df927c7182",
"metadata": {},
"source": [
"### Part 1: What do you get if you add up the middle page number from the correctly-ordered updates?\n",
"\n",
"I'll define `is_correct` to determine if an update is in the correct order, and `sum_of_correct_middles` to add up the middle numbers of the correct updates:"
]
},
{
"cell_type": "code",
"execution_count": 40,
"id": "78898d37-46ff-4367-9d89-b2a107a90aa1",
"metadata": {},
"outputs": [],
"source": [
"def sum_of_correct_middles(rules: Set[Ints], updates: Tuple[Ints]) -> int:\n",
" \"\"\"The sum of the middle elements of each update that is correct.\"\"\"\n",
" return sum(middle(update) for update in updates if is_correct(update, rules))\n",
"\n",
"def is_correct(update: Ints, rules: Set[Ints]) -> bool:\n",
" \"\"\"An update is correct if no pair of pages violates a rule in the rules set.\"\"\"\n",
" return not any((second, first) in rules for (first, second) in combinations(update, 2))\n",
"\n",
"def middle(sequence) -> object: return sequence[len(sequence) // 2]"
]
},
{
"cell_type": "code",
"execution_count": 41,
"id": "b1c87359-1d2d-4a90-8305-9d152ce5d547",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 5.1: .001 seconds, answer 5762 ok"
]
},
"execution_count": 41,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(5.1, 5762, lambda:\n",
" sum_of_correct_middles(rules, updates))"
]
},
{
"cell_type": "markdown",
"id": "80da4fd9-b11e-4dbb-8d22-2071d1a89827",
"metadata": {},
"source": [
"### Part 2: What do you get if you add up the middle page numbers after correctly re-ordering the incorrect updates?\n",
"\n",
"In Part 2 we have to find the incorrect updates, re-order them into a correct order, and again sum the middle page numbers.\n",
"Since I have already defined `is_correct`, i could just generate all permutations of each update and find one that `is_correct`. That would work great if the longest update consists of only 5 pages, as it does in the example input. But what is the longest update in the actual input?"
]
},
{
"cell_type": "code",
"execution_count": 43,
"id": "d8718c3e-0b3b-49ce-8cca-abd82aa788d7",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"23"
]
},
"execution_count": 43,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"max(map(len, updates))"
]
},
{
"cell_type": "markdown",
"id": "4449200f-dd19-48f1-94b2-7304daa9fa00",
"metadata": {},
"source": [
"That's not great. With 23 numbers there are 23! permutations, which is over 25 sextillion. So instead, here's my strategy:\n",
"\n",
"- `sum_of_corrected_middles` will find the incorrect rules, perform a correction on each, and sum the middle numbers.\n",
"- `correction` will sort an update, obeying the rules. It used to be that Python's `sort` method allowed a `cmp` keyword to compare two values; there is vestigial support for this with the `functools.cmp_to_key` function. I will **sort** each update so that page *m* comes before page *n* if (*m*, *n*) is in the rules.\n",
"- Sorting will be BOUT a sextillion times faster than enumerating permutations."
]
},
{
"cell_type": "code",
"execution_count": 45,
"id": "7222dc1c-067f-4bb5-84e1-3c2fc72fd53a",
"metadata": {},
"outputs": [],
"source": [
"def sum_of_corrected_middles(rules, updates) -> int:\n",
" \"\"\"The sum of the middle elements of each update that is correct.\"\"\"\n",
" incorrect = [update for update in updates if not is_correct(update, rules)]\n",
" corrected = [correction(update, rules) for update in incorrect]\n",
" return sum(map(middle, corrected))\n",
" \n",
"def correction(update: Ints, rules) -> Ints:\n",
" \"\"\"Reorder the update to make it correct.\"\"\"\n",
" def rule_lookup(m, n): return +1 if (m, n) in rules else -1 \n",
" return sorted(update, key=functools.cmp_to_key(rule_lookup))"
]
},
{
"cell_type": "code",
"execution_count": 46,
"id": "dc1fbda9-2cfd-442a-afef-12c9b0d2b17f",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 5.2: .001 seconds, answer 4130 ok"
]
},
"execution_count": 46,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(5.2, 4130, lambda:\n",
" sum_of_corrected_middles(rules, updates))"
]
},
{
"cell_type": "markdown",
"id": "53b1ccbc-01ae-43d0-a75f-3f9389fdd3c9",
"metadata": {},
"source": [
"I have to say, I'm pleased that this day I got both parts right with no errors (and in fact, the same for the previous days). I was worried I might have my `+1` and `-1` backwards in `cmp_to_key`, but so far, everything has gone smoothly. (However, even if I started solving the second the puzzles are released (which I don't), I wouldn't show up on the leaderboard; I'm still *way* slower than the skilled contest programmers."
]
},
{
"cell_type": "markdown",
"id": "38258423-e3b8-4bae-8aeb-28f07f0d5a35",
"metadata": {},
"source": [
"# [Day 6](https://adventofcode.com/2024/day/6): Guard Gallivant\n",
"\n",
"Today's input is a 2D map of the manufacturing lab, with \"`.`\" indicating an empty space, \"`#`\" indicating an obstruction, and \"`^`\" indicating the position of the security guard."
]
},
{
"cell_type": "code",
"execution_count": 49,
"id": "6ec71cf8-c43d-457e-8e14-0e9eb99b956a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 130 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"........#........................................#......#........#.............................. ...\n",
"....................................#......#.....#............#.............#..........#........ ...\n",
"......................#.......................................................#................. ...\n",
".......#..#..#....#...#...#....#..............#......#.......#...#................#.......#..... ...\n",
"......................#....##...#.......#....#.......................................#.......... ...\n",
"...#............................#........................................#...................... ...\n",
"....................#............#...............#......#.........#...........#................. ...\n",
"............................#......#...#................#.............#......................... ...\n",
"...\n"
]
}
],
"source": [
"lab_grid = Grid(parse(6))"
]
},
{
"cell_type": "markdown",
"id": "4ba233f4-90aa-4249-9569-10288c34940d",
"metadata": {},
"source": [
"### Part 1: How many distinct positions will the guard visit before leaving the mapped area?\n",
"\n",
"The guard follows this protocol: If there is something directly in front of you, turn right 90 degrees.\n",
"Otherwise, take a step forward.\n",
"\n",
"I'll define `follow_path` to output a list of all the positions the guard occupies. I realize the puzzle is only asking for a count of the positions, but the path might be useful for Part 2, or for debugging, so I'll return it. I worried that it is also possible for a path to become a loop, but the problem statement says that can't happen, so I won't test for it."
]
},
{
"cell_type": "code",
"execution_count": 51,
"id": "95f0b409-a6d6-47bc-8ce5-1c2df80f2b18",
"metadata": {},
"outputs": [],
"source": [
"def follow_path(grid: Grid, guard='^', facing=North) -> List[Point]:\n",
" \"\"\"A list of all points in the path followed by the guard.\"\"\"\n",
" path = grid.findall(guard) # A one-element list of positions, e.g. [(3, 4)]\n",
" while (ahead := add2(path[-1], facing)) in grid:\n",
" if grid[ahead] == '#':\n",
" facing = make_turn(facing, 'R')\n",
" else:\n",
" path.append(ahead)\n",
" return path"
]
},
{
"cell_type": "code",
"execution_count": 52,
"id": "f4be3d1f-7f24-4d55-8221-df0026178e1e",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 6.1: .002 seconds, answer 5329 ok"
]
},
"execution_count": 52,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(6.1, 5329, lambda: \n",
" len(set(follow_path(lab_grid))))"
]
},
{
"cell_type": "markdown",
"id": "eaf72ac3-ade0-4479-a090-1d0f292ecc27",
"metadata": {},
"source": [
"I initially had a **bug**; I asked for the length of the path, not the length of the **set** of positions in the path.\n",
" \n",
"### Part 2: How many different positions could you choose for an obstruction to put the guard in a loop?\n",
"\n",
"The historians would like to place a single obstacle so that the guard *will* get stuck in a loop, rather than exiting the grid. They want to know all possible positions for the obstacle. What do we know about such positions?\n",
"- An obstacle position must be somewhere on the guard's path, otherwise it would have no effect.\n",
"- The instructions say it can't be the guard's initial position.\n",
"- A loop is when the guard's path returns to the same position with the same facing. This suggests that my Part 1 solution was not completely helpful: to find duplicate positions in the path I would need a set of position/facing pairs, not just positions.\n",
"- I can make slightly less work by only storing the corners of the path: the places where the guard turns. \n",
"- The simplest approach for finding obstacle positions is to temporarily place an obstacle on each point on the path, one at a time, and see if it leads to a loop.\n",
"- There are 5,329 positions on the path, so the runtime should be about 5,000 times longer than Part 1; on the order of 10 seconds or so. I'll try it, and if it seems too slow, I'll try to think of something better."
]
},
{
"cell_type": "code",
"execution_count": 54,
"id": "1718fecb-aa3e-4162-9948-1c06d4ec5e8a",
"metadata": {},
"outputs": [],
"source": [
"def is_loopy_path(grid: Grid, guard_pos, facing=North) -> bool:\n",
" \"\"\"Does the path followed by the guard form a loop?\"\"\"\n",
" path = {(guard_pos, facing)}\n",
" while (ahead := add2(guard_pos, facing)) in grid:\n",
" if grid[ahead] == '#':\n",
" facing = make_turn(facing, 'R')\n",
" if (guard_pos, facing) in path:\n",
" return True\n",
" path.add((guard_pos, facing))\n",
" else:\n",
" guard_pos = ahead\n",
" return False\n",
" \n",
"def find_loopy_obstacles(grid: Grid) -> Iterable[Point]:\n",
" \"\"\"All positions in which placing an obstacle would result in a loopy path for the guard.\"\"\"\n",
" guard_pos = the(grid.findall('^'))\n",
" for pos in set(follow_path(grid)) - {guard_pos}:\n",
" grid[pos] = '#' # Temporarily place an obstacle \n",
" if is_loopy_path(grid, guard_pos):\n",
" yield pos\n",
" grid[pos] = '.' # Remove the obstacle"
]
},
{
"cell_type": "code",
"execution_count": 55,
"id": "36196264-eb33-4fc0-95d5-06c985105ebf",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 6.2: 1.968 seconds, answer 2162 ok"
]
},
"execution_count": 55,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(6.2, 2162, lambda:\n",
" quantify(find_loopy_obstacles(lab_grid)))"
]
},
{
"cell_type": "markdown",
"id": "9f3ee6f9-7ec7-4248-ae52-1804fdc81dbd",
"metadata": {},
"source": [
"That was a bit slow, but I'll take it. I had a **bug** when I was keeping a set of previously visited states to detect loops; the bug went away when I switched to the step-count limit."
]
},
{
"cell_type": "markdown",
"id": "9eae8cf2-8c97-418e-b00b-3ea0187da526",
"metadata": {},
"source": [
"# [Day 7](https://adventofcode.com/2024/day/7): Bridge Repair\n",
"\n",
"The narrative for today involves fixing a bridge, and each line of our input represents a calibration equation for the bridge. Unfortunately, some nearby elephants stole all the operators from the equations, so all that is left are the integers:"
]
},
{
"cell_type": "code",
"execution_count": 58,
"id": "c1c6cee8-122c-43c9-8c7d-ed8980ea2b76",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 850 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"202998336: 686 9 7 62 2 673\n",
"19275222: 361 3 7 170 65 5 223\n",
"23101: 7 694 916 4 6\n",
"2042426: 6 34 2 423 3\n",
"40369523: 8 880 91 45 23\n",
"46629044796: 990 471 4 4 796\n",
"1839056: 3 42 2 4 3 258 703 4 8\n",
"26205: 2 9 5 9 9 4 3 7 44 5 8 7\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 850 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"(202998336, 686, 9, 7, 62, 2, 673)\n",
"(19275222, 361, 3, 7, 170, 65, 5, 223)\n",
"(23101, 7, 694, 916, 4, 6)\n",
"(2042426, 6, 34, 2, 423, 3)\n",
"(40369523, 8, 880, 91, 45, 23)\n",
"(46629044796, 990, 471, 4, 4, 796)\n",
"(1839056, 3, 42, 2, 4, 3, 258, 703, 4, 8)\n",
"(26205, 2, 9, 5, 9, 9, 4, 3, 7, 44, 5, 8, 7)\n",
"...\n"
]
}
],
"source": [
"equations = parse(7, ints)"
]
},
{
"cell_type": "markdown",
"id": "be207b67-a970-4f79-85be-5d62b7cedd9f",
"metadata": {},
"source": [
" "
]
},
{
"cell_type": "markdown",
"id": "2e31d28f-97b1-4a3d-a298-18fcad297150",
"metadata": {},
"source": [
"### Part 1: What is the total calibration result of possibly true equations?\n",
"\n",
"Our task is to find operators to balance each equation. The input \"`3267: 81 40 27`\" can be made into the equation \"`3267 = 81 + 40 * 27`\", with the understanding that all evaluations are done left-to-right, so this is \"`3267 = ((81 + 40) * 27)`\". The two allowable operators are addition and multiplication. Our task is to compute the sum of all the equations that can be balanced.\n",
"\n",
"The straightforward approach is to try both operators on every number. If there are *n* numbers in an equation then there will be 2*n*-2 possible equations; is that going to be a problem?"
]
},
{
"cell_type": "code",
"execution_count": 61,
"id": "6fa3907c-0e1a-4d4a-9fc3-f809b9325674",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"13"
]
},
"execution_count": 61,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"max(map(len, equations))"
]
},
{
"cell_type": "markdown",
"id": "e0d9b0b2-fe1e-434e-b84e-c044da3d3673",
"metadata": {},
"source": [
"No problem! With 13 numbers on a line there are 211 = 2048 equations; a small number. I'll define `can_be_calibrated` to keep a set of `results`, updating the set for each new number and each possible operator. Although the instructions were a bit vague, it appears that when they talk about \"numbers\" in the equations they mean \"positive integers\". That means that neither addition nor multiplication can cause a number to decrease, so once a result exceeds the target, we'll drop it."
]
},
{
"cell_type": "code",
"execution_count": 63,
"id": "5dfe0edf-cf29-4623-bb2c-6180f832f4d7",
"metadata": {},
"outputs": [],
"source": [
"def can_be_calibrated(numbers: ints, operators=(operator.add, operator.mul)) -> bool:\n",
" \"\"\"Can the tuple of numbers be calibrated as a correct equation using '+' and '*' ?\"\"\"\n",
" target, first, *rest = numbers\n",
" results = {first} # A set of all possible results of the partial computation\n",
" for y in rest:\n",
" results = [op(x, y) for x in results if x <= target for op in operators]\n",
" return target in results"
]
},
{
"cell_type": "code",
"execution_count": 64,
"id": "3085596d-f5ec-4ba8-b05a-cf70cf276a0c",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 7.1: .014 seconds, answer 1985268524462 ok"
]
},
"execution_count": 64,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(7.1, 1985268524462, lambda:\n",
" sum(numbers[0] for numbers in equations if can_be_calibrated(numbers)))"
]
},
{
"cell_type": "markdown",
"id": "62a5fe5f-e23f-4420-87a8-47d8be02fbc0",
"metadata": {},
"source": [
"### Part 2: What is the total calibration result of possibly true equations, allowing concatenation?\n",
"\n",
"In Part 2, we add a third operator: concatentation. The equation \"`192: 17 8 14`\" can be balanced by concatenated 17 and 8 to get 178, and then adding 14: \"`192 = ((17 || 8) + 14)`\". With three operators, the equation with 11 operators now has 311 = 177,147 possibilities, almost 100 times more than Part 1, so this will take a few seconds:"
]
},
{
"cell_type": "code",
"execution_count": 66,
"id": "393a50cf-f136-446a-a97e-c501669ce89f",
"metadata": {},
"outputs": [],
"source": [
"operators3 = (operator.add, operator.mul, lambda x, y: int(str(x) + str(y))) "
]
},
{
"cell_type": "code",
"execution_count": 67,
"id": "f8e75ea3-e8ba-4b33-8efe-8bf74357e35d",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 7.2: .801 seconds, answer 150077710195188 ok"
]
},
"execution_count": 67,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(7.2, 150077710195188, lambda:\n",
" sum(numbers[0] for numbers in equations if can_be_calibrated(numbers, operators3)))"
]
},
{
"cell_type": "markdown",
"id": "128b4857-ed3e-49f5-97f5-9d9afd46408d",
"metadata": {},
"source": [
"That was easy, but it is the second-slowest runtime so far. I can make it a wee bit faster:"
]
},
{
"cell_type": "code",
"execution_count": 69,
"id": "6fe6adad-a3a6-49b8-b49e-6098b27e3a44",
"metadata": {},
"outputs": [],
"source": [
"operators3 = (operator.add, operator.mul, lambda x, y: x * 10 ** num_digits(y) + y)\n",
"\n",
"def num_digits(n: int) -> int:\n",
" \"\"\"The number of digits in a positive integer.\"\"\"\n",
" result = 1\n",
" while n >= 10:\n",
" result += 1\n",
" n /= 10\n",
" return result"
]
},
{
"cell_type": "code",
"execution_count": 70,
"id": "ffb673f1-af9d-4d15-8f8d-92e29489dd78",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 7.2: .656 seconds, answer 150077710195188 ok"
]
},
"execution_count": 70,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(7.2, 150077710195188, lambda:\n",
" sum(numbers[0] for numbers in equations if can_be_calibrated(numbers, operators3)))"
]
},
{
"cell_type": "markdown",
"id": "2e5693b7-dab8-4f89-a000-c69ee75a11c9",
"metadata": {},
"source": [
"# [Day 8](https://adventofcode.com/2024/day/8): Resonant Collinearity\n",
"\n",
"Another grid input, this one a map of antenna locations. Each different non-\"`.`\" character denotes an antenna of a given frequency."
]
},
{
"cell_type": "code",
"execution_count": 72,
"id": "cf6361a7-e3bc-42ec-ae16-f9eec166055e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 50 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"..................................................\n",
".................................C................\n",
".e..........7O....................................\n",
".....................................z............\n",
"......................t.........C.......k.........\n",
"............h................................9....\n",
".............5.7....O.............9C..............\n",
".......5.O................T.......................\n",
"...\n"
]
}
],
"source": [
"antennas = Grid(parse(8))"
]
},
{
"cell_type": "markdown",
"id": "c8e1006d-37bc-432e-bf1f-7a639287382a",
"metadata": {},
"source": [
"### Part 1: How many unique locations within the bounds of the map contain an antinode?\n",
"\n",
"An **antinode** occurs at a point that is perfectly in line with two antennas of the same frequency, but only when one of the antennas is twice as far away as the other.\n",
"\n",
"That means that if two antennas are at points *A* and *B*, then the two antinodal points are at 2*A* - *B* and 2*B* - A. If there are three or more antennas with the same frequency then we consider each pair of them in turn. So all we have to do is group the antennas by frequency, compute the antinodes for each pair with the same frequency, and determine which of those antinodal points are on the grid."
]
},
{
"cell_type": "code",
"execution_count": 74,
"id": "22180ce8-5d03-4aee-8c73-62f2afbddf71",
"metadata": {},
"outputs": [],
"source": [
"def antinodes(antennas: Grid) -> Set[Point]:\n",
" \"\"\"The set of all antinodal points in the grid.\n",
" (That is, points that are of distance d and 2d from same frequency antennas.)\"\"\"\n",
" groups = [antennas.findall(f) for f in set(antennas.values()) if f != '.']\n",
" return union(antinodes2(A, B, antennas)\n",
" for points in groups\n",
" for A, B in combinations(points, 2))\n",
"\n",
"def antinodes2(A: Point, B: Point, antennas: Grid) -> Set[Point]:\n",
" \"\"\"The set of antinodal points for two antenna points, A and B.\"\"\"\n",
" return {P for P in {sub(mul(A, 2), B), sub(mul(B, 2), A)}\n",
" if P in antennas}"
]
},
{
"cell_type": "code",
"execution_count": 75,
"id": "dd173ce9-cbbb-4282-b43f-c7cff662bd90",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 8.1: .003 seconds, answer 220 ok"
]
},
"execution_count": 75,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(8.1, 220, lambda:\n",
" len(antinodes(antennas)))"
]
},
{
"cell_type": "markdown",
"id": "ff79d605-813a-46ac-8473-a1198be0e99f",
"metadata": {},
"source": [
"### Part 2: How many unique locations within the bounds of the map contain an updated antinode?\n",
"\n",
"For Part 2, the updated definition of antinodes means that they can now occur at *any* point that is exactly on line with two antennas of the same frequency, regardless of distance. So if the two antennas are *A* and *B* then the antinodal points can be found by starting at *A* and going step by step in the direction of the vector *A* - *B* and also in the direction *B* - *A*, going as far as you can while staying on the grid. The `Grid.follow_line` method facilitates that.\n",
"\n",
"I'll parametrize `updated_antinodes` so it can handle both parts:"
]
},
{
"cell_type": "code",
"execution_count": 77,
"id": "d30f8ce9-f186-46a0-a2e7-f74eceae6905",
"metadata": {},
"outputs": [],
"source": [
"def antinodes(antennas: Grid, antinodes2=antinodes2) -> Set[Point]:\n",
" \"\"\"The set of all updated antinodal points in the grid.\n",
" (That is, points that are on a line with two same frequency antennas.)\"\"\"\n",
" groups = [antennas.findall(f) for f in set(antennas.values()) if f != '.']\n",
" return union(antinodes2(A, B, antennas)\n",
" for points in groups\n",
" for A, B in combinations(points, 2))\n",
"\n",
"def updated_antinodes2(A: Point, B: Point, antennas: Grid) -> Set[Point]:\n",
" \"\"\"The set of updated antinodal points for two antenna points, A and B.\"\"\"\n",
" return (set(antennas.follow_line(A, sub(A, B))) | \n",
" set(antennas.follow_line(A, sub(B, A))))"
]
},
{
"cell_type": "code",
"execution_count": 78,
"id": "6bf85b57-8b8f-4196-9903-6d5fe082f404",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 8.1: .003 seconds, answer 220 ok"
]
},
"execution_count": 78,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(8.1, 220, lambda:\n",
" len(antinodes(antennas)))"
]
},
{
"cell_type": "code",
"execution_count": 79,
"id": "f232952c-5fc6-4696-a8b1-d0b54137ac02",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 8.2: .003 seconds, answer 813 ok"
]
},
"execution_count": 79,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(8.2, 813, lambda:\n",
" len(antinodes(antennas, updated_antinodes2)))"
]
},
{
"cell_type": "markdown",
"id": "9696a986-b6a2-4530-b55b-9db959ef7485",
"metadata": {},
"source": [
"I got both of these right the first time (except for some simple typos: a mismatched paren and typing `grid` when I meant the grid called `antennas`)."
]
},
{
"cell_type": "markdown",
"id": "d4835cad-7777-4636-b9af-52cc9782b2b8",
"metadata": {},
"source": [
"# [Day 9](https://adventofcode.com/2024/day/9): Disk Fragmenter\n",
"\n",
"Today we're confronted with a computer disk that needs to be compressed to gain some contiguous free space. The contents of the disk is represented in the **disk map** format: a string of digits, where the digits alternate between the number of blocks of a file, followed by the number of blocks of free space. We'll parse that as a tuple of digits:"
]
},
{
"cell_type": "code",
"execution_count": 82,
"id": "0e944f9e-5c16-440c-b12e-178058a87048",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 1 str:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"692094513253604282899448234539616972499153261626907217394161512944107098953354935354419233821564 ...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 1 tuple:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"(6, 9, 2, 0, 9, 4, 5, 1, 3, 2, 5, 3, 6, 0, 4, 2, 8, 2, 8, 9, 9, 4, 4, 8, 2, 3, 4, 5, 3, 9, 6, 1, ...\n"
]
}
],
"source": [
"disk_map = the(parse(9, digits))"
]
},
{
"cell_type": "markdown",
"id": "99d40379-65e1-4872-8c68-17ba4925c24e",
"metadata": {},
"source": [
"\n",
"\n",
"### Part 1: Compress the hard drive. What is the resulting filesystem checksum? \n",
"\n",
"The disk map \"`12345`\" means that there is 1 block for the first file (which has ID number 0), followed by 2 empty blocks, then 3 blocks for the second file (with ID number 1), followed by 4 empty blocks, and finally 5 blocks for the third file (with ID number 2). It makes sense to convert this into a **disk layout** format, which would be \"`0..111....22222`\", where \"`.`\" represents an empty block.\n",
"\n",
"To **compress** a disk layout, move file blocks one at a time starting by taking the rightmost non-empty block and moving it to the leftmost empty position; repeat until no more moves are possible.\n",
"\n",
"The final answer is a **checksum** of the compressed disk: the sum of the product of the block position times the file ID number for all non-empty blocks."
]
},
{
"cell_type": "code",
"execution_count": 84,
"id": "76e8454d-a2f3-4b6b-92df-182116cf46e0",
"metadata": {},
"outputs": [],
"source": [
"empty = '.'\n",
"\n",
"def disk_layout(disk_map: Ints) -> list:\n",
" \"\"\"Convert a disk map into a disk layout.\"\"\"\n",
" def empties(i): return (disk_map[i] * [empty] if i < len(disk_map) else [])\n",
" return append(disk_map[i] * [id] + empties(i + 1)\n",
" for id, i in enumerate(range(0, len(disk_map), 2)))\n",
"\n",
"def compress_layout(layout: list) -> list:\n",
" \"\"\"Mutate layout by moving blocks one at a time from the end to the leftmost free space.\"\"\"\n",
" N = len(layout)\n",
" free = -1 # Start looking for free space from the left\n",
" end = N # Start looking for non-empty blocks from the right\n",
" while True:\n",
" free = first(i for i in range(free + 1, N) if layout[i] is empty)\n",
" end = first(i for i in range(end - 1, 0, -1) if layout[i] is not empty)\n",
" if free is None or free >= end:\n",
" return layout\n",
" layout[free], layout[end] = layout[end], empty\n",
"\n",
"def checksum(layout: list) -> list:\n",
" \"\"\"The sum of the product of the block position times the file ID number for all non-empty blocks.\"\"\"\n",
" return sum(i * id for i, id in enumerate(layout) if id is not empty)"
]
},
{
"cell_type": "code",
"execution_count": 85,
"id": "2aa7e2b9-844e-49ed-b41b-4a4cecff86b7",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 9.1: .020 seconds, answer 6332189866718 ok"
]
},
"execution_count": 85,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(9.1, 6332189866718, lambda:\n",
" checksum(compress_layout(disk_layout(disk_map))))"
]
},
{
"cell_type": "markdown",
"id": "2c05a497-cc66-4698-b88b-25c33eea224a",
"metadata": {},
"source": [
"### Part 2: Compress the hard drive with the new method. What is the resulting filesystem checksum? \n",
"\n",
"In Part 2, there is a new method of compressing the disk, where we move full files rather than a block at a time. Again we start on the right, and try to move a file to the leftmost position where it will fit. If there is no such position, the file doesn't move. `compress_layout2` implements this new method, performing a move by swapping two [**slices**](https://docs.python.org/3/library/functions.html#slice) of the disk layout: \n",
"\n",
" layout[file], layout[free] = layout[free], layout[file]`\n",
"\n",
"To find all the slices that indicate files, it is easier to run through the disk map than the disk layout. The function `file_slices` quickly finds all such slices.\n",
"\n",
"Finding a free space for a file is more difficult, because we need to find one that is big enough. I'll run through the whole layout from left-to-right each time. This will make it *O*(*n*2) rather than *O*(*n*), but hopefully it won't be too slow. (If I wanted to speed it up I could have an array of starting positions for each desired size of free space.)"
]
},
{
"cell_type": "code",
"execution_count": 87,
"id": "fcf4d832-3d7d-4987-aa57-e6e0f1df16bf",
"metadata": {},
"outputs": [],
"source": [
"def compress_layout2(disk_map: Ints) -> list:\n",
" \"\"\"Mutate layout by moving files one at a time from the end to the leftmost free space.\"\"\"\n",
" layout = disk_layout(disk_map)\n",
" for file in file_slices(disk_map):\n",
" if free := find_freespace(layout, file):\n",
" layout[file], layout[free] = layout[free], layout[file]\n",
" return layout\n",
"\n",
"def file_slices(disk_map: Ints) -> List[slice]:\n",
" \"\"\"Given a disk map, find all the slice positions of files in the disk layout (last one first).\"\"\"\n",
" slices = []\n",
" block = 0\n",
" for i, length in enumerate(disk_map):\n",
" if i % 2 == 0:\n",
" slices.append(slice(block, block + length))\n",
" block += length\n",
" slices.reverse()\n",
" return slices\n",
"\n",
"def find_freespace(layout, file_slice) -> Optional[slice]:\n",
" \"\"\"Find a slice position big enough to fit the given file slice, or return None if there is no position.\"\"\"\n",
" length = file_slice.stop - file_slice.start\n",
" run = 0\n",
" for i in range(layout.index(empty), len(layout)):\n",
" if i >= file_slice.start:\n",
" return None # We only want to move a file left, not right\n",
" elif layout[i] is empty:\n",
" run += 1\n",
" if run == length:\n",
" return slice(i + 1 - length, i + 1)\n",
" else:\n",
" run = 0\n",
" return None"
]
},
{
"cell_type": "code",
"execution_count": 88,
"id": "e3036875-88d0-496e-9d2f-facd0e80a5b2",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 9.2: 2.731 seconds, answer 6353648390778 ok"
]
},
"execution_count": 88,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(9.2, 6353648390778, lambda:\n",
" checksum(compress_layout2(disk_map)))"
]
},
{
"cell_type": "markdown",
"id": "24c0e7d7-6ac7-4e4b-9557-bd4e215ad0a9",
"metadata": {},
"source": [
"I got the right answer, but I confess I had an off-by-one **bug** in `find_freespace` on the first try."
]
},
{
"cell_type": "markdown",
"id": "7a900425-fe22-4d2f-8d1d-46c319c109e9",
"metadata": {},
"source": [
"# [Day 10](https://adventofcode.com/2024/day/10): Hoof It\n",
"\n",
"Today's input is a topological map, with digits indicating the elevation of each terrain position."
]
},
{
"cell_type": "code",
"execution_count": 91,
"id": "5804fb03-05f3-402f-b6cc-6804c5d22512",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 60 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"432109865210212123765432101234321098543289654320132112121058\n",
"045678774324301012892343023445456787650198763013241001034569\n",
"187678789465692321001056014896234986456787012894653212123678\n",
"296589921056789433217837895687145675323891233765784589238987\n",
"345437835434576544786921278761010014210710321212098676521067\n",
"032126546323465435695430789760121223121653450303145125430678\n",
"123010567810156543212345699859834321056544067654236012321589\n",
"543213498987657665401030787348765430187432198765987622345432\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 60 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"(4, 3, 2, 1, 0, 9, 8, 6, 5, 2, 1, 0, 2, 1, 2, 1, 2, 3, 7, 6, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, ...\n",
"(0, 4, 5, 6, 7, 8, 7, 7, 4, 3, 2, 4, 3, 0, 1, 0, 1, 2, 8, 9, 2, 3, 4, 3, 0, 2, 3, 4, 4, 5, 4, 5, ...\n",
"(1, 8, 7, 6, 7, 8, 7, 8, 9, 4, 6, 5, 6, 9, 2, 3, 2, 1, 0, 0, 1, 0, 5, 6, 0, 1, 4, 8, 9, 6, 2, 3, ...\n",
"(2, 9, 6, 5, 8, 9, 9, 2, 1, 0, 5, 6, 7, 8, 9, 4, 3, 3, 2, 1, 7, 8, 3, 7, 8, 9, 5, 6, 8, 7, 1, 4, ...\n",
"(3, 4, 5, 4, 3, 7, 8, 3, 5, 4, 3, 4, 5, 7, 6, 5, 4, 4, 7, 8, 6, 9, 2, 1, 2, 7, 8, 7, 6, 1, 0, 1, ...\n",
"(0, 3, 2, 1, 2, 6, 5, 4, 6, 3, 2, 3, 4, 6, 5, 4, 3, 5, 6, 9, 5, 4, 3, 0, 7, 8, 9, 7, 6, 0, 1, 2, ...\n",
"(1, 2, 3, 0, 1, 0, 5, 6, 7, 8, 1, 0, 1, 5, 6, 5, 4, 3, 2, 1, 2, 3, 4, 5, 6, 9, 9, 8, 5, 9, 8, 3, ...\n",
"(5, 4, 3, 2, 1, 3, 4, 9, 8, 9, 8, 7, 6, 5, 7, 6, 6, 5, 4, 0, 1, 0, 3, 0, 7, 8, 7, 3, 4, 8, 7, 6, ...\n",
"...\n"
]
}
],
"source": [
"topo = Grid(parse(10, digits))"
]
},
{
"cell_type": "markdown",
"id": "d951807a-3611-445f-84ee-352221a25968",
"metadata": {},
"source": [
"### Part 1: What is the sum of the scores of all trailheads on your topographic map?\n",
"\n",
"A **trailhead** is any position with elevation 0, and a **peak** is any position with elevation 9. The **score** of a trailhead is the number of peaks that can be reached by following a path where each step increases the elevation by exactly 1. All moves are in one of the four cardinal directions (north/south/east/west).\n",
"\n",
"I'll keep a set of points on the frontier of possible paths, updating this set on each iteratation from 1 to 9, by looking at each point on the frontier and seeing which of the neighboring points `p` have the right elevation:"
]
},
{
"cell_type": "code",
"execution_count": 93,
"id": "76b5379e-ee19-4607-91b8-88ec7b38023f",
"metadata": {},
"outputs": [],
"source": [
"def score(topo: Grid, trailhead: Point) -> int:\n",
" \"\"\"How many peaks can be reached from this trailhead?\"\"\"\n",
" frontier = {trailhead}\n",
" for elevation in range(1, 10):\n",
" frontier = {p for p in union(map(topo.neighbors, frontier))\n",
" if topo[p] == elevation}\n",
" return len(frontier)"
]
},
{
"cell_type": "code",
"execution_count": 94,
"id": "97cf05f7-fa56-4a90-b2d8-2cd4d9b81f95",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 10.1: .005 seconds, answer 744 ok"
]
},
"execution_count": 94,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(10.1, 744, lambda:\n",
" sum(score(topo, head) for head in topo.findall([0])))"
]
},
{
"cell_type": "markdown",
"id": "4656eb08-b12a-4a02-92b8-ac23f2361387",
"metadata": {},
"source": [
"### Part 2: What is the sum of the ratings of all trailheads?\n",
"\n",
"The **rating** of a trailhead is the number of distinct paths from the trailhead to a peak.\n",
"\n",
"As in Part 1, I'll keep a frontier and update it on each iteration from 1 to 9, but this time the frontier will be a counter of `{position: count}` where the count indicates the number of paths to that position. On each iteration I'll look at each point `f` on the frontier and see which of the neighboring points `p` have the right elevation, and increment the counts for those points by the count for `f`. This approach is linear in the number of positions, whereas if I followed all possible paths depth-first there could be an exponential number of paths."
]
},
{
"cell_type": "code",
"execution_count": 96,
"id": "b763450f-a565-4936-bee4-e531c2eeebdb",
"metadata": {},
"outputs": [],
"source": [
"def rating(topo: Grid, trailhead: Point) -> int:\n",
" \"\"\"How many distinct paths are there from this trailhead to any peak?\"\"\"\n",
" frontier = Counter({trailhead: 1})\n",
" for elevation in range(1, 10):\n",
" frontier = accumulate((p, frontier[f]) \n",
" for f in frontier\n",
" for p in topo.neighbors(f) if topo[p] == elevation)\n",
" return sum(frontier.values())"
]
},
{
"cell_type": "code",
"execution_count": 97,
"id": "f8a87032-6556-4fc9-9bb8-573611aee8dc",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 10.2: .006 seconds, answer 1651 ok"
]
},
"execution_count": 97,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(10.2, 1651, lambda:\n",
" sum(rating(topo, head) for head in topo.findall([0])))"
]
},
{
"cell_type": "markdown",
"id": "af410d30-7096-4be6-bb20-904b3c8e2f59",
"metadata": {},
"source": [
"Today I went pretty fast (for me); I started a few minutes late and finished in 15 minutes. From the point of view of a competitive coder I did foolish things like write docstrings and use variables of more than one letter, so while this time was fast for me, it placed well out of the top 1000.\n",
"\n",
"Here's a visualization of the map:"
]
},
{
"cell_type": "code",
"execution_count": 99,
"id": "4b35defa-a19e-46c5-bd04-3af55bea14e4",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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",
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"def plot_topo(topo: Grid):\n",
" \"\"\"Show the map with a colormap from blue to red.\"\"\"\n",
" plt.figure()\n",
" scatter = plt.scatter(Xs(topo), Ys(topo), c=list(topo.values()), \n",
" cmap='coolwarm', marker='s', s=12)\n",
" plt.colorbar(scatter, label='Elevation')\n",
" plt.axis('square')\n",
" plt.axis('off')\n",
"\n",
"plot_topo(topo)"
]
},
{
"cell_type": "markdown",
"id": "3e01d7f5-d0f0-4e7b-8cab-eef2afc02f6b",
"metadata": {},
"source": [
"# [Day 11](https://adventofcode.com/2024/day/11): Plutonian Pebbles\n",
"\n",
"Today's narrative involves a straight line of stones, each of which has a number enscribed on it. The input is a single line of these numbers:"
]
},
{
"cell_type": "code",
"execution_count": 101,
"id": "76b68cef-d8de-4145-b65c-b254fedf1671",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 1 str:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"0 27 5409930 828979 4471 3 68524 170\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 1 tuple:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"(0, 27, 5409930, 828979, 4471, 3, 68524, 170)\n"
]
}
],
"source": [
"stones = the(parse(11, ints))"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "a7302dc5-5163-4f0b-bdcc-8c00e367391c",
"metadata": {},
"source": [
"### Part 1: How many stones will you have after blinking 25 times?\n",
"\n",
"Every time you blink, the stones appear to change, according to these rules:\n",
"- A stone marked 0 changes to 1.\n",
"- Otherwise, a stone with an even number of digits splits into two stones, with the first and second halves of those digits.\n",
"- Otherwise, the stone's number is multiplied by 2024.\n",
"\n",
"\n",
"\n",
"\n",
"I'll define `blink` to simulate the effect of a given number of blinks, and `change_stone` to change a single stone, returning a list of wither one or two stones (the two stones computed by `split_stone`):"
]
},
{
"cell_type": "code",
"execution_count": 103,
"id": "1513df56-3d6f-42cf-8aec-1bdbeb991d90",
"metadata": {},
"outputs": [],
"source": [
"def blink(stones: Ints, blinks=25) -> List[int]:\n",
" \"\"\"Simulate the changes in the list of stones after blinking `blinks` times.\"\"\"\n",
" for _ in range(blinks):\n",
" stones = append(map(change_stone, stones))\n",
" return stones\n",
" \n",
"def change_stone(stone: int) -> List[int]:\n",
" \"\"\"Change a single stone into one or two, according to the rules.\"\"\"\n",
" digits = str(stone)\n",
" return ([1] if stone == 0 else\n",
" split_stone(digits) if len(digits) % 2 == 0 else\n",
" [stone * 2024])\n",
"\n",
"def split_stone(digits: str) -> List[int]:\n",
" \"\"\"Split a stone into two halves.\"\"\"\n",
" half = len(digits) // 2\n",
" return [int(digits[:half]), int(digits[half:])]"
]
},
{
"cell_type": "code",
"execution_count": 104,
"id": "eff17cd0-a2c7-4d69-bc55-c0ef97917915",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 11.1: .068 seconds, answer 194482 ok"
]
},
"execution_count": 104,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(11.1, 194482, lambda:\n",
" len(blink(stones)))"
]
},
{
"cell_type": "markdown",
"id": "2f65e94f-43e8-4f08-85df-827928c57e0b",
"metadata": {},
"source": [
"### Part 2: How many stones would you have after blinking a total of 75 times?\n",
"\n",
"It looks like the number of stones is roughly doubling every 1 or 2 blinks, so for 75 blinks we could have trillions of stones. I'd like something more efficient. I note that:\n",
"- Although the puzzle makes it clear that the stones are in a line, it turns out their position in the line is irrelevant.\n",
"- Because all the even-digit numbers get split in half, it seems like many small numbers will appear multiple times.\n",
"- (In the given example, after 6 blinks the number 2 appears 4 times.)\n",
"- Therefore, I'll keep a `Counter` of stones rather than a `list` of stones."
]
},
{
"cell_type": "code",
"execution_count": 106,
"id": "707b5a97-0296-48df-bdab-e34064cc67c2",
"metadata": {},
"outputs": [],
"source": [
"def blink2(stones: Ints, blinks=25) -> Counter:\n",
" \"\"\"Simulate the changes after blinking `blinks` times and return a Counter of stones.\"\"\"\n",
" counts = Counter(stones)\n",
" for _ in range(blinks):\n",
" counts = accumulate((s, counts[stone]) \n",
" for stone in counts \n",
" for s in change_stone(stone))\n",
" return counts"
]
},
{
"cell_type": "markdown",
"id": "f5bf07ce-b48e-40db-8992-b9b571e66554",
"metadata": {},
"source": [
"Now we can re-run Part 1 (it should be slightly faster), and run Part 2 without fear of having trillion-element lists:"
]
},
{
"cell_type": "code",
"execution_count": 108,
"id": "efdcdbf8-e8ec-4a85-9d09-90a20e08c66a",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 11.1: .002 seconds, answer 194482 ok"
]
},
"execution_count": 108,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(11.1, 194482, lambda:\n",
" total(blink2(stones, 25)))"
]
},
{
"cell_type": "code",
"execution_count": 109,
"id": "657b1f13-ffcc-44c6-84f1-398fa2fcdac7",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 11.2: .060 seconds, answer 232454623677743 ok"
]
},
"execution_count": 109,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(11.2, 232454623677743, lambda:\n",
" total(blink2(stones, 75)))"
]
},
{
"cell_type": "markdown",
"id": "ce377749-b3e2-4ca4-b50d-e7c3d2e7201a",
"metadata": {},
"source": [
"Again, I did pretty well, with no errors, and moving at what I thought was a good pace, but I didn't even crack the top 2000 on the leaderboard. I guess I spent too much time writing docstrings and type hints, and refactoring as I went."
]
},
{
"cell_type": "markdown",
"id": "391cec1e-32fe-4e6e-81c2-4e38469b15e3",
"metadata": {},
"source": [
"# [Day 12](https://adventofcode.com/2024/day/12): Garden Groups\n",
"\n",
"Today's input is yet another 2D map. This one depicts different garden plots on a farm, each plot planted with a crop, indicated by a letter (maybe \"I\" is iceberg lettuce and \"U\" is udo, and so on):"
]
},
{
"cell_type": "code",
"execution_count": 112,
"id": "8161ee7e-76e3-499a-abf8-a607991c9602",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 140 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"IIIIIIIIIIIIIIIIIIIIIUUUUUUUUJLLLLAAAAAAMMMAUUUUPPXPZZZZZZZZZZZXXXXXXXXXXXXXXXXXXXXXFFFFFFFFFFZZ ...\n",
"IIIIIIIIIIIIIIIIIIIIUUUUUUUUUJALEAAAAAAAAAAAAUUUUPXPPZZZZZHHHHHHXHXXXXXXXXXXXXXXXXXXFXFFFFFFFFZZ ...\n",
"IIIIIIIIIIIIIIIIIIIIUUUUUUUUUJAAAAAAAAAAAAAAUUPPPPPPPZZZZZZZHHHHHHHXXXXXXXXXXXXXXXXXXXFFFFFFFFFZ ...\n",
"IIIIIIIIIIIAAIIIIIIIIIUUUUUUUJJAAAAAAAAAAAAAAVVPPPPPPPPZHHHHHHHHHHHXXXXXXXXXXXXXXXXXFFFFFFFFFFFZ ...\n",
"IOOIIIIIIIIAAIIIIIIIIIIUUUQVUJJAAAAAAAAAAAAAEVVZPPPPPPPHHHHHHHHHHHHXXXXXXXXXXXXXXXXXXXXFFFFFFLLL ...\n",
"OOOOOOOOIIAAAAAIIIIIIIIUQQQQVQJAAAAAAAAAAAAAAVVPPPPPIIHHHHHHHHHHHHHXXXXXUXXXXXXUUXXBBBSFFFLLLLLL ...\n",
"OOOOOOOOIAAAAAAIIIIIIQQQQQQQQQQQEADDAAAAAAAAHHVVPPPIIIIHHHHHHHHHHHHHXXXUUUUUXXUUUXXBBBSFFFLLELLL ...\n",
"OOOOOOOOIIIAAIIIIIQQQQQQQQQQTQJQEEDDDAAAAAAHHVVVVPIIIIIHHHHHHHHHHHHHXUUUUUUUUUUUMMXBBBSSSFFFLLLL ...\n",
"...\n"
]
}
],
"source": [
"farm = Grid(parse(12))"
]
},
{
"cell_type": "markdown",
"id": "bb95a10b-1f83-4940-a68b-b94696c3aab3",
"metadata": {},
"source": [
"### Part 1: What is the total price of fencing all regions on your map?\n",
"\n",
"We are asked to calculate the cost of putting fences around each **region** (a region is a set of plots with the same crop that abut each other horizontally or vertically). The price of the fence for a region is defined as the product of the region's area and its perimeter. If we represent a region as a set of (x, y) points, then the area is easy: it is just the number of points. The perimeter length can be computed by, for each plot point in the region, looking at each of the four directions and counting cases where the adjacent plot in that direction is *not* in the region. (Initially I had a **bug** in that I looked at the `farm.neighbors` of each plot. That doesn't work because a plot on the edge of the grid should count as part of the perimeter.)"
]
},
{
"cell_type": "code",
"execution_count": 114,
"id": "79f91f38-e325-44f2-9e53-b64ce12d9d35",
"metadata": {},
"outputs": [],
"source": [
"Region = Set[Point]\n",
"region_area = len\n",
"\n",
"def fence_price(farm: Grid) -> int:\n",
" \"\"\"Total price of fences for all the regions in the farm.\"\"\"\n",
" return sum(map(region_price, regions(farm)))\n",
"\n",
"def region_price(region) -> int: return region_area(region) * perimeter_length(region)\n",
"\n",
"def perimeter_length(region: Region) -> int:\n",
" \"\"\"The number of sides on the perimeter of the region.\"\"\"\n",
" return quantify(add2(plot, d) not in region for plot in region for d in directions4)"
]
},
{
"cell_type": "markdown",
"id": "9524ee15-c378-4cd4-a79b-14fb99b17cb3",
"metadata": {},
"source": [
"To find all the regions I'll start at a point and do a [flood fill](https://en.wikipedia.org/wiki/Flood_fill) to neighboring points with the same region letter, keeping track of points that have already been found so as to not duplicate them. The function `regions` iterates over all points to make sure it finds every region, and `flood_fill` recursively expands to all points that neighbor `p` and have the same crop (letter). This function mutates the set `already_found` as it goes (and also mutates the `region` it is building up)."
]
},
{
"cell_type": "code",
"execution_count": 116,
"id": "1fbabbfb-50c8-4197-8517-e7cee9582765",
"metadata": {},
"outputs": [],
"source": [
"def regions(farm: Grid) -> List[Region]:\n",
" \"\"\"Find all the regions in the farm.\"\"\"\n",
" already_found = set() # Set of plots already accounted for\n",
" return [flood_fill(p, farm, set(), already_found) \n",
" for p in farm if p not in already_found]\n",
"\n",
"def flood_fill(p: Point, grid: Grid, region: set, already_found: set) -> set:\n",
" \"\"\"Starting at point p, recursively add all neighboring points to `region`, keeping track of `already_found`.\"\"\"\n",
" if p not in already_found:\n",
" region.add(p)\n",
" already_found.add(p)\n",
" for p2 in grid.neighbors(p):\n",
" if farm[p2] == farm[p]:\n",
" flood_fill(p2, grid, region, already_found)\n",
" return region"
]
},
{
"cell_type": "code",
"execution_count": 117,
"id": "cdaf655b-d12c-4973-b19b-3132e5e691c6",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 12.1: .033 seconds, answer 1402544 ok"
]
},
"execution_count": 117,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(12.1, 1402544, lambda:\n",
" fence_price(farm))"
]
},
{
"cell_type": "markdown",
"id": "b3383560-2bbc-4dfc-b643-feb103876823",
"metadata": {},
"source": [
"### Part 2: What is the new total price of fencing all regions on your map, with the bulk discount?\n",
"\n",
"In Part 2 we get a **bulk discount** on the fencing; we only need to pay for the number of straight line sides on the perimeter, not the total length of the perimeter. For example, a 10 x 10 square has perimeter 40, but has only 4 sides; that's a 90% discount!\n",
"\n",
"It took me a while to figure out a good approach for this. At first I was reminded of the Convex Hull problem, for which I have [a notebook](https://github.com/norvig/pytudes/blob/main/ipynb/Convex%20Hull.ipynb). But that's not really appropriate here; our regions could be non-convex, and the set of points in a region are not the same as the vertexes of a polygon (e.g., a region with one point has 4 sides, not 0).\n",
"\n",
"A better idea is to start with the perimeter length and subtract one for every case in which a points has an edge in one direction (e.g., an edge to the North) and also has a neighbor with the same edge. To be precise, I'll look for four cases:\n",
"- A point with an edge to the North whose neighbor to the East also has an edge to the North\n",
"- A point with an edge to the East whose neighbor to the South also has an edge to the East\n",
"- A point with an edge to the South whose neighbor to the West also has an edge to the South\n",
"- A point with an edge to the West whose neighbor to the North also has an edge to the West\n",
"\n",
"Here are two diagrams of two regions with \"`X`\" and \"`W`\" crops with a \"`-`\" marking each place where a perimeter piece would be subtracted.\n",
"\n",
" .X-... .-------..\n",
" -X-.X- .WWWWWWWW-\n",
" -XXXX- -WWWWWWWW-\n",
" ..XXX. -WWWWWWWW.\n",
" ...--. ..-------.\n",
"\n",
"I'll parameterize `fence_price` to take a `region_price` parameter, and show that the change is backwards compatible to Part 1:"
]
},
{
"cell_type": "code",
"execution_count": 119,
"id": "38c30e15-3a33-40c2-b734-163a15af7a8a",
"metadata": {},
"outputs": [],
"source": [
"def fence_price(farm: Grid, region_price=region_price) -> int:\n",
" \"\"\"Total price of fences for all the regions in the farm, given the price function for a region.\"\"\"\n",
" return sum(map(region_price, regions(farm)))\n",
"\n",
"def discount_region_price(region) -> int: return region_area(region) * region_sides(region)\n",
" \n",
"def region_sides(region):\n",
" \"\"\"How many straight-line sides does this region have?\"\"\"\n",
" def has_edge(p: Point, d: Vector): return p in region and add2(p, d) not in region\n",
" def neighbor(p: Point, d: Vector): return add2(p, make_turn(d, 'R'))\n",
" subtract = quantify(has_edge(p, d) and has_edge(neighbor(p, d), d)\n",
" for p in region\n",
" for d in directions4)\n",
" return perimeter_length(region) - subtract"
]
},
{
"cell_type": "code",
"execution_count": 120,
"id": "72175812-dcd0-4f1b-9efa-0dceeeafa609",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 12.1: .051 seconds, answer 1402544 ok"
]
},
"execution_count": 120,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(12.1, 1402544, lambda:\n",
" fence_price(farm))"
]
},
{
"cell_type": "code",
"execution_count": 121,
"id": "9defcd35-91bc-41d4-a16f-bb7a4ede75e7",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 12.2: .042 seconds, answer 862486 ok"
]
},
"execution_count": 121,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(12.2, 862486, lambda: \n",
" fence_price(farm, discount_region_price))"
]
},
{
"cell_type": "markdown",
"id": "faa9d750-a619-40dc-965c-a9a8e2543e32",
"metadata": {},
"source": [
"# [Day 13](https://adventofcode.com/2024/day/13): Claw Contraption\n",
"\n",
"Today's puzzle involves arcade claw machines. Each input paragraph describes a machine with two buttons, **A** and **B**; every push of a button moves the claw a specified amount in the X and Y directions. The prize is at a specified location. We can parse the input by defining a `parse_claw` function and a `Claw` datatype to represent a claw machine:"
]
},
{
"cell_type": "code",
"execution_count": 123,
"id": "e78f45c0-c420-4661-aad2-14e122b4473b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 1279 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Button A: X+24, Y+19\n",
"Button B: X+32, Y+86\n",
"Prize: X=1072, Y=2062\n",
"\n",
"Button A: X+70, Y+12\n",
"Button B: X+11, Y+43\n",
"Prize: X=3400, Y=3872\n",
"\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 320 Claws:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Claw(A=(24, 19), B=(32, 86), Prize=(1072, 2062))\n",
"Claw(A=(70, 12), B=(11, 43), Prize=(3400, 3872))\n",
"Claw(A=(48, 77), B=(34, 12), Prize=(6510, 1583))\n",
"Claw(A=(76, 60), B=(29, 88), Prize=(5506, 6300))\n",
"Claw(A=(16, 75), B=(68, 20), Prize=(17148, 13490))\n",
"Claw(A=(46, 13), B=(33, 63), Prize=(15572, 9644))\n",
"Claw(A=(21, 79), B=(44, 32), Prize=(2433, 5147))\n",
"Claw(A=(13, 45), B=(36, 11), Prize=(4612, 19164))\n",
"...\n"
]
}
],
"source": [
"Claw = namedtuple('Claw', 'A, B, Prize') # A claw machine\n",
"\n",
"def parse_claw(text: str) -> Claw:\n",
" \"\"\"Parse a description into a Claw object.\"\"\"\n",
" ax, ay, bx, by, px, py = ints(text)\n",
" return Claw((ax, ay), (bx, by), (px, py))\n",
" \n",
"claws = parse(13, parse_claw, sections=paragraphs)"
]
},
{
"cell_type": "markdown",
"id": "7f6da888-9288-42f1-bc51-2eb2a66d34a1",
"metadata": {},
"source": [
"### Part 1: What is the fewest tokens you would have to spend to win all possible prizes?\n",
"\n",
"We are told that for some machines it is possible to reach the prize and others it is impossible. We are also told that you need to spend 3 tokens to press the **A** button and 1 token to press the **B** button. There is a hint that no button will need to be pushed more than 100 times. It is usually a good idea to accept the hint, so I will do the following:\n",
"- Exhaustively try from every number from 0 to 100 presses of the **A** button.\n",
"- For each number, figure out what the resulting position of the claw would be after those presses.\n",
"- Calculate how many presses of the **B** button would be required to get to the prize's X location.\n",
"- If the number of presses is an integer, and would also arrive at the prize's Y location, then yield that solution.\n",
"- Out of these solutions, the cheapest solution is the one that needs the fewest tokens.\n",
"- (If there are no solutions, `cheapest_solution` will return 0, whcih is convenient for adding the results up.)"
]
},
{
"cell_type": "code",
"execution_count": 125,
"id": "c2c4bbc9-42cd-483d-8da2-97cf051e93fe",
"metadata": {},
"outputs": [],
"source": [
"def solve_claw(claw: Claw, limit=100) -> Iterable[Tuple[int, int]]:\n",
" \"\"\"All possible (A-presses, B-presses) solutions to reach the prize on this machine.\"\"\"\n",
" A, B, Prize = claw\n",
" for Apresses in range(limit + 1):\n",
" pos = mul(A, Apresses)\n",
" if X_(pos) > X_(Prize) or Y_(pos) > Y_(Prize):\n",
" return\n",
" diff = sub2(Prize, pos)\n",
" Bpresses = X_(diff) / X_(B)\n",
" if Bpresses.is_integer() and Bpresses * Y_(B) == Y_(diff):\n",
" yield (Apresses, int(Bpresses))\n",
"\n",
"def cheapest_solution(claw: Claw) -> int: \n",
" \"\"\"The minimum cost out of every possible solution to this claw machine.\"\"\"\n",
" return min((3 * A + 1 * B for (A, B) in solve_claw(claw)), default=0)"
]
},
{
"cell_type": "code",
"execution_count": 126,
"id": "f5638ed4-1e59-4b9f-b1fc-427d2eb0d036",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 13.1: .010 seconds, answer 29598 ok"
]
},
"execution_count": 126,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(13.1, 29598, lambda:\n",
" sum(map(cheapest_solution, claws)))"
]
},
{
"cell_type": "markdown",
"id": "3a36e76c-dd5b-4d80-ae88-9980e7d274ac",
"metadata": {},
"source": [
"### Part 2: What is the fewest tokens you would have to spend to win all possible prizes, now?\n",
"\n",
"In Part 2, we discover that the prize is actually much farther away, by 10,000,000,000,000 (ten trillion) steps in both the X and Y coordinates. Theoretically, we could still use `solve_claw` with a bigger `limit`, but that would take a *long* time.\n",
"\n",
"Instead I can solve each claw machine *mathematically* rather than by trial and error. I know that an equation over the integers is called a [Diophantine equation](https://en.wikipedia.org/wiki/Diophantine_equation), but I don't remember much about them. What are the equations of a claw machine? For my first machine, \n",
"\n",
" Button A: X+24, Y+19\n",
" Button B: X+32, Y+86\n",
" Prize: X=1072, Y=2062\n",
"\n",
"there are two linear equations to solve simultaneously:\n",
"\n",
" 24 A + 32 B = 1072\n",
" 19 A + 86 B = 2062\n",
"\n",
"In general, two linear equations in two unknowns can have zero solutions (if the lines are parallel), an infinite number of solutions (if the lines are the same), or exactly one solution (otherwise). The further constraint that *A* and *B* have to be non-negative integers may rule out some of these solutions, but mostly we are looking for the one point where two lines intersect.\n",
"\n",
"I defined `solve_claw` to yield multiple solutions, but let's see how many solutions each claw machine yields:"
]
},
{
"cell_type": "code",
"execution_count": 128,
"id": "df8da2ae-52f9-409b-a54f-ad7d21b32e45",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Counter({0: 168, 1: 152})"
]
},
"execution_count": 128,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"Counter(quantify(solve_claw(c)) for c in claws)"
]
},
{
"cell_type": "markdown",
"id": "f0f97d47-5741-4c6e-ae96-5b3161ee2e56",
"metadata": {},
"source": [
"So, what is the intersection point of two lines? You could [look it up](https://www.wolframalpha.com/input?i=solve+for+a%2C+b%3A+p+*+a+%2B+q+*+b+%3D+r++and+++s+*+a+%2B+t+*+b+%3D+u) or work it out by hand. Given the two equations over the two variables *a* and *b*:\n",
"\n",
" p * a + q * b = r\n",
" s * a + t * b = u\n",
"\n",
"The solution is:\n",
"\n",
" a = (r * t - q * u) / (p * t - q * s)\n",
" b = (r * s - p * u) / (q * s - p * t)\n",
"\n",
"So we can rewrite `solve_claw` to use this:"
]
},
{
"cell_type": "code",
"execution_count": 130,
"id": "6bbd0934-d962-4c93-940b-810651e9e568",
"metadata": {},
"outputs": [],
"source": [
"def solve_claw(claw: Claw) -> Iterable[Tuple[int, int]]:\n",
" \"\"\"All possible (A-presses, B-presses) solutions to reach the prize on this machine.\"\"\"\n",
" ((p, s), (q, t), (r, u)) = claw\n",
" a = (r * t - q * u) / (p * t - q * s)\n",
" b = (r * s - p * u) / (q * s - p * t)\n",
" if a >= 0 and b >= 0 and a.is_integer() and b.is_integer():\n",
" yield (int(a), int(b))"
]
},
{
"cell_type": "markdown",
"id": "af52d98e-50e9-4c4d-875f-1ed2b3dc8111",
"metadata": {},
"source": [
"We'll need a function to move the claws further away, then we're ready to answer Part 2:"
]
},
{
"cell_type": "code",
"execution_count": 132,
"id": "dd38ba4c-44ba-426b-b1c8-0e10adbdd642",
"metadata": {},
"outputs": [],
"source": [
"def move_prize(claw, delta=(10**13, 10**13)) -> Claw:\n",
" \"\"\"Move the claw's prize by the given displacement, delta.\"\"\"\n",
" return claw._replace(Prize=(add(claw.Prize, delta)))"
]
},
{
"cell_type": "code",
"execution_count": 133,
"id": "9f578b3e-6b6d-4eb0-9228-c98122a84747",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 13.2: .000 seconds, answer 93217456941970 ok"
]
},
"execution_count": 133,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(13.2, 93217456941970, lambda:\n",
" sum(cheapest_solution(move_prize(claw)) for claw in claws))"
]
},
{
"cell_type": "markdown",
"id": "db917eed-07e5-4409-a92d-b26534a1fcd4",
"metadata": {},
"source": [
"The refactored `solve_claw` is compatible with Part 1, and should be a bit faster:"
]
},
{
"cell_type": "code",
"execution_count": 135,
"id": "609ed4ce-548c-4af4-8e09-c621aca0124e",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 13.1: .000 seconds, answer 29598 ok"
]
},
"execution_count": 135,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(13.1, 29598, lambda:\n",
" sum(map(cheapest_solution, claws)))"
]
},
{
"cell_type": "markdown",
"id": "2576ce68-3f1e-4887-ab0b-62fa94cbe55a",
"metadata": {},
"source": [
"# [Day 14](https://adventofcode.com/2024/day/14 ): Restroom Redoubt\n",
"\n",
"Today's narratives involve robots roaming around a restroom. Each line of the input describes a robot's position and velocity in two dimensions:"
]
},
{
"cell_type": "code",
"execution_count": 137,
"id": "1a5f5875-426d-47ea-a35a-405c39ced5dd",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 500 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"p=62,20 v=85,-14\n",
"p=88,18 v=-70,97\n",
"p=51,21 v=35,-22\n",
"p=19,56 v=2,45\n",
"p=93,11 v=-87,26\n",
"p=90,67 v=44,-13\n",
"p=99,34 v=82,57\n",
"p=55,42 v=82,22\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 500 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"(62, 20, 85, -14)\n",
"(88, 18, -70, 97)\n",
"(51, 21, 35, -22)\n",
"(19, 56, 2, 45)\n",
"(93, 11, -87, 26)\n",
"(90, 67, 44, -13)\n",
"(99, 34, 82, 57)\n",
"(55, 42, 82, 22)\n",
"...\n"
]
}
],
"source": [
"robots = parse(14, ints)"
]
},
{
"cell_type": "markdown",
"id": "818a939d-2038-4574-a20b-c1194d403f2c",
"metadata": {},
"source": [
"### Part 1: What will the safety factor be after exactly 100 seconds have elapsed?\n",
"\n",
"Each second the robots move according to their velocity, and they wrap around when they reach the edge of their world, which has width 101 and height 103. They can pass through each other. We're asked to figure out where they are after 100 seconds. I can see that it is not necessary to step the robots each time step; we can just multiply their velocity by time, add it to their initial position, and then take the result modulo the width or height. That's done in `robot_dance`. We're asked to figure out how many robots end up in each **quadrant**. There are four quadrants, but a robot that is exactly on a middle line, either horizontally or vertically, doesn't count). Finally, the **safety factor** is the product of the count of robots in each quadrant."
]
},
{
"cell_type": "code",
"execution_count": 139,
"id": "be22ac94-7401-4cf6-ab83-e43775536af7",
"metadata": {},
"outputs": [],
"source": [
"def robot_dance(robots, steps=100, width=101, height=103) -> List[Point]:\n",
" \"\"\"Move each robot for `steps` number of steps.\"\"\"\n",
" return [((x + steps * dx) % width, (y + steps * dy) % height)\n",
" for (x, y, dx, dy) in robots]\n",
"\n",
"def quadrant_counts(positions, w=101//2, h=103//2) -> Counter:\n",
" \"\"\"How many robots are in each quadrant? Ignore robots exactly in the middle.\"\"\"\n",
" return Counter((sign(x - w), sign(y - h)) \n",
" for (x, y) in positions if x != w and y != h)\n",
"\n",
"def safety_factor(robots) -> int:\n",
" \"\"\"The product of the counts in each of the four quadrants.\"\"\"\n",
" return prod(quadrant_counts(robot_dance(robots)).values())"
]
},
{
"cell_type": "code",
"execution_count": 140,
"id": "69093001-79aa-463a-b801-51cd5b4de4eb",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 14.1: .000 seconds, answer 216027840 ok"
]
},
"execution_count": 140,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(14.1, 216027840, lambda:\n",
" safety_factor(robots))"
]
},
{
"cell_type": "markdown",
"id": "a0cb27ad-82ff-4dcb-85cf-596b16ae9875",
"metadata": {},
"source": [
"### Part 2: What is the fewest number of seconds that must elapse for the robots to display a picture of a Christmas Tree?\n",
"\n",
"I was so sure Part 2 was going to ask for 10 trillion steps. But I was wrong. \n",
"\n",
"Instead they ask the very vague question of when a picture emerges. They gave the hint that \"*very rarely, most of the robots should arrange themselves into a picture of a Christmas tree.*\" Since I don't know exactly how to specify a Christmas tree picture, I see two options:\n",
"- I could build an animation, and watch as each frame is displayed, stopping when I see something that looks like a tree.\n",
"- The phrase \"*most of the robots should arrange themselves*\" suggests that most of the robots are in one area. I could check for that. \n",
"\n",
"I'll try the animation first. Google and Stack overflow led me to the `matplotlib.animation.FuncAnimation` function:"
]
},
{
"cell_type": "code",
"execution_count": 142,
"id": "664c686e-0c3d-43b8-970f-88c0bf47dbf6",
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.animation\n",
"\n",
"def animate_robots(robots, times: Ints):\n",
" \"\"\"Animate the robot dance over the given time steps.\"\"\"\n",
" plt.rcParams[\"animation.html\"] = \"jshtml\"\n",
" plt.rcParams['figure.dpi'] = 150 \n",
" plt.ioff()\n",
" fig, ax = plt.subplots()\n",
" def animate(t: int):\n",
" points = robot_dance(robots, t)\n",
" plt.cla()\n",
" ax.yaxis.set_inverted(True)\n",
" plt.plot(*T(points), 'o')\n",
" plt.title(f'{t} seconds')\n",
" return matplotlib.animation.FuncAnimation(fig, animate, frames=times)"
]
},
{
"cell_type": "markdown",
"id": "92048f28-a6a4-4c12-847f-62884177dec5",
"metadata": {},
"source": [
"Here's what it looks like:"
]
},
{
"cell_type": "code",
"execution_count": 144,
"id": "87843969-cb37-4fa5-9788-6a1b71c43521",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"
\n",
"\n",
"\n",
"\n"
],
"text/plain": [
""
]
},
"execution_count": 144,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"animate_robots(robots, range(3))"
]
},
{
"cell_type": "markdown",
"id": "6f722d21-dc5e-473c-b9d1-c71915cbeb6f",
"metadata": {},
"source": [
"The animations take up a lot of space in the stored `.ipynb` file, so I only show 3 frames here. I looked at hundreds of frames, but no Christmas tree emerged. \n",
"\n",
"So, on to the second idea. If the hint had mentioned \"*all of the robots*\", I would look for a frame with the minimum bounding box. But for \"*most of the robots*,\" I'll look for a frame that minimizes the mean distance from the centroid. I'll use `taxi_distance`; if that doesn't work I'll try regular `distance`:"
]
},
{
"cell_type": "code",
"execution_count": 146,
"id": "9563d49c-54a3-439f-a833-f48c2a070609",
"metadata": {},
"outputs": [],
"source": [
"def mean_distance_to_centroid(points, distance=taxi_distance) -> float:\n",
" \"\"\"The mean of each point's distance to the centroid.\"\"\"\n",
" centroid = (mean(Xs(points)), mean(Ys(points)))\n",
" return mean(distance(p, centroid) for p in points)\n",
"\n",
"def most_clustered_time(robots, times=range(10_000)) -> int:\n",
" \"\"\"Out of all the given time steps, the one that has the robots clustered closest together.\"\"\"\n",
" return min(times, key=lambda t: mean_distance_to_centroid(robot_dance(robots, t)))"
]
},
{
"cell_type": "code",
"execution_count": 147,
"id": "ab8c7e3b-f400-4976-ad0d-5f92cbadec02",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"
\n",
"\n",
"\n",
"\n"
],
"text/plain": [
""
]
},
"execution_count": 147,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"t = most_clustered_time(robots)\n",
"animate_robots(robots, [t])"
]
},
{
"cell_type": "markdown",
"id": "6a4794b4-1d3e-4d82-b886-4416ef5b6453",
"metadata": {},
"source": [
"**It worked!** \n",
"\n",
"(At first I had a **bug**: the Christmas tree was upside down, because the Y-axis was upside down. So I added the `yaxis.set_inverted` call to `animate_robots`.)"
]
},
{
"cell_type": "code",
"execution_count": 149,
"id": "75434bc8-35ae-4d8b-b747-01d773472541",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 14.2: 1.893 seconds, answer 6876 ok"
]
},
"execution_count": 149,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(14.2, 6876, lambda:\n",
" most_clustered_time(robots, range(7000)))"
]
},
{
"cell_type": "markdown",
"id": "08cae29b-9b07-4e8d-b2d1-f3798f6c7cd0",
"metadata": {},
"source": [
"# [Day 15](https://adventofcode.com/2024/day/15): Warehouse Woes\n",
"\n",
"Today's input is in two sections: the first section is a grid map describing a warehouse with walls (`#`) and boxes (`O`) and one robot (`@`). The second part is a set of instructions for how the robot moves (arrows). I'll parse the two sections as paragraphs, then parse each paragraph:"
]
},
{
"cell_type": "code",
"execution_count": 151,
"id": "20be45ec-f6fc-472c-9b49-872b7334528c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 71 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"##################################################\n",
"#..OO..O.O....O...O..O...O.......O...OO##....O.OO#\n",
"#O.#..#OOO#..O..OO...O.O.....OO..O#O#.OOO.....#..#\n",
"#.....O.......OO.#OO....#O.OO.O..OO.O.O.O..##....#\n",
"#.O....O...O#...#...OO..#..O........#O..#..O..O..#\n",
"#O....O...O.O..OO..OO..#OO.#OO.O......##..O..O...#\n",
"#..O.##..#O...O...#.#.O.O..O.#......O..#.#...O...#\n",
"#O.O.........O..O........OO....OO......O.....O#..#\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 2 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"##################################################\n",
"#..OO..O.O....O...O..O...O.......O...OO##.... ...\n",
">><^v><>^v<vv><<^^>>>>^^><>^<^^vvv>>><><>v^^^^<<^<^><>>>v><>>>^>><>v>v>^v>>v>>>v ...\n"
]
}
],
"source": [
"warehouse_woes = parse(15, sections=paragraphs)\n",
"warehouse = Grid(parse(warehouse_woes[0]))\n",
"arrows = cat(parse(warehouse_woes[1]))"
]
},
{
"cell_type": "markdown",
"id": "9a4aed93-5205-4946-b04c-637f3b095e4b",
"metadata": {},
"source": [
"### Part 1: What is the sum of all boxes' GPS coordinates after moving?\n",
"\n",
"The robot attempts to follow each instruction arrow (for example, moving East when the arrow is `>`). The robot is strong enough to push a line of any number of boxes sitting in front of it; that's its job. However, if a push is blocked by a wall, then neither the robot nor any boxes will move.\n",
"\n",
"The **GPS coordinate** of a box at (*x*, *y*) is *x* + 100*y*.\n",
"\n",
"I'll implement `obey_arrows` to make the arrow moves on a copy of the warehouse. For each arrow it finds all boxes that might move (and the robot) with `moveables`, then `make_movement` checks if they would bump into a wall, and if not, they all move."
]
},
{
"cell_type": "code",
"execution_count": 153,
"id": "248d51c0-424e-4bbb-bbf4-37cdc18313e1",
"metadata": {},
"outputs": [],
"source": [
"def gps_coordinate(box: Point) -> int: return X_(box) + 100 * Y_(box)\n",
"\n",
"def obey_arrows(warehouse: Grid, arrows: str) -> Grid:\n",
" \"\"\"Return a grid in which the robot has followed `arrows`, pushing boxes around.\"\"\"\n",
" grid = warehouse.copy()\n",
" robot_pos = the(grid.findall('@'))\n",
" for arrow in arrows:\n",
" dir = arrow_direction[arrow]\n",
" robot_and_boxes = moveables(grid, robot_pos, dir)\n",
" if make_movement(grid, robot_and_boxes, dir):\n",
" robot_pos = add2(robot_pos, dir)\n",
" return grid\n",
"\n",
"def moveables(grid, start: Point, dir: Vector) -> List[Point]:\n",
" \"\"\"The positions of moveable things (the robot and maybe boxes) going in direction `dir` from the `start`.\"\"\"\n",
" def moveable(pos): return grid[pos] in ('@', 'O')\n",
" return list(takewhile(moveable, grid.follow_line(start, dir)))\n",
"\n",
"def make_movement(grid, to_move: List[Point], dir: Vector) -> bool:\n",
" \"\"\"Try to move the objects in the `to_move` positions in direction `dir`; return True if they move.\"\"\"\n",
" if grid[add2(to_move[-1], dir)] == '#':\n",
" return False\n",
" else:\n",
" for p in reversed(to_move):\n",
" grid[add2(p, dir)] = grid[p]\n",
" grid[p] = '.'\n",
" return True"
]
},
{
"cell_type": "code",
"execution_count": 154,
"id": "52e93218-6ac2-4cf7-aced-f4f3a3df77b5",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 15.1: .025 seconds, answer 1563092 ok"
]
},
"execution_count": 154,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(15.1, 1563092, lambda:\n",
" sum(map(gps_coordinate, obey_arrows(warehouse, arrows).findall('O'))))"
]
},
{
"cell_type": "markdown",
"id": "84dcd7fd-0f4a-4377-b61e-22bd5a0a51a9",
"metadata": {},
"source": [
"### Part 2: What is the sum of all boxes' final GPS coordinates on the double-wide grid?\n",
"\n",
"In Part 2, there is another warehouse that is similar to the first, but twice as wide. Each position in the original warehouse is replaced by two copies of the contents, except that the robot is replaced by `@.` and a box, `O`, is replaced by `[]`, indicating the two halves of a double-wide box. The rules for movement and pushing are the same, except that when the robot is moving North or South and is pushing a box, that box, since it is two positions wide, can push *two* boxes if they are lined up right. (Presumably, those two boxes could then push three boxes in the next row, and so on, although the problem description did not explicitly specify that). Finally, the GPS coordinates are taken for the `[` part of the box.\n",
"\n",
"So this is mostly the same as Part 1, but the criteria of what to move is different. Here are the changes I'll make:\n",
"- I'll need to make the double-wide grid, by applying `doublewide` to the original text to get `warehouse2`.\n",
"- I'll redefine `make_movement` to check for a wall in front of *any* of the boxes, not just the last one, and to move them all in one `update`, rather than one at a time.\n",
"- I'll redefine `moveables` to deal with either regular or double-wide boxes (but not both in one maze).\n",
"\n",
"The redefinitions are backwards-comnpatible, which I'll demonstrate by re-running Part 1 before running Part 2:"
]
},
{
"cell_type": "code",
"execution_count": 156,
"id": "2d9afbc1-75b2-479d-81fc-fbab69d25753",
"metadata": {},
"outputs": [],
"source": [
"def doublewide(text: str) -> str:\n",
" \"\"\"Make the grid twice as wide.\"\"\"\n",
" return text.replace('#', '##').replace('O', '[]').replace('.', '..').replace('@', '@.')\n",
" \n",
"warehouse2 = Grid(parse(doublewide(warehouse_woes[0])))\n",
"\n",
"def make_movement(grid, to_move: Collection[Point], dir: Vector) -> bool:\n",
" \"\"\"Try to move the objects in the `to_move` positions in direction `dir`; return True if they move.\"\"\"\n",
" def go(p): return add2(p, dir)\n",
" if any(grid[go(p)] == '#' for p in to_move):\n",
" return False\n",
" else:\n",
" updates = {go(p): grid[p] for p in to_move}\n",
" grid.update({p: '.' for p in to_move})\n",
" grid.update(updates)\n",
" return True\n",
"\n",
"def moveables(grid, start_pos, dir) -> Set[Point]:\n",
" \"\"\"The positions of moveable things (robot and maybe boxes) going in direction from the start.\"\"\" \n",
" ahead = add2(start_pos, dir)\n",
" if dir in (East, West) or grid[ahead] == 'O': # Single line push\n",
" def moveable(pos): return grid[pos] in ('@', 'O', '[', ']')\n",
" return set(takewhile(moveable, grid.follow_line(start_pos, dir)))\n",
" else: # Potential non-linear push\n",
" results = {start_pos}\n",
" if grid[ahead] in ('[', ']'): results |= moveables(grid, ahead, dir) \n",
" if grid[ahead] == '[': results |= moveables(grid, add2(ahead, East), dir)\n",
" if grid[ahead] == ']': results |= moveables(grid, add2(ahead, West), dir)\n",
" return results"
]
},
{
"cell_type": "code",
"execution_count": 157,
"id": "7af49bbf-dd10-4221-9096-6f548dec44c0",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 15.1: .029 seconds, answer 1563092 ok"
]
},
"execution_count": 157,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(15.1, 1563092, lambda:\n",
" sum(map(gps_coordinate, obey_arrows(warehouse, arrows).findall('O'))))"
]
},
{
"cell_type": "code",
"execution_count": 158,
"id": "99246602-a51e-41aa-a7e9-7cdbc8d449ca",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 15.2: .042 seconds, answer 1582688 ok"
]
},
"execution_count": 158,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(15.2, 1582688, lambda:\n",
" sum(map(gps_coordinate, obey_arrows(warehouse2, arrows).findall('['))))"
]
},
{
"cell_type": "markdown",
"id": "4361f8c3-1d15-441e-9a41-b1ea20707651",
"metadata": {},
"source": [
"I had a frustrating time **debugging** this one; this was by far my worst performance. First I had a silly typo in `moveables`; I was able to fix it after looking at the results on the smaller test problem:"
]
},
{
"cell_type": "code",
"execution_count": 160,
"id": "47c99f0f-ab65-4ae1-a308-42668feacdd5",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"##############\n",
"##...[].##..##\n",
"##...@.[]...##\n",
"##....[]....##\n",
"##..........##\n",
"##..........##\n",
"##############\n"
]
}
],
"source": [
"xwarehouse = Grid(parse(doublewide(\"\"\"\\\n",
"#######\n",
"#...#.#\n",
"#.....#\n",
"#..OO@#\n",
"#..O..#\n",
"#.....#\n",
"#######\"\"\")))\n",
"\n",
"xarrows = \" Node:\n",
" \"\"\"The lowest-cost path through the maze.\"\"\"\n",
" start, end = the(maze.findall('S')), the(maze.findall('E'))\n",
" problem = MazeSearchProblem(grid=maze, initial=State(start, East), goal=end)\n",
" return A_star_search(problem)\n",
"\n",
"class MazeSearchProblem(GridProblem):\n",
" \"\"\"A GridProblem where a turn costs 1000 points, a step ahead 1.\"\"\"\n",
" \n",
" def actions(self, state):\n",
" ahead = add2(state.pos, state.facing)\n",
" return ['L', 'R'] + ([ahead] if self.grid[ahead] != '#' else [])\n",
" \n",
" def result(self, state, action) -> State:\n",
" return (State(state.pos, make_turn(state.facing, action)) if action in ('L', 'R') else\n",
" State(add2(state.pos, state.facing), state.facing))\n",
" \n",
" def action_cost(self, s1, action, s2) -> int: return 1000 if action in ('L', 'R') else 1\n",
"\n",
" def is_goal(self, state) -> bool: return self.grid[state.pos] == 'E'\n",
"\n",
" def h(self, node) -> int: return taxi_distance(node.state.pos, self.goal)"
]
},
{
"cell_type": "code",
"execution_count": 166,
"id": "4974c14c-86da-4b86-a55c-6ae8781df207",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 16.1: .147 seconds, answer 103512 ok"
]
},
"execution_count": 166,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(16.1, 103512, lambda:\n",
" reindeer_path(maze).path_cost)"
]
},
{
"cell_type": "markdown",
"id": "030e5786-cd2a-405f-b4d4-460cefab7e32",
"metadata": {},
"source": [
"### Part 2: How many tiles are part of at least one of the best paths through the maze?\n",
"\n",
"In Part 2 we're asked to find the total number of positions that are part of *any* best path from start to end. I'll write a new version of `best_first_search` to return all the paths that have the same cost as the best path. I thought this would be easy, but I had **another bug** where I forgot to change the `<` to a `<=` in the fourth-to-last line of `all_paths_best_first_search`:"
]
},
{
"cell_type": "code",
"execution_count": 168,
"id": "692abfea-f9c8-477c-8369-386cdd2a7606",
"metadata": {},
"outputs": [],
"source": [
"def reindeer_best_positions(maze) -> Node:\n",
" \"\"\"All the positions on any best path from start to end.\"\"\"\n",
" start, end = the(maze.findall('S')), the(maze.findall('E'))\n",
" problem = MazeSearchProblem(grid=maze, initial=State(start, East), goal=end)\n",
" paths = list(all_paths_best_first_search(problem, f=lambda n: n.path_cost + problem.h(n)))\n",
" return union({state.pos for state in path_states(path)} for path in paths)\n",
"\n",
"def all_paths_best_first_search(problem, f) -> List[Node]:\n",
" \"Search nodes with minimum f(node) value first, return all paths with minimum cost.\"\n",
" node = Node(problem.initial)\n",
" frontier = PriorityQueue([node], key=f)\n",
" reached = {problem.initial: node}\n",
" while frontier:\n",
" node = frontier.pop()\n",
" if problem.is_goal(node.state):\n",
" return [node] + [path for (cost, path) in frontier.items\n",
" if cost == node.path_cost and problem.is_goal(path.state)]\n",
" for child in expand(problem, node):\n",
" s = child.state\n",
" if s not in reached or child.path_cost <= reached[s].path_cost:\n",
" reached[s] = child\n",
" frontier.add(child)\n",
" return []"
]
},
{
"cell_type": "code",
"execution_count": 169,
"id": "ea9bf9f3-0e6b-4949-a641-6b3db2fd9d32",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 16.2: .855 seconds, answer 554 ok"
]
},
"execution_count": 169,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(16.2, 554, lambda:\n",
" len(reindeer_best_positions(maze)))"
]
},
{
"cell_type": "markdown",
"id": "91607c18-56c3-4432-8da8-cfe570887bc0",
"metadata": {},
"source": [
"# [Day 17](https://adventofcode.com/2024/day/17): Chronospatial Computer\n",
"\n",
"Today we have to help debug a faulty computer. This particular computer has three registers, A, B, and C, and a program consisting of a sequence of octal digits. The input is a description of the state of the computer:"
]
},
{
"cell_type": "code",
"execution_count": 171,
"id": "50fd3cb8-0a6f-4edd-b5cb-f7b1e6f9d987",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 5 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Register A: 52042868\n",
"Register B: 0\n",
"Register C: 0\n",
"\n",
"Program: 2,4,1,7,7,5,0,3,4,4,1,7,5,5,3,0\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 5 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"('Register', 'A', 52042868)\n",
"('Register', 'B', 0)\n",
"('Register', 'C', 0)\n",
"()\n",
"('Program', 2, 4, 1, 7, 7, 5, 0, 3, 4, 4, 1, 7, 5, 5, 3, 0)\n"
]
},
{
"data": {
"text/plain": [
"Computer(A=52042868, B=0, C=0, prog=[2, 4, 1, 7, 7, 5, 0, 3, 4, 4, 1, 7, 5, 5, 3, 0])"
]
},
"execution_count": 171,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"Computer = namedtuple('Computer', 'A, B, C, prog')\n",
" \n",
"def initialize(information) -> Computer:\n",
" \"\"\"Initialize the computer with the debugging information.\"\"\"\n",
" kwds = {}\n",
" for info in information:\n",
" match info:\n",
" case ['Register', L, val]: kwds[L] = val\n",
" case ['Program', *vals]: kwds['prog'] = vals\n",
" return Computer(**kwds)\n",
" \n",
"computer = initialize(parse(17, atoms))\n",
"computer"
]
},
{
"cell_type": "markdown",
"id": "031ec910-e249-487b-ae95-b98abae3b9cf",
"metadata": {},
"source": [
"### Part 1: What do you get if you use commas to join the output values?\n",
"\n",
"Now we have to run the program. The computer has eight opcodes; each opcode operates on the following value in memory; some instructions take the operand literally, and some compute a **combo** value from it (equal to register A, B, or C if the value is 4, 5, or 6, respectively; otherwise the value itself). There is a program counter (pc) that increments by 2, unless there is a branch instuction (opcode 3). See the [day's description](https://adventofcode.com/2024/day/17) for details on how the opcodes work. Or look at my `run_program` function, which yields the outputs from running the program:"
]
},
{
"cell_type": "code",
"execution_count": 173,
"id": "d98f88cc-c435-43fc-bcb5-52e6dd70fdb1",
"metadata": {},
"outputs": [],
"source": [
"def run_program(computer) -> Iterable[int]:\n",
" \"\"\"Run the program on the computer, yielding each output.\"\"\"\n",
" A, B, C, prog = computer\n",
" pc = 0\n",
" while pc < len(prog):\n",
" op, val = prog[pc:pc+2]\n",
" pc += 2\n",
" combo = (A if val == 4 else B if val == 5 else C if val == 6 else val)\n",
" match op:\n",
" case 0: A = A // (2 ** combo)\n",
" case 6: B = A // (2 ** combo)\n",
" case 7: C = A // (2 ** combo)\n",
" case 1: B = B ^ val\n",
" case 4: B = B ^ C\n",
" case 2: B = combo % 8\n",
" case 5: yield combo % 8\n",
" case 3: \n",
" if A: pc = val"
]
},
{
"cell_type": "markdown",
"id": "6348d561-9ac9-437c-8bcd-6fbd91d1ebf5",
"metadata": {},
"source": [
"The answer for Part 1 is easy:"
]
},
{
"cell_type": "code",
"execution_count": 175,
"id": "860f24e5-92ad-4361-8920-102ebc573598",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 17.1: .000 seconds, answer 2,1,0,1,7,2,5,0,3 ok"
]
},
"execution_count": 175,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(17.1, '2,1,0,1,7,2,5,0,3', lambda:\n",
" cat(run_program(computer), ','))"
]
},
{
"cell_type": "markdown",
"id": "d3248e42-ab8e-4fdb-8e19-439766275c37",
"metadata": {},
"source": [
"### Part 2: What is the lowest positive initial value for register A that causes the program to output a copy of itself?\n",
"\n",
"In Part 2, we find that register A has been corrupted, and we need to restore it to the value that will make the program output a copy of itself (a [Quine](https://en.wikipedia.org/wiki/Quine_%28computing%29)). I was afraid of this! AoC always has a problem where you have to write an interpreter for a program in some obscure language, but then in Part 2 you have to actually understand what the program is doing; you can't use brute force. (I tried brute force up to A=10,000,000 with no luck.)\n",
"\n",
"To try to understand my program, here it is in pseudocode:\n",
"\n",
" top: B = A % 8 # 2, 4\n",
" B = B ^ 7 # 1, 7\n",
" C = A / 2 ** B # 7, 5\n",
" A = A / 2 ** 3 # 0, 3\n",
" B = B ^ C # 4, 4\n",
" B = B ^ 7 # 1, 7\n",
" output B # 5, 5\n",
" if A: goto top # 3, 0\n",
"\n",
"I can summarize that as:\n",
"\n",
" top: B and C are defined in terms of the last octal digit of A, and prior value of B\n",
" A is shifted to eliminate the last octal digit\n",
" output B \n",
" if A: goto top \n",
"\n",
"So I realized that one octal digit of `A` is eliminated on each pass through the loop, and when `A` hits zero, we exit. Each pass outputs one octal digit, so `A` in octal has to be the same length as my program; somewhere in the ten trillion range. Good thing I gave up on brute force. \n",
"\n",
"I realized that I should go right-to-left, outputing one octal digit at a time, and appending one octal digit at a time to `A`. After some trial and error I got the following:"
]
},
{
"cell_type": "code",
"execution_count": 177,
"id": "bb745303-dd20-486f-bbdd-7ae77f995c2c",
"metadata": {},
"outputs": [],
"source": [
"def run_with(computer=computer, **kwds) -> Ints: \n",
" \"\"\"Run the program with registers set to the given values.\"\"\"\n",
" return list(run_program(computer._replace(**kwds)))"
]
},
{
"cell_type": "code",
"execution_count": 178,
"id": "54ac9b9c-70d9-4356-91c5-40fb285634b4",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[2, 4, 1, 7, 7, 5, 0, 3, 4, 4, 1, 7, 5, 5, 3, 0]"
]
},
"execution_count": 178,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"computer.prog"
]
},
{
"cell_type": "code",
"execution_count": 179,
"id": "36d89d9a-c8fc-41be-820b-cb13e40793c0",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[0]"
]
},
"execution_count": 179,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"run_with(A=0o7)"
]
},
{
"cell_type": "code",
"execution_count": 180,
"id": "892fef38-5f9b-4370-a242-f7a10df5487b",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[3, 0]"
]
},
"execution_count": 180,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"run_with(A=0o72)"
]
},
{
"cell_type": "code",
"execution_count": 181,
"id": "b3d11d5e-30c2-419f-bc11-3f26fbdddfbb",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[5, 3, 0]"
]
},
"execution_count": 181,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"run_with(A=0o726)"
]
},
{
"cell_type": "markdown",
"id": "f40f775f-bb9d-459c-80c3-ac09b6b42904",
"metadata": {},
"source": [
"That is, my program ends in `5,3,0`, and an octal `7` outputs a `0`; an octal `0o72` outputs `3,0`, and octal `0o726` outputs `5,3,0`. So here's my approach for finding the Quine program:\n",
"- I'm going to keep a set of candidate values for `A` as the set `As`.\n",
"- The set starts with just `{0}`.\n",
"- On each iteration I try appending each octal digit to each element of the set `As`.\n",
"- I keep the candidate `A` values whose output matches the tail of the program's output.\n",
"- Iterate this for each digit and return the set of `A` values that produce the whole program.\n",
"- Take the minium of the set."
]
},
{
"cell_type": "code",
"execution_count": 183,
"id": "a49c6de9-4e6b-47e5-bcf0-2972a95c1af3",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 17.2: .024 seconds, answer 267265166222235 ok"
]
},
"execution_count": 183,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def quine(computer) -> Set[int]:\n",
" \"\"\"Find the values of `A` that cause the output to match the program.\"\"\"\n",
" As = {0}\n",
" for d in reversed(range(len(computer.prog))):\n",
" tail = computer.prog[d:]\n",
" candidates = {(A << 3) + i for A in As for i in range(8)}\n",
" As = {A for A in candidates if run_with(A=A) == tail}\n",
" return As\n",
"\n",
"answer(17.2, 267265166222235, \n",
" lambda: min(quine(computer)))"
]
},
{
"cell_type": "markdown",
"id": "c7f9e900-45be-401f-a6f2-a1d7b751fae6",
"metadata": {},
"source": [
"# [Day 18](https://adventofcode.com/2024/day/18): RAM Run\n",
"\n",
"In today's narrative, we're inside a computer, on a 2D memory board, and bytes are falling down, at specified (x, y) positions, as given in our input:"
]
},
{
"cell_type": "code",
"execution_count": 185,
"id": "d14e1966-2feb-4553-9a0a-12595ef4f7d7",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 3450 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"40,65\n",
"17,1\n",
"34,45\n",
"31,51\n",
"29,43\n",
"25,9\n",
"14,27\n",
"5,29\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 3450 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"(40, 65)\n",
"(17, 1)\n",
"(34, 45)\n",
"(31, 51)\n",
"(29, 43)\n",
"(25, 9)\n",
"(14, 27)\n",
"(5, 29)\n",
"...\n"
]
}
],
"source": [
"falling_bytes = parse(18, ints)"
]
},
{
"cell_type": "markdown",
"id": "1229cec7-a456-4dd6-a668-8a00591c63f7",
"metadata": {},
"source": [
"### Part 1: What is the minimum number of steps needed to reach the exit?\n",
"\n",
"When a byte falls it creates a barrier. Our task is to find a path that avoids the barriers, from the start in the upper left to the exit in the lower right of a 71 x 71 grid that is the memory board. \n",
"\n",
"This is another search problem, like the maze in Day 16, but without the complications (e.g. costs for turning right or left). The problem description for today says that we should first consider just the first kilobyte (1024 falling bytes), and I was worried that if I just hand those points to my `Grid` class, it wouldn't cover the whole 71 x 71 grid. Therefore, I created a grid with empty spaces, and then updated with the falling bytes. The function `memory_path` returns a path, and we can then ask for its length to get the answer."
]
},
{
"cell_type": "code",
"execution_count": 187,
"id": "83af4751-38c9-4830-a2fa-78515b59bc97",
"metadata": {},
"outputs": [],
"source": [
"def memory_path(falling_bytes: Tuple[Point], width=71, height=71) -> Grid:\n",
" \"\"\"Make a Grid of the given size with the points as obstacles.\"\"\"\n",
" grid = Grid(['.' * width] * height)\n",
" grid.update({p: '#' for p in falling_bytes})\n",
" problem = GridProblem(grid=grid, initial=(0, 0), goal=sub(grid.size, (1, 1)))\n",
" return A_star_search(problem)"
]
},
{
"cell_type": "code",
"execution_count": 188,
"id": "29da25e2-f3c2-43e3-8769-1d4fcecb807b",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 18.1: .014 seconds, answer 344 ok"
]
},
"execution_count": 188,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(18.1, 344, lambda:\n",
" len(memory_path(falling_bytes[:1024])))"
]
},
{
"cell_type": "markdown",
"id": "792bc31e-964d-47fe-a09b-6a95724e641e",
"metadata": {},
"source": [
"### Part 2: What are the coordinates of the first byte that will prevent the exit from being reachable from your starting position?\n",
"\n",
"After 1024 bytes fall there is a path from start to exit, but as more bytes fall we might have to switch to a different path, and eventually there will be no path. We're asked for the `x,y` position of the first falling byte that blocks off all paths. I can think of two ways to handle this:\n",
"1) Add falling bytes one at a time and repeat the A-star search each time. **Slow!**\n",
"2) Add falling bytes in binary search fashion: We know adding no bytes is **good** for getting a path and adding all of them is **bad**; try half way and then update **good** or **bad** depending on whether we found a path. **Fast!**"
]
},
{
"cell_type": "code",
"execution_count": 190,
"id": "4c0a8dcb-c3af-45e7-9273-8776e8c3ea1d",
"metadata": {},
"outputs": [],
"source": [
"def memory_blocker(falling_bytes, good=1024) -> Point:\n",
" \"\"\"Which falling byte is the first to block a path to the exit? Do binary search.\"\"\"\n",
" bad = len(falling_bytes)\n",
" while bad - good > 1:\n",
" mid = (good + bad) // 2\n",
" if memory_path(falling_bytes[:mid]) == search_failure:\n",
" bad = mid\n",
" else:\n",
" good = mid\n",
" return falling_bytes[bad - 1]"
]
},
{
"cell_type": "code",
"execution_count": 191,
"id": "22371144-5d51-440a-918f-a63de73b13ad",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 18.2: .033 seconds, answer 46,18 ok"
]
},
"execution_count": 191,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(18.2, '46,18', lambda:\n",
" cat(memory_blocker(falling_bytes), ','))"
]
},
{
"cell_type": "markdown",
"id": "552f1c6e-052e-43d0-b13f-e8f66f274d63",
"metadata": {},
"source": [
"I admit I initially had an off-by-one **bug** here. I was inconsistent on whether `bad` should be an index into the falling bytes, or the stop of a slice.\n",
"\n",
"I realize that for Part 1 I should have had separate functions for making the grid and solving it; then in Part 2 I could have incrementally modified the grid each time rather than creating a whole new Grid (and Problem). But it runs in under 1/10 second, so it is not worth changing it."
]
},
{
"cell_type": "markdown",
"id": "702d6ea3-bcb1-4c90-8d67-9e686cd0155f",
"metadata": {},
"source": [
"# [Day 19](https://adventofcode.com/2024/day/19): Linen Layout\n",
"\n",
"Today's input is in two sections, the first a list of towel designs (with each letter of a word specifying a color), and the second section a list of desired patterns to be made by adjoining towels."
]
},
{
"cell_type": "code",
"execution_count": 194,
"id": "689df92b-92d7-44e6-8b4e-d67bf3f153df",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 402 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"rub, wugrr, gww, wgurb, urur, ggb, bw, uubbu, uw, uggru, gwwgrgb, wbbb, rgb, wuuur, bgruggug, ru ...\n",
"\n",
"rurgggubugbuwugbwgggwbwwwrbrbgwwwwuwuugrwbrbwgguwr\n",
"ruurbrwgrurubwrurugubgurgruurwgugwgrwuwbrww\n",
"gwbrwwruwubrwgubggggubwwrurugbbuuugwwburbw\n",
"wwgwugrrrbwbgwburguwgbrgrwurugbbgubuuugbguur\n",
"ubgbbrruwbgbrrgwwwwwrwgggggrwbgwuwgrwrbwbrg\n",
"rurbbgrbwbggugrbwwbwrwuwuwbwwbwugwrubbww\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 2 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"('rub', 'wugrr', 'gww', 'wgurb', 'urur', 'ggb', 'bw', 'uubbu', 'uw', 'uggru', 'gwwgrgb', 'wbbb', ...\n",
"('rurgggubugbuwugbwgggwbwwwrbrbgwwwwuwuugrwbrbwgguwr', 'ruurbrwgrurubwrurugubgurgruurwgugwgrwuwb ...\n"
]
}
],
"source": [
"patterns, designs = parse(19, atoms, sections=paragraphs)"
]
},
{
"cell_type": "markdown",
"id": "581ff31b-ec09-4ae5-84c7-3b3ffbbd9362",
"metadata": {},
"source": [
"### Part 1: How many designs are possible?\n",
"\n",
"The task is to see how many of the designs can posibly be made by concatenating one or more of the patterns (with repetitions allowed). I can do this by checking if some pattern starts the design and recursively checking if the rest of the design is possible:"
]
},
{
"cell_type": "code",
"execution_count": 196,
"id": "e0b96545-5f60-4c8e-9a0f-c77608c19128",
"metadata": {},
"outputs": [],
"source": [
"def is_possible_design(design, patterns=patterns) -> bool:\n",
" \"\"\"Is it possible to make `design` by concatenating some of the `patterns`?\"\"\"\n",
" return (design == '' \n",
" or any(design.startswith(p) and is_possible_design(design[len(p):], patterns) \n",
" for p in patterns))"
]
},
{
"cell_type": "code",
"execution_count": 197,
"id": "3c3fba1b-d3e5-494e-aad0-42ca0566ae1f",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 19.1: .040 seconds, answer 242 ok"
]
},
"execution_count": 197,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(19.1, 242, lambda:\n",
" quantify(designs, is_possible_design))"
]
},
{
"cell_type": "markdown",
"id": "c7a03f27-c1af-4561-a887-ff4f9b7b0b51",
"metadata": {},
"source": [
"### Part 2: What do you get if you add up the number of different ways you could make each design?\n",
"\n",
"For Part 2 we need to count all the ways that each design can be made. Since there can be an exponential number of ways for each design, I'm going to `cache` intermediate results. The number of ways is computed by consiering each pattern that starts the design and recursively summing up the count of the number of ways for the rest of the design."
]
},
{
"cell_type": "code",
"execution_count": 199,
"id": "77d4fae1-d506-4733-9bb2-467619012f97",
"metadata": {},
"outputs": [],
"source": [
"@cache\n",
"def count_ways(design, patterns=patterns) -> int:\n",
" \"\"\"How many ways can the design be made from the patterns?\"\"\"\n",
" return (1 if design == '' else\n",
" sum(count_ways(design[len(p):], patterns)\n",
" for p in patterns if design.startswith(p)))"
]
},
{
"cell_type": "code",
"execution_count": 200,
"id": "14116eca-0b0f-484a-a169-9726e4ac7fbf",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 19.2: .183 seconds, answer 595975512785325 ok"
]
},
"execution_count": 200,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(19.2, 595975512785325, lambda:\n",
" sum(map(count_ways, designs)))"
]
},
{
"cell_type": "markdown",
"id": "b1199e79-bb09-4d68-89e6-b235c5f987c5",
"metadata": {},
"source": [
"That was so easy that I'm willing to explore a bit more. First, here's a check that says a design is possible if and only if there is at least one way to make it: "
]
},
{
"cell_type": "code",
"execution_count": 202,
"id": "f992b197-be14-47dc-8678-10cc63a1afd7",
"metadata": {},
"outputs": [],
"source": [
"for d in designs:\n",
" assert is_possible_design(d) == (count_ways(d) >= 1)"
]
},
{
"cell_type": "markdown",
"id": "a003849a-ae35-4c86-8d48-b954908d636b",
"metadata": {},
"source": [
"Here is an alternative approach to Part 1 that turns out to be faster. It uses one big regular expression to match against all the designs. The regular expression matches one or more repetitions of one of the patterns, as a whole-line match. This is matched against all the designs, concatened together with newlines. We do this all in one call to `re.compile` and one call to `findall`, to keep as much as possible running in the C library rather than in user-written Python code. (Unfortunately, this approach cannot be applied directly to Part 2.)"
]
},
{
"cell_type": "code",
"execution_count": 204,
"id": "57296f79-d0a0-4de3-bb1c-b3ed84ba1a59",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 19.1: .004 seconds, answer 242 ok"
]
},
"execution_count": 204,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def possible_designs(designs, patterns) -> List[str]:\n",
" \"\"\"All the `designs` that can be made by concatenating `patterns`.\"\"\"\n",
" regex = re.compile(r\"^(\" + \"|\".join(patterns) + r\")+$\", re.MULTILINE)\n",
" return regex.findall('\\n'.join(designs))\n",
"\n",
"answer(19.1, 242, lambda:\n",
" len(possible_designs(designs, patterns)))"
]
},
{
"cell_type": "markdown",
"id": "efc9dc54-cdcc-40d5-85d5-b8ed8da61ba2",
"metadata": {},
"source": [
"# [Day 20](https://adventofcode.com/2024/day/20): Race Condition\n",
"\n",
"Yet another problem with a grid, this one depicting a racetrack:"
]
},
{
"cell_type": "code",
"execution_count": 206,
"id": "156dcbf7-79ec-41a7-a9f2-397a827b9856",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 141 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"################################################################################################ ...\n",
"#...#...#...........#...#...#...#...#.....#...###...#...###.........#...#...#.......#...#...#... ...\n",
"#.#.#.#.#.#########.#.#.#.#.#.#.#.#.#.###.#.#.###.#.#.#.###.#######.#.#.#.#.#.#####.#.#.#.#.#.#. ...\n",
"#.#...#.#...#.....#...#...#...#.#.#.#...#...#...#.#...#...#.......#...#...#.#.....#.#.#...#.#.#. ...\n",
"#.#####.###.#.###.#############.#.#.###.#######.#.#######.#######.#########.#####.#.#.#####.#.#. ...\n",
"#.#.....###...###.......#.......#.#.....#.......#.......#...#.....#...#.....#...#.#.#.....#...#. ...\n",
"#.#.###################.#.#######.#######.#############.###.#.#####.#.#.#####.#.#.#.#####.#####. ...\n",
"#.#.#...###...#...#.....#.......#.......#.###...#...###...#.#...#...#...###...#.#.#...###.....#. ...\n",
"...\n"
]
}
],
"source": [
"racetrack = Grid(parse(20))"
]
},
{
"cell_type": "markdown",
"id": "57918fd0-bb72-4c5e-8e1f-3bcf164a1d72",
"metadata": {},
"source": [
"### How many cheats would save you at least 100 picoseconds?\n",
"\n",
"We are told that the grid depicts a single path from start (`S`) to end (`E`). Each step (in one of the four cardinal directions) takes one picosecond. But you are allowed to cheat once during the race by going through a wall: you can take two steps where the first step is into a wall and the second is back on the track. We are asked how many such cheats would save 100 picoseconds or more.\n",
"\n",
"This is an all-paths-to-the-goal problem, which should make you think [Dijkstra's algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm). My function `dijkstra` will return a dict of `{(x, y): distance_to_end}`. It works by maintaining a queue, `Q` of points to be considered, and one at a time popping a point off the queue, and for each neighbor that is not a wall, add the neighbor to the queue and record the distance to the neighbor as being one more than the distance to the point in the dict `D`. Since we are told the grid is single-path, we don't have to worry about updating entries in `D` for a second path to a point.\n",
"\n",
"Then my function `cheats` yields all `(start_position, end_position, time_saved)` tuples where the time saved is at least the given lower bound."
]
},
{
"cell_type": "code",
"execution_count": 208,
"id": "4c4ef05c-b548-49f9-b092-847e9752e745",
"metadata": {},
"outputs": [],
"source": [
"def dijkstra(grid, end_char='E') -> Dict[Point, int]:\n",
" \"\"\"All-paths distances from each point to the end square on the grid: {(x, y): distance}.\"\"\"\n",
" end = the(grid.findall(end_char))\n",
" D = {end: 0}\n",
" Q = [end]\n",
" while Q:\n",
" p = Q.pop()\n",
" for p2 in grid.neighbors(p):\n",
" if grid[p2] != '#' and p2 not in D:\n",
" Q.append(p2)\n",
" D[p2] = D[p] + 1\n",
" return D\n",
"\n",
"def cheats(racetrack, lower_bound=1) -> Iterable[Tuple[Point, Point, int]]:\n",
" \"\"\"All ways of cheating by taking one step onto a wall and a second step back on track.\"\"\"\n",
" D = dijkstra(racetrack, 'E')\n",
" return ((p1, p3, t)\n",
" for p1 in D\n",
" for p2 in racetrack.neighbors(p1) if racetrack[p2] == '#'\n",
" for p3 in racetrack.neighbors(p2)\n",
" if p3 in D and (t := D[p1] - D[p3] - 2) >= lower_bound)"
]
},
{
"cell_type": "code",
"execution_count": 209,
"id": "1bbd8b72-c503-4384-aaea-a5bed45a4491",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 20.1: .029 seconds, answer 1343 ok"
]
},
"execution_count": 209,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(20.1, 1343, lambda:\n",
" quantify(cheats(racetrack, 100)))"
]
},
{
"cell_type": "markdown",
"id": "0f86c3a4-47e7-4731-8acf-9133b3e41838",
"metadata": {},
"source": [
"At first I had a puzzling **bug**. I had a lot of confidence in my code, so I addressed it by carefully re-reading the problem description. Then I realized my mistake: I didn't count the 2 picoseconds of cheating as part of the total time. I fixed that by inserting the \"`-2`\" in the last line of `cheats`. In the process of carefully re-reading, I realized that the path through the grid is a single path; I didn't need a queue of points in `dijkstra`; the queue will always be just one point. But changing it wouldn't have a big effect on efficiency, so I'll keep it as is.\n",
"\n",
"### How many big cheats would save you at least 100 picoseconds?\n",
"\n",
"In Part 2 you can use a much bigger cheat, of up to 20 picoseconds. (But you can still only use one cheat.) I'll tackle this by looking at all points in a neighborhood of radius 20 from each starting point on the path."
]
},
{
"cell_type": "code",
"execution_count": 211,
"id": "d370e24c-9b82-4415-82a5-7afe2be17654",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 20.2: .764 seconds, answer 982891 ok"
]
},
"execution_count": 211,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def big_cheats(racetrack, lower_bound=1, radius=20) -> Iterable[Tuple[Point, Point, int]]:\n",
" \"\"\"All ways of cheating by taking up to `radius` steps through walls and back to the track.\"\"\"\n",
" D = dijkstra(racetrack, 'E')\n",
" return ((p1, p2, t)\n",
" for p1 in D\n",
" for p2 in neighborhood(p1, radius)\n",
" if p2 in D and (t := D[p1] - D[p2] - taxi_distance(p1, p2)) >= lower_bound)\n",
"\n",
"def neighborhood(point, radius) -> List[Point]:\n",
" \"\"\"All points within `radius` of `point` (using taxi distance).\"\"\"\n",
" (x, y) = point\n",
" return [(x + dx, y+ dy) \n",
" for dx in range(-radius, radius + 1)\n",
" for dy in range(-(radius - abs(dx)), radius - abs(dx) + 1)]\n",
"\n",
"answer(20.2, 982891, lambda:\n",
" quantify(big_cheats(racetrack, 100, 20)))"
]
},
{
"cell_type": "markdown",
"id": "0022ec12-5359-455f-ace8-c8615d7cd1d2",
"metadata": {},
"source": [
"This solution is backwards-compatible with Part 1:"
]
},
{
"cell_type": "code",
"execution_count": 213,
"id": "332e9c72-db9a-4b90-a649-45f7bf955e84",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 20.1: .023 seconds, answer 1343 ok"
]
},
"execution_count": 213,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(20.1, 1343, lambda:\n",
" quantify(big_cheats(racetrack, 100, 2)))"
]
},
{
"cell_type": "markdown",
"id": "c3317844-2b4a-4756-8a59-b765aa467445",
"metadata": {},
"source": [
"# Summary\n",
"\n",
"So far, I've solved all the puzzles. The median run time is about 10 milliseconds, but three problems take over a second."
]
},
{
"cell_type": "code",
"execution_count": 215,
"id": "34813fc9-a000-4cd8-88ae-692851b3242c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Puzzle 1.1: .000 seconds, answer 1830467 ok\n",
"Puzzle 1.2: .000 seconds, answer 26674158 ok\n",
"Puzzle 2.1: .000 seconds, answer 257 ok\n",
"Puzzle 2.2: .002 seconds, answer 328 ok\n",
"Puzzle 3.1: .001 seconds, answer 156388521 ok\n",
"Puzzle 3.2: .000 seconds, answer 75920122 ok\n",
"Puzzle 4.1: .033 seconds, answer 2401 ok\n",
"Puzzle 4.2: .027 seconds, answer 1822 ok\n",
"Puzzle 5.1: .001 seconds, answer 5762 ok\n",
"Puzzle 5.2: .001 seconds, answer 4130 ok\n",
"Puzzle 6.1: .002 seconds, answer 5329 ok\n",
"Puzzle 6.2: 1.968 seconds, answer 2162 ok\n",
"Puzzle 7.1: .014 seconds, answer 1985268524462 ok\n",
"Puzzle 7.2: .656 seconds, answer 150077710195188 ok\n",
"Puzzle 8.1: .003 seconds, answer 220 ok\n",
"Puzzle 8.2: .003 seconds, answer 813 ok\n",
"Puzzle 9.1: .020 seconds, answer 6332189866718 ok\n",
"Puzzle 9.2: 2.731 seconds, answer 6353648390778 ok\n",
"Puzzle 10.1: .005 seconds, answer 744 ok\n",
"Puzzle 10.2: .006 seconds, answer 1651 ok\n",
"Puzzle 11.1: .002 seconds, answer 194482 ok\n",
"Puzzle 11.2: .060 seconds, answer 232454623677743 ok\n",
"Puzzle 12.1: .051 seconds, answer 1402544 ok\n",
"Puzzle 12.2: .042 seconds, answer 862486 ok\n",
"Puzzle 13.1: .000 seconds, answer 29598 ok\n",
"Puzzle 13.2: .000 seconds, answer 93217456941970 ok\n",
"Puzzle 14.1: .000 seconds, answer 216027840 ok\n",
"Puzzle 14.2: 1.893 seconds, answer 6876 ok\n",
"Puzzle 15.1: .029 seconds, answer 1563092 ok\n",
"Puzzle 15.2: .042 seconds, answer 1582688 ok\n",
"Puzzle 16.1: .147 seconds, answer 103512 ok\n",
"Puzzle 16.2: .855 seconds, answer 554 ok\n",
"Puzzle 17.1: .000 seconds, answer 2,1,0,1,7,2,5,0,3 ok\n",
"Puzzle 17.2: .024 seconds, answer 267265166222235 ok\n",
"Puzzle 18.1: .014 seconds, answer 344 ok\n",
"Puzzle 18.2: .033 seconds, answer 46,18 ok\n",
"Puzzle 19.1: .004 seconds, answer 242 ok\n",
"Puzzle 19.2: .183 seconds, answer 595975512785325 ok\n",
"Puzzle 20.1: .023 seconds, answer 1343 ok\n",
"Puzzle 20.2: .764 seconds, answer 982891 ok\n",
"\n",
"Correct: 40/40\n",
"\n",
"Time in seconds: 0.014 median, 0.241 mean, 9.638 total.\n"
]
}
],
"source": [
"summary(answers)"
]
}
],
"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.12.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}