Add files via upload

This commit is contained in:
Peter Norvig
2021-12-07 12:17:30 -08:00
committed by GitHub
parent 601947e5d4
commit 3471b327f5

View File

@@ -8,13 +8,13 @@
"\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 points. \n",
"I'm going to do [Advent of Code 2021](https://adventofcode.com/) (AoC), but I won't be competing for points. \n",
"\n",
"I 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.\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). In this notebook I won't refactor the code for part 1 based on what I see in part 2.\n",
"\n",
"# Day 0: Imports and Utilities\n",
"# Day 0: Preparations\n",
"\n",
"First, imports that I have used in past AoC years:"
]
@@ -28,8 +28,8 @@
"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 Dict, Tuple, Set, List, Iterator, Optional, Union\n",
"from statistics import mean\n",
"from typing import *\n",
"from statistics import mean, median\n",
"\n",
"import functools\n",
"import math\n",
@@ -47,19 +47,22 @@
"Now 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",
" def solve_it(numbers) -> int: 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",
"That is, `parse(1, int)` will parse the data file for day 1 in the format of one `int` per line and return those ints as a tuple. Then a new function I define for the day (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 that the correct answers are computed. So it will look like this:\n",
"\n",
" in1 = parse(1, int)\n",
" \n",
" def solve_it(numbers): return ...\n",
" def solve_it(numbers) -> int: return ...\n",
" answer(1.1, solve_it(in1), 123)\n",
" \n",
" def solve_it2(numbers): return ...\n",
" def solve_it2(numbers) -> int: return ...\n",
" answer(1.2, solve_it2(in1), 123456)\n",
" \n"
" \n",
"For more complex puzzles, I will include some `assert` statements to show that I am getting the right partial results on the small example in the puzzle description.\n",
"\n",
"Here's `parse` and `answer`:"
]
},
{
@@ -69,7 +72,7 @@
"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",
" \"\"\"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",
@@ -93,29 +96,33 @@
"outputs": [],
"source": [
"def ints(text: str) -> Tuple[int]:\n",
" \"Return a tuple of all the integers in text, ignoring non-numbers.\"\n",
" \"\"\"Return a tuple of all the integers in text, ignoring non-number characters.\"\"\"\n",
" return mapt(int, re.findall('-?[0-9]+', text))\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",
" \"\"\"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",
" \"\"\"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",
" x = float(text)\n",
" return round(x) if round(x) == x else x\n",
" except ValueError:\n",
" return text"
" return text\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": [
"Some additional useful utility functions:"
"A few additional utility functions:"
]
},
{
@@ -128,12 +135,6 @@
" \"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",
@@ -141,24 +142,16 @@
" 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",
"def prod(numbers) -> float: # Will be math.prod in Python 3.8\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 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",
"def median(numbers) -> float: return sorted(numbers)[len(numbers) // 2]\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 mapt(fn, *args):\n",
" \"map(fn, *args) and return the result as a tuple.\"\n",
" return tuple(map(fn, *args))"
"def dotproduct(A, B) -> float: return sum(a * b for a, b in zip(A, B))"
]
},
{
@@ -167,7 +160,7 @@
"source": [
"# [Day 1](https://adventofcode.com/2021/day/1): Sonar Sweep\n",
"\n",
"The input is a list of integers.\n",
"The input is a list of integers, such as \"`148`\", one per line.\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?"
@@ -241,7 +234,7 @@
"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.)"
"1. Follow *revised* instructions and report the product of your final horizontal position and depth. (The \"down\" and \"up\" instructions no longer change depth, rather they change *aim*, and going forward *n* units both increments position by *n* and depth by *aim* × *n*.)"
]
},
{
@@ -318,7 +311,7 @@
"source": [
"# [Day 3](https://adventofcode.com/2021/day/3): Binary Diagnostic\n",
"\n",
"The input is a list of bit strings.\n",
"The input is a list of bit strings, such as \"`101000111100`\".\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)?"
@@ -355,7 +348,9 @@
" 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",
"def uncommon(strs, i) -> str: \n",
" \"\"\"The bit that is least common in position i.\"\"\"\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",
@@ -366,6 +361,7 @@
" 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)"
@@ -389,7 +385,8 @@
],
"source": [
"def select(strs, common_fn, i=0) -> str:\n",
" \"\"\"Select a str from strs according to common_fn.\"\"\"\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",
" if len(strs) == 1:\n",
" return strs[0]\n",
" else:\n",
@@ -397,10 +394,11 @@
" 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",
"def life_support(strs) -> int: \n",
" \"\"\"The product of oxygen (most common select) and CO2 (least common select) rates.\"\"\"\n",
" return int(select(strs, common), 2) * int(select(strs, uncommon), 2)\n",
" \n",
"answer(3.2, life(in3), 6775520)"
"answer(3.2, life_support(in3), 6775520)"
]
},
{
@@ -409,7 +407,7 @@
"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",
"The first line of the input is a permutation of the integers 0-99. That is followed by bingo boards (5 lines of 5 ints each), 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",
@@ -448,26 +446,27 @@
"Board = Tuple[int]\n",
"Line = List[int]\n",
"B = 5\n",
"def sq(x, y) -> int: return x + B * y\n",
"def sq(x, y) -> int: \"The index number of the square at (x, y)\"; 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",
" 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",
" \"\"\"Are any boards winners due to the number just called (and previously drawn numbers)?\"\"\"\n",
" \"\"\"Boards 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(all(board[n] in drawn for n in line)\n",
" for line in lines(board.index(just_called)))]\n",
" and any(filled(board, line) 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",
" 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 score of the first winning board?\"\"\"\n",
" \"\"\"What is the final score of the first winning board?\"\"\"\n",
" drawn = set()\n",
" for num in order:\n",
" drawn.add(num)\n",
@@ -496,7 +495,7 @@
],
"source": [
"def bingo_last(boards, order) -> int: \n",
" \"\"\"What is the score of the last winning board?\"\"\"\n",
" \"\"\"What is the final score of the last winning board?\"\"\"\n",
" boards = set(boards)\n",
" drawn = set()\n",
" for num in order:\n",
@@ -515,10 +514,10 @@
"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",
"The input is a list of \"lines\" denoted by start and end x,y points, e.g. \"`0,9 -> 5,9`\". I'll represent a line as a 4-tuple of integers, e.g. `(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?"
"2. Consider all of the lines (including diagonals, which are all at ±45°). At how many points do at least two lines overlap?"
]
},
{
@@ -569,6 +568,13 @@
"answer(5.1, overlaps(in5), 7436)"
]
},
{
"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": 19,
@@ -586,11 +592,6 @@
}
],
"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",
@@ -600,8 +601,14 @@
" 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",
@@ -614,7 +621,7 @@
"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",
"The input is a single line of ints, each one 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?"
@@ -633,7 +640,7 @@
"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."
"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."
]
},
{
@@ -653,7 +660,7 @@
}
],
"source": [
"Fish = Counter # Represent a school of fish as a Counter of their timers\n",
"Fish = Counter # Represent a school of fish as a Counter of their timer-ages\n",
"\n",
"def simulate(fish, days=1) -> Tuple[Fish, int]:\n",
" \"\"\"Simulate the aging and birth of fish over `days`;\n",
@@ -764,13 +771,14 @@
],
"source": [
"def fuel_cost2(positions) -> int:\n",
" \"\"\"How much fuel does it cost to get everyone to the best alignment point, with nonlinear fuel costs?\"\"\"\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(travel_cost(p, align) for p in positions)\n",
" return min(sum(burn_rate2(p, align) for p in positions)\n",
" for align in range(min(positions), max(positions)))\n",
"\n",
"def travel_cost(p, align) -> int:\n",
" \"\"\"The first step costs 1, the second 2, etc: triangular numbers.\"\"\"\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",
@@ -781,7 +789,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"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. However, what we really want to minimize is (steps<sup>2</sup> + steps), so that does not quite apply. Still, let's see what happens when we align to the mean:"
"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?"
]
},
{
@@ -792,7 +800,7 @@
{
"data": {
"text/plain": [
"95519083.0"
"490.543"
]
},
"execution_count": 26,
@@ -802,15 +810,14 @@
],
"source": [
"positions = in7\n",
"align = mean(positions)\n",
"sum(travel_cost(p, align) for p in positions)"
"mean(positions)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Wait, that's even *better* than the correct answer; what's up with that? It must be that the mean is not an integer:"
"Let's sum the burn rates for this alignment point in three ways: rounded down, as is, and rounded up:"
]
},
{
@@ -821,7 +828,7 @@
{
"data": {
"text/plain": [
"490.543"
"95519693"
]
},
"execution_count": 27,
@@ -830,14 +837,7 @@
}
],
"source": [
"mean(positions)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We get the correct answer if we round the mean down (but not if we round up):"
"sum(burn_rate2(p, 490) for p in positions)"
]
},
{
@@ -848,7 +848,7 @@
{
"data": {
"text/plain": [
"95519693"
"95519083.0"
]
},
"execution_count": 28,
@@ -857,8 +857,7 @@
}
],
"source": [
"align = 490\n",
"sum(travel_cost(p, align) for p in positions)"
"sum(burn_rate2(p, 490.543) for p in positions)"
]
},
{
@@ -878,8 +877,14 @@
}
],
"source": [
"align = 491\n",
"sum(travel_cost(p, align) for p in positions)"
"sum(burn_rate2(p, 491) for p in positions)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We see that rounding down gives the right answer, keeping the mean as is gives a total fuel cost that is *better* than the correct answer (but is apparently not a legal alignment point), and rounding up does a bit worse."
]
},
{