\n",
"\n",
"# Advent of Code 2021\n",
"\n",
"I'm going to solve some [Advent of Code 2021](https://adventofcode.com/) (AoC) puzzles, but I won't be competing for time. \n",
"\n",
"I also won't explain each puzzle here; you'll have to click on each day's link (e.g. [Day 1](https://adventofcode.com/2021/day/1)) to understand the puzzle. \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 description of part 2. So there is a tension of wanting the solution to part 1 to provide general components that might be re-used in part 2, without falling victim to [YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it). Except for errors, I will show the code as I developed it; I won't go back and refactor the code for part 1 when I see part 2."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Day 0: Imports and Utilities\n",
"\n",
"Below are the imports and utility functions that I found to be generally useful for these kinds of problems. But first two functions that I will use each day, `parse` and `answer`. I will start by writing something like this for part 1 of Day 1:\n",
"\n",
" in1 = parse(1, int)\n",
" def solve_it(numbers): return ...\n",
" solve(in1)\n",
" \n",
"That is, `parse(1, int)` will parse the data file for day 1 in the format of one integer per line; then some function (here `solve_it`) will (hopefully) compute the correct answer. I'll then submit the answer to AoC and verify it is correct. If it is, I'll move on to solve part 2. When I get them both done, I'll use the function `answers` to assert the correct answers. So it will look like this:\n",
"\n",
" in1 = parse(1, int)\n",
" \n",
" def solve_it(numbers): return ...\n",
" answer(1.1, solve_it(in1), ...)\n",
" \n",
" def solve_it2(numbers): return ...\n",
" answer(1.2, solve_it2(in1), ...)\n",
" \n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"def parse(day, parser=str, sep='\\n') -> tuple:\n",
" \"Split the day's input file into sections separated by `sep`, and apply `parser` to each.\"\n",
" sections = open(f'AOC2021/input{day}.txt').read().rstrip().split(sep)\n",
" return mapt(parser, sections)\n",
"\n",
"def answer(puzzle_number, got, expected) -> bool:\n",
" \"\"\"Verify the answer we got was expected.\"\"\"\n",
" assert got == expected, f'For {puzzle_number}, expected {expected} but got {got}.'\n",
" return True"
]
},
{
"cell_type": "code",
"execution_count": 2,
"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 functools import lru_cache\n",
"from typing import Dict, Tuple, Set, List, Iterator, Optional, Union\n",
"\n",
"import operator\n",
"import math\n",
"import ast\n",
"import sys\n",
"import re\n",
"\n",
"Char = str # Type used to indicate a single character\n",
"cat = ''.join\n",
"flatten = chain.from_iterable\n",
"cache = lru_cache(None)"
]
},
{
"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",
"def first(iterable, default=None) -> object:\n",
" \"Return first item in iterable, or default.\"\n",
" return next(iter(iterable), default)\n",
"\n",
"def rest(sequence) -> object: return sequence[1:]\n",
"\n",
"def multimap(items: Iterable[Tuple]) -> dict:\n",
" \"Given (key, val) pairs, return {key: [val, ....], ...}.\"\n",
" result = defaultdict(list)\n",
" for (key, val) in items:\n",
" result[key].append(val)\n",
" return result\n",
"\n",
"def prod(numbers) -> float: # Will be math.prod in Python 3.8, but I'm in 3.7\n",
" \"The product of an iterable of numbers.\" \n",
" result = 1\n",
" for n in numbers:\n",
" result *= n\n",
" return result\n",
"\n",
"def ints(text: str) -> Tuple[int]:\n",
" \"Return a tuple of all the integers in text, ignoring non-numbers.\"\n",
" return tuple(map(int, re.findall('-?[0-9]+', text)))\n",
"\n",
"def sign(x) -> int:\n",
" \"\"\"The sign of a number: +1, 0, or -1.\"\"\"\n",
" return (0 if x == 0 else +1 if x > 0 else -1)\n",
"\n",
"Atom = Union[float, int, str]\n",
"\n",
"def atoms(text: str, sep=None) -> Tuple[Atom]:\n",
" \"Parse text into atoms (numbers or strs).\"\n",
" return tuple(map(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",
" val = float(text)\n",
" return round(val) if round(val) == val else val\n",
" except ValueError:\n",
" return text\n",
" \n",
"def dotproduct(A, B) -> float: return sum(a * b for a, b in zip(A, B))\n",
"\n",
"def mapt(fn, *args):\n",
" \"map(fn, *args) and return the result as a tuple.\"\n",
" return tuple(map(fn, *args))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 1](https://adventofcode.com/2021/day/1): Sonar Sweep\n",
"\n",
"The input is a list of integers.\n",
"\n",
"1. How many numbers increase from the previous number?\n",
"2. How many sliding windows of 3 numbers increase from the previous window?"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"in1 = parse(1, int)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def increases(nums: List[int]) -> int:\n",
" \"\"\"How many numbers are bigger than the previous one?\"\"\"\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": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 6,
"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 bigger 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": [
"# [Day 2](https://adventofcode.com/2021/day/2): Dive! \n",
"\n",
"The input is a list of instructions, such as \"`forward 10`\".\n",
"\n",
"1. Follow instructions and report the product of your final horizontal position and depth.\n",
"1. Follow *revised* instructions and report the product of your final horizontal position and depth. (There is an \"aim\" which is increased by down and up instructions. Depth is changed not by down and up, but by going forward *n* units, which changes depth by aim × *n* units"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"in2 = parse(2, atoms)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def drive(instructions) -> int:\n",
" \"\"\"What is the product of position and depth after following instructions?\"\"\"\n",
" pos = depth = 0\n",
" for (op, n) in instructions:\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), 1670340)"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def drive2(instructions) -> int:\n",
" \"\"\"What is rthe product of position and depth after following instructions?\n",
" This time we have to keep track of `aim` as well.\"\"\"\n",
" pos = depth = aim = 0\n",
" for (op, n) in instructions:\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), 1954293920)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 3](https://adventofcode.com/2021/day/3): Binary Diagnostic\n",
"\n",
"The input is a list of bit strings.\n",
"\n",
"1. What is the power consumption (product of gamma and epsilon rates)?\n",
"2. What is the life support rating (product of oxygen and CO2)?"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"in3 = parse(3, str) # Parse into bit strings, (e.g. '1110'), not binary ints (e.g. 14)"
]
},
{
"cell_type": "code",
"execution_count": 31,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 31,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def common(strs, i) -> str: \n",
" \"\"\"The bit that is most common in position i.\"\"\"\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) -> str: 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",
" return int(epsilon(strs), 2) * int(gamma(strs), 2)\n",
" \n",
"answer(3.1, power(in3), 2261546)"
]
},
{
"cell_type": "code",
"execution_count": 30,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 30,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def select(strs, common_fn, i=0) -> str:\n",
" \"\"\"Select a str from strs according to common_fn.\"\"\"\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(selected, common_fn, i + 1)\n",
"\n",
"def life(strs) -> int: \n",
" return int(select(strs, common), 2) * int(select(strs, uncommon), 2)\n",
" \n",
"answer(3.2, life(in3), 6775520)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 4](https://adventofcode.com/2021/day/4): Giant Squid\n",
"\n",
"The first line of the input is a permutation of the integers 0-99. That is followed by 5 × 5 bingo boards, each separated by two newlines.\n",
"\n",
"1. What will your final score be if you choose the first bingo board to win?\n",
"2. What will your final score be if you choose the last bingo board to win?\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`. \n",
"\n",
"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": [],
"source": [
"order, *boards = in4 = parse(4, ints, sep='\\n\\n')"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"Board = Tuple[int]\n",
"Line = List[int]\n",
"B = 5\n",
"def sq(x, y) -> int: return x + B * y\n",
"\n",
"def lines(square) -> Tuple[Line, Line]:\n",
" \"\"\"The two lines through square number `square`.\"\"\"\n",
" x, y = square % B, square // B\n",
" return [sq(x, y) for x in range(B)], [sq(x, y) for y in range(B)]\n",
"\n",
"def bingo_winners(boards, drawn, just_called) -> List[Board]:\n",
" \"\"\"Are any boards winners due to the number just called (and previously drawn numbers)?\"\"\"\n",
" return [board for board in boards\n",
" if just_called in board\n",
" and any(quantify(board[n] in drawn for n in line) == B \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",
" return sum(n for n in board if n not in drawn) * just_called\n",
"\n",
"def bingo(boards, order) -> int: \n",
" \"\"\"What is the 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": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def bingo_last(boards, order) -> int: \n",
" \"\"\"What is the score of the last winning board?\"\"\"\n",
" boards = set(boards)\n",
" drawn = set()\n",
" for num in order:\n",
" drawn.add(num)\n",
" winners = bingo_winners(boards, drawn, num)\n",
" boards -= set(winners)\n",
" if not boards:\n",
" return bingo_score(winners[-1], drawn, num)\n",
" \n",
"answer(4.2, bingo_last(boards, order), 26936)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 5](https://adventofcode.com/2021/day/5): Hydrothermal Venture\n",
"\n",
"The input is a list of \"lines\" denoted by start and end points, e.g. \"`0,9 -> 5,9`\". I'll represent that line as the tuple `(0, 9, 5, 9)`.\n",
"\n",
"1. Consider only horizontal and vertical lines. At how many points do at least two lines overlap?\n",
"2. Consider all of the lines (including diagonals). At how many points do at least two lines overlap?"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [],
"source": [
"in5 = parse(5, ints)"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def points(line) -> bool:\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 cover(x1, x2) -> range:\n",
" \"\"\"All the ints from x1 to x2, inclusive, with x1, x2 in either order.\"\"\"\n",
" return range(min(x1, x2), max(x1, x2) + 1)\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, overlaps(in5), 7436)"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"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",
"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",
"assert points((1, 1, 1, 3), False) == [(1, 1), (1, 2), (1, 3)]\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.2, overlaps(in5, True), 21104)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 6](https://adventofcode.com/2021/day/6): Lanternfish\n",
"\n",
"The input is a single line of ints, each describing the age of a lanternfish. Over time, they age and reproduce in a specified way.\n",
"\n",
"1. Find a way to simulate lanternfish. How many lanternfish would there be after 80 days?\n",
"2. How many lanternfish would there be after 256 days?"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {},
"outputs": [],
"source": [
"in6 = parse(6, ints)[0]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Although the puzzle description treats each fish individually, I won't take the bait (pun intended). Instead, I'll use a Counter of fish, and treat all the fish of each age group together. I have a hunch that part 2 will involve a ton-o'-fish."
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 25,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"Fish = Counter # Represent a school of fish as a Counter of their timers\n",
"\n",
"def simulate(fish, days=1) -> Tuple[Fish, int]:\n",
" \"\"\"Simulate the aging and birth of fish over `days`;\n",
" return the Counter of fish and the total number of fish.\"\"\"\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, sum(fish.values())\n",
" \n",
"assert simulate(Fish((3, 4, 3, 1, 2))) == (Fish((2, 3, 2, 0, 1)), 5)\n",
"assert simulate(Fish((2, 3, 2, 0, 1))) == (Fish((1, 2, 1, 6, 0, 8)), 6)\n",
"\n",
"answer(6.1, simulate(Fish(in6), 80)[1], 350917)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"My hunch was right, so part 2 is easy:"
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 26,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(6.2, simulate(Fish(in6), 256)[1], 1592918715629)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 7](https://adventofcode.com/2021/day/7):"
]
},
{
"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
}