{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
Peter Norvig, 1–25 Dec 2021
\n", "\n", "# Advent of Code 2021\n", "\n", "I'm doing [Advent of Code 2021](https://adventofcode.com/) (AoC) this year. I won't be competing for points\\, just for fun.\n", "\n", "You'll have to click on each day's link (e.g. [Day 1](https://adventofcode.com/2021/day/1)) to see the full description of each puzzle; I won't repeat them in this notebook. \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). In this notebook I won't refactor the code for part 1 based on what I see 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", "\n", "import functools\n", "import math\n", "import re\n", "\n", "cat = ''.join\n", "flatten = chain.from_iterable\n", "cache = functools.lru_cache(None)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here are two functions that I will use each day, `parse` and `answer`, and two parser functions, `ints` and `atoms`:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "def parse(day, parser=str, sep='\\n') -> tuple:\n", " \"\"\"Split the day's input file into entries 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\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", "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 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", " 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):\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:" ] }, { "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 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\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 total(counts: Counter) -> int: \n", " \"\"\"The sum of all the counts in a Counter\"\"\"\n", " return sum(counts.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))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 1](https://adventofcode.com/2021/day/1): Sonar Sweep\n", "\n", "This year's 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", "Each entry in the input is an integer depth measurement, such as \"`148`\".\n", "\n", "1. How many measurements are larger than the previous measurement?\n", "2. Consider sums of a three-measurement sliding window. How many sums are larger than the previous sum?" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "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": [ "# Note: Daily Code Pattern\n", "\n", "Each day will follow a pattern similar to the code above for Day 1:\n", "\n", "1. I'll first parse the input file, in this case with `in1 = parse(1, int)`. The \"`1`\" means to read the file for day 1; `int` means each entry of the input file is parsed as an `int`. The resulting integers will be returned as a tuple. By default, each entry is a single line of input; the `sep` keyword for `parse` can be used for input files in which that is not true.\n", "2. I'll write a function to solve the problem. In this case, the function call is `increases(in1)`.\n", "3. I'll then submit the answer to AoC and verify it is correct. \n", "4. If it is correct, I'll moe on to part 2. \n", "5. When both parts are correct, I'll use the function `answers` to record the correct answer, and assert that the computation matches the answer. \n", "6. For more complex puzzles, I will include some `assert` statements to show that I am getting the right partial results on the small example given in the puzzle description." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 2](https://adventofcode.com/2021/day/2): Dive! \n", "\n", "Each entry in the input is a command, like \"`forward 1`\", \"`down 2`\", or \"`up 3`\".\n", "\n", "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?\n", "1. 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? (New interpretation: 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": 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(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), 1670340)" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 9, "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), 1954293920)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 3](https://adventofcode.com/2021/day/3): Binary Diagnostic\n", "\n", "Each entry in the input is a bit string, such as \"`101000111100`\".\n", "\n", "1. What is the power consumption of the submarine (product of gamma and epsilon rates)?\n", "2. What is the life support rating of the submarine (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": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 11, "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: \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", " 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": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 12, "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", " 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", "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. Bingo games will be played against a giant squid.\n", "\n", "1. What will your final score be if you choose the first bingo board to win?\n", "2. Figure out which board will win last. Once it wins, what would its final score be?\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: \"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", " 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", " \"\"\"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(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", " 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": "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 final 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": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 5](https://adventofcode.com/2021/day/5): Hydrothermal Venture\n", "\n", "Each entry in the input is a \"line\" 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, which are all at ±45°). 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": "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": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 18, "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), 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", "The input is a single line of comma-separated integers, each one the age of a lanternfish. Over time, the lanternfish 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, int, sep=',')" ] }, { "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": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 20, "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": [ "My hunch was right, so part 2 is simple:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(6.2, total(simulate(Fish(in6), 256)), 1592918715629)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 7](https://adventofcode.com/2021/day/7): The Treachery of Whales\n", "\n", "The input is a single line of comma-separated integers, each one the horizontal position of a crab (in its own submarine).\n", "\n", "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 travel costs one unit of fuel.)\n", "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": 22, "metadata": {}, "outputs": [], "source": [ "in7 = parse(7, int, sep=',')" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 23, "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": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 24, "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": 25, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "490.543" ] }, "execution_count": 25, "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": 26, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{490: 95519693, 491: 95519725, 490.543: 95519083.0}" ] }, "execution_count": 26, "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)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 8](https://adventofcode.com/2021/day/8): Seven Segment Search\n", "\n", "Each entry in the input is 10 patterns followed by 4 output values, in the form:\n", "\n", " be cfbegad cbdgef fgaecd cgeb fdcge agebfd fecdb fabcd edb | edb cefdb eb gcbe\n", " \n", "Each pattern and output value represents a digit, with each letter representing one of the segments in a [7-segment display](https://en.wikipedia.org/wiki/Seven-segment_display). The mapping of letters to segments is unknown, but is consistent within each entry.\n", "\n", "1. In the output values, how many times do digits 1, 4, 7, or 8 appear?\n", "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?\n", "\n", "The entry above would be parsed as:\n", "\n", " (('be', 'cfbegad', 'cbdgef', 'fgaecd', 'cgeb', 'fdcge', 'agebfd', 'fecdb', 'fabcd', 'edb'), \n", " ('edb', 'cefdb', 'eb', 'gcbe'))\n", " \n", "I refer to the left- and right-hand sides of the line with \"lhs\" and \"rhs\". " ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [], "source": [ "in8 = parse(8, lambda line: mapt(atoms, line.split('|')))" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def count1478(entries) -> int:\n", " \"\"\"How many of the rhs digits in all the entries are a 1, 4, 7, or 8?\"\"\"\n", " # (1, 4, 7, 8) have (2, 4, 3, 7) segments, respectively.\n", " return sum(len(digit) in (2, 4, 3, 7) for (lhs, rhs) in entries for digit in rhs)\n", "\n", "answer(8.1, count1478(in8), 493)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Part 2 is **tricky**. The digits other than 1, 4, 7, 8 are ambiguous. To figure out which is which in each entry I could do some fancy constraint satisfaction, but that sounds hard. Or I could exhaustively try all permutations of the segments. 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 rhs and forms it into an `int`.\n", " - Note that `get_digit` applies the translator, but then has to sort the letters to get a key that can be looked up in the `segment_map` dict.\n", "- Finally, sum up the decoding of each entry." ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 29, "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 into an integer representing the 4 output digits\"\"\"\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): ???" ] } ], "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 }