\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 in part 2 (although I may edit the code for clarity, without changing the initial approach).\n",
"\n",
"# Day 0: Preparations\n",
"\n",
"First, imports that I have used in past AoC years:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from __future__ import annotations\n",
"from collections import Counter, defaultdict, namedtuple, deque\n",
"from itertools import permutations, combinations, chain, count as count_from, product as cross_product\n",
"from typing import *\n",
"from statistics import mean, median\n",
"from math import ceil, inf\n",
"from functools import lru_cache\n",
"import matplotlib.pyplot as plt\n",
"import re"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Each day's work will consist of three tasks, denoted by three bulleted section:\n",
"- **Input**: Parse the day's input file. I will use the function `parse(day, parser, sep)`, which:\n",
" - Reads the input file for `day`.\n",
" - Prints out the first few lines of the file (to remind me, and the notebook reader, what's there).\n",
" - Breaks the file into a sequence of *entries* separated by `sep` (default newline).\n",
" - Applies `parser` to each entry and returns the results as a tuple.\n",
" - Useful parser functions include `ints`, `digits`, `atoms`, `words`, and the built-ins `int` and `str`.\n",
"- **Part 1**: Write code to compute the answer to Part 1, and submit the answer to the AoC site. Use the function `answer` to record the correct answer here in the notebook and serve as a regression test when the notebook is re-run. If there are non-trivial components in this code, I might provide unit tests for them using `assert`.\n",
"- **Part 2**: Repeat coding and `answer` for Part 2.\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"def answer(puzzle_number, got, expected) -> bool:\n",
" \"\"\"Verify the answer we got was the expected answer.\"\"\"\n",
" assert got == expected, f'For {puzzle_number}, expected {expected} but got {got}.'\n",
" return True\n",
"\n",
"def parse(day, parser=str, sep='\\n', print_lines=7) -> tuple:\n",
" \"\"\"Split the day's input file into entries separated by `sep`, and apply `parser` to each.\"\"\"\n",
" text = open(f'AOC2021/input{day}.txt').read()\n",
" entries = mapt(parser, text.rstrip().split(sep))\n",
" if print_lines:\n",
" lines = text.splitlines()[:print_lines]\n",
" head = f'First {len(lines)} lines of Day {day} input (which is parsed into {len(entries)} entries)'\n",
" print(f'{head}:\\n{\"-\" * len(head)}')\n",
" for line in lines:\n",
" print(line if len(line) <= 100 else line[:100] + ' ...')\n",
" return entries\n",
"\n",
"def ints(text: str) -> Tuple[int]:\n",
" \"\"\"A tuple of all the integers in text, ignoring non-number characters.\"\"\"\n",
" return mapt(int, re.findall('-?[0-9]+', text))\n",
"\n",
"def digits(text: str) -> Tuple[int]:\n",
" \"\"\"A tuple of all the digits in text (as ints 0–9), ignoring non-digit characters.\"\"\"\n",
" return mapt(int, re.findall('[0-9]', text))\n",
"\n",
"def words(text: str) -> List[str]:\n",
" \"\"\"A list of all the alphabetic words in text, ignoring non-letters.\"\"\"\n",
" return re.findall('[a-zA-Z]+', text)\n",
"\n",
"Char = str # Intended as a one-character string\n",
"\n",
"Atom = Union[float, int, str]\n",
"\n",
"def atoms(text: str, sep=None) -> Tuple[Atom]:\n",
" \"\"\"A tuple of all the atoms (numbers or strs) in text.\n",
" By default, atoms are space-separated but you can change that with `sep`.\"\"\"\n",
" return mapt(atom, text.split(sep))\n",
"\n",
"def atom(text: str) -> Atom:\n",
" \"\"\"Parse text into a single float or int or str.\"\"\"\n",
" try:\n",
" x = float(text)\n",
" return round(x) if round(x) == x else x\n",
" except ValueError:\n",
" return text\n",
" \n",
"def mapt(fn, *args) -> tuple:\n",
" \"\"\"map(fn, *args) and return the result as a tuple.\"\"\"\n",
" return tuple(map(fn, *args))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A few additional utility functions that I have used in the past:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"def quantify(iterable, pred=bool) -> int:\n",
" \"\"\"Count the number of items in iterable for which pred is true.\"\"\"\n",
" return sum(1 for item in iterable if pred(item))\n",
"\n",
"class multimap(defaultdict):\n",
" \"\"\"A mapping of {key: [val1, val2, ...]}.\"\"\"\n",
" def __init__(self, pairs: Iterable[tuple], symmetric=False):\n",
" \"\"\"Given (key, val) pairs, return {key: [val, ...], ...}.\n",
" If `symmetric` is True, treat (key, val) as (key, val) plus (val, key).\"\"\"\n",
" self.default_factory = list\n",
" for (key, val) in pairs:\n",
" self[key].append(val)\n",
" if symmetric:\n",
" self[val].append(key)\n",
"\n",
"def prod(numbers) -> float: # Will be math.prod in Python 3.8\n",
" \"\"\"The product formed by multiplying numbers together.\"\"\"\n",
" result = 1\n",
" for n in numbers:\n",
" result *= n\n",
" return result\n",
"\n",
"def total(counter: Counter) -> int: \n",
" \"\"\"The sum of all the counts in a Counter.\"\"\"\n",
" return sum(counter.values())\n",
"\n",
"def sign(x) -> int: return (0 if x == 0 else +1 if x > 0 else -1)\n",
" \n",
"def dotproduct(A, B) -> float: return sum(a * b for a, b in zip(A, B))\n",
"\n",
"def transpose(matrix) -> list: return list(zip(*matrix))\n",
"\n",
"cat = ''.join\n",
"flatten = chain.from_iterable\n",
"cache = lru_cache(None)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A lot of puzzles seem to involve (x, y) points on a rectangular grid, so I'll define `Point` and `Grid`:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"Point = Tuple[int, int] # (x, y) points on a grid\n",
"\n",
"neighbors4 = ((0, 1), (1, 0), (0, -1), (-1, 0)) \n",
"neighbors8 = ((1, 1), (1, -1), (-1, 1), (-1, -1)) + neighbors4\n",
"\n",
"class Grid(dict):\n",
" \"\"\"A 2D grid, implemented as a mapping of {(x, y): cell_contents}.\"\"\"\n",
" def __init__(self, mapping=(), rows=(), neighbors=neighbors4):\n",
" \"\"\"Initialize with, e.g., either `mapping={(0, 0): 1, (1, 0): 2, ...}`,\n",
" or `rows=[(1, 2, 3), (4, 5, 6)].\n",
" `neighbors` is a collection of (dx, dy) deltas to neighboring points.`\"\"\"\n",
" self.update(mapping if mapping else\n",
" {(x, y): val \n",
" for y, row in enumerate(rows) \n",
" for x, val in enumerate(row)})\n",
" self.width = max(x for x, y in self) + 1\n",
" self.height = max(y for x, y in self) + 1\n",
" self.deltas = neighbors\n",
" \n",
" def copy(self) -> Grid: return Grid(self, neighbors=self.deltas)\n",
" \n",
" def neighbors(self, point) -> List[Point]:\n",
" \"\"\"Points on the grid that neighbor `point`.\"\"\"\n",
" x, y = point\n",
" return [(x+dx, y+dy) for (dx, dy) in self.deltas \n",
" if (x+dx, y+dy) in self]\n",
" \n",
" def to_rows(self) -> List[List[object]]:\n",
" \"\"\"The contents of the grid in a rectangular list of lists.\"\"\"\n",
" return [[self[x, y] for x in range(self.width)]\n",
" for y in range(self.height)]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This year's AoC theme involves Santa's Elves on a submarine. [Gary J Grady](https://twitter.com/GaryJGrady/) has some nice drawings to set the scene:\n",
"\n",
"\n",
"\n",
"# [Day 1](https://adventofcode.com/2021/day/1): Sonar Sweep\n",
"\n",
"\n",
"- **Input**: Each entry in the input is an integer depth measurement.\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"First 7 lines of Day 1 input (which is parsed into 2000 entries):\n",
"----------------------------------------------------------------\n",
"148\n",
"167\n",
"168\n",
"169\n",
"182\n",
"188\n",
"193\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": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def increases(nums: List[int]) -> int:\n",
" \"\"\"How many measurements are larger than the previous measurement?\"\"\"\n",
" return quantify(nums[i] > nums[i - 1] \n",
" for i in range(1, len(nums)))\n",
"\n",
"answer(1.1, increases(in1), 1400)"
]
},
{
"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": 7,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def window_increases(nums: List[int], w=3) -> int:\n",
" \"\"\"How many sliding windows of `w` numbers have a sum larger than the previous window?\"\"\"\n",
" return quantify(sum(nums[i:i+w]) > sum(nums[i-1:i-1+w])\n",
" for i in range(1, len(nums) + 1 - w))\n",
"\n",
"answer(1.2, window_increases(in1), 1429)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's take a look at where the depths are taking us:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"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 entry 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', 1)`."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"First 7 lines of Day 2 input (which is parsed into 1000 entries):\n",
"----------------------------------------------------------------\n",
"forward 2\n",
"down 7\n",
"down 8\n",
"forward 9\n",
"down 8\n",
"forward 9\n",
"forward 8\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": 10,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"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, drive(in2), 1_670_340)"
]
},
{
"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": 11,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"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, drive2(in2), 1_954_293_920)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 3](https://adventofcode.com/2021/day/3): Binary Diagnostic\n",
"\n",
"- **Input**: Each entry 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": 12,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"First 7 lines of Day 3 input (which is parsed into 1000 entries):\n",
"----------------------------------------------------------------\n",
"101000111100\n",
"000011111101\n",
"011100000100\n",
"100100010000\n",
"011110010100\n",
"101001100000\n",
"110001010000\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": 13,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"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, power(in3), 2261546)"
]
},
{
"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": 14,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"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, life_support(in3), 6775520)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 4](https://adventofcode.com/2021/day/4): Giant Squid\n",
"\n",
"- **Input**: The first entry of the input is a permutation of the integers 0-99. Subsequent entries are bingo boards: 5 lines of 5 ints each. Entries 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": 15,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"First 7 lines of Day 4 input (which is parsed into 101 entries):\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,34, ...\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",
"19 22 16 13 3\n"
]
}
],
"source": [
"order, *boards = in4 = parse(4, ints, sep='\\n\\n')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1**: What will your final score be if you choose the first bingo board to win?"
]
},
{
"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": 16,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 16,
"metadata": {},
"output_type": "execute_result"
}
],
"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, bingo(boards, order), 39902)"
]
},
{
"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": 17,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"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, bingo_last(boards, order), 26936)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 5](https://adventofcode.com/2021/day/5): Hydrothermal Venture\n",
"\n",
"- **Input**: Each entry 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": 18,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"First 7 lines of Day 5 input (which is parsed into 500 entries):\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",
"949,41 -> 13,977\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": 19,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 19,
"metadata": {},
"output_type": "execute_result"
}
],
"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",
"def cover(*xs) -> range:\n",
" \"\"\"All the ints from the min of the arguments to the max, inclusive.\"\"\"\n",
" return range(min(xs), max(xs) + 1)\n",
"\n",
"answer(5.1, overlaps(in5), 7436)"
]
},
{
"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": 20,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 20,
"metadata": {},
"output_type": "execute_result"
}
],
"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, overlaps(in5, False), 7436)\n",
"answer(5.2, overlaps(in5, True), 21104)"
]
},
{
"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. Over time, the lanternfish age and reproduce in a specified way."
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"First 1 lines of Day 6 input (which is parsed into 300 entries):\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,5,1, ...\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*(1), not *O*(*n*). I have a hunch that Part 2 will involve a ton-o'-fish."
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 22,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"Fish = Counter # Represent a school of fish as a Counter of their timer-ages\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",
"\n",
"answer(6.1, total(simulate(Fish(in6), 80)), 350917)"
]
},
{
"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 simple:"
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 23,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(6.2, total(simulate(Fish(in6), 256)), 1_592_918_715_629)"
]
},
{
"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": 24,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"First 1 lines of Day 7 input (which is parsed into 1000 entries):\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,101, ...\n"
]
}
],
"source": [
"in7 = parse(7, int, sep=',')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The idea is that if the crabs can all align in one horizontal position, they can save you from a giant whale.\n",
"\n",
"- **Part 1**: Determine the horizontal position that the crabs can align to using the least fuel possible. How much fuel must they spend to align to that position? (Each unit of horizontal travel costs one unit of fuel.)"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 25,
"metadata": {},
"output_type": "execute_result"
}
],
"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 = median(positions)\n",
" return sum(abs(p - align) for p in positions)\n",
"\n",
"answer(7.1, fuel_cost(in7), 352707)"
]
},
{
"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! How much fuel must they spend to align to that position? (Now for each crab the first unit of travel costs 1, the second 2, the third 3, and so on.) "
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 26,
"metadata": {},
"output_type": "execute_result"
}
],
"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 range(min(positions), max(positions) + 1))\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, fuel_cost2(in7), 95519693)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Note**: 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": 27,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"490.543"
]
},
"execution_count": 27,
"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": 28,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{490: 95519693, 491: 95519725, 490.543: 95519083.0}"
]
},
"execution_count": 28,
"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": 29,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[376.0, 490.543]"
]
},
"execution_count": 29,
"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": [
""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 8](https://adventofcode.com/2021/day/8): Seven Segment Search\n",
"\n",
"- **Input**: Each entry in the input consists of 10 patterns followed by a \"`|`\", followed by 4 output values.\n",
" \n",
"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. The mapping of letters to segments is unknown, but is consistent within each entry."
]
},
{
"cell_type": "code",
"execution_count": 30,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"First 7 lines of Day 8 input (which is parsed into 200 entries):\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",
"fbecdga gcdbea cegab fc cafe cfg ebgdf cbgfe afbgec bagcdf | feac acegb bfagce gcafbe\n"
]
}
],
"source": [
"in8 = parse(8, lambda line: mapt(atoms, line.split('|')))"
]
},
{
"cell_type": "code",
"execution_count": 31,
"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": [
"\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 2, 4, 3, or 7 segments?*"
]
},
{
"cell_type": "code",
"execution_count": 32,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 32,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def count1478(entries) -> int:\n",
" \"\"\"How many of the rhs digits in the entries are a 1, 4, 7, or 8?\"\"\"\n",
" return quantify(len(value) in (2, 4, 3, 7) \n",
" for (lhs, rhs) in entries for value in rhs)\n",
"\n",
"answer(8.1, count1478(in8), 493)"
]
},
{
"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'` 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 7 letters. That sounds easy! Here's my plan:\n",
"- Make a list of the 7! = 5,040 possible string 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": 33,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 33,
"metadata": {},
"output_type": "execute_result"
}
],
"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, sum(map(decode, in8)), 1010460)"
]
},
{
"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": 34,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"First 7 lines of Day 9 input (which is parsed into 100 entries):\n",
"---------------------------------------------------------------\n",
"9897656789865467895698765469899988672134598894345689864101378965457932349943210987654789653198789434\n",
"8789542499996878954329984398789976561012987789245678953212567892345791998899329899765678969997668912\n",
"7678943978987989965998993297649875432129876567956789864487678991056899877778939769886789998766457899\n",
"4578999868998996899867894976532986543299876476897899987569899989167898766567898654998898998655345678\n",
"2456987657679535679756799988643498657987654345789978899789998878998919954349997543219967987543237889\n",
"1234896545568986798645678999754989767898765456998769759899987765789329863238898659301256798793156891\n",
"2346789432379997987434689489899879898919876567899954346998796434678997642127789798512345989989247892\n"
]
}
],
"source": [
"in9 = Grid(rows=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": 35,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 35,
"metadata": {},
"output_type": "execute_result"
}
],
"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, total_risk(in9), 607)"
]
},
{
"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",
"Given that definition of *basin,* 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": 36,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 36,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def find_basins(grid) -> Dict[Point, Point]:\n",
" \"\"\"Compute `basins` as a map of {point: low_point} 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",
"def flood_size_products(grid, b=3) -> int:\n",
" \"\"\"The product of the sizes of the `b` largest basins.\"\"\"\n",
" basins = find_basins(grid)\n",
" return prod(c for _, c in Counter(basins.values()).most_common(b))\n",
"\n",
"answer(9.2, flood_size_products(in9), 900864)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"That's the correct answer; I'm done. But I do want to check that the set of low points is the same as the set of basins I identified:"
]
},
{
"cell_type": "code",
"execution_count": 37,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"249"
]
},
"execution_count": 37,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"assert set(low_points(in9)) == set(find_basins(in9).values())\n",
"\n",
"len(low_points(in9))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"I would like to visualize the basins. I'll use a scatter plot that displays the height 9 locations in yellow and the height 0 locations in deep purple, with a gradient in between:"
]
},
{
"cell_type": "code",
"execution_count": 38,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
""
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"def show(in9, low=None):\n",
" plt.figure(figsize=(10, 10))\n",
" C = [in9[p] for p in in9]\n",
" plt.scatter(*transpose(in9), marker='s', s=10, c=C, cmap=plt.get_cmap('plasma'))\n",
" if low: plt.plot(*transpose(low_points(in9)), low, markersize=4)\n",
" plt.axis('square'); plt.axis('off')\n",
" \n",
"show(in9)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can optionally display the low points. Here I'll display them as black diamonds:"
]
},
{
"cell_type": "code",
"execution_count": 39,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
""
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"show(in9, 'kD')"
]
},
{
"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 entry in the input is a string of opening and closing brackets: `[({<` and `>})]`.\n"
]
},
{
"cell_type": "code",
"execution_count": 40,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"First 7 lines of Day 10 input (which is parsed into 102 entries):\n",
"----------------------------------------------------------------\n",
"[(([{<{(<{{[({{}{}}{[]()})<{{}()}>]}}(([{{{}[]}[[]()]}[<{}[]]{()()}]](({{}{}}{{}()}))){[{({}())[[\n",
"<(({[<([{({[{{<>()}}[{<>()}({}{})]]<{<()<>>{[]()}}(((){}>[[][]])>}([{<[]{}>(<>[])}]))<[[[[[][]\n",
"(<<(<{{{{<<<[(()<>){()<>}][[()()]]>{<{[]{}}<<>()>>}>{(<{<>}([]{})><(<>())<(){}>>)<(([]{})(()()))<<() ...\n",
"[[[[<[{[(<{{{({}<>)((){})}((()())[()()])}}><[([((){})]<[()[]]{{}<>}>)[[{[]<>}][([]{})[{}()]]]]>)<{(<\n",
"[<(<[[((<{((<<<>[]>><<<>{}>>){<[{}<>][<>[]]><<<>()>[(){}]>})[<{[{}<>][(){}]}<[[]<>][{}[]])>{([<>[]][\n",
"(([[[[<([[{([{<>()}{()<>}][((){})]){[{[]<>}({}<>)][(<><>)[()[]]]}}<{{({}{}){[]{}}}<{<><>}({}{})>}>\n",
"{{{[<(<([<{({{[]()}[{}()]}{<()<>>(()<>)})}><<[{<()()>(()[])}<<<>[]]>][<{()}{<><>}>({{}[]})]>>](\n"
]
}
],
"source": [
"in10 = parse(10)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Ideally, the brackets are balanced, but entries 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",
"\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": 41,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 41,
"metadata": {},
"output_type": "execute_result"
}
],
"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, sum(analyze_syntax(line)[0] for line in in10), 367059)"
]
},
{
"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": 42,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 42,
"metadata": {},
"output_type": "execute_result"
}
],
"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, median_completion_score(in10), 1_952_146_692)"
]
},
{
"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": 43,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"First 7 lines of Day 11 input (which is parsed into 10 entries):\n",
"---------------------------------------------------------------\n",
"1224346384\n",
"5621128587\n",
"6388426546\n",
"1556247756\n",
"1451811573\n",
"1832388122\n",
"2748545647\n"
]
}
],
"source": [
"in11 = Grid(rows=parse(11, digits), neighbors=neighbors8)"
]
},
{
"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": 44,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 44,
"metadata": {},
"output_type": "execute_result"
}
],
"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, simulate_flashes(in11, 100), 1591)"
]
},
{
"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": 45,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 45,
"metadata": {},
"output_type": "execute_result"
}
],
"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, simulate_flashes2(in11), 314)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 12](https://adventofcode.com/2021/day/11): Passage Pathing\n",
"\n",
"- **Input**: Each entry 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": 46,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"First 7 lines of Day 12 input (which is parsed into 22 entries):\n",
"---------------------------------------------------------------\n",
"xx-xh\n",
"vx-qc\n",
"cu-wf\n",
"ny-LO\n",
"cu-DR\n",
"start-xx\n",
"LO-vx\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": 47,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 47,
"metadata": {},
"output_type": "execute_result"
}
],
"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, quantify(search_paths(['start'], neighbors)), 4167)"
]
},
{
"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 the 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": 48,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 48,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def search_paths2(path, neighbors):\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, quantify(search_paths2(['start'], neighbors)), 98441)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 13](https://adventofcode.com/2021/day/13): Transparent Origami\n",
"\n",
"- **Input**: The input is in two sections: (1) a set of dots, e.g. \"`6,10`\". (2) a list of fold instructions, e.g. \"`fold along y=7`\".\n",
"\n",
"My `parse` command is not set up to parse different sections differently, so I'll only ask `parse` to do a minimal amount of work, just parsing the input file into two sections, each a list of lines. Then I'll further process the two sections to get two variables:\n",
"- `dots`: a set of `(x, y)` points, such as `(103, 224)`. \n",
"- `folds`: a list of fold instructions, each implemented as a tuple, such as `('y', 7)` or `('x', 15)`."
]
},
{
"cell_type": "code",
"execution_count": 49,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"First 7 lines of Day 13 input (which is parsed into 2 entries):\n",
"--------------------------------------------------------------\n",
"103,224\n",
"624,491\n",
"808,688\n",
"1076,130\n",
"700,26\n",
"55,794\n",
"119,724\n"
]
}
],
"source": [
"in13 = parse(13, str.splitlines, sep='\\n\\n')\n",
"dots = {ints(line) for line in in13[0]}\n",
"folds = [(words(line)[2], ints(line)[0]) for line in in13[1]]"
]
},
{
"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.\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": 50,
"metadata": {},
"outputs": [],
"source": [
"def fold(dots, instruction) -> Set[Point]: \n",
" \"\"\"The set of dots that result from following the fold instruction.\"\"\"\n",
" 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": 51,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 51,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(13.1, len(fold(dots, folds[0])), 638)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 2**: Finish folding the transparent paper according to the instructions. What is the code?"
]
},
{
"cell_type": "code",
"execution_count": 52,
"metadata": {},
"outputs": [],
"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(*transpose(dots), marker='s')\n",
" plt.axis('equal'); plt.gca().invert_yaxis()"
]
},
{
"cell_type": "code",
"execution_count": 53,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 53,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/png": "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\n",
"text/plain": [
""
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"answer(13.2, origami(dots, folds), None) # actual answer: \"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": 54,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"First 7 lines of Day 14 input (which is parsed into 102 entries):\n",
"----------------------------------------------------------------\n",
"ONSVVHNCFVBHKVPCHCPV\n",
"\n",
"VO -> C\n",
"VV -> S\n",
"HK -> H\n",
"FC -> C\n",
"VB -> V\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 (that is, the pairs overlap). All insertions happen simultaneously during a step."
]
},
{
"cell_type": "code",
"execution_count": 55,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 55,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def pair_insertion(polymer, rules, steps) -> 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 pairs(seq) -> list: return [seq[i:i+2] for i in range(len(seq) - 1)]\n",
"\n",
"def quantity_diff(polymer) -> int:\n",
" \"\"\"The count of most common element minus the count of least common element.\"\"\"\n",
" counts = list(Counter(polymer).values())\n",
" return max(counts) - min(counts)\n",
"\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, quantity_diff(pair_insertion(polymer, rules, 10)), 3259)"
]
},
{
"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 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": 56,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Counter({'NN': 1, 'NC': 1, 'CB': 1})"
]
},
"execution_count": 56,
"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 each letter count by 2 to avoid double counting. However 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": 57,
"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": 58,
"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": 59,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Counter({'B': 1, 'N': 2, 'C': 1})"
]
},
"execution_count": 59,
"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": 60,
"metadata": {},
"outputs": [],
"source": [
"assert letter_counts(Counter(pairs('NNCB')), 'NNCB') == letter_counts(Counter(pairs('NCBN')), 'NCBN')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now the new function `pair_insertion_diff` can call on `pair_insertion2` to solve Part 2 (as well as Part 1):"
]
},
{
"cell_type": "code",
"execution_count": 61,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 61,
"metadata": {},
"output_type": "execute_result"
}
],
"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_diff(polymer, rules, steps):\n",
" \"\"\"Most common minus least common after `steps` of pair insertion.\"\"\"\n",
" return quantity_diff(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_diff('NNCB', rules={'NN': 'C', 'NC': 'B', 'CB': 'H'}, steps=1) == 1\n",
"\n",
"answer(14.1, pair_insertion_diff(polymer, rules, 10), 3_259)\n",
"answer(14.2, pair_insertion_diff(polymer, rules, 40), 3_459_174_981_021)"
]
},
{
"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."
]
},
{
"cell_type": "code",
"execution_count": 62,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"First 7 lines of Day 15 input (which is parsed into 100 entries):\n",
"----------------------------------------------------------------\n",
"4249856395422795894919869133487611581179923326874763428673979547991221931142777981153991369468629849\n",
"5812974178739823463799939791688998895568796557798392761499941349143539572865883254186633218867928826\n",
"3699989976298596286299499129934993241824395574879938998946914116375199242199151918863674914554714898\n",
"5682435936794718871685718386458294198391116125679589438794914499278679393779734596558953699438589518\n",
"7681197997388219696918569664119968498599547892968929425479817979816979144947916716989874825679487436\n",
"9981166198272997899142698141878643123757515999788822988261499197559193945291512682763935126815448215\n",
"8849481991861599951293183728419792414164347979985169641698899853377259811688489269959429131918919179\n"
]
}
],
"source": [
"in15 = Grid(rows=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",
"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."
]
},
{
"cell_type": "code",
"execution_count": 63,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 63,
"metadata": {},
"output_type": "execute_result"
}
],
"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)\n",
" cost = Grid({p: inf for p in grid}) # cost[p] is cost of best known path from start to p\n",
" frontier = {start}\n",
" while frontier:\n",
" p = frontier.pop()\n",
" new_cost = 0 if p is start else (grid[p] + min(cost[b] for b in grid.neighbors(p)))\n",
" if new_cost < cost[p]:\n",
" cost[p] = new_cost\n",
" frontier.update(grid.neighbors(p))\n",
" return cost[goal]\n",
"\n",
"answer(15.1, search_grid(in15), 687)"
]
},
{
"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": 64,
"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)\n",
"\n",
"def clock_mod(i, m) -> int:\n",
" \"\"\"i % m, but replace a result of 0 with m\"\"\"\n",
" # This is like a clock, where 24 mod 12 is 12, not 0.\n",
" return (i % m) or m"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"With 250,000 points in the full map, I think it will take several minutes (not hours, not seconds), to find the optimal path with the `search_grid` algorithm. I really wish I had used A* search; I think it would run in just a few seconds."
]
},
{
"cell_type": "code",
"execution_count": 65,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 4min 21s, sys: 271 ms, total: 4min 22s\n",
"Wall time: 4min 22s\n"
]
},
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 65,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"%time answer(15.2, search_grid(repeat_grid(in15, 5)), 2957)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"OK, 5 minutes wasn't so bad. I may come back and improve on this later (or maybe not).\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",
""
]
},
{
"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). For now I will leave the input as a string of hex digits."
]
},
{
"cell_type": "code",
"execution_count": 66,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"First 1 lines of Day 16 input (which is parsed into 1 entries):\n",
"--------------------------------------------------------------\n",
"220D790065B2745FF004672D99A34E5B33439D96CEC80373C0068663101A98C406A5E7395DC1804678BF25A4093BFBDB886C ...\n"
]
}
],
"source": [
"in16 = parse(16)[0]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- **Part 1:** The puzzle is to parse this hexadecimal transmission into data packets, according to the rules contained in [the instructions](https://adventofcode.com/2021/day/16), and add up all of the version numbers.\n",
"\n",
"The gist of [the instructions](https://adventofcode.com/2021/day/16) is to consider the hexadecimal sequence as a bit string, parse the bit string into bit fields, and construct nested packets based on the values of the fields. Here are basic types for `Bits` (bit string) and `Packet`, along with functions to convert from a hexadecimal string to a bit string, and from there to an int: "
]
},
{
"cell_type": "code",
"execution_count": 67,
"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",
" 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 various 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": 68,
"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",
" bit, nibble, bits = bits[0], bits[1:5], bits[5:]\n",
" literal += nibble\n",
" if bit == '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",
" subpackets = [] \n",
" if I == 0:\n",
" L, bits = parse_int(15, bits)\n",
" target = len(bits) - L\n",
" while len(bits) > target:\n",
" packet, bits = parse_packet(bits)\n",
" subpackets.append(packet)\n",
" else:\n",
" L, bits = parse_int(11, bits)\n",
" for _ 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 of the parsed packet and its subpackets:"
]
},
{
"cell_type": "code",
"execution_count": 69,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 69,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def subpackets(packet) -> List[Packet]: \n",
" \"\"\"The subpackets of a packet.\"\"\"\n",
" return packet.contents if isinstance(packet.contents, list) else []\n",
"\n",
"def sum_versions(packet) -> int:\n",
" \"\"\"The sum of the version numbers of this packet and all its subpackets (recursively).\"\"\"\n",
" return packet.V + sum(sum_versions(p) for p in subpackets(packet))\n",
" \n",
"packet16, _ = parse_packet(bits_from_hex(in16))\n",
"answer(16.1, sum_versions(packet16), 989)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This was way 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": 70,
"metadata": {},
"outputs": [],
"source": [
"assert (bits_from_hex('D2FE28') \n",
" == '110100101111111000101000')\n",
"\n",
"assert (bits_from_hex('38006F45291200') \n",
" == '00111000000000000110111101000101001010010001001000000000')\n",
"\n",
"assert (parse_packet('110100101111111000101000') \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": 71,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 71,
"metadata": {},
"output_type": "execute_result"
}
],
"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 subpackets(packet)]\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, eval_packet(packet16), 7936430475134)"
]
}
],
"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
}