\n",
"\n",
"# Advent of Code 2021\n",
"\n",
"I'm doing [Advent of Code](https://adventofcode.com/2021) (AoC) this year. I'm not competing for points, just participating for fun.\n",
"\n",
"To fully understand each puzzle's instructions, click on the link (e.g. [**Day 1**](https://adventofcode.com/2021/day/1)); I give only brief summaries here. \n",
"\n",
"Part of the idea of AoC is that you have to make some design choices to solve Part 1 *before* you get to see the instructions for Part 2. So there is a tension of wanting the solution to Part 1 to provide components that can be re-used in Part 2, without falling victim to [YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it). In this notebook I won't refactor the code for Part 1 after I see what is requested in Part 2 (although I may edit the code for clarity, without changing the initial approach). Sometimes I will explore further, inventing my own \"Part 3\".\n",
"\n",
"This year's AoC theme involves Santa's Elves on a submarine. Gary J. Grady ([@GaryJGrady](https://twitter.com/GaryJGrady/) on Twitter) has some nice drawings to set the scene:\n",
"\n",
"\n",
"\n",
"# Day 0: Preparations\n",
"\n",
"I put some imports and functions that I thought would be useful in a notebook of utility functions, [AdventUtils.ipynb](AdventUtils.ipynb)."
]
},
{
"cell_type": "code",
"execution_count": 81,
"metadata": {},
"outputs": [],
"source": [
"%run AdventUtils.ipynb\n",
"\n",
"current_year = 2021"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 1](https://adventofcode.com/2021/day/1): Sonar Sweep\n",
"\n",
"\n",
"- **Input**: Each item in the input is an integer depth measurement.\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 2000 lines:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"148\n",
"167\n",
"168\n",
"169\n",
"182\n",
"188\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 2000 ints:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"148\n",
"167\n",
"168\n",
"169\n",
"182\n",
"188\n",
"...\n"
]
}
],
"source": [
"in1 = parse(1, int)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1**: **How many measurements are larger than the previous measurement?**"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.000 seconds for correct answer: 1,400\n"
]
}
],
"source": [
"def increases(measurements: Sequence[int]) -> int:\n",
" \"\"\"How many measurements are larger than the previous measurement?\"\"\"\n",
" return quantify(measurements[i] > measurements[i - 1] \n",
" for i in range(1, len(measurements)))\n",
"\n",
"answer(1.1, 1400, lambda: increases(in1))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: Consider sums of a three-measurement sliding window. **How many sums are larger than the previous sum?**"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.001 seconds for correct answer: 1,429\n"
]
}
],
"source": [
"def windows(sequence, width) -> List[Sequence]:\n",
" \"\"\"All sliding (overlapping) windows of given `width` in sequence.\"\"\"\n",
" return [sequence[i:i+width] \n",
" for i in range(len(sequence) + 1 - width)]\n",
"\n",
"answer(1.2, 1429, lambda: increases(mapt(sum, windows(in1, 3))))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 3**: Visualization\n",
"\n",
"Let's take a look at where the depths are taking us:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
""
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"plt.plot(in1, 'b.'); plt.ylabel('Depth'); plt.gca().invert_yaxis();"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"It looks like Gary Grady was right; the submarine is descending at a steep angle."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 2](https://adventofcode.com/2021/day/2): Dive! \n",
"\n",
"- **Input**: Each item in the input is a command name (\"forward\", \"down\", or \"up\") followed by an integer.\n",
"\n",
"I'll parse a command into a tuple like `('forward', 2)`."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 1000 lines:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"forward 2\n",
"down 7\n",
"down 8\n",
"forward 9\n",
"down 8\n",
"forward 9\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 1000 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"('forward', 2)\n",
"('down', 7)\n",
"('down', 8)\n",
"('forward', 9)\n",
"('down', 8)\n",
"('forward', 9)\n",
"...\n"
]
}
],
"source": [
"in2 = parse(2, atoms)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1**: Calculate the horizontal position and depth you would have after following the planned course. **What do you get if you multiply your final horizontal position by your final depth?**"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.000 seconds for correct answer: 1,670,340\n"
]
}
],
"source": [
"def drive(commands) -> int:\n",
" \"\"\"What is the product of position and depth after following commands?\"\"\"\n",
" pos = depth = 0\n",
" for (op, n) in commands:\n",
" if op == 'forward': pos += n\n",
" if op == 'down': depth += n\n",
" if op == 'up': depth -= n\n",
" return pos * depth\n",
"\n",
"answer(2.1, 1_670_340, lambda: drive(in2))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: Using the new interpretation of the commands, calculate the horizontal position and depth you would have after following the planned course. **What do you get if you multiply your final horizontal position by your final depth?**\n",
"\n",
"The *new interpretation* is that the \"down\" and \"up\" commands no longer change depth, rather they change *aim*, and going forward *n* units both increments position by *n* and depth by *aim* × *n*."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.000 seconds for correct answer: 1,954,293,920\n"
]
}
],
"source": [
"def drive2(commands) -> int:\n",
" \"\"\"What is the product of position and depth after following commands?\n",
" This time we have to keep track of `aim` as well.\"\"\"\n",
" pos = depth = aim = 0\n",
" for (op, n) in commands:\n",
" if op == 'forward': pos += n; depth += aim * n\n",
" if op == 'down': aim += n\n",
" if op == 'up': aim -= n\n",
" return pos * depth\n",
"\n",
"answer(2.2, 1_954_293_920, lambda: drive2(in2))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 3](https://adventofcode.com/2021/day/3): Binary Diagnostic\n",
"\n",
"- **Input**: Each item in the input is a bit string of `0`s and `1`s.\n",
"\n",
"I'll parse them as strings; I won't convert them into ints."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 1000 lines:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"101000111100\n",
"000011111101\n",
"011100000100\n",
"100100010000\n",
"011110010100\n",
"101001100000\n",
"...\n"
]
}
],
"source": [
"in3 = parse(3)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1**: Use the binary numbers in your diagnostic report to calculate the gamma rate and epsilon rate, then multiply them together. **What is the power consumption of the submarine?**"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.001 seconds for correct answer: 2,261,546\n"
]
}
],
"source": [
"def common(strs, i) -> Char: # '1' or '0'\n",
" \"\"\"The bit that is most common in position i among strs.\"\"\"\n",
" bits = [s[i] for s in strs]\n",
" return '1' if bits.count('1') >= bits.count('0') else '0'\n",
"\n",
"def uncommon(strs, i) -> Char: # '1' or '0'\n",
" \"\"\"The bit that is least common in position i among strs.\"\"\"\n",
" return '1' if common(strs, i) == '0' else '0'\n",
"\n",
"def epsilon(strs) -> str:\n",
" \"\"\"The bit string formed from most common bit at each position.\"\"\"\n",
" return cat(common(strs, i) for i in range(len(strs[0])))\n",
"\n",
"def gamma(strs) -> str:\n",
" \"\"\"The bit string formed from most uncommon bit at each position.\"\"\"\n",
" return cat(uncommon(strs, i) for i in range(len(strs[0])))\n",
"\n",
"def power(strs) -> int: \n",
" \"\"\"Product of epsilon and gamma rates.\"\"\"\n",
" return int(epsilon(strs), 2) * int(gamma(strs), 2)\n",
" \n",
"answer(3.1, 2261546, lambda: power(in3))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: Use the binary numbers in your diagnostic report to calculate the oxygen generator rating and CO2 scrubber rating, then multiply them together. **What is the life support rating of the submarine?**\n",
"\n",
"This time I'll have a single function, `select_str` which selects the str that survives the process of picking strs with the most common or uncommon bit at each position. Then I call `select_str` with `common` to get the oxygen rating and `uncommon` to get the CO2 rating."
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.000 seconds for correct answer: 6,775,520\n"
]
}
],
"source": [
"def select_str(strs, common_fn, i=0) -> str:\n",
" \"\"\"Select a str from strs according to common_fn:\n",
" Going left-to-right, repeatedly select just the strs that have the right i-th bit.\n",
" When only one string is remains, return it.\"\"\"\n",
" if len(strs) == 1:\n",
" return strs[0]\n",
" else:\n",
" bit = common_fn(strs, i)\n",
" selected = [s for s in strs if s[i] == bit]\n",
" return select_str(selected, common_fn, i + 1)\n",
"\n",
"def life_support(strs) -> int: \n",
" \"\"\"The product of oxygen (most common select) and CO2 (least common select) rates.\"\"\"\n",
" return int(select_str(strs, common), 2) * int(select_str(strs, uncommon), 2)\n",
" \n",
"answer(3.2, 6775520, lambda: life_support(in3))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 4](https://adventofcode.com/2021/day/4): Giant Squid\n",
"\n",
"- **Input**: The first item of the input is a permutation of the integers 0-99. Subsequent items are bingo boards: 5 lines of 5 ints each. items are separated by *two* newlines. \n",
"\n",
"I'll represent a board as a tuple of 25 ints; that makes `parse` easy: the permutation of integers and the bingo boards can both be parsed by `ints`. (Bingo games will be played against a giant squid; we get to choose which board we want to play.)"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 601 lines:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"73,42,95,35,13,40,99,92,33,30,83,1,36,93,59,90,55,25,77,44,37,62,41,47,80,23,51,61,21,20,76,8,71 ...\n",
"\n",
"91 5 64 81 34\n",
"15 99 31 63 65\n",
"45 39 54 93 83\n",
"51 14 23 86 32\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 101 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"(73, 42, 95, 35, 13, 40, 99, 92, 33, 30, 83, 1, 36, 93, 59, 90, 55, 25, 77, 44, 37, 62, 41, 47, ...\n",
"(91, 5, 64, 81, 34, 15, 99, 31, 63, 65, 45, 39, 54, 93, 83, 51, 14, 23, 86, 32, 19, 22, 16, 13, 3)\n",
"(20, 83, 38, 85, 70, 69, 12, 14, 26, 84, 19, 76, 45, 78, 99, 22, 80, 90, 33, 46, 75, 31, 21, 6, 28)\n",
"(22, 52, 65, 75, 2, 91, 12, 45, 18, 94, 38, 66, 85, 39, 1, 24, 36, 55, 74, 3, 89, 14, 79, 99, 48)\n",
"(19, 58, 95, 22, 6, 48, 28, 57, 30, 72, 12, 67, 15, 37, 18, 33, 1, 49, 90, 60, 35, 41, 47, 11, 84)\n",
"(89, 27, 65, 68, 19, 38, 83, 21, 81, 91, 67, 61, 87, 30, 10, 36, 45, 66, 56, 4, 82, 71, 44, 96, 90)\n",
"...\n"
]
}
],
"source": [
"order, *boards = in4 = parse(4, ints, sep='\\n\\n')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1**: To guarantee victory against the giant squid, figure out which board will win first. **What will your final score be if you choose that board?**"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"I'm worried about an ambiguity: what if two boards win at the same time? I'll have to assume Eric arranged it so that can't happen. I'll define `bingo_winners` to return a list of boards that win when a number has just been called, and I'll arbitrarily choose the first of them."
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.004 seconds for correct answer: 39,902\n"
]
}
],
"source": [
"B = 5 # Bingo board is size B by B.\n",
"Board = Tuple[int] # B * B ints\n",
"Line = List[int] # B ints\n",
"\n",
"def lines(square) -> Tuple[Line, Line]:\n",
" \"\"\"The two lines (horizontal and vertical) through square number `square`.\"\"\"\n",
" def sq(x, y) -> int: return x + B * y\n",
" return ([sq(x, square // B) for x in range(B)], \n",
" [sq(square % B, y) for y in range(B)])\n",
"\n",
"def bingo_winners(boards, drawn, just_called) -> List[Board]:\n",
" \"\"\"Board(s) that win due to the number just called.\"\"\"\n",
" def filled(board, line) -> bool: return all(board[n] in drawn for n in line)\n",
" return [board for board in boards\n",
" if just_called in board\n",
" and any(filled(board, line) \n",
" for line in lines(board.index(just_called)))]\n",
"\n",
"def bingo_score(board, drawn, just_called) -> int:\n",
" \"\"\"Sum of unmarked numbers multiplied by the number just called.\"\"\"\n",
" unmarked = sum(n for n in board if n not in drawn)\n",
" return unmarked * just_called\n",
"\n",
"def bingo(boards, order) -> int: \n",
" \"\"\"What is the final score of the first winning board?\"\"\"\n",
" drawn = set()\n",
" for num in order:\n",
" drawn.add(num)\n",
" winners = bingo_winners(boards, drawn, num)\n",
" if winners:\n",
" return bingo_score(winners[0], drawn, num)\n",
"\n",
"answer(4.1, 39902, lambda: bingo(boards, order))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: Figure out which board will win last. **Once it wins, what would its final score be?**"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.007 seconds for correct answer: 26,936\n"
]
}
],
"source": [
"def bingo_last(boards, order) -> int: \n",
" \"\"\"What is the final score of the last winning board?\"\"\"\n",
" remaining_boards = set(boards)\n",
" drawn = set()\n",
" for num in order:\n",
" drawn.add(num)\n",
" winners = bingo_winners(remaining_boards, drawn, num)\n",
" remaining_boards -= set(winners)\n",
" if not remaining_boards:\n",
" return bingo_score(winners[-1], drawn, num)\n",
" \n",
"answer(4.2, 26936, lambda: bingo_last(boards, order))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 5](https://adventofcode.com/2021/day/5): Hydrothermal Venture\n",
"\n",
"- **Input**: Each item in the input is a \"line\" denoted by start and end x,y points, e.g. \"`0,9 -> 5,9`\". \n",
"\n",
"I'll represent a line as a 4-tuple of integers, e.g. `(0, 9, 5, 9)`."
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 500 lines:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"409,872 -> 409,963\n",
"149,412 -> 281,280\n",
"435,281 -> 435,362\n",
"52,208 -> 969,208\n",
"427,265 -> 884,265\n",
"779,741 -> 779,738\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 500 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"(409, 872, 409, 963)\n",
"(149, 412, 281, 280)\n",
"(435, 281, 435, 362)\n",
"(52, 208, 969, 208)\n",
"(427, 265, 884, 265)\n",
"(779, 741, 779, 738)\n",
"...\n"
]
}
],
"source": [
"in5 = parse(5, ints)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1**: Consider only horizontal and vertical lines. **At how many points do at least two lines overlap?**"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.041 seconds for correct answer: 7,436\n"
]
}
],
"source": [
"def points(line) -> List[Point]:\n",
" \"\"\"All the (integer) points on a line.\"\"\"\n",
" x1, y1, x2, y2 = line\n",
" if x1 == x2:\n",
" return [(x1, y) for y in cover(y1, y2)]\n",
" elif y1 == y2:\n",
" return [(x, y1) for x in cover(x1, x2)]\n",
" else: # non-orthogonal lines not allowed\n",
" return []\n",
" \n",
"def overlaps(lines) -> int:\n",
" \"\"\"How many points overlap 2 or more lines?\"\"\"\n",
" counts = Counter(flatten(map(points, lines)))\n",
" return quantify(counts[p] >= 2 for p in counts)\n",
"\n",
"answer(5.1, 7436, lambda: overlaps(in5))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: Consider all of the lines (including diagonals, which are all at ±45°). **At how many points do at least two lines overlap?**"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For Part 2 I'll redefine `points` and `overlaps` in a way that doesn't break Part 1:"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.051 seconds for correct answer: 7,436\n",
"0.076 seconds for correct answer: 21,104\n"
]
}
],
"source": [
"def points(line, diagonal=False) -> bool:\n",
" \"\"\"All the (integer) points on a line; optionally allow diagonal lines.\"\"\"\n",
" x1, y1, x2, y2 = line\n",
" if diagonal or x1 == x2 or y1 == y2:\n",
" dx, dy = sign(x2 - x1), sign(y2 - y1)\n",
" length = max(abs(x2 - x1), abs(y2 - y1))\n",
" return [(x1 + k * dx, y1 + k * dy) for k in range(length + 1)]\n",
" else: # non-orthogonal lines not allowed when diagonal is False\n",
" return []\n",
" \n",
"def overlaps(lines, diagonal=False) -> int:\n",
" \"\"\"How many points overlap 2 or more lines?\"\"\"\n",
" counts = Counter(flatten(points(line, diagonal) for line in lines))\n",
" return quantify(counts[p] >= 2 for p in counts)\n",
"\n",
"assert points((1, 1, 1, 3), False) == [(1, 1), (1, 2), (1, 3)]\n",
"assert points((1, 1, 3, 3), False) == []\n",
"assert points((1, 1, 3, 3), True) == [(1, 1), (2, 2), (3, 3)]\n",
"assert points((9, 7, 7, 9), True) == [(9, 7), (8, 8), (7, 9)]\n",
"\n",
"answer(5.1, 7436, lambda: overlaps(in5, diagonal=False)) # Make sure it still works\n",
"answer(5.2, 21104, lambda: overlaps(in5, diagonal=True))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 6](https://adventofcode.com/2021/day/6): Lanternfish\n",
"\n",
"- **Input**: The input is comma-separated integers, each the age of a lanternfish (according to its internal timer). \n",
"\n",
"Over time, the lanternfish age and reproduce: Each day, their timer decrements by one. The day after it reaches 0 it is reset to 6 and a new lanternfish is born with an internal timer of 8."
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 1 line:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"5,4,3,5,1,1,2,1,2,1,3,2,3,4,5,1,2,4,3,2,5,1,4,2,1,1,2,5,4,4,4,1,5,4,5,2,1,2,5,5,4,1,3,1,4,2,4,2, ...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 300 ints:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"5\n",
"4\n",
"3\n",
"5\n",
"1\n",
"1\n",
"...\n"
]
}
],
"source": [
"in6 = parse(6, int, sep=',')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1**: Find a way to simulate lanternfish. **How many lanternfish would there be after 80 days?**\n",
"\n",
"Although the puzzle instructions treats each fish individually, I won't take the bait (pun intended). \n",
"\n",
"Instead, I'll use a `Counter` of fish, and process all the fish of each age group together, all at once. That way each update will be *O*(9) = *O*(1), not *O*(*n*). I have a hunch that Part 2 will involve a ton-o'-fish."
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.000 seconds for correct answer: 350,917\n"
]
}
],
"source": [
"Fish = Counter # Represent a school of fish as a Counter of their internal timers\n",
"\n",
"def simulate(fish, days=1) -> Fish:\n",
" \"\"\"Simulate the aging and birth of fish over `days`.\"\"\"\n",
" for day in range(days):\n",
" fish = Fish({t - 1: fish[t] for t in fish})\n",
" if -1 in fish: # births\n",
" fish[6] += fish[-1]\n",
" fish[8] = fish[-1]\n",
" del fish[-1]\n",
" return fish\n",
" \n",
"assert simulate(Fish((3, 4, 3, 1, 2))) == Fish((2, 3, 2, 0, 1))\n",
"assert simulate(Fish((2, 3, 2, 0, 1))) == Fish((1, 2, 1, 6, 0, 8))\n",
"assert Fish((1, 1, 1, 6, 8, 6)) == {1: 3, 6: 2, 8: 1}\n",
"\n",
"answer(6.1, 350917, lambda: total(simulate(Fish(in6), 80)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: **How many lanternfish would there be after 256 days?**\n",
"\n",
"My hunch was right, so part 2 is straightforward:"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.001 seconds for correct answer: 1,592,918,715,629\n"
]
}
],
"source": [
"answer(6.2, 1_592_918_715_629, lambda: total(simulate(Fish(in6), 256)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"That's over a trillion lanternfish. Latest [estimates](https://www.google.com/search?q=how+many+fish+are+in+the+sea) say that there are in fact trillions of fish in the sea. But not trillions of lanternfish, and not increasing from 300 to over a trillion in just 256 days.\n",
"\n",
""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 7](https://adventofcode.com/2021/day/7): The Treachery of Whales\n",
"\n",
"- **Input**: The input is a single line of comma-separated integers, each the horizontal position of a crab (in its own submarine)."
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 1 line:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"1101,1,29,67,1102,0,1,65,1008,65,35,66,1005,66,28,1,67,65,20,4,0,1001,65,1,65,1106,0,8,99,35,67, ...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 1000 ints:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"1101\n",
"1\n",
"29\n",
"67\n",
"1102\n",
"0\n",
"...\n"
]
}
],
"source": [
"in7 = parse(7, int, sep=',')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The idea is that if you can get the crabs to all align in one horizontal position, they can save you from a giant whale by opening up an escape route to a cave.\n",
"\n",
"\n",
"\n",
"- **Part 1**: Determine the horizontal position that the crabs can align to using the least fuel possible. (Each unit of horizontal travel costs one unit of fuel.) **How much fuel must they spend to align to that position?**"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.000 seconds for correct answer: 352,707\n"
]
}
],
"source": [
"def fuel_cost(positions) -> int:\n",
" \"\"\"How much fuel does it cost to get everyone to the best alignment point?\"\"\"\n",
" # I happen to know that the best alignment point is the median\n",
" align = int(median(positions))\n",
" return sum(abs(p - align) for p in positions)\n",
"\n",
"answer(7.1, 352707, lambda: fuel_cost(in7))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: Determine the horizontal position that the crabs can align to using the least fuel possible so they can make you an escape route! (Now for each crab the first unit of travel costs 1, the second 2, the third 3, and so on.) **How much fuel must they spend to align to that position?**"
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.361 seconds for correct answer: 95,519,693\n"
]
}
],
"source": [
"def fuel_cost2(positions) -> int:\n",
" \"\"\"How much fuel does it cost to get everyone to the best alignment point, \n",
" with nonlinear fuel costs?\"\"\"\n",
" # I don't know the best alignment point, so I'll try all of them\n",
" return min(sum(burn_rate2(p, align) for p in positions)\n",
" for align in cover(*positions))\n",
"\n",
"def burn_rate2(p, align) -> int:\n",
" \"\"\"The first step costs 1, the second 2, etc. (i.e. triangular numbers).\"\"\"\n",
" steps = abs(p - align)\n",
" return steps * (steps + 1) // 2\n",
"\n",
"answer(7.2, 95519693, lambda: fuel_cost2(in7))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 3**: Analysis and Visualization\n",
"\n",
"Now that I got the right answer and have some time to think about it, if the travel cost were exactly quadratic, we would be minimizing the sum of square distances, and Legendre and Gauss knew that the **mean**, not the **median**, is the alignment point that does that. What's the mean of the positions?"
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"490.543"
]
},
"execution_count": 24,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"positions = in7\n",
"mean(positions)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"That's not an integer, but I'll try it, along with the integers above and below it:"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{490: 95519693, 491: 95519725, 490.543: 95519083.0}"
]
},
"execution_count": 25,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"{align: sum(burn_rate2(p, align) for p in positions)\n",
" for align in [490, 491, mean(positions)]}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We see that rounding down gives the right answer, rounding up does a bit worse, and using the exact mean gives a total fuel cost that is *better* than the correct answer (but is apparently not a legal alignment point). A reddit user with the name CrashAndSideburns looked more carefully into the use of the mean, and wrote [a paper](https://www.reddit.com/r/adventofcode/comments/rawxad/2021_day_7_part_2_i_wrote_a_paper_on_todays/) showing that the best alignment point must be within ±0.5 from the mean.\n",
"\n",
"Below I show a histogram of the number of crabs at each range of horizontal positions, along with red stars for the two alignment points (median and mean)."
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[376.0, 490.543]"
]
},
"execution_count": 26,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/png": "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\n",
"text/plain": [
""
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"stars = [median(in7), mean(in7)]\n",
"plt.hist(in7, bins=33, rwidth=0.8); \n",
"plt.plot(stars, [50, 50], 'r*')\n",
"plt.ylabel('Number of Crabs'); plt.xlabel('Position')\n",
"stars"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 8](https://adventofcode.com/2021/day/8): Seven Segment Search\n",
"\n",
"- **Input**: Each item in the input consists of 10 *patterns* followed by a \"`|`\", followed by 4 *output values*.\n",
" \n",
"I'll split on the `|` and then extract atoms from both sides:"
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 200 lines:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"daegb gadbcf cgefda edcfagb dfg acefbd fdgab fg bdcfa fcgb | cdfgba fgbc dbfac gfadbc\n",
"bdfc dcbegf bf egfbcda gebad cfgaed bfe edfgc aegfcb gebdf | fb fb bcdfaeg fcgdeb\n",
"cebdgaf bfcd gceab bf bfcea gceafd ecdfa fegdab bfcade fba | dfcb dagfbe fbaged bfa\n",
"efabcg aegcdb fgaed fac dgafbc becf eadcgbf aegfc fc cagbe | ecgfa agdef eagfc gdceab\n",
"fcdae cdeabf fga gf gabfde cgadb gadebfc cgfe aegcdf afgcd | fbgadce gadefb fag bafegd\n",
"gecadbf bgc dacgf gaecbf cbeda dbfg bgdca bg bafcgd gdacef | cdgfa fceabg dgfb dgabc\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 200 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"(('daegb', 'gadbcf', 'cgefda', 'edcfagb', 'dfg', 'acefbd', 'fdgab', 'fg', 'bdcfa', 'fcgb'), ('cd ...\n",
"(('bdfc', 'dcbegf', 'bf', 'egfbcda', 'gebad', 'cfgaed', 'bfe', 'edfgc', 'aegfcb', 'gebdf'), ('fb ...\n",
"(('cebdgaf', 'bfcd', 'gceab', 'bf', 'bfcea', 'gceafd', 'ecdfa', 'fegdab', 'bfcade', 'fba'), ('df ...\n",
"(('efabcg', 'aegcdb', 'fgaed', 'fac', 'dgafbc', 'becf', 'eadcgbf', 'aegfc', 'fc', 'cagbe'), ('ec ...\n",
"(('fcdae', 'cdeabf', 'fga', 'gf', 'gabfde', 'cgadb', 'gadebfc', 'cgfe', 'aegcdf', 'afgcd'), ('fb ...\n",
"(('gecadbf', 'bgc', 'dacgf', 'gaecbf', 'cbeda', 'dbfg', 'bgdca', 'bg', 'bafcgd', 'gdacef'), ('cd ...\n",
"...\n"
]
}
],
"source": [
"in8 = parse(8, lambda line: mapt(atoms, line.split('|')))"
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {},
"outputs": [],
"source": [
"assert in8[0] == (('daegb', 'gadbcf', 'cgefda', 'edcfagb', 'dfg', 'acefbd', 'fdgab', 'fg', 'bdcfa', 'fcgb'), \n",
" ('cdfgba', 'fgbc', 'dbfac', 'gfadbc'))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Each pattern and output value represents a digit on a [7-segment display](https://en.wikipedia.org/wiki/Seven-segment_display), with each letter a–g representing one of the 7 segments that is turned on in that digit. The mapping of letters to segments differs for each input item, but is consistent across all the digits within each item. Here's one mapping:\n",
"\n",
" aaaa\n",
" b c\n",
" b c\n",
" dddd\n",
" e f\n",
" e f\n",
" gggg\n",
" \n",
"\n",
"\n",
"- **Part 1**: **In the output values, how many times do digits 1, 4, 7, or 8 appear?**\n",
"\n",
"That's the same as asking *how many output values have a length of 2, 4, 3, or 7 segments?*"
]
},
{
"cell_type": "code",
"execution_count": 29,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.000 seconds for correct answer: 493\n"
]
}
],
"source": [
"def lengths2437(data):\n",
" \"\"\"Count the output values with lengths 2, 4, 3, 7.\"\"\"\n",
" return quantify(len(value) in (2, 4, 3, 7) \n",
" for (lhs, rhs) in data \n",
" for value in rhs)\n",
"\n",
"answer(8.1, 493, lambda: lengths2437(in8))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: For each entry, determine all of the wire/segment connections and decode the four-digit output values. **What do you get if you add up all of the output values?**"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Part 2 is *tricky*. The first output value `'cdfgba'` has 6 segments, so it could be either a 0, 6, or 9. To figure out which one it is I could do some fancy constraint satisfaction. That sounds hard. Or I could exhaustively try all permutations of the ways the 7 letters can map to the 7 segments. That sounds easy! Here's my plan:\n",
"- Make a list of the 7! = 5,040 possible `str.maketrans` translators that permute `'abcdefg'`.\n",
"- Decode an entry by trying all translators and keeping the one that maps all of the ten lhs patterns to a valid digit. `decode` then applies the translator to the four rhs values, concatenates them, and converts the result into an `int`.\n",
" - Note that `get_digit` must *sort* the translated letters to get a key that can be looked up in `segment_map`.\n",
"- Finally, sum up the decoding of each entry."
]
},
{
"cell_type": "code",
"execution_count": 30,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.599 seconds for correct answer: 1,010,460\n"
]
}
],
"source": [
"segments7 = 'abcdefg'\n",
"segment_map = {'abcefg': '0', 'cf': '1', 'acdeg': '2', 'acdfg': '3', 'bcdf': '4',\n",
" 'abdfg': '5', 'abdefg': '6', 'acf': '7', 'abcdefg': '8', 'abcdfg': '9'}\n",
"\n",
"translators = [str.maketrans(segments7, cat(p)) for p in permutations(segments7)]\n",
"\n",
"def get_digit(pattern, translator) -> Optional[Char]:\n",
" \"\"\"Translate the pattern, and return a digit '0' to '9' if valid.\"\"\"\n",
" return segment_map.get(cat(sorted(pattern.translate(translator))))\n",
"\n",
"def decode(entry) -> int:\n",
" \"\"\"Decode an entry's rhs into a 4-digit integer.\"\"\"\n",
" lhs, rhs = entry\n",
" for t in translators:\n",
" if all(get_digit(pattern, t) for pattern in lhs):\n",
" return int(cat(get_digit(pattern, t) for pattern in rhs))\n",
"\n",
"answer(8.2, 1010460, lambda: sum(map(decode, in8)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 9](https://adventofcode.com/2021/day/9): Smoke Basin\n",
"\n",
"- **Input:** The input is a *heightmap*: a 2D array of characters '0'–'9' representing the heights on the ocean floor. \n",
"\n",
"I'll use `parse` to get a tuple of rows (where each row is a tuple of digits), and turn that into a `Grid`."
]
},
{
"cell_type": "code",
"execution_count": 31,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 100 lines:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"9897656789865467895698765469899988672134598894345689864101378965457932349943210987654789653198789434\n",
"8789542499996878954329984398789976561012987789245678953212567892345791998899329899765678969997668912\n",
"7678943978987989965998993297649875432129876567956789864487678991056899877778939769886789998766457899\n",
"4578999868998996899867894976532986543299876476897899987569899989167898766567898654998898998655345678\n",
"2456987657679535679756799988643498657987654345789978899789998878998919954349997543219967987543237889\n",
"1234896545568986798645678999754989767898765456998769759899987765789329863238898659301256798793156891\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 100 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"(9, 8, 9, 7, 6, 5, 6, 7, 8, 9, 8, 6, 5, 4, 6, 7, 8, 9, 5, 6, 9, 8, 7, 6, 5, 4, 6, 9, 8, 9, 9, 9, ...\n",
"(8, 7, 8, 9, 5, 4, 2, 4, 9, 9, 9, 9, 6, 8, 7, 8, 9, 5, 4, 3, 2, 9, 9, 8, 4, 3, 9, 8, 7, 8, 9, 9, ...\n",
"(7, 6, 7, 8, 9, 4, 3, 9, 7, 8, 9, 8, 7, 9, 8, 9, 9, 6, 5, 9, 9, 8, 9, 9, 3, 2, 9, 7, 6, 4, 9, 8, ...\n",
"(4, 5, 7, 8, 9, 9, 9, 8, 6, 8, 9, 9, 8, 9, 9, 6, 8, 9, 9, 8, 6, 7, 8, 9, 4, 9, 7, 6, 5, 3, 2, 9, ...\n",
"(2, 4, 5, 6, 9, 8, 7, 6, 5, 7, 6, 7, 9, 5, 3, 5, 6, 7, 9, 7, 5, 6, 7, 9, 9, 9, 8, 8, 6, 4, 3, 4, ...\n",
"(1, 2, 3, 4, 8, 9, 6, 5, 4, 5, 5, 6, 8, 9, 8, 6, 7, 9, 8, 6, 4, 5, 6, 7, 8, 9, 9, 9, 7, 5, 4, 9, ...\n",
"...\n"
]
}
],
"source": [
"in9 = Grid(parse(9, digits))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1**: Find all of the low points on your heightmap. **What is the sum of the risk levels of all low points on your heightmap?**\n",
"\n",
"A low point is a point where all the neighbors are higher. The risk level is 1 more than the height of the low point."
]
},
{
"cell_type": "code",
"execution_count": 32,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.015 seconds for correct answer: 607\n"
]
}
],
"source": [
"def low_points(grid) -> List[Point]:\n",
" \"\"\"All low points on grid.\"\"\"\n",
" return [p for p in grid \n",
" if all(grid[p] < grid[nbr] for nbr in grid.neighbors(p))]\n",
"\n",
"def total_risk(grid) -> int:\n",
" \"\"\"Sum of height + 1 for all low points on grid.\"\"\"\n",
" return sum(grid[p] + 1 for p in low_points(grid))\n",
"\n",
"answer(9.1, 607, lambda: total_risk(in9))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: **What do you get if you multiply together the sizes of the three largest basins?**\n",
" \n",
"I thought there was an ambiguity in the definition of *basin*: what happens if there is a high point that is not of height 9, but has low points on either side of it? Wouldn't that high point then be part of two basins? The puzzle instructions says *Locations of height 9 do not count as being in any basin, and all other locations will always be part of exactly one basin.* I decided this must mean that the heightmap is carefully arranged so that every basin has only one low point and is surrounded by either edges or height 9 locations.\n",
"\n",
"With that assumption, I can associate each location with its low point using a [flood fill](https://en.wikipedia.org/wiki/Flood_fill) that starts from each low point. I can then get the sizes of the three largest (most common) basins and multiply them together."
]
},
{
"cell_type": "code",
"execution_count": 33,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.032 seconds for correct answer: 900,864\n"
]
}
],
"source": [
"def find_basins(grid) -> Dict[Point, Point]:\n",
" \"\"\"Compute `basins` as a dict of {point: low_point_of_point's_basin} for each point in grid.\"\"\"\n",
" basins = {} # A dict mapping each non-9 location to its low point.\n",
" def flood_fill(p, low):\n",
" \"\"\"Spread from p in all directions until hitting a 9;\n",
" mark each point p as being part of the basin with `low` point.\"\"\"\n",
" if grid[p] < 9 and p not in basins:\n",
" basins[p] = low\n",
" for p2 in grid.neighbors(p):\n",
" flood_fill(p2, low)\n",
" for p in low_points(grid):\n",
" flood_fill(p, low=p)\n",
" return basins\n",
"\n",
"answer(9.2, 900864, lambda: \n",
" prod(count for low_point, count in \n",
" Counter(find_basins(in9).values()).most_common(3)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 3**: Visualization\n",
"\n",
"I'll make a plot to display height 9 locations in yellow and height 0 locations in deep purple, with a gradient in between:"
]
},
{
"cell_type": "code",
"execution_count": 34,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
""
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"def show_heights(in9, low=None):\n",
" plt.figure(figsize=(10, 10))\n",
" C = [in9[p] for p in in9]\n",
" plt.scatter(*T(in9), marker='s', s=10, c=C, cmap=plt.get_cmap('plasma'))\n",
" if low: plt.plot(*T(low_points(in9)), low, markersize=4)\n",
" plt.axis('square'); plt.axis('off')\n",
" \n",
"show_heights(in9)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can optionally display the low points. Here I'll display them as red diamonds:"
]
},
{
"cell_type": "code",
"execution_count": 35,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
""
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"show_heights(in9, low='rD')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Apropos to *Smoke* and *Water,* and to the color scheme of my plot, Gary Grady's drawing for the day references [Deep Purple](https://www.youtube.com/watch?v=_zO6lWfvM0g):\n",
"\n",
""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 10](https://adventofcode.com/2021/day/10): Syntax Scoring\n",
"\n",
"- **Input**: Each item in the input is a string of opening and closing brackets: `[({<` and `>})]`.\n"
]
},
{
"cell_type": "code",
"execution_count": 36,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 102 lines:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"[(([{<{(<{{[({{}{}}{[]()})<{{}()}>]}}(([{{{}[]}[[]()]}[<{}[]]{()()}]](({{}{}}{{}()}))){[{({}())[[\n",
"<(({[<([{({[{{<>()}}[{<>()}({}{})]]<{<()<>>{[]()}}(((){}>[[][]])>}([{<[]{}>(<>[])}]))<[[[[[][]\n",
"(<<(<{{{{<<<[(()<>){()<>}][[()()]]>{<{[]{}}<<>()>>}>{(<{<>}([]{})><(<>())<(){}>>)<(([]{})(()())) ...\n",
"[[[[<[{[(<{{{({}<>)((){})}((()())[()()])}}><[([((){})]<[()[]]{{}<>}>)[[{[]<>}][([]{})[{}()]]]]>)<{(<\n",
"[<(<[[((<{((<<<>[]>><<<>{}>>){<[{}<>][<>[]]><<<>()>[(){}]>})[<{[{}<>][(){}]}<[[]<>][{}[]])>{([<>[]][\n",
"(([[[[<([[{([{<>()}{()<>}][((){})]){[{[]<>}({}<>)][(<><>)[()[]]]}}<{{({}{}){[]{}}}<{<><>}({}{})>}>\n",
"...\n"
]
}
],
"source": [
"in10 = parse(10)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Ideally, the brackets are balanced, but items might be *corrupted* (an extra closing bracket of the wrong kind appears in the wrong place) or *incomplete* (one or more closing brackets are missing from the end).\n",
" \n",
"- **Part 1**: Find the first illegal character in each corrupted line of the navigation subsystem. **What is the total syntax error score for those errors?**\n",
"\n",
"The instructions for Part 1 say *Some of the lines aren't corrupted, just incomplete; you can ignore these lines for now.* That suggests we will not ignore the incomplete lines in Part 2. So I'll define `analyze_syntax` to return a tuple of two values: an error score for use in Part 1, and the missing characters for an incomplete line, which I suspect will be used in Part 2."
]
},
{
"cell_type": "code",
"execution_count": 37,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.001 seconds for correct answer: 367,059\n"
]
}
],
"source": [
"error_scores = {')': 3, ']': 57, '}': 1197, '>': 25137}\n",
"open_close = {'(': ')', '[': ']', '{': '}', '<': '>'}\n",
"\n",
"def analyze_syntax(line) -> Tuple[int, str]:\n",
" \"\"\"A tuple of (error_score, missing_chars) for this line.\"\"\"\n",
" stack = [''] # A stack of closing characters we are looking for.\n",
" for c in line:\n",
" if c == stack[-1]: # A correctly matched closing bracket\n",
" stack.pop()\n",
" elif c in open_close: # A new opening bracket\n",
" stack.append(open_close[c])\n",
" else: # An erroneous closing bracket\n",
" return error_scores[c], cat(reversed(stack))\n",
" return 0, cat(reversed(stack))\n",
" \n",
"answer(10.1, 367059, lambda: sum(analyze_syntax(line)[0] for line in in10))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: Find the completion string for each incomplete line, score the completion strings, and sort the scores. **What is the middle score?**\n",
"\n",
"I was right; Part 2 uses the missing characters (now called a *completion string*). To score the completion string, we treat it as a base-5 number, as shown in `completion_score`."
]
},
{
"cell_type": "code",
"execution_count": 38,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.001 seconds for correct answer: 1,952,146,692\n"
]
}
],
"source": [
"def completion_score(completion:str) -> int:\n",
" \"\"\"The completion score for the completion string (the missing characters).\"\"\"\n",
" score = completion.translate(str.maketrans(')]}>', '1234'))\n",
" return int(score, base=5)\n",
"\n",
"def median_completion_score(lines) -> int:\n",
" \"\"\"The median completion score out of all the uncorrupted lines.\"\"\"\n",
" scores = (completion_score(completion) \n",
" for e, completion in map(analyze_syntax, lines) \n",
" if e == 0)\n",
" return median(scores)\n",
"\n",
"answer(10.2, 1_952_146_692, lambda: median_completion_score(in10))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 11](https://adventofcode.com/2021/day/11): Dumbo Octopus\n",
"\n",
"- **Input**: The input is a 2D array of characters `0`–`9` representing the energy levels of bioluminescent [dumbo octopuses](https://www.youtube.com/watch?v=eih-VSaS2g0)."
]
},
{
"cell_type": "code",
"execution_count": 39,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 10 lines:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"1224346384\n",
"5621128587\n",
"6388426546\n",
"1556247756\n",
"1451811573\n",
"1832388122\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 10 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"(1, 2, 2, 4, 3, 4, 6, 3, 8, 4)\n",
"(5, 6, 2, 1, 1, 2, 8, 5, 8, 7)\n",
"(6, 3, 8, 8, 4, 2, 6, 5, 4, 6)\n",
"(1, 5, 5, 6, 2, 4, 7, 7, 5, 6)\n",
"(1, 4, 5, 1, 8, 1, 1, 5, 7, 3)\n",
"(1, 8, 3, 2, 3, 8, 8, 1, 2, 2)\n",
"...\n"
]
}
],
"source": [
"in11 = Grid(parse(11, digits), directions8)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1**: Given the starting energy levels of the dumbo octopuses in your cavern, simulate 100 steps. **How many total flashes are there after 100 steps?**\n",
"\n",
"On each step, each octopus increases by one energy unit; then the ones with an energy level over 9 emit a flash, which makes their neighbors get one more energy unit (potentially causing others to flash); then the flashers reset to zero energy."
]
},
{
"cell_type": "code",
"execution_count": 40,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.008 seconds for correct answer: 1,591\n"
]
}
],
"source": [
"def simulate_flashes(grid, steps=100) -> int:\n",
" \"\"\"Simulate octopus flashes for `steps` steps and return total number of flashes.\"\"\"\n",
" grid = grid.copy() # Don't mutate the original grid\n",
" flashes = 0\n",
" for step in range(steps):\n",
" flashers = set()\n",
" for p in grid:\n",
" grid[p] += 1\n",
" for p in grid:\n",
" check_flash(grid, p, flashers)\n",
" for p in flashers:\n",
" grid[p] = 0\n",
" flashes += len(flashers)\n",
" return flashes\n",
"\n",
"def check_flash(grid, p, flashers):\n",
" \"\"\"Check if grid[p] flashes, and if so, recursively spread.\"\"\"\n",
" if grid[p] > 9 and p not in flashers:\n",
" flashers.add(p)\n",
" for p2 in grid.neighbors(p):\n",
" grid[p2] += 1\n",
" check_flash(grid, p2, flashers)\n",
" \n",
"answer(11.1, 1591, lambda: simulate_flashes(in11, 100))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: If you can calculate the exact moments when the octopuses will all flash simultaneously, you should be able to navigate through the cavern. **What is the first step during which all octopuses flash?**\n",
"\n",
"I feel a bit bad that I have to copy/paste/edit the whole simulation function, changing just the number of steps and the return. But at least I don't have to copy the `check_flash` function."
]
},
{
"cell_type": "code",
"execution_count": 41,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.027 seconds for correct answer: 314\n"
]
}
],
"source": [
"def simulate_flashes2(grid) -> int:\n",
" \"\"\"Simulate octopus flashes and return the first step during which all octopuses flash.\"\"\"\n",
" grid = grid.copy() # Don't mutate the original grid\n",
" for step in count_from(1):\n",
" flashers = set()\n",
" for p in grid:\n",
" grid[p] += 1\n",
" for p in grid:\n",
" check_flash(grid, p, flashers)\n",
" for p in flashers:\n",
" grid[p] = 0\n",
" if len(flashers) == len(grid):\n",
" return step\n",
" \n",
"answer(11.2, 314, lambda: simulate_flashes2(in11))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 12](https://adventofcode.com/2021/day/11): Passage Pathing\n",
"\n",
"- **Input**: Each item in the input is a connection between two caves. Big caves are written in uppercase, small caves in lowercase. `start` and `end` are two special caves with the obvious meaning."
]
},
{
"cell_type": "code",
"execution_count": 42,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 22 lines:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"xx-xh\n",
"vx-qc\n",
"cu-wf\n",
"ny-LO\n",
"cu-DR\n",
"start-xx\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 22 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"('xx', 'xh')\n",
"('vx', 'qc')\n",
"('cu', 'wf')\n",
"('ny', 'LO')\n",
"('cu', 'DR')\n",
"('start', 'xx')\n",
"...\n"
]
}
],
"source": [
"in12 = parse(12, words)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1**: **How many paths through this cave system are there that visit small caves at most once?**\n",
"\n",
"My approach is as follows:\n",
"- I'll define a path as a list of cave names: `['start', ..., 'end']`.\n",
"- I'll construct `neighbors` as a mapping from a cave to the list of caves it connects to.\n",
"- I'll do depth-first search, starting from the trivial path `['start']` and returning all possible paths. "
]
},
{
"cell_type": "code",
"execution_count": 43,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.013 seconds for correct answer: 4,167\n"
]
}
],
"source": [
"Path = List[str]\n",
" \n",
"def search_paths(path, neighbors) -> Iterable[Path]:\n",
" \"\"\"All paths that start with `path` and lead to 'end' using `neighbors`.\n",
" Small caves can only be visited once.\"\"\"\n",
" if path[-1] == 'end':\n",
" yield [path]\n",
" else:\n",
" for cave in neighbors[path[-1]]:\n",
" if cave.isupper() or cave not in path:\n",
" yield from search_paths(path + [cave], neighbors)\n",
"\n",
"neighbors = multimap(in12, symmetric=True)\n",
" \n",
"answer(12.1, 4167, lambda: quantify(search_paths(['start'], neighbors)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: After reviewing the available paths, you realize you might have time to visit a single small cave twice. However, the caves named `start` and `end` can only be visited exactly once each. Given these new rules, **how many paths through this cave system are there?**\n",
"\n",
"At first I felt bad that I would again have to copy/paste/edit the code for Part 1. I felt better when I realized that the revised function `search_paths2` would have need to call the original `search_paths`: once a path returns to a small cave for the second time, the remainder of the search should be under the `search_paths` rules, not the `search_paths2` rules."
]
},
{
"cell_type": "code",
"execution_count": 44,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.317 seconds for correct answer: 98,441\n"
]
}
],
"source": [
"def search_paths2(path, neighbors) -> Iterable[Path]:\n",
" \"\"\"Find all paths that start with `path` and lead to 'end' using `neighbors`.\n",
" Small caves can only be visited once, except one of them may be visited twice.\"\"\"\n",
" if path[-1] == 'end':\n",
" yield [path]\n",
" else:\n",
" for cave in neighbors[path[-1]]:\n",
" if cave.isupper() or cave not in path:\n",
" yield from search_paths2(path + [cave], neighbors)\n",
" elif cave.islower() and cave != 'start':\n",
" yield from search_paths(path + [cave], neighbors)\n",
" \n",
"answer(12.2, 98441, lambda: quantify(search_paths2(['start'], neighbors)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 13](https://adventofcode.com/2021/day/13): Transparent Origami\n",
"\n",
"- **Input**: The input is a set of dots, e.g. \"`6,10`\", followed by an ordered list of fold instructions, e.g. \"`fold along y=7`\".\n",
"\n",
"My `parse` command is not set up to parse two different sections, so I'll ask `parse` only to parse each line into a tuple of atoms. Then I'll further process the items to get two variables:\n",
"- `dots`: a set of `(x, y)` points, such as `(6, 10)`. \n",
"- `folds`: a list of fold instructions such as `('fold', 'along', 'y', 7)`."
]
},
{
"cell_type": "code",
"execution_count": 45,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 789 lines:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"103,224\n",
"624,491\n",
"808,688\n",
"1076,130\n",
"700,26\n",
"55,794\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 789 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"(103, 224)\n",
"(624, 491)\n",
"(808, 688)\n",
"(1076, 130)\n",
"(700, 26)\n",
"(55, 794)\n",
"...\n"
]
}
],
"source": [
"in13 = parse(13, atoms)"
]
},
{
"cell_type": "code",
"execution_count": 46,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(123, 456, 'fold', 'along', 'x', 3)"
]
},
"execution_count": 46,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"atoms('123,456 fold along x=3')"
]
},
{
"cell_type": "code",
"execution_count": 47,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[('fold', 'along', 'x', 655),\n",
" ('fold', 'along', 'y', 447),\n",
" ('fold', 'along', 'x', 327),\n",
" ('fold', 'along', 'y', 223),\n",
" ('fold', 'along', 'x', 163),\n",
" ('fold', 'along', 'y', 111),\n",
" ('fold', 'along', 'x', 81),\n",
" ('fold', 'along', 'y', 55),\n",
" ('fold', 'along', 'x', 40),\n",
" ('fold', 'along', 'y', 27),\n",
" ('fold', 'along', 'y', 13),\n",
" ('fold', 'along', 'y', 6)]"
]
},
"execution_count": 47,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"dots = {item for item in in13 if len(item) == 2} \n",
"folds = [item for item in in13 if len(item) > 2]\n",
"folds"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The idea of this puzzle is that the dots are on transparent paper, and when following the `fold along y=7` instruction, all the dots below the line `y=7` are reflected above the line: they retain the same distance form the `y=7` line, but their `y` value becomes less than `7`. Similarly, for an `x` fold, all the points to the right of the line are reflected to the left. When we finish the folds, a code message will appear (which we can then use to activate the infrared thermal imaging camera system).\n",
"\n",
"- **Part 1**: **How many dots are visible after completing just the first fold instruction on your transparent paper?**"
]
},
{
"cell_type": "code",
"execution_count": 48,
"metadata": {},
"outputs": [],
"source": [
"def fold(dots, instruction) -> Set[Point]: \n",
" \"\"\"The set of dots that result from following the fold instruction.\"\"\"\n",
" fold, along, x_or_y, line = instruction\n",
" if x_or_y == 'x':\n",
" return {(line - abs(line - x), y) for (x, y) in dots}\n",
" else:\n",
" return {(x, line - abs(line - y)) for (x, y) in dots}"
]
},
{
"cell_type": "code",
"execution_count": 49,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.000 seconds for correct answer: 638\n"
]
}
],
"source": [
"answer(13.1, 638, lambda: len(fold(dots, folds[0])))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: Finish folding the transparent paper according to the instructions. **What code do you use to activate the infrared thermal imaging camera system?**"
]
},
{
"cell_type": "code",
"execution_count": 50,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.012 seconds for correct answer: CJCKBAPB\n"
]
},
{
"data": {
"image/png": "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\n",
"text/plain": [
""
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"def origami(dots, instructions) -> None:\n",
" \"\"\"Follow all the instructions and plot the resulting dots.\"\"\"\n",
" for instruction in instructions:\n",
" dots = fold(dots, instruction)\n",
" plt.scatter(*T(dots), marker='o')\n",
" plt.axis('equal'); plt.axis('off'); plt.gca().invert_yaxis()\n",
" \n",
"answer(13.2, \"CJCKBAPB\", lambda: origami(dots, folds) or \"CJCKBAPB\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"I kind of cheated here. I didn't want to write an OCR program, so I relied on my own eyes to look at the dots and see the code.\n",
"\n",
"**Note**: My transparent paper was folded 12 times. Is that physically feasible? [Britney Gallivan](https://www.youtube.com/watch?v=AfPDvhKvaa0&) says yes (barely)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 14](https://adventofcode.com/2021/day/14): Extended Polymerization"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Input**: The input is a a polymer template (a string of one-letter element names, such as \"`NNCB`\") followed by a list of pair insertion rules (such as \"`CH -> B`\", meaning that a `B` should be inserted into the middle of each `CH` pair).\n",
"\n",
"I'll parse each line of the input into a list of `words` (thus ignoring the \"`->`\" characters); then pick out:\n",
"- `polymer`: the sole word on the first line.\n",
"- `rules`: the third through last lines, converted into a dict, like `{'CH': 'B', ...}`."
]
},
{
"cell_type": "code",
"execution_count": 51,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 102 lines:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"ONSVVHNCFVBHKVPCHCPV\n",
"\n",
"VO -> C\n",
"VV -> S\n",
"HK -> H\n",
"FC -> C\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 102 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"('ONSVVHNCFVBHKVPCHCPV',)\n",
"()\n",
"('VO', 'C')\n",
"('VV', 'S')\n",
"('HK', 'H')\n",
"('FC', 'C')\n",
"...\n"
]
}
],
"source": [
"in14 = parse(14, words)\n",
"polymer = in14[0][0]\n",
"rules = dict(in14[2:])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1**: Apply 10 steps of pair insertion to the polymer template and find the most and least common elements in the result. **What do you get if you take the quantity of the most common element and subtract the quantity of the least common element?**\n",
"\n",
"Pair insertion means inserting the element on the right hand side of a rule between each two-element pair. All two-element substrings are considered as pairs, including overlapping ones (my utility function `pairs` handles this). All insertions happen simultaneously during a step."
]
},
{
"cell_type": "code",
"execution_count": 52,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.005 seconds for correct answer: 3,259\n"
]
}
],
"source": [
"def pair_insertion(polymer: str, rules, steps: int) -> str:\n",
" \"\"\"Insert elements into polymer according to rules; repeat `steps` times.\"\"\"\n",
" for _ in range(steps):\n",
" polymer = cat(pair[0] + rules[pair]\n",
" for pair in pairs(polymer)) + polymer[-1]\n",
" return polymer\n",
"\n",
"def quantity_difference(polymer) -> int:\n",
" \"\"\"The count of most common element minus the count of least common element.\"\"\"\n",
" counts = Counter(polymer).values()\n",
" return max(counts) - min(counts)\n",
"\n",
"def pairs(seq: Sequence) -> List[Sequence]: \"All overlapping pairs\"; return windows(seq, 2)\n",
"\n",
"assert polymer == 'ONSVVHNCFVBHKVPCHCPV'\n",
"assert rules['VO'] == 'C'\n",
"assert pairs('NNCB') == ['NN', 'NC', 'CB']\n",
"assert pair_insertion('NNCB', rules={'NN': 'C', 'NC': 'B', 'CB': 'H'}, steps=1) == 'NCNBCHB'\n",
"\n",
"answer(14.1, 3259, lambda: quantity_difference(pair_insertion(polymer, rules, 10)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: Apply 40 steps of pair insertion to the polymer template and find the most and least common elements in the result. **What do you get if you take the quantity of the most common element and subtract the quantity of the least common element?**\n",
"\n",
"The instructions warn us that the resulting polymer after 40 steps will be *trillions* of elements long. So it isn't feasible to just call `pair_insertion` with steps=40. Instead, I'll employ the same trick as with the lanternfish in Day 6: use a `Counter` of element pairs so that, for example, all the `'NC'` pairs in the polymer are handled simultaneously in one operation, rather than handling each one individually. No matter how many steps we do, there are only 100 distinct element pairs, so iterating over them 40 times should be very fast. \n",
"\n",
"Here's an example Counter of element pairs:"
]
},
{
"cell_type": "code",
"execution_count": 53,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Counter({'NN': 1, 'NC': 1, 'CB': 1})"
]
},
"execution_count": 53,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"Counter(pairs('NNCB'))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"What letters does this represent? The complication is that the pairs overlap, so, if we added up the counts for all the times that, say, the letter `'C'` appears in keys of the Counter, we'd get 2; but it should be 1. We can divide the sum by 2 to avoid double counting, but the first and last letters in the polymer are *not* double-counted, so we need to add back 1/2 for each of those letters. Fortunately the first and last letters are invariant under pair insertion, so we can do this adjustment at the end; we don't have to do it for each step.\n",
"\n",
"So all in all there are three representations of a polymer:"
]
},
{
"cell_type": "code",
"execution_count": 54,
"metadata": {},
"outputs": [],
"source": [
"Polymer = str # e.g. 'NNCB'\n",
"PairCounter = Counter[str] # e.g. Counter({'NN': 1, 'NC': 1, 'CB': 1})\n",
"LetterCounter = Counter[Char] # e.g. Counter({'N': 2, 'C': 1, 'B': 1})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Here's how we convert a PairCounter into a LetterCounter:"
]
},
{
"cell_type": "code",
"execution_count": 55,
"metadata": {},
"outputs": [],
"source": [
"def letter_counts(pair_ctr: PairCounter, polymer: Polymer) -> LetterCounter:\n",
" \"\"\"Return a Counter of the letters in the polymer described by the `pair_ctr`.\"\"\"\n",
" letters = set(flatten(pair_ctr))\n",
" def letter_count(L) -> int:\n",
" return int(sum(pair_ctr[L+M] + pair_ctr[M+L] for M in letters) / 2\n",
" + (L == polymer[0]) / 2 + (L == polymer[-1]) / 2)\n",
" return Counter({L: letter_count(L) for L in letters})"
]
},
{
"cell_type": "code",
"execution_count": 56,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Counter({'B': 1, 'C': 1, 'N': 2})"
]
},
"execution_count": 56,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"letter_counts(Counter(pairs('NNCB')), 'NNCB')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's make sure it works when the first and last letters are the same:"
]
},
{
"cell_type": "code",
"execution_count": 57,
"metadata": {},
"outputs": [],
"source": [
"assert (letter_counts(Counter(pairs('NNCB')), 'NNCB')\n",
" == letter_counts(Counter(pairs('NCBN')), 'NCBN'))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now the new function `pair_insertion_difference` can call on `pair_insertion2` to solve Part 2 (as well as Part 1):"
]
},
{
"cell_type": "code",
"execution_count": 58,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.000 seconds for correct answer: 3,259\n",
"0.003 seconds for correct answer: 3,459,174,981,021\n"
]
}
],
"source": [
"def pair_insertion2(polymer, rules, steps) -> PairCounter:\n",
" \"\"\"Insert elements into polymer according to rules; repeat `steps` times.\n",
" Return a Counter of element pairs.\"\"\"\n",
" pair_ctr = Counter(pairs(polymer))\n",
" for _ in range(steps):\n",
" pair_ctr2 = Counter()\n",
" for LM in pair_ctr:\n",
" pair_ctr2[LM[0] + rules[LM]] += pair_ctr[LM]\n",
" pair_ctr2[rules[LM] + LM[1]] += pair_ctr[LM]\n",
" pair_ctr = pair_ctr2\n",
" return pair_ctr\n",
"\n",
"def pair_insertion_difference(polymer, rules, steps):\n",
" \"\"\"Most common minus least common after `steps` of pair insertion.\"\"\"\n",
" return quantity_difference(letter_counts(pair_insertion2(polymer, rules, steps), polymer))\n",
"\n",
"assert Counter(pairs('NNCB')) == Counter({'NN': 1, 'NC': 1, 'CB': 1})\n",
"assert pair_insertion2('NNCB', rules={'NN': 'C', 'NC': 'B', 'CB': 'H'}, steps=1) == (\n",
" Counter({'NC': 1, 'CN': 1, 'NB': 1, 'BC': 1, 'CH': 1, 'HB': 1}))\n",
"assert letter_counts(Counter({'NC': 1, 'CN': 1, 'NB': 1, 'BC': 1, 'CH': 1, 'HB': 1}), 'NNCB') == (\n",
" Counter({'N': 2, 'C': 2, 'B': 2, 'H': 1}))\n",
"assert pair_insertion_difference('NNCB', rules={'NN': 'C', 'NC': 'B', 'CB': 'H'}, steps=1) == 1\n",
"\n",
"answer(14.1, 3259, lambda: pair_insertion_difference(polymer, rules, 10))\n",
"answer(14.2, 3459174981021, lambda: pair_insertion_difference(polymer, rules, 40))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 3**: Polymer length?\n",
"\n",
"The instructions didn't ask, but I want to know the length of the polymer that was created after 40 steps:"
]
},
{
"cell_type": "code",
"execution_count": 59,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"20,890,720,927,744\n"
]
}
],
"source": [
"length = total(pair_insertion2(polymer, rules, 40))\n",
"print(f'{length:,d}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Almost 21 trillion. Good to know."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 15](https://adventofcode.com/2021/day/15): Chiton\n",
"\n",
"- **Input**: The input is a square grid of *risk levels* (each one digit, 1–9) for locations in the cave."
]
},
{
"cell_type": "code",
"execution_count": 60,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 100 lines:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"4249856395422795894919869133487611581179923326874763428673979547991221931142777981153991369468629849\n",
"5812974178739823463799939791688998895568796557798392761499941349143539572865883254186633218867928826\n",
"3699989976298596286299499129934993241824395574879938998946914116375199242199151918863674914554714898\n",
"5682435936794718871685718386458294198391116125679589438794914499278679393779734596558953699438589518\n",
"7681197997388219696918569664119968498599547892968929425479817979816979144947916716989874825679487436\n",
"9981166198272997899142698141878643123757515999788822988261499197559193945291512682763935126815448215\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 100 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"(4, 2, 4, 9, 8, 5, 6, 3, 9, 5, 4, 2, 2, 7, 9, 5, 8, 9, 4, 9, 1, 9, 8, 6, 9, 1, 3, 3, 4, 8, 7, 6, ...\n",
"(5, 8, 1, 2, 9, 7, 4, 1, 7, 8, 7, 3, 9, 8, 2, 3, 4, 6, 3, 7, 9, 9, 9, 3, 9, 7, 9, 1, 6, 8, 8, 9, ...\n",
"(3, 6, 9, 9, 9, 8, 9, 9, 7, 6, 2, 9, 8, 5, 9, 6, 2, 8, 6, 2, 9, 9, 4, 9, 9, 1, 2, 9, 9, 3, 4, 9, ...\n",
"(5, 6, 8, 2, 4, 3, 5, 9, 3, 6, 7, 9, 4, 7, 1, 8, 8, 7, 1, 6, 8, 5, 7, 1, 8, 3, 8, 6, 4, 5, 8, 2, ...\n",
"(7, 6, 8, 1, 1, 9, 7, 9, 9, 7, 3, 8, 8, 2, 1, 9, 6, 9, 6, 9, 1, 8, 5, 6, 9, 6, 6, 4, 1, 1, 9, 9, ...\n",
"(9, 9, 8, 1, 1, 6, 6, 1, 9, 8, 2, 7, 2, 9, 9, 7, 8, 9, 9, 1, 4, 2, 6, 9, 8, 1, 4, 1, 8, 7, 8, 6, ...\n",
"...\n"
]
}
],
"source": [
"in15 = Grid(parse(15, digits))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1**: You start in the top left position, your destination is the bottom right position, and you cannot move diagonally. **What is the lowest total risk of any path from the top left to the bottom right?** (Don't count the risk level of your starting position.)\n",
"\n",
"Gary Grady's drawing represents the risk involved in finding a path that avoids bumping into the ceiling above or the chitons below.\n",
"\n",
"\n",
"\n",
"I'll use a search that updates a grid of the `cost` of the best known path from start to each point. The cost for each point is initially infinite (because we don't know any paths), and is updated each time we find a better path to the point. Whenever we find a better path to a point, we see if that will lead to a better path for the neighbors."
]
},
{
"cell_type": "code",
"execution_count": 61,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.890 seconds for correct answer: 687\n"
]
}
],
"source": [
"def search_grid(grid, start=(0, 0), goal=None) -> int:\n",
" \"\"\"The total cost of the best path from start to goal (which defaults to bottom right).\"\"\"\n",
" goal = goal or max(grid) # default bottom right\n",
" path_cost = Grid({p: inf for p in grid}) # cost of best known path from start to p\n",
" frontier = {start} # Set of grid points to consider for possible improvement to path_cost\n",
" while frontier:\n",
" p = frontier.pop()\n",
" new_cost = 0 if p is start else (grid[p] + min(path_cost[b] for b in grid.neighbors(p)))\n",
" if new_cost < path_cost[p]:\n",
" path_cost[p] = new_cost\n",
" frontier.update(grid.neighbors(p))\n",
" return path_cost[goal]\n",
"\n",
"answer(15.1, 687, lambda: search_grid(in15))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: The entire cave is actually five times larger in both dimensions. Your original map tile repeats to the right and downward; each time the tile repeats, all of its risk levels are 1 higher than the tile immediately up or left of it. However, risk levels above 9 wrap back around to 1. Using the full map, **what is the lowest total risk of any path from the top left to the bottom right?**\n",
"\n",
"Here's how to define the full map of the cave:"
]
},
{
"cell_type": "code",
"execution_count": 77,
"metadata": {},
"outputs": [],
"source": [
"def repeat_grid(grid, repeat=5):\n",
" \"\"\"Extend the grid to be `repeat` times larger in both directions.\n",
" Values within each repeated block are increased by 1 for each repetition to the right or down,\n",
" but values over 9 wrap around to 1.\"\"\"\n",
" w, h = grid.width, grid.height \n",
" return Grid({(x + xr * w, y + yr * h): clock_mod(grid[x, y] + xr + yr, 9)\n",
" for xr in range(repeat) \n",
" for yr in range(repeat)\n",
" for x, y in grid})"
]
},
{
"cell_type": "code",
"execution_count": 78,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(250000, 10000)"
]
},
"execution_count": 78,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"full_map = repeat_grid(in15, 5)\n",
"len(full_map), len(in15)"
]
},
{
"cell_type": "code",
"execution_count": 85,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Node((499, 499))"
]
},
"execution_count": 85,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"####\n",
"%run AdventUtils.ipynb\n",
"\n",
"class ChitonProblem(Problem):\n",
" def actions(self, loc): return self.grid.neighbors(loc)\n",
" def result(self, loc1, loc2): return loc2\n",
" def action_cost(self, s1, a, s2): return self.grid[s2]\n",
" def h(self, node): return manhatten_distance(node.state, self.goal) # Never overestimate!\n",
" \n",
"astar_search(ChitonProblem(initial=(0, 0), goal=max(full_map), grid=full_map)).path_cost"
]
},
{
"cell_type": "code",
"execution_count": 86,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"2957"
]
},
"execution_count": 86,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"_.path_cost"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"With 250,000 points in the full map, `search_grid` takes about 5 minutes (I tried it). That's too slow, so I grabbed the [A* search](https://en.wikipedia.org/wiki/A*_search_algorithm) from my [AoC 2017](https://github.com/norvig/pytudes/blob/main/ipynb/Advent%202017.ipynb) notebook, and supplied it with the proper functions to make a move, compute the cost of a move, and estimate the distance to the goal (the `h_func` or \"heuristic function\"). A* is guaranteed to find an optimal path if the heuristic function never overestimates the cost from a state to the goal, so I will use the [Manhattan distance](https://en.wikipedia.org/wiki/Taxicab_geometry) as my heuristic–this is the same as assuming that every risk level in the remainder of the path will be 1, the lowest possible."
]
},
{
"cell_type": "code",
"execution_count": 64,
"metadata": {},
"outputs": [],
"source": [
"from heapq import heappop, heappush\n",
"\n",
"def Astar(start, neighbors, h_func, step_cost) -> Tuple[int, list]:\n",
" \"\"\"Find a (cost, path) tuple for the lowest-cost path from start to a goal.\n",
" A goal is any state `s` such that `h_func(s) == 0`.\"\"\"\n",
" frontier = [(h_func(start), start)] # A priority queue, ordered by path_cost(s) + h(s)\n",
" previous = {start: None} # start state has no previous state; other states will\n",
" path_cost = {start: 0} # The cost of the best path to a state.\n",
" path = lambda s: ([] if (s is None) else path(previous[s]) + [s])\n",
" while frontier:\n",
" (f, s) = heappop(frontier)\n",
" if h_func(s) == 0:\n",
" return path_cost[s], path(s)\n",
" for s2 in neighbors(s):\n",
" g = path_cost[s] + step_cost(s, s2)\n",
" if s2 not in path_cost or g < path_cost[s2]:\n",
" heappush(frontier, (g + h_func(s2), s2))\n",
" path_cost[s2] = g\n",
" previous[s2] = s\n",
" \n",
"def Astar_search_grid(grid) -> Tuple[int, list]:\n",
" \"\"\"The (risk, path) tuple of the best path from upper-left to bottom-right on grid.\"\"\"\n",
" goal = max(grid)\n",
" def h_func(s): return abs(sum(goal) - sum(s)) # estimated path cost from s to goal\n",
" def step_cost(_, s2): return grid[s2] # cost of moving to s2\n",
" return Astar((0, 0), grid.neighbors, h_func, step_cost)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"With A* search the run time is greatly improved, down from 5 minutes to 1 second on Part 2 (and a lesser improvement on Part 1)."
]
},
{
"cell_type": "code",
"execution_count": 72,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.036 seconds for correct answer: 687\n"
]
},
{
"ename": "TypeError",
"evalue": "unsupported operand type(s) for +: 'int' and 'tuple'",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0manswer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m15.1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m687\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mlambda\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mAstar_search_grid\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0min15\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0manswer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m15.2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2957\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mlambda\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mAstar_search_grid\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfull_map\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;32m\u001b[0m in \u001b[0;36manswer\u001b[0;34m(puzzle, correct, code)\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mpretty\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;34mf'{x:,d}'\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mis_int\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0mstart\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtime\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 9\u001b[0;31m \u001b[0mgot\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcode\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 10\u001b[0m \u001b[0mdt\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtime\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0mstart\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 11\u001b[0m \u001b[0mans\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpretty\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgot\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0manswer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m15.1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m687\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mlambda\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mAstar_search_grid\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0min15\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0manswer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m15.2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2957\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mlambda\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mAstar_search_grid\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfull_map\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;32m\u001b[0m in \u001b[0;36mAstar_search_grid\u001b[0;34m(grid)\u001b[0m\n\u001b[1;32m 24\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mh_func\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mabs\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msum\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgoal\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0msum\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m# estimated path cost from s to goal\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 25\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mstep_cost\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0m_\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0ms2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mgrid\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms2\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;31m# cost of moving to s2\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 26\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mAstar\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mgrid\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mneighbors\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mh_func\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstep_cost\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;32m\u001b[0m in \u001b[0;36mAstar\u001b[0;34m(start, neighbors, h_func, step_cost)\u001b[0m\n\u001b[1;32m 13\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mpath_cost\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpath\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 14\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ms2\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mneighbors\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 15\u001b[0;31m \u001b[0mg\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpath_cost\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mstep_cost\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0ms2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 16\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0ms2\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mpath_cost\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0mg\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0mpath_cost\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms2\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0mheappush\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfrontier\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mg\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mh_func\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0ms2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mTypeError\u001b[0m: unsupported operand type(s) for +: 'int' and 'tuple'"
]
}
],
"source": [
"answer(15.1, 687, lambda: Astar_search_grid(in15)[0])\n",
"answer(15.2, 2957, lambda: Astar_search_grid(full_map)[0])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 3**: Visualization\n",
"\n",
"Here we see the two paths on the two grids:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def plot_search_grid(grid, fmt='g-'):\n",
" \"\"\"PLot the path from start to goal.\"\"\"\n",
" risk, path = Astar_search_grid(grid)\n",
" plt.plot(*T(path), fmt); plt.gca().invert_yaxis()\n",
" plt.title(f'Path with {len(path) - 1} steps; risk level {risk}')\n",
" \n",
"plot_search_grid(in15)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plot_search_grid(full_map)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 16](https://adventofcode.com/2021/day/16): Packet Decoder\n",
"\n",
"- **Input**: The input is a single line containing a sequence of hexadecimal digits, a message using the Buoyancy Interchange Transmission System (BITS). \n",
"\n",
"\n",
"\n",
"For now I will leave the input as a string of hex digits:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"in16 = parse(16)[0]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1:** Decode the structure of your hexadecimal-encoded BITS transmission; **what do you get if you add up the version numbers in all packets?**\n",
"\n",
"The gist of [the instructions](https://adventofcode.com/2021/day/16) is to consider the hexadecimal sequence as a bit string, divide the bit string into bit fields, and construct nested packets based on the values of the fields. Here are basic types for `Bits` (a bit string) and `Packet` (which contains a version number `V`, a type ID `T`, and a `contents` field which can be either a number or a list of packets), along with functions to convert from a hexadecimal string to a bit string, and from there to an int: "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"Bits = str # a string of '0's and '1's\n",
"Packet = namedtuple('Packet', 'V, T, contents') # V is version; T is type ID\n",
"\n",
"def bits_from_hex(hex) -> Bits: \n",
" \"\"\"Convert a hexadecimal string into a bit string, making sure each hex digit is 4 bits.\"\"\"\n",
" # I could have used just `bin(int(hex, 16))`, except that wouldn't left-zero-pad when needed.\n",
" return cat(f'{int(x, 16):04b}' for x in hex)\n",
"\n",
"def int2(bits: Bits) -> int: \n",
" \"\"\"Convert a bit string into an int.\"\"\"\n",
" return int(bits, 2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To parse the bit string into packets, I will have four functions that start with the word `parse_` and return a tuple of two values: the object parsed (either an int or a packet) and the remaining bits that were not parsed."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def parse_int(L, bits) -> Tuple[int, Bits]:\n",
" \"\"\"Parse an integer from the first L bits; return the int and the remaining bits.\"\"\"\n",
" return int2(bits[:L]), bits[L:]\n",
"\n",
"def parse_packet(bits) -> Tuple[Packet, Bits]:\n",
" \"\"\"Parse a packet; return it and the remaining bits.\"\"\"\n",
" V, T, bits = int2(bits[0:3]), int2(bits[3:6]), bits[6:]\n",
" parser = parse_literal_packet if T == 4 else parse_operator_packet\n",
" return parser(V, T, bits)\n",
" \n",
"def parse_literal_packet(V, T, bits) -> Tuple[Packet, Bits]:\n",
" \"\"\"Build a packet with a literal value; return it and the remaining bits.\"\"\"\n",
" literal = ''\n",
" while True:\n",
" prefix, group, bits = bits[0], bits[1:5], bits[5:]\n",
" literal += group\n",
" if prefix == '0':\n",
" return Packet(V, T, int2(literal)), bits\n",
"\n",
"def parse_operator_packet(V, T, bits) -> Tuple[Packet, Bits]:\n",
" \"\"\"Build a packet with subpackets; return it and the remaining bits.\"\"\"\n",
" I, bits = parse_int(1, bits)\n",
" L, bits = parse_int((15, 11)[I], bits)\n",
" subpackets = [] \n",
" if I == 0: # Parse L bits of subpackets\n",
" subpacket_bits, bits = bits[:L], bits[L:]\n",
" while subpacket_bits:\n",
" packet, subpacket_bits = parse_packet(subpacket_bits)\n",
" subpackets.append(packet)\n",
" else: # Parse L subpackets\n",
" for p in range(L):\n",
" packet, bits = parse_packet(bits)\n",
" subpackets.append(packet) \n",
" return Packet(V, T, subpackets), bits"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now we're ready to solve the puzzle by summing up the version numbers, `V`, of all the packets:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def nested_packets(packet) -> Iterator[Packet]: \n",
" \"\"\"The packet and all its subpackets.\"\"\"\n",
" yield packet\n",
" if packet.T != 4: \n",
" for p in packet.contents:\n",
" yield from nested_packets(p)\n",
"\n",
"packet16, _ = parse_packet(bits_from_hex(in16))\n",
"\n",
"answer(16.1, 989, lambda: sum(p.V for p in nested_packets(packet16)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This was more code than previous days! Here are some assertions I used to make sure I was on the right track:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"assert (bits_from_hex('D2FE28') == '110100101111111000101000')\n",
"\n",
"assert (int2(bits_from_hex('D2FE28')) == 13827624)\n",
"\n",
"assert (bits_from_hex('38006F45291200') \n",
" == '00111000000000000110111101000101001010010001001000000000')\n",
"\n",
"assert (parse_int(4, '011100111') == (7, '00111'))\n",
"\n",
"assert (parse_packet('110100101111111000101000') \n",
" == parse_literal_packet(6, 4, '101111111000101000')\n",
" == (Packet(V=6, T=4, contents=2021), '000'))\n",
"\n",
"assert (parse_packet('00111000000000000110111101000101001010010001001000000000')\n",
" == (Packet(V=1, T=6, contents=[Packet(V=6, T=4, contents=10), \n",
" Packet(V=2, T=4, contents=20)]),\n",
" '0000000'))\n",
"\n",
"assert (parse_packet('11101110000000001101010000001100100000100011000001100000')\n",
" == (Packet(V=7, T=3, contents=[Packet(V=2, T=4, contents=1), \n",
" Packet(V=4, T=4, contents=2), \n",
" Packet(V=1, T=4, contents=3)]),\n",
" '00000'))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: **What do you get if you evaluate the expression represented by your hexadecimal-encoded BITS transmission?**\n",
"\n",
"The evaluation rules are that a literal packet evaluates to the number that is its contents, and an operator packet applies an operator determined by the type id (in the `packet.T` field) to the list of values formed by evaluating the subpackets. I put the operators into the `packet_ops` dict."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def eval_packet(packet) -> int:\n",
" \"\"\"Evaluate a packet according to the operator rules.\"\"\"\n",
" if packet.T == 4:\n",
" return packet.contents\n",
" else:\n",
" vals = [eval_packet(p) for p in packet.contents]\n",
" return packet_ops[packet.T](vals)\n",
" \n",
"packet_ops = {0: sum, 1: prod, 2: min, 3: max, \n",
" 5: lambda v: int(v[0] > v[1]), \n",
" 6: lambda v: int(v[0] < v[1]), \n",
" 7: lambda v: int(v[0] == v[1])}\n",
"\n",
"answer(16.2, 7936430475134, lambda: eval_packet(packet16))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 17](https://adventofcode.com/2021/day/17): Trick Shot\n",
"\n",
"- **Input**: The input is a short string describing the x and y coordinates of a target area.\n",
"\n",
"Because the input is so short, I will copy it literally here instead of reading it from a file. I use `ints` to extract the four integers."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"in17 = ints(\"target area: x=257..286, y=-101..-57\")\n",
"in17"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The puzzle involves firing a probe and checking if it hits the target area. The probe starts from an initial position with an initial velocity, and traverses a path according to the physics described in the instructions: each time step position is incremented by velocity, but gravity causes it to gain a -1 in `y` velocity; drag causes it lose 1 in `x` velocity.\n",
"\n",
"\n",
"\n",
"- **Part 1**: Find the initial velocity that causes the probe to reach the highest `y` position and still eventually be within the target area after some time step. **What is the highest `y` position it reaches on this trajectory?**\n",
"\n",
"First I'll define two classes:\n",
"- `Target` keeps track of the `Xs` and `Ys` that define the target area.\n",
"- `Probe` keeps track of:\n",
" - The `x` and `y` position coordinates\n",
" - The `vx` and `vy` velocity values\n",
" - A boolean `hit` which is True if the probe hit the target at some point in its path\n",
" - The `highest` height it ever reached."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class Target:\n",
" \"\"\"The target has a range of Xs and Ys coordinates.\"\"\"\n",
" def __init__(self, x0, x1, y0, y1): self.Xs, self.Ys = cover(x0, x1), cover(y0, y1) \n",
" \n",
"Probe = namedtuple('Probe', 'x, y, vx, vy, hit, highest', \n",
" defaults=(0, 0, 0, 0, False, 0))\n",
"\n",
"target17 = Target(*in17)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The function `probe_step` simulates the physics of the world for one time step: incrementing the probe's position by its velocity vector, changing the `xv` velocity due to drag and the `yv` velocity due to gravity, and tracking the `hit` and `highest` values.\n",
"\n",
"The function `probe_steps` simulates for multiple time steps; until the probe has passed the target. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def probe_steps(probe, target=target17, do=ignore) -> Probe:\n",
" \"\"\"Simulate the probe until it passes the target.\n",
" You can optionally `do` something to the probe on each time step.\"\"\"\n",
" maxx, miny = max(target.Xs), min(target.Ys)\n",
" do(probe)\n",
" while probe.x <= maxx and probe.y >= miny:\n",
" probe = probe_step(probe, target)\n",
" do(probe)\n",
" return probe\n",
"\n",
"def probe_step(probe, target) -> Probe:\n",
" \"\"\"Simulate the physics of the probe for one time step.\"\"\"\n",
" x, y, vx, vy, hit, highest = probe\n",
" return Probe(x=x + vx, y=y + vy, \n",
" vx=sign(vx) * (abs(vx) - 1), vy=vy - 1,\n",
" hit=hit or (x in target.Xs and y in target.Ys),\n",
" highest=max(highest, y + vy))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def probe_steps(probe, target=target17, do=ignore) -> Probe:\n",
" \"\"\"Simulate the probe until it passes the target.\n",
" You can optionally `do` something to the probe on each time step.\"\"\"\n",
" maxx, miny = max(target.Xs), min(target.Ys)\n",
" do(probe)\n",
" while probe.x <= maxx and probe.y >= miny:\n",
" x, y, vx, vy, hit, highest = probe\n",
" probe = Probe(x=x + vx, y=y + vy, \n",
" vx=sign(vx) * (abs(vx) - 1), vy=vy - 1,\n",
" hit=hit or (x in target.Xs and y in target.Ys),\n",
" highest=max(highest, y + vy))\n",
" do(probe)\n",
" return probe"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For example:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"probe_steps(Probe(vx=24, vy=5))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"By experimentation, I found that:\n",
"- Any `vx<23` will never reach the target (regardless of `vy`).\n",
"- A `vx=23` value means that the probe will have an `x` velocity of zero when it is inside the width of the target. \n",
"- Any `vx>23` will eventually pass beyond the target width (and might or might not hit the target along the way). \n",
"- This is because 23 is the only value that leads to a sequence of decreasing `vx` values adding up to an `x` position that is within the x=257..286 target area: 23 + 22 + 21 + ... + 2 + 1 = 276. (As with Day 7, we're dealing with triangular numbers.) Here's the demonstration:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"{vx: [sum(range(vx, 0, -1)), sum(range(vx, 0, -1)) in target17.Xs]\n",
" for vx in [22, 23, 24]}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Specifying `do=print` is useful for experimentation:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"probe_steps(Probe(vx=23, vy=7), do=print)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Once I found the critical `vx=23` value, I figured I could simply vary the `vy` values to find the highest height:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def highest_height(vxs, vys) -> int:\n",
" \"\"\"The highest height reached by a probe that hits the target, among all vx and vy values.\"\"\"\n",
" probes = [probe_steps(Probe(vx=vx, vy=vy)) for vx in vxs for vy in vys]\n",
" return max(probe.highest for probe in probes if probe.hit)\n",
" \n",
"answer(17.1, 5050, lambda: highest_height(vxs=[23], vys=range(150)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"I recognize 5,050 as the 100th triangular number (just as Gauss did, as [legend has it](https://www.americanscientist.org/article/gausss-day-of-reckoning)), and of course the highest height of any trajectory must be a triangular number, because `vy` decreases by one each step.\n",
"\n",
"- **Part 2**: **How many distinct initial velocity values cause the probe to be within the target area after some time step?**\n",
" \n",
"I can try a bunch of `vx` and `vy` values. For `vx`, start at the critical 23 value and go up to the maximum of the target area (meaning that the probe hits the right edge of the target area on the first time step). For `vy`, start with a negative value that would hit the bottom of the target area on the first time step, and go up to 100. (Anything more than `vy=100` would end up passing through the target without touching it: the probe's `y` value would be above the target on one step and below it on the next.)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def probe_hits(velocities: Iterable[Point]) -> int:\n",
" \"\"\"How many of these velocities cause the probe to hit the target?\"\"\"\n",
" return quantify(probe_steps(Probe(vx=vx, vy=vy)).hit \n",
" for vx, vy in velocities)\n",
"\n",
"answer(17.2, 2223, lambda:\n",
" probe_hits(cross_product(range(23, max(target17.Xs) + 1), \n",
" range(min(target17.Ys), 101))))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 3**: Visualization\n",
"\n",
"I'd like to understand things a bit better with some visualization. The function `plot_probes` plots the target as a black box and plots the paths of various probes with different initial velocities in different colors:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def plot_probes(velocities: List[Point], target=target17) -> None:\n",
" \"\"\"Plot the target as a black box and the paths of probes with colored lines.\"\"\"\n",
" plt.plot(*box(target.Xs, target.Ys), 'k-', linewidth=4)\n",
" for (vx, vy) in velocities:\n",
" path = []\n",
" probe_steps(Probe(vx=vx, vy=vy), do=path.append)\n",
" plt.plot([p.x for p in path], [p.y for p in path], '.:', label=f'({vx}, {vy})')\n",
" plt.legend()\n",
" \n",
"def box(Xs, Ys) -> Tuple[List[int], List[int]]:\n",
" \"\"\"A tuple of (x_coords, y_coords) to draw a box around the (x, y) points.\"\"\"\n",
" x1, x2, y1, y2 = min(Xs), max(Xs), min(Ys), max(Ys)\n",
" return [x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Below are four paths: two that hit the target, and two that miss:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
" plot_probes([(24, 5), (32, 0), (34, -5), (36, -10)])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Below we see that `vx=22` is doomed to stall before it reaches the target area; `vx=23` is the critical value that stalls and falls into the target area; and `vx=24` shoots beyond the target area before stalling."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
" plot_probes([(22, 12), (23, 10), (24, 8)])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Below is `vx=23` paired with three different `vy` velocities:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
" plot_probes([(23, 16), (23, 11), (23, 7)])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"When `vx < vy` the probe's path begins by bending up (e.g. for `(23, 32)`); when `vx = vy` the path is a straight line (with ever-slowing speed); and when `vx > vy` the path bends down (e.g. for `(23, 8)`). But all paths eventually stall and fall due to drag."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
" plot_probes([(23, 32), (23, 23), (23, 8)])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Below, initial velocity `(23, 100)`, yields the high point of 5050 before stalling and falling into the target:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
" plot_probes([(23, 100)])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note the y-axis on this plot is ten times more than the previous plot. That's why the target box looks so squished and the path slope looks shallow. If we equalize the two axes, we can see how steep the slope is (and why this day is titled \"Trick Shot\"):"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
" plot_probes([(23, 100)]); plt.axis('square');"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"I can now see a more efficient way to deal with this problem:\n",
" - The movement in `x` and `y` are independent, so we can treat them separately:\n",
" - For each `vx`, determine in which time step(s) a probe could intersect `target.Xs`.\n",
" - For each `vy`, determine in which time step(s) a probe could intersect `target.Ys`.\n",
" - Now if a time step has 10 `vx` and 12 `vy` intersects, then that's 120 hits. \n",
" - The number of simulations we have to do is the sum of the lengths of `vxs` and `vys`, not their product.\n",
" - This technique could have cut the number of simulations from about 50,000 to about 500. \n",
" - But 50,000 is a small number and the code runs in well under a second, so we don't need a re-implementation"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 18](https://adventofcode.com/2021/day/18): Snailfish\n",
"\n",
"- **Input**: The input is the math homework for some snailfish: each item is a *snailfish number*. A snailfish number is either a regular number (a non-negative integer) or a bracketed pair of two snailfish numbers separated by a comma. \n",
"\n",
"For now I'll leave each line of the input as a string:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"in18 = parse(18)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1**: Add up all of the snailfish numbers in the order they appear. **What is the magnitude of the final sum?**\n",
"\n",
"From [the Day 18 instructions](https://adventofcode.com/2021/day/18), I determined that I will need to do the following:\n",
"- **Add** two snailfish numbers.\n",
"- **Reduce** a snailfish number to simplified form.\n",
"- **Split** a regular number that is inside a snailfish number.\n",
"- **Explode** a deeply-nested pair inside a snailfish number.\n",
"- Compute the **magnitude** of a snailfish number.\n",
"\n",
"What representation of snailfish numbers can best handle these operations? I considered three candidates:\n",
"1. **String**: a string in the same format as the input file. I could manipulate them with regular expressions. But as my friend Jamie Zawinski [famously said](http://regex.info/blog/2006-09-15/247), \"now I have two problems.\"\n",
"2. **Tree**: a nested tree of pairs, each a Python list of two elements. The input string can be directly converted into this form with `functools.literal_eval`. But **explode** needs to find the \"previous\" and \"next\" numbers–that would require walking up and down the tree, or maintaining next/previous pointers. Sounds complicated.\n",
"3. **Flat list**: A flat linear list of regular numbers, with annotations giving the nesting level of each number. \n",
"\n",
"I decided to go with the flat list. The split and explode operations should be easy. Finding the previous and next number is trivial. However, for the **magnitude** calculation I think I will want to convert to the nested tree form. \n",
"\n",
"I will define these three data types:\n",
"- `Snum`: a *snailfish number*. Implemented as a mutable flat list of `Num` objects. \n",
"- `Num`: a *regular number*. A mutable object with two fields: `.n`, the number itself, and `.level`, the nesting level. \n",
"- `Tree`: a *snailfish number* represented by either an `int` or a list of two `Tree` elements.\n",
"\n",
"For example, the snailfish number `[[7,[8,9]],10]` would be represented as:\n",
"\n",
" Snum([Num(7, level=2), Num(8, level=3), Num(9, level=3), Num(10, level=1)])\n",
"\n",
"Here are the three type definitions:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class Snum(list): \n",
" \"\"\"An Snum is a list of Num components.\"\"\"\n",
"\n",
"@dataclass()\n",
"class Num:\n",
" \"\"\"A \"regular number\" within an Snum, annotated with its nesting level.\"\"\"\n",
" n: int\n",
" level: int\n",
" \n",
"Tree = Union[int, List['Tree']] # An int like `1` or a list like `[2,3]` or `[1,[2,3]]`."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Below are the four main operations. (I wanted to be sure I got the instructions right, so I copied them mostly verbatim into the docstrings.) Note that `split` and `explode` mutate their argument, and return `True` if they changed the argument. That simplifies the control flow in `snum_reduce`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def snum_add(left: Snum, right: Snum) -> Snum:\n",
" \"\"\"To add two snailfish numbers, form a pair from the left and right \n",
" parameters of the addition operator. Snailfish numbers must always be reduced.\"\"\"\n",
" snum = Snum(Num(x.n, x.level + 1) for x in left + right)\n",
" return snum_reduce(snum)\n",
"\n",
"def snum_reduce(snum) -> Snum:\n",
" \"\"\"Mutate snum until it is in reduced form.\n",
" To reduce a snailfish number, you must repeatedly do the first action \n",
" that applies to the snailfish number: (1) explode (2) split.\"\"\"\n",
" while explode(snum) or split(snum):\n",
" continue\n",
" return snum\n",
"\n",
"def split(snum) -> bool:\n",
" \"\"\"If any regular number is 10 or greater, the leftmost such regular number splits.\n",
" Mutate the snum and return True if there was a split; False if not.\n",
" To split a regular number, replace it with a pair; the left element of the pair \n",
" should be the regular number divided by two and rounded down, while the right element \n",
" of the pair should be the regular number divided by two and rounded up. \n",
" For example, 10 becomes [5,5], 11 becomes [5,6], 12 becomes [6,6], and so on.\"\"\"\n",
" i = first(i for i, s in enumerate(snum) if s.n >= 10)\n",
" if i is None: \n",
" return False\n",
" else: # The number to split is snum[i]\n",
" level = snum[i].level\n",
" L, R = snum[i].n // 2, (snum[i].n + 1) // 2\n",
" snum[i] = Num(L, level + 1)\n",
" snum.insert(i + 1, Num(R, level + 1))\n",
" return True\n",
" \n",
"def explode(snum) -> bool:\n",
" \"\"\"If any pair is nested inside four pairs, the leftmost such pair explodes.\n",
" Mutate the snum and return True if there was an explode; False if not.\n",
" To explode a pair, the pair's left value is added to the first regular number \n",
" to the left of the exploding pair (if any), and the pair's right value is added \n",
" to the first regular number to the right of the exploding pair (if any). \n",
" Exploding pairs will always consist of two regular numbers. \n",
" Then, the entire exploding pair is replaced with the regular number 0.\"\"\"\n",
" i = first(i for i, s in enumerate(snum) if s.level > 4)\n",
" if i is None:\n",
" return False\n",
" else: # the exploding pair is: [snum[i], snum[i + 1]]\n",
" if i - 1 >= 0: # The pair's left value is added to the number to the left\n",
" snum[i - 1].n += snum[i].n\n",
" if i + 2 < len(snum): # The pair's right value is added to the number to the right\n",
" snum[i + 2].n += snum[i + 1].n\n",
" snum[i:i+2] = [Num(0, snum[i].level - 1)] # Replace the pair with a `0`\n",
" return True"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Here are routines to convert from a string into an `Snum`, and from an `Snum` to a `Tree`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def snum_from_str(snum_str: str) -> Snum:\n",
" \"\"\"Convert a string representing a snailfish number into an Snum (a list of Nums).\"\"\"\n",
" level = 0\n",
" result = []\n",
" # Break the string into pieces, e.g. '[[7,8],10]' => '[[', '7', ',', '8', '],', '10', ']'\n",
" for piece in re.split(r'(\\d+)', snum_str):\n",
" if piece[0] in '0123456789':\n",
" result.append(Num(int(piece), level))\n",
" else:\n",
" level += piece.count('[') - piece.count(']')\n",
" return result\n",
" \n",
"def tree_from_snum(snum) -> Tree:\n",
" \"\"\"Convert an Snum into a nested tree of two-element pairs.\"\"\"\n",
" q = deque(snum)\n",
" def grab(level):\n",
" return (q.popleft().n if q[0].level == level \n",
" else [grab(level + 1), grab(level + 1)])\n",
" return grab(level=0)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Finally, the **magnitude**. I can't see an easy way to directly compute it on an `Snum`, so I'll convert to a `Tree` first."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def snum_magnitude(snum) -> int: \n",
" \"\"\"The magnitude of a pair is 3 times the magnitude of its left element \n",
" plus 2 times the magnitude of its right element. \n",
" The magnitude of a regular number is just that number.\"\"\"\n",
" def mag(tree): return tree if is_int(tree) else 3 * mag(tree[0]) + 2 * mag(tree[1])\n",
" return mag(tree_from_snum(snum))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now the puzzle solution:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"homework = mapt(snum_from_str, in18)\n",
"\n",
"answer(18.1, 4457, lambda: snum_magnitude(functools.reduce(snum_add, homework)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Great! It worked the first time! That justifies my choice of a flat list as the easiest representation for `Snum`.\n",
"\n",
"- **Part 2**: **What is the largest magnitude of any sum of two different snailfish numbers from the homework assignment?**\n",
"\n",
"My implementation is fast enough that I can try all possibilities:\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"answer(18.2, 4784, lambda:\n",
" max(snum_magnitude(snum_add(L, R)) \n",
" for L in homework \n",
" for R in homework if L != R))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 19](https://adventofcode.com/2021/day/19): Beacon Scanner \n",
"\n",
"- **Input**: The input is a sequence of *scanner reports*, where each report is a list of the relative three-dimensional distances to beacons that the scanner can see.\n",
"\n",
"I'll `parse` the input into sections, then split each section into lines, ignoring the first line with the scanner number, and parsing the remaining lines of each section into a sequence of 3D points, and collecting them into an object of type `Scanner`, which I define as a subclass of `set`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class Scanner(set):\n",
" \"\"\"A Scanner is a set of points representing the beacons that the scanner can see.\"\"\"\n",
" \n",
"in19 = parse(19, lambda section: Scanner(mapt(ints, section.splitlines()[1:])), '\\n\\n')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1**: Unfortunately, while each scanner can report the positions of all detected beacons relative to itself, the scanners do not know their own position. You'll need to determine the positions of the beacons and scanners yourself. Unfortunately, there's a second problem: the scanners also don't know their rotation or facing direction. Assemble the full map of beacons. **How many beacons are there?**\n",
"\n",
"The first thing I want to do is figure out how important efficiency is. `parse` said there are only 36 scanners. On average, how many beacons does each scanner see?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"mean(map(len, in19))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Not too bad; I was worried there could be thousands of beacons. There are:\n",
" - *S* = 36 scanners\n",
" - *B* = 26 mean beacons per scanner\n",
" - 24 orientations of each scanner (per the instructions)\n",
"\n",
"At first I was confused: I thought that if there are 3! permutations of `(x, y, z)`, and 23 ways to have or not have a minus sign on each dimension, then there should be 6 × 8 = 48 different orientations. But the instructions say 24. I decided that if you don't permute (x, y, z), and just negate the x component, that's equivalent to a mirror image, which we don't want. You can get a mirroring with a minus sign on any of the three components, or by reversing the order of the components (e.g. `(x, y, z)` going to `(z, y, x)`) but not if you just rotate the order (e.g. `(x, y, z)` going to `(z, x, y)`). Two mirrorings reverse things back to normal, so the 24 valid orientations are those with a rotation and an even number of minus signs, or a reversal in the ordering and an odd number of minus signs. \n",
"\n",
"Since I'm going to be applying these transforms often, I will compile them into functions. I'll do that by constructing strings and then applying `eval` to each string."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"Point3D = Tuple[int, int, int] # A 3D Point, like (1, 2, -3).\n",
"\n",
"def transformers(signs, orders) -> List[Callable]: \n",
" \"\"\"Strings that define 3D transformations of signs and variable ordering.\"\"\"\n",
" return [transformer(f'({a}{x}, {b}{y}, {c}{z})')\n",
" for a,b,c in signs for x,y,z in orders]\n",
"\n",
"def transformer(transform: str) -> Callable:\n",
" \"\"\"Turn this transformer string into a callable function.\"\"\"\n",
" fn = eval('lambda points: {' + transform + ' for x, y, z in points}')\n",
" fn.__name__ = 'Orient' + transform\n",
" return fn\n",
" \n",
"\n",
"orient_fns = (transformers([' ', '-- ', ' --', '- -'], ['xyz', 'yzx', 'zxy']) + \n",
" transformers(['---', '- ', ' - ', ' -'], ['zyx', 'xzy', 'yxz']))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Here's a test applying the 24 orientation functions to a tiny set `B` of 2 beacons:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"assert len(orient_fns) == 24\n",
"\n",
"B = {(1, 2, 3), (-4, -5, -6)}\n",
"\n",
"assert [(fn.__name__, fn(B)) for fn in orient_fns] == [\n",
" ('Orient( x, y, z)', {(-4, -5, -6), (1, 2, 3)}),\n",
" ('Orient( y, z, x)', {(-5, -6, -4), (2, 3, 1)}),\n",
" ('Orient( z, x, y)', {(-6, -4, -5), (3, 1, 2)}),\n",
" ('Orient(-x, -y, z)', {(-1, -2, 3), (4, 5, -6)}),\n",
" ('Orient(-y, -z, x)', {(-2, -3, 1), (5, 6, -4)}),\n",
" ('Orient(-z, -x, y)', {(-3, -1, 2), (6, 4, -5)}),\n",
" ('Orient( x, -y, -z)', {(-4, 5, 6), (1, -2, -3)}),\n",
" ('Orient( y, -z, -x)', {(-5, 6, 4), (2, -3, -1)}),\n",
" ('Orient( z, -x, -y)', {(-6, 4, 5), (3, -1, -2)}),\n",
" ('Orient(-x, y, -z)', {(-1, 2, -3), (4, -5, 6)}),\n",
" ('Orient(-y, z, -x)', {(-2, 3, -1), (5, -6, 4)}),\n",
" ('Orient(-z, x, -y)', {(-3, 1, -2), (6, -4, 5)}),\n",
" ('Orient(-z, -y, -x)', {(-3, -2, -1), (6, 5, 4)}),\n",
" ('Orient(-x, -z, -y)', {(-1, -3, -2), (4, 6, 5)}),\n",
" ('Orient(-y, -x, -z)', {(-2, -1, -3), (5, 4, 6)}),\n",
" ('Orient(-z, y, x)', {(-3, 2, 1), (6, -5, -4)}),\n",
" ('Orient(-x, z, y)', {(-1, 3, 2), (4, -6, -5)}),\n",
" ('Orient(-y, x, z)', {(-2, 1, 3), (5, -4, -6)}),\n",
" ('Orient( z, -y, x)', {(-6, 5, -4), (3, -2, 1)}),\n",
" ('Orient( x, -z, y)', {(-4, 6, -5), (1, -3, 2)}),\n",
" ('Orient( y, -x, z)', {(-5, 4, -6), (2, -1, 3)}),\n",
" ('Orient( z, y, -x)', {(-6, -5, 4), (3, 2, -1)}),\n",
" ('Orient( x, z, -y)', {(-4, -6, 5), (1, 3, -2)}),\n",
" ('Orient( y, x, -z)', {(-5, -4, 6), (2, 1, -3)})\n",
"]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"That looks good. The next step is to find pairs of scanners that can see the same beacons. The instructions say that every scanner has at least one other scanner with which it shares at least 12 detected beacons. How can I find the matches, and do it efficiently? Some options:\n",
"1. For every pair of scanners, I could apply every orientation transformation to *one of them, then try every alignment of a point in that transformation to a point in the other scanner, then count the number of matching points with that alignment, and see if the count exceeds 12. Total complexity: 24 *S*2 *B*3 operations.\n",
"2. For every scanner, compute the distance between each pair of points. (This set of distances will be invariant under orientation transformations, so we've saved a factor of 24.) Now compare distance sets between pairs of scanners and see which ones exceed 12×11/2 = 66 matches. Total complexity: *S* *B*2 + *S*2*B*2 operations. (Note: we actually store squared distances, not distances, to avoid possible round-off error and to save the runtime cost of computing square roots.)\n",
"\n",
"Option 2 it is! The algorithm will be:\n",
"\n",
" keep track of lists of `aligned` and `unaligned` scanners\n",
" add the first unaligned scanner to the list of aligned scanners\n",
" whenever an aligned scanner is added:\n",
" check to see if each unaligned can scanner can be aligned with it\n",
" while there are unaligned scanners:\n",
" pop off one unaligned scanner S\n",
" find an unaligned scanner R that c\n",
" \n",
"**NOTE: I started below, but did not complete the solution.**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def align_scanners(scanners, k=12) -> Set[Point3D]:\n",
" \"\"\"Align all scanners\"\"\"\n",
" for S in scanners:\n",
" S.distances2 = {distance2(p, p2) for p, p2 in combinations(S, 2)}\n",
" aligned, unaligned = [], scanners\n",
" return align_scanner(unaligned.pop(), aligned, unaligned, k)\n",
"\n",
"def add_aligned_scanner(S, aligned, unaligned, k) -> List[Scanner]:\n",
" \"\"\"Add S to aligned; recursively align other scanners with S; return aligned.\"\"\"\n",
" aligned.append(S)\n",
" unaligned.remove(S)\n",
" for R in unaligned.copy():\n",
" if R not in aligned and len(S.distances2 & R.distances2) >= k * (k - 1) // 2:\n",
" add_aligned_scanner(align(R, S), aligned, unaligned, k)\n",
" \n",
"def distance2(p: Point3D, p2: Point3D) -> int:\n",
" \"\"\"Squared distance between two 3D points.\"\"\"\n",
" return (p[0]-p2[0]) ** 2 + (p[1]-p2[1]) ** 2 + (p[2]-p2[2]) ** 2\n",
"\n",
"def align(R: Scanner, S: Scanner, k=12) -> Scanner:\n",
" \"Orient and offset Scanner R to align with Scanner S; return the new version of R.\"\n",
" for orient in orient_fns:\n",
" R2 = orient(R)\n",
" for offset in offsets(S1, A):\n",
" S2 = Scanner(add3D(s, offset) for s in S)\n",
" if len(S2 & A) >= k:\n",
" S2.distances2 = S.distances2\n",
" return S2\n",
" raise ValueError(\"Can't align scanners\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now that we can find two scanners that are alignable, we have to actually align them:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def offsets(S, A) -> Iterator[Point3D]:\n",
" \"\"\"All offsets that would make some point in S align with some point in A.\"\"\"\n",
" return (minus3D(a, s) for s in S for a in A)\n",
"\n",
"def minus3D(A: Point3D, B: Point3D) -> Point3D:\n",
" return (A[0] - B[0], A[1] - B[1], A[2] - B[2])\n",
"\n",
"def add3D(A: Point3D, B: Point3D) -> Point3D:\n",
" return (A[0] + B[0], A[1] + B[1], A[2] + B[2])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 20](https://adventofcode.com/2021/day/20): Trench Map\n",
"\n",
"- **Input**: The input is in two sections: first, a 512-character string describing an image enhancement algorithm; second a grid of pixels depicting an image.\n",
"\n",
"I'll parse the file into two sections, calling the first segment `rules` and the second `image`. Then I'll convert the `image` to a `Grid`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"rules, image = in20 = parse(20, sep='\\n\\n')\n",
"image = Grid(image.splitlines())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1**: Start with the original input image and apply the image enhancement algorithm twice, being careful to account for the infinite size of the images. **How many pixels are lit in the resulting image?**"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**It is a truth universally acknowledged,** that an Eric Wastl in possession of an Advent of Code, must be in want of a [Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) puzzle. This is it! The \"image enhancement algorithm\" defines how pixels turn on and off. I've done Life before, e.g. [here](Life.ipynb) and [here](https://adventofcode.com/2020/day/17). This should be easy! \n",
"\n",
"**Unfortunately**, there's a twist: spontaneous generation. I was *shocked* to find `rules[0] == '#'` in my enhancement rules. (That wasn't the case for the example in the instructions.) This means that every dark pixel in the middle of empty dark space will spontaneously become light pixel in the next generation, resulting in an infinite number of light pixels.\n",
"\n",
"\n",
"\n",
"**Fortunately**, `rules[255] == '.'`, which means that almost all of the infinite number of light pixels will become dark again in the following generation. It's as if we convert between a positive image and a negative on each enhancement.\n",
"\n",
"Therefore, my strategy will be:\n",
"- In generation 0 (and every even generation) we will have a finite set of *light* pixels. Use them to generate a finite set of *dark* pixels.\n",
"- In generation 1 (and every odd generation), we will have a finite set of *dark* pixels. Use them to generate a finite set of *light* pixels.\n",
"- Generating a dark pixel requires at least one neighboring light pixel, and generating a light pixel requires at least one neighboring dark pixel. So to know what pixels to consider as candidates for the next generation, I can use the current set of pixels and their neighbors. So I won't need the `Grid`, just the `set` of light pixels in it. I'll create a subclass of `set` called `Pixels` which is just like `set` but keeps track of the `.color` of pixels, light or dark."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"light = '#'\n",
"dark = '.'\n",
"\n",
"class Pixels(set):\n",
" \"\"\"A set of pixel Points; self.color says if they are light or dark.\"\"\"\n",
" def __init__(self, pixels, color=light):\n",
" self.update(pixels)\n",
" self.color = color\n",
"\n",
"def enhance(pixels, n=1, rules=rules) -> Pixels:\n",
" \"\"\"The set of pixels that result from applying enhancement rules `n` times.\"\"\"\n",
" for generation in range(n):\n",
" candidate_pixels = set(flatten(map(neighborhood, pixels)))\n",
" negative = dark if pixels.color == light else light\n",
" pixels = Pixels((p for p in candidate_pixels\n",
" if rules[pixel_sum(p, pixels)] == negative),\n",
" negative)\n",
" return pixels\n",
"\n",
"def pixel_sum(point, pixels):\n",
" \"\"\"The sum of the 9 pixels in the neighborhood. If `pixels.color == light`,\n",
" add up the neighborhood points that are in `pixels`; if not,\n",
" add up the neighborhood points that are not in `pixels`.\"\"\"\n",
" return sum((256 >> i) * ((p in pixels) == (pixels.color == light))\n",
" for i, p in enumerate(neighborhood(point)))\n",
"\n",
"def neighborhood(point) -> List[Point]:\n",
" \"The nine points surrounding `point` (including `point` itself).\"\n",
" (x, y) = point\n",
" return [(x-1, y-1), (x, y-1), (x+1, y-1), \n",
" (x-1, y), (x, y), (x+1, y), \n",
" (x-1, y+1), (x, y+1), (x+1, y+1)]\n",
"\n",
"pixels = Pixels(p for p in image if image[p] == light) # pixels in my image"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"answer(20.1, 5437, lambda: len(enhance(pixels, 2)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: Start again with the original input image and apply the image enhancement algorithm 50 times. **How many pixels are lit in the resulting image?**\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"answer(20.2, 19340, lambda: len(enhance(pixels, 50)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 21](https://adventofcode.com/2021/day/21): Dirac Dice\n",
"\n",
"\n",
"We're playing a two-player game with the following rules: On each turn a player rolls the die three times, adds them up, and moves forward that many spaces on a circular board consisting of spaces marked 1 to 10; space 10 wraps around to 1. The player increases their score by the space they land on; first player to 1000 points wins.\n",
"\n",
"- **Input**: My input is the text \"`Player 1 starting position: 5 \\ Player 2 starting position: 6`\".\n",
"\n",
"I won't bother to read this from the file; I'll just translate it into this statement:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"start_positions = (5, 6)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1**: Play a practice game using the deterministic 100-sided die. The moment either player wins, **what do you get if you multiply the score of the losing player by the number of times the die was rolled during the game?**\n",
"\n",
"I'll implement a deterministic die as an iterator: `cycle(range(1, 101)` returns 1 first, then 2, etc. up to 100, then goes back to 1. My function `play_dice` keeps track of positions and scores for both players and returns the product of the number of dice rolled times the loser's score."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def play_dice(die: Iterator, positions=start_positions, target=1000) -> int:\n",
" \"\"\"Play the dice game with the given die until someone scores `target`.\n",
" Return total_number_of_dice_rolled * loser_score.\"\"\"\n",
" positions = list(positions)\n",
" scores = [0, 0]\n",
" for turn in count_from(1):\n",
" player = (turn - 1) % 2\n",
" roll = next(die) + next(die) + next(die)\n",
" positions[player] = clock_mod(positions[player] + roll, 10)\n",
" scores[player] += positions[player]\n",
" if scores[player] >= 1000:\n",
" return 3 * turn * min(scores)\n",
" \n",
"answer(21.1, 1002474, lambda: play_dice(die=cycle(range(1, 101))))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: This time we play with a quantum die. Using your given starting positions, determine every possible outcome. **Find the player that wins in more universes; in how many universes does that player win?**\n",
"\n",
"A three-sided quantum die splits the universe into three copies every time it is rolled: one where the outcome of the roll was 1, one where it was 2, and one where it was 3. We need to track all these universes. But for this game a player only needs 21 points to win; not 1000.\n",
"\n",
"The instructions warn us that there will be *trillions* of universes, so I'm concerned about efficiency. I must avoid enumerating all possibile universes one by one. I should either use a `Counter` of game states (as with the lanternfish in Day 6), or use dynamic programming (which I can implement with caching on a recursive function). \n",
"\n",
"I decided to go with the recursive function. I'll define `play_dice2(pos1, pos2, score1, score2, target)` as a function that returns a tuple of (number of wins, number of losses) for player 1, given the positions of the two players, the scores of the two players, and the target winning score, under the assumption that it is player 1's turn to roll. Note that in the recursive call to `play_dice2` it is player 2's turn, so the arguments and return values are swapped. I did it this way, rather than have two-element sequences for the positions and scores, because this way cuts in half the number of different game states I have to track."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dice3_rolls = mapt(sum, cross_product([1, 2, 3], repeat=3))\n",
"\n",
"@cache\n",
"def play_dice2(pos1, pos2, score1, score2, target) -> Tuple[int, int]:\n",
" \"\"\"The number of (winning universes, losing universes) for player 1,\n",
" given that player 1 is at `pos1` with `score1` points (and likewise for player 2).\"\"\"\n",
" wins, losses = 0, 0\n",
" if score2 >= target: # Player 2 has won; record a loss for player 1\n",
" losses += 1 \n",
" else: # Player 1 takes their turn; then count wins and loses for the game\n",
" for roll in dice3_rolls:\n",
" newpos1 = clock_mod(pos1 + roll, 10)\n",
" roll_losses, roll_wins = play_dice2(pos2, newpos1, score2, score1 + newpos1, target)\n",
" wins += roll_wins\n",
" losses += roll_losses\n",
" return wins, losses\n",
"\n",
"answer(21.2, 919758187195363, lambda: max(play_dice2(*start_positions, 0, 0, 21)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"I had some ideas to make this more efficient (such as iterating over a Counter of `dice3_rolls`) but it is fast enough as is.\n",
"\n",
"How many different game states did we explore?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"play_dice2.cache_info()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Only 24,841 distinct game states."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 22](https://adventofcode.com/2021/day/22): Reactor Reboot\n",
"\n",
"- **Input**: The input is a list of steps for rebooting the ship's reactor, such as \"`on x=10..12,y=10..12,z=10..12`\".\n",
"\n",
"I'll parse each line into a tuple consisting of either `\"on\"` or `\"off\"` followed by three ranges (using `cover` because the high end is inclusive):"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"Step = Tuple[str, range, range, range]\n",
"\n",
"def parse_reboot_step(line: str) -> Step:\n",
" \"\"\"Parse a line into a reboot step description.\"\"\"\n",
" x1, x2, y1, y2, z1, z2 = ints(line)\n",
" return (line.split()[0], cover(x1, x2), cover(y1, y2), cover(z1, z2))\n",
"\n",
"in22 = parse(22, parse_reboot_step)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The intent of each step is to turn on or off all the individual cubes in the reactor core that are within the *cuboid* specified by the three ragnes. So a 3x3x3 cuboid specifies 27 cubes.\n",
"\n",
"- **Part 1**: Execute the reboot steps. Afterward, considering only cubes in the region x=-50..50,y=-50..50,z=-50..50, **how many cubes are on?**\n",
"\n",
"[I have a bad feeling about this](https://www.youtube.com/watch?v=S74rvpc6W60). The example in the instructions has a small enough number of cubes that we can just brute-force enumerate them. But I suspect in Part 2 there will be trillions of cubes and brute force willl no longer work. [It's a trap!](https://www.youtube.com/watch?v=4F4qzPbcFiA) \n",
"\n",
"Still, I'll go ahead and do the brute-force enumeration for Part 1: for each step that describes a cuboid that falls within the region, form the set of all the cubes in the cuboid, and add or subtract them from a running set of `cubes`. Each cube is denoted by a single `(x, y, z)` triple of integer coordinates. Note that it is guaranteed that every step describes a cuboid that is either entirely within or entirely outside the position range -50..50 on all three dimensions, so I only need to check one of the three dimensions."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def count_cubes(steps, region=cover(-50, 50)) -> int:\n",
" \"\"\"Follow `steps` to turn on or off those cubes that are in `region`, and count 'on' cubes.\"\"\"\n",
" cubes = set()\n",
" for (flip, xs, ys, zs) in steps:\n",
" if xs.start < region.start or xs.stop > region.stop:\n",
" pass # step is outside the region\n",
" elif flip == \"on\":\n",
" cubes |= set(cross_product(xs, ys, zs))\n",
" else:\n",
" cubes -= set(cross_product(xs, ys, zs))\n",
" return len(cubes) \n",
"\n",
"answer(22.1, 533863, lambda: count_cubes(in22))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: Starting again with all cubes off, execute all reboot steps. Afterward, considering all cubes, **how many cubes are on?**\n",
"\n",
"Well, I was *wrong*. There aren't going to be *trillions* of cubes; there are going to be *quadrillions*. \n",
"\n",
"I will need a way to handle steps without explicitly enumerating the cubes. If no cuboids intersected, it would be easy: for each \"on\" cuboid, multiply the lengths of the three sides to get the volume, then add up the volumes. But the intersections complicate things.\n",
"\n",
"I'll start with some [exploratory data analysis](https://en.wikipedia.org/wiki/Exploratory_data_analysis) to answer some questions.\n",
"\n",
"**Question**: how many of the steps are \"on\"?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"Counter(step[0] for step in in22)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Answer**: There are 420 steps, of which about 3/4 are \"on\" steps.\n",
"\n",
"**Question**: How big are the cuboids? "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"volumes = [len(x) * len(y) * len(z) / 1e12 for (on, x, y, z) in in22]\n",
"plt.hist(volumes, rwidth=0.8, bins=36, \n",
" label=f'cuboid volume in trillions (mean {mean(volumes):.1f})')\n",
"plt.legend();"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Answer**: An average cuboid consists of 7.6 trillion cubes. A big one has 35 trillion, but most are under 10 trillion. \n",
"\n",
"**Question**: How long are the sides of the cuboids?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"lengths = [[len(step[i]) for step in in22] for i in (1,2,3)]\n",
"plt.hist(lengths, bins=25, label=list('xyz'))\n",
"plt.legend();"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Answer**: The histogram above breaks out the lengths of the sides of the cuboids by x, y, and z dimensions. It looks like a roughly normal distribution (although noisy) in each dimension with mean around 20,000, except that we can see a spike just above length 0 for the 20 small cuboids that are used in Part 1 (plus a few other cuboids that happen to have small ranges in one of the `x` or `z` dimensions, but not `y`).\n",
"\n",
"**Question**: Can I visualize the actual cuboids?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plt.figure(figsize=(6, 6)) # Make it square\n",
"for (on, xs, ys, zs) in in22:\n",
" plt.plot(*box(xs, ys), '-')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Answer**: The plot above gives a 2D projection into the XY plane. (You could also project to XZ or YZ planes, but I don't think that would be very different. To do 3D you'd need animation. A 3D VR headset would help.) \n",
"\n",
"We can also look more closely at a sample of every tenth cuboid, this time in the YZ plane:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plt.figure(figsize=(6, 6))\n",
"for (on, xs, ys, zs) in in22[::10]:\n",
" plt.plot(*box(ys, zs), '-')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Question**: What's a good strategy for counting cubes?\n",
"\n",
"**Answer**: You can see in the plots above that when two rectangles intersect, they can leave non-rectangular pieces (and in 3 dimensions, non-cuboid pieces). That's the difficulty. The key is to break these pieces up into smaller pieces that are all cuboids. The algorithm will be:\n",
"\n",
"1. Find all the **split points** in the first dimension–values of `x` that start or end a cuboid. Create a list of **bins**, one covering the range between each pair of adjacent split points.\n",
"\n",
"2. Assign each step to the bin(s) it overlaps. For example (in 2 dimensions), suppose that step 1 is a 7x4 \"on\" step and step 2 is a 6x4 \"off\" step, and that they span the locations shown below left. When we split on `x` we get the three bins shown below right (all 1s in the left bin, all 2s in the right bin, and both 1 and 2 in the middle bin):\n",
"\n",
" 1111111 1111 111\n",
" 1111111 1111 111\n",
" 1111222222 1111 222 222\n",
" 1111222222 1111 222 222\n",
" 222222 222 222\n",
" 222222 222 222\n",
"\n",
"\n",
"3. Recurse if there are more dimensions to split on. In the example, we need to split on `y`. The two outside bins aren't split (because they only have one step), but the middle bin is split into 3 bins. Notice that each bin is a rectangle (and in 3 dimensions, each bin would be a cuboid). \n",
"\n",
" 111\n",
" 1111 111\n",
" 1111\n",
" 1111 222 222\n",
" 1111 222 222\n",
" 222\n",
" 222 222\n",
" 222\n",
" \n",
"\n",
"4. For each bin that is \"on\", multiply their side lengths to get their volume, and add up the volumes to get the total number of \"on\" cubes. In the example, the \"on\" bins (with step 1) are one 4x4 cuboid and one 3x2 cuboid, giving a total of 22 cubes.\n",
"\n",
"For the real input data, I would probably end up with a million or so bins. Adding up the volumes of a million bins will be faster than enumerating a quadrillion cubes.\n",
" \n",
"Below is the implementation. For the `Bin` data type, I'll have a field for the dimension we are splitting on; one for the range of values covered in that dimension by the bin; and one for a list of `kids`: At first it holds a list of all the steps that at least partially overlap the bin, but then (for the `x` and `y` dimensions but not `z`) that list of steps will be replaced by a list of bins."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"@dataclass\n",
"class Bin:\n",
" dim: int # 1, 2, or 3 for x, y, or z dimension\n",
" range: range # locations that are covered by this bin, on the given dimension\n",
" kids: Union[List[Step], List['Bin']] # Steps that overlap this bin (may be converted to bins)\n",
"\n",
"def bins_from_steps(steps, dims=(1, 2, 3)) -> List[Bin]:\n",
" \"\"\"Convert the steps to a list of bins.\"\"\"\n",
" # (1) Find all the split points in the first dimension. Create a list of bins.\n",
" dim, *more_dims = dims\n",
" splits = sorted(set(flatten((step[dim].start, step[dim].stop) for step in steps)))\n",
" bins = [Bin(dim, range(*pair), []) for pair in pairs(splits)]\n",
" # (2) Assign each step to the bin(s) it overlaps.\n",
" for bin in bins:\n",
" bin.kids = [step for step in steps if overlaps(bin.range, step[dim])]\n",
" # (3) Recurse if there are more dimensions\n",
" if more_dims:\n",
" for bin in bins:\n",
" bin.kids = bins_from_steps(bin.kids, more_dims)\n",
" return bins\n",
"\n",
"def overlaps(A: range, B: range) -> bool:\n",
" \"\"\"Do these ranges overlap?\"\"\"\n",
" return (B.start <= A.start < B.stop) or (A.start <= B.start < A.stop)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Here is the example, showing the three bins at the `x` level, and five at the `y` level:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"steps2 = [('on', range(0, 7), range(0, 4)), \n",
" ('off', range(4, 9), range(2, 6))]\n",
"\n",
"assert (bins_from_steps(steps2, (1, 2)) == \n",
" [Bin(1, range(0, 4), \n",
" [Bin(2, range(0, 4), [('on', range(0, 7), range(0, 4))])]), \n",
" Bin(1, range(4, 7), \n",
" [Bin(2, range(0, 2), [('on', range(0, 7), range(0, 4))]), \n",
" Bin(2, range(2, 4), [('on', range(0, 7), range(0, 4)), \n",
" ('off', range(4, 9), range(2, 6))]), \n",
" Bin(2, range(4, 6), [('off', range(4, 9), range(2, 6))])]), \n",
" Bin(1, range(7, 9), \n",
" [Bin(2, range(2, 6), [('off', range(4, 9), range(2, 6))])])])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The test passes! The only thing that remains is to count up the number of cubes in the \"on\" cuboids. I'll define `count_cubes2` so that it can handle Part 1 as well as Part 2:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def count_cubes2(steps, region=None) -> int:\n",
" \"\"\"Follow `steps` to turn on or off cubes, and count 'on' cubes.\"\"\"\n",
" if region:\n",
" steps = [s for s in steps if s[1].start >= region.start and s[1].stop <= region.stop]\n",
" return sum(count_cubes_in_bin(bin) for bin in bins_from_steps(steps))\n",
"\n",
"def count_cubes_in_bin(bin) -> int:\n",
" \"\"\"How many \"on\" cubes are in this bin? Recurse down to `dim == 3`, keeping track of \n",
" the lengths of all three sides, and return the product for bins that are \"on\".\"\"\"\n",
" N = len(bin.range)\n",
" if bin.dim < 3:\n",
" return N * sum(map(count_cubes_in_bin, bin.kids))\n",
" elif bin.kids and (bin.kids[-1][0] == 'on'):\n",
" return N\n",
" else:\n",
" return 0"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's go ahead and solve the complete puzzle without any more tests:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"answer(22.1, 533863, lambda: count_cubes2(in22, region=cover(-50, 50)))\n",
"answer(22.2, 1261885414840992, lambda: count_cubes2(in22))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We got the right answers. Part 1 runs 50 times faster; Part 2 takes over 5 seconds. One idea to make it faster: I compare every step to every bin. This could probably be speeded up with a binary search. I chose not to do that because binary searches are notorious for off-by-one errors; doubly so when searching over ranges of numbers, not a single number."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 23](https://adventofcode.com/2021/day/23): Amphipod\n",
"\n",
"**NOTE: I started below, but did not complete the solution.** This should be an easy application of A-star search to a grid problem, but I didn't find the time to complete it, because of holiday season obligations."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class HGrid(Grid):\n",
" \"\"\"A hashable grid; can be used as a key in dicts.\"\"\"\n",
" def __hash__(self): return hash(tuple(sorted(self.items())))\n",
" \n",
"in23 = HGrid(parse(23))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"amphipods = A, B, C, D = 'ABCD' # The four kinds of creature\n",
"amphi_costs= {A:1, B:10, C:100, D:1000} # Cost of each step\n",
"goal_cols = {A: 3, B: 5, C: 7, D: 9} # Column each Amphipod type aspires to\n",
"hall_row = 1 # Row where the hall spaces are\n",
"side_rows = {2, 3} # Rows every Amphipod aspires to\n",
"side_rooms = {(goal_cols[L], r) for r in side_rows for L in amphipods} # All the spaces off of the hallway\n",
"hallway = {(c, hall_row) for c in range(1, 12)} # Hallway spaces\n",
"hall_stops = {(c, r) for (c, r) in hallway if c not in goal_cols.values()} # Hallway spaces where you're allowed to stop\n",
"\n",
"@dataclass\n",
"class Move:\n",
" start: Point\n",
" end: Point\n",
" what: Char\n",
" \n",
"def extract_state(grid, kinds):\n",
" \"\"\"The parts of the grid of a certain kind.\"\"\"\n",
" return {loc: grid[loc] for loc in grid if grid[loc] in kinds}\n",
"\n",
"def clear(loc, dest, state):\n",
" \"\"\"Is the path from `loc` to `dest` clear of Amphipods in state?\"\"\"\n",
" Δx = sign(X_(dest) - X_(loc))\n",
" Δy = sign(Y_(dest) - Y_(loc))\n",
" return all((X_(loc) + i * Δx, Y_(loc) + i * Δy) not in state\n",
" for i in range(1, distance(loc, dest) + 1))\n",
"\n",
"def amphi_moves(state) -> Iterable[Move]:\n",
" \"\"\"All the moves that can be made from a state.\"\"\"\n",
" for loc in state:\n",
" # If you're in a side room and you have a clear path to the doorway ...\n",
" if loc in side_rooms and clear(loc, (X_(loc), hall_row), state):\n",
" # ... then you can move to any hall stop that you have a clear path to\n",
" for hall in hall_stops:\n",
" if clear((X_(loc), hall_row), hall, state):\n",
" yield Move(loc, hall, state[loc])\n",
" # If you're in the hallway and you have a clear path to your doorway ... \n",
" goal_col = goal_cols[state[loc]]\n",
" if loc in hallway and clear(loc, (goal_col, hall_row), state):\n",
" # ... then you can move to the top empty space in your side room if no foreign occupants\n",
" occupants = {L for (x, y), L in state.items() if x == goal_col}\n",
" if not occupants or occupants == {state[loc]}:\n",
" top_row = max(y for y in side_rows if (goal_col, y) not in state)\n",
" yield Move(loc, (goal_col, top_row), state[loc])\n",
" \n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"Δc = 0\n",
"Δc"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from time import perf_counter as pfc\n",
"from heapq import heappop, heappush\n",
"\n",
"\n",
"def read_puzzle(filename):\n",
" with open(filename) as f:\n",
" return ''.join([c for c in f.read() if c in 'ABCD.'])\n",
"\n",
"\n",
"def can_leave_room(puzzle, room_pos):\n",
" for a in room_pos:\n",
" if puzzle[a] == '.': continue\n",
" return a\n",
"\n",
"\n",
"def blocked(a,b,puzzle):\n",
" step = 1 if a Dict[str, int]:\n",
" \"\"\"Run a program with an input string on the submarine's ALU,\n",
" and return a dict of the program variables (and {'error': op} if appropriate).\"\"\"\n",
" inputs = iter(inputstr)\n",
" M = dict(w=0, x=0, y=0, z=0) # Memory\n",
" for op, a, *rest in program:\n",
" b = M.get(rest[0], rest[0]) if rest else None\n",
" if op == 'inp': \n",
" M[a] = int(next(inputs))\n",
" elif op == 'add': \n",
" M[a] += b\n",
" elif op == 'mul': \n",
" M[a] *= b\n",
" elif op == 'div': \n",
" if b == 0: return {'error': 1, **M}\n",
" M[a] //= b\n",
" elif op == 'mod': \n",
" if M[a] < 0 or b <= 0: return {'error': 2, **M}\n",
" M[a] %= b\n",
" elif op == 'eql': \n",
" M[a] = int(M[a] == b)\n",
" return M\n",
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can run a program:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"alu(program, '12345678901234')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's figure out where the input instructions are:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"{i for i, ins in enumerate(program) if ins[0] == 'inp'}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"They're every 18 instructions. Let's look at the program divided into 18-instruction segments:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def instr(i) -> str: return f'{i:3d}: ' + ' '.join(map(str, program[i])).ljust(9)\n",
"\n",
"for i in range(18):\n",
" print(\"| \".join(instr(i + 18 * j) for j in range(6)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Each column is similar, but there are different constants for instructions 4–5 and 15 in each column:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"{i: {instr(i + 18 * j)[5:].strip() for j in range(6)} for i in range(18)}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's try every possible digit as the first input, and see what the program computes right up to the point where it is ready to read the second digit:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"{d: alu(in24[:18], d) for d in '0123456789'}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"It looks like after processing one digit, *d*, we have:\n",
"- w = *d*\n",
"- x = 1\n",
"- y = z = *d* + 6\n",
" \n",
"Let's try it for every two-digit input:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def inputs(n) -> List[str]: return [str(i).zfill(n) for i in range(10 ** n)]\n",
"\n",
"{i: alu(in24[:36], i) for i in inputs(2)}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"So, *w* is the last digit of the input; *x* is always 1; *y* is 6 more than the last digit, and *z* is 162 + 26 times the first digit plus the last digit.\n",
"\n",
"**I was unable to figure out was going on.**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"{i: alu(in24, i) for i in ['10000000000000', \n",
" '12345678901234', \n",
" '11111111111111',\n",
" '23456789876543']}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 25](): Sea Cucumber\n",
"\n",
"- **Input**: The input is a map of east-moving (`>`) and south-moving (`v`) sea cucumbers.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"in25 = Grid(parse(25))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1**: Every step, the sea cucumbers in the east-facing herd attempt to move forward one location, then the sea cucumbers in the south-facing herd attempt to move forward one location. When a herd moves forward, every sea cucumber in the herd first simultaneously considers whether there is a sea cucumber in the adjacent location it's facing (even another sea cucumber facing the same direction), and then every sea cucumber facing an empty location simultaneously moves into that location. **Find somewhere safe to land your submarine. What is the first step on which no sea cucumbers move?**\n",
"\n",
"Since the herd moves simultaneously, I won't directly update the grid; rather I will keep track of the herd of east-goers (as a set of points) and the herd of south-goers, and update each set on each step. (After extracting the two herds from the grid, it is only used for its width and height)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def move_cukes(grid) -> int:\n",
" \"\"\"Move sea cucumbers according to rules until they stop moving.\n",
" Return the number of steps it took.\"\"\"\n",
" east = {p for p in grid if grid[p] == '>'}\n",
" south = {p for p in grid if grid[p] == 'v'}\n",
" for step in count_from(1):\n",
" new_east = set(move_herd(east, south, 1, 0, grid.width, grid.height))\n",
" new_south = set(move_herd(south, new_east, 0, 1, grid.width, grid.height))\n",
" if east == new_east and south == new_south:\n",
" return step\n",
" east, south = new_east, new_south\n",
"\n",
"def move_herd(herd: Set[Point], other_herd, dx, dy, width, height) -> Iterator[Point]:\n",
" \"\"\"The new positions of the herd; they each move by (dx, dy) if that space is open.\"\"\"\n",
" for (x, y) in herd:\n",
" p2 = ((x + dx) % width, (y + dy) % height)\n",
" yield p2 if (p2 not in herd and p2 not in other_herd) else (x, y)\n",
" \n",
"answer(25.1, 424, lambda: move_cukes(in25))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Unfortunately for me, Part 2 of Day 25 just asked if you had solved all the other puzzles. I hadn't."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Submarine Captain's Log, Supplemental\n",
"\n",
"Here is a summary of the results:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"answers"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"I didn't complete days **19, 23, and 24**. 19 is a bit tedious, but shouldn't be conceptually hard. 23 is easy; just need to apply standard graph-search ideas. For 24, I have no idea. I need to reverse engineer the program to understand what it is actually doing. \n",
"\n",
"We see that only the following puzzles took more than a second of run time:\n",
"- **15.2 Chiton**: just over 1 second for search through the 500x500 grid \n",
"- **20.2 Trench Map**: 4 seconds for enhancing the 100x100 grid for 50 generations\n",
"- **22.2 Reactor reboot**: 5 seconds for counting a quadrillion cubes"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"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.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 4
}