diff --git a/ipynb/Advent-2021.ipynb b/ipynb/Advent-2021.ipynb index 72223b9..ddee8ef 100644 --- a/ipynb/Advent-2021.ipynb +++ b/ipynb/Advent-2021.ipynb @@ -12,258 +12,67 @@ "\n", "To fully understand each puzzle's instructions, click on the link (e.g. [**Day 1**](https://adventofcode.com/2021/day/1)); I give only brief summaries here. \n", "\n", - "Part of the idea of AoC is that you have to make some design choices to solve Part 1 *before* you get to see the instructions for Part 2. So there is a tension of wanting the solution to Part 1 to provide components that can be re-used in Part 2, without falling victim to [YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it). In this notebook I won't refactor the code for Part 1 after I see what is requested in Part 2 (although I may edit the code for clarity, without changing the initial approach).\n", + "Part of the idea of AoC is that you have to make some design choices to solve Part 1 *before* you get to see the instructions for Part 2. So there is a tension of wanting the solution to Part 1 to provide components that can be re-used in Part 2, without falling victim to [YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it). In this notebook I won't refactor the code for Part 1 after I see what is requested in Part 2 (although I may edit the code for clarity, without changing the initial approach). Sometimes I will explore further, inventing my own \"Part 3\".\n", + "\n", + "This year's AoC theme involves Santa's Elves on a submarine. Gary J. Grady ([@GaryJGrady](https://twitter.com/GaryJGrady/) on Twitter) has some nice drawings to set the scene:\n", + "\n", + "\n", "\n", "# Day 0: Preparations\n", "\n", - "Imports that I have used in past AoC years:" + "I put some imports and functions that I thought would be useful in a notebook of utility functions, [AdventUtils.ipynb](AdventUtils.ipynb)." ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 81, "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" + "%run AdventUtils.ipynb\n", + "\n", + "current_year = 2021" ] }, { "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 in the file).\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**: Understand the day's instructions and:\n", - " - Write code to compute the answer to Part 1.\n", - " - Record the answer with the `answer` function, which also serves as a unit test when the notebook is re-run.\n", - "- **Part 2**: Understand the second part of the instructions and:\n", - " - Write code and record `answer` for Part 2.\n", - " \n", - "Occasionally I'll introduce a **Part 3** where I explore beyond the instructions.\n", + "# [Day 1](https://adventofcode.com/2021/day/1): Sonar Sweep\n", "\n", - "Here are the helper functions for `answer` and `parse`:" + "\n", + "- **Input**: Each item in the input is an integer depth measurement.\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", - " fname = f'AOC2021/input{day}.txt'\n", - " text = open(fname).read()\n", - " entries = mapt(parser, text.rstrip().split(sep))\n", - " if print_lines:\n", - " all_lines = text.splitlines()\n", - " lines = all_lines[:print_lines]\n", - " head = f'{fname} ➜ {len(text)} chars, {len(all_lines)} lines; first {len(lines)} lines:'\n", - " dash = \"-\" * 100\n", - " print(f'{dash}\\n{head}\\n{dash}')\n", - " for line in lines:\n", - " print(trunc(line))\n", - " print(f'{dash}\\nparse({day}) ➜ {len(entries)} entries:\\n'\n", - " f'{dash}\\n{trunc(str(entries))}\\n{dash}')\n", - " return entries\n", - "\n", - "def trunc(s: str, left=70, right=25, dots=' ... ') -> str: \n", - " \"\"\"All of string s if it fits; else left and right ends of s with dots in the middle.\"\"\"\n", - " dots = ' ... '\n", - " return s if len(s) <= left + right + len(dots) else s[:left] + dots + s[-right:]" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "Char = str # Intended as the type of a one-character string\n", - "Atom = Union[float, int, str]\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(r'-?[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(r'[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(r'[a-zA-Z]+', text)\n", - "\n", - "def atoms(text: str) -> Tuple[Atom]:\n", - " \"\"\"A tuple of all the atoms (numbers or symbol names) in text.\"\"\"\n", - " return mapt(atom, re.findall(r'[a-zA-Z_0-9.+-]+', text))\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": 4, - "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 x in numbers:\n", - " result *= x\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 transpose(matrix) -> list: return list(zip(*matrix))\n", - "\n", - "def nothing(*args) -> None: return None\n", - "\n", - "cat = ''.join\n", - "flatten = chain.from_iterable\n", - "cache = lru_cache(None)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Some past puzzles involve (x, y) points on a rectangular grid, so I'll define `Point` and `Grid`:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "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": 6, - "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "----------------------------------------------------------------------------------------------------\n", - "AOC2021/input1.txt ➜ 9756 chars, 2000 lines; first 7 lines:\n", - "----------------------------------------------------------------------------------------------------\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 2000 lines:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "148\n", "167\n", "168\n", "169\n", "182\n", "188\n", - "193\n", - "----------------------------------------------------------------------------------------------------\n", - "parse(1) ➜ 2000 entries:\n", - "----------------------------------------------------------------------------------------------------\n", - "(148, 167, 168, 169, 182, 188, 193, 209, 195, 206, 214, 219, 225, 219, ... , 5604, 5623, 5626, 5625)\n", - "----------------------------------------------------------------------------------------------------\n" + "...\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Parsed representation ➜ 2000 ints:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "148\n", + "167\n", + "168\n", + "169\n", + "182\n", + "188\n", + "...\n" ] } ], @@ -275,64 +84,58 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 1**: How many measurements are larger than the previous measurement?" + "- **Part 1**: **How many measurements are larger than the previous measurement?**" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 3, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.000 seconds for correct answer: 1,400\n" + ] } ], "source": [ - "def increases(nums: List[int]) -> int:\n", + "def increases(measurements: Sequence[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", + " return quantify(measurements[i] > measurements[i - 1] \n", + " for i in range(1, len(measurements)))\n", "\n", - "answer(1.1, increases(in1), 1400)" + "answer(1.1, 1400, lambda: increases(in1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: Consider sums of a three-measurement sliding window. How many sums are larger than the previous sum?" + "- **Part 2**: Consider sums of a three-measurement sliding window. **How many sums are larger than the previous sum?**" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 4, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.001 seconds for correct answer: 1,429\n" + ] } ], "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", + "def windows(sequence, width) -> List[Sequence]:\n", + " \"\"\"All sliding (overlapping) windows of given `width` in sequence.\"\"\"\n", + " return [sequence[i:i+width] \n", + " for i in range(len(sequence) + 1 - width)]\n", "\n", - "answer(1.2, window_increases(in1), 1429)" + "answer(1.2, 1429, lambda: increases(mapt(sum, windows(in1, 3))))" ] }, { @@ -346,7 +149,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -379,35 +182,40 @@ "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", + "- **Input**: Each item in the input is a command name (\"forward\", \"down\", or \"up\") followed by an integer.\n", "\n", "I'll parse a command into a tuple like `('forward', 2)`." ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "----------------------------------------------------------------------------------------------------\n", - "AOC2021/input2.txt ➜ 7723 chars, 1000 lines; first 7 lines:\n", - "----------------------------------------------------------------------------------------------------\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 1000 lines:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "forward 2\n", "down 7\n", "down 8\n", "forward 9\n", "down 8\n", "forward 9\n", - "forward 8\n", - "----------------------------------------------------------------------------------------------------\n", - "parse(2) ➜ 1000 entries:\n", - "----------------------------------------------------------------------------------------------------\n", - "(('forward', 2), ('down', 7), ('down', 8), ('forward', 9), ('down', 8) ... ard', 3), ('forward', 6))\n", - "----------------------------------------------------------------------------------------------------\n" + "...\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Parsed representation ➜ 1000 tuples:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "('forward', 2)\n", + "('down', 7)\n", + "('down', 8)\n", + "('forward', 9)\n", + "('down', 8)\n", + "('forward', 9)\n", + "...\n" ] } ], @@ -419,23 +227,20 @@ "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?" + "- **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": 11, + "execution_count": 7, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.000 seconds for correct answer: 1,670,340\n" + ] } ], "source": [ @@ -448,32 +253,29 @@ " if op == 'up': depth -= n\n", " return pos * depth\n", "\n", - "answer(2.1, drive(in2), 1_670_340)" + "answer(2.1, 1_670_340, lambda: drive(in2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: Using the new interpretation of the commands, calculate the horizontal position and depth you would have after following the planned course. What do you get if you multiply your final horizontal position by your final depth? \n", + "- **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": 12, + "execution_count": 8, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.000 seconds for correct answer: 1,954,293,920\n" + ] } ], "source": [ @@ -487,7 +289,7 @@ " if op == 'up': aim -= n\n", " return pos * depth\n", "\n", - "answer(2.2, drive2(in2), 1_954_293_920)" + "answer(2.2, 1_954_293_920, lambda: drive2(in2))" ] }, { @@ -496,35 +298,30 @@ "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", + "- **Input**: Each item in the input is a bit string of `0`s and `1`s.\n", "\n", "I'll parse them as strings; I won't convert them into ints." ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "----------------------------------------------------------------------------------------------------\n", - "AOC2021/input3.txt ➜ 13000 chars, 1000 lines; first 7 lines:\n", - "----------------------------------------------------------------------------------------------------\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 1000 lines:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "101000111100\n", "000011111101\n", "011100000100\n", "100100010000\n", "011110010100\n", "101001100000\n", - "110001010000\n", - "----------------------------------------------------------------------------------------------------\n", - "parse(3) ➜ 1000 entries:\n", - "----------------------------------------------------------------------------------------------------\n", - "('101000111100', '000011111101', '011100000100', '100100010000', '0111 ... 1100111', '110111100100')\n", - "----------------------------------------------------------------------------------------------------\n" + "...\n" ] } ], @@ -536,23 +333,20 @@ "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?" + "- **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": 14, + "execution_count": 10, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.001 seconds for correct answer: 2,261,546\n" + ] } ], "source": [ @@ -577,32 +371,29 @@ " \"\"\"Product of epsilon and gamma rates.\"\"\"\n", " return int(epsilon(strs), 2) * int(gamma(strs), 2)\n", " \n", - "answer(3.1, power(in3), 2261546)" + "answer(3.1, 2261546, lambda: power(in3))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: Use the binary numbers in your diagnostic report to calculate the oxygen generator rating and CO2 scrubber rating, then multiply them together. What is the life support rating of the submarine?\n", + "- **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": 15, + "execution_count": 11, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.000 seconds for correct answer: 6,775,520\n" + ] } ], "source": [ @@ -621,7 +412,7 @@ " \"\"\"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)" + "answer(3.2, 6775520, lambda: life_support(in3))" ] }, { @@ -630,35 +421,40 @@ "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", + "- **Input**: The first item of the input is a permutation of the integers 0-99. Subsequent items are bingo boards: 5 lines of 5 ints each. items are separated by *two* newlines. \n", "\n", "I'll represent a board as a tuple of 25 ints; that makes `parse` easy: the permutation of integers and the bingo boards can both be parsed by `ints`. (Bingo games will be played against a giant squid; we get to choose which board we want to play.)" ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "----------------------------------------------------------------------------------------------------\n", - "AOC2021/input4.txt ➜ 7890 chars, 601 lines; first 7 lines:\n", - "----------------------------------------------------------------------------------------------------\n", - "73,42,95,35,13,40,99,92,33,30,83,1,36,93,59,90,55,25,77,44,37,62,41,47 ... 7,84,86,45,75,60,15,14,11\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 601 lines:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "73,42,95,35,13,40,99,92,33,30,83,1,36,93,59,90,55,25,77,44,37,62,41,47,80,23,51,61,21,20,76,8,71 ...\n", "\n", "91 5 64 81 34\n", "15 99 31 63 65\n", "45 39 54 93 83\n", "51 14 23 86 32\n", - "19 22 16 13 3\n", - "----------------------------------------------------------------------------------------------------\n", - "parse(4) ➜ 101 entries:\n", - "----------------------------------------------------------------------------------------------------\n", - "((73, 42, 95, 35, 13, 40, 99, 92, 33, 30, 83, 1, 36, 93, 59, 90, 55, 2 ... 69, 17, 49, 91, 30, 33))\n", - "----------------------------------------------------------------------------------------------------\n" + "...\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Parsed representation ➜ 101 tuples:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "(73, 42, 95, 35, 13, 40, 99, 92, 33, 30, 83, 1, 36, 93, 59, 90, 55, 25, 77, 44, 37, 62, 41, 47, ...\n", + "(91, 5, 64, 81, 34, 15, 99, 31, 63, 65, 45, 39, 54, 93, 83, 51, 14, 23, 86, 32, 19, 22, 16, 13, 3)\n", + "(20, 83, 38, 85, 70, 69, 12, 14, 26, 84, 19, 76, 45, 78, 99, 22, 80, 90, 33, 46, 75, 31, 21, 6, 28)\n", + "(22, 52, 65, 75, 2, 91, 12, 45, 18, 94, 38, 66, 85, 39, 1, 24, 36, 55, 74, 3, 89, 14, 79, 99, 48)\n", + "(19, 58, 95, 22, 6, 48, 28, 57, 30, 72, 12, 67, 15, 37, 18, 33, 1, 49, 90, 60, 35, 41, 47, 11, 84)\n", + "(89, 27, 65, 68, 19, 38, 83, 21, 81, 91, 67, 61, 87, 30, 10, 36, 45, 66, 56, 4, 82, 71, 44, 96, 90)\n", + "...\n" ] } ], @@ -670,7 +466,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 1**: What will your final score be if you choose the first bingo board to win?" + "- **Part 1**: To guarantee victory against the giant squid, figure out which board will win first. **What will your final score be if you choose that board?**" ] }, { @@ -682,18 +478,15 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 13, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.004 seconds for correct answer: 39,902\n" + ] } ], "source": [ @@ -729,30 +522,27 @@ " if winners:\n", " return bingo_score(winners[0], drawn, num)\n", "\n", - "answer(4.1, bingo(boards, order), 39902)" + "answer(4.1, 39902, lambda: bingo(boards, order))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: Figure out which board will win last. Once it wins, what would its final score be?" + "- **Part 2**: Figure out which board will win last. **Once it wins, what would its final score be?**" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 14, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.007 seconds for correct answer: 26,936\n" + ] } ], "source": [ @@ -767,7 +557,7 @@ " if not remaining_boards:\n", " return bingo_score(winners[-1], drawn, num)\n", " \n", - "answer(4.2, bingo_last(boards, order), 26936)" + "answer(4.2, 26936, lambda: bingo_last(boards, order))" ] }, { @@ -777,41 +567,53 @@ "" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 5](https://adventofcode.com/2021/day/5): Hydrothermal Venture\n", "\n", - "- **Input**: Each entry in the input is a \"line\" denoted by start and end x,y points, e.g. \"`0,9 -> 5,9`\". \n", + "- **Input**: Each item in the input is a \"line\" denoted by start and end x,y points, e.g. \"`0,9 -> 5,9`\". \n", "\n", "I'll represent a line as a 4-tuple of integers, e.g. `(0, 9, 5, 9)`." ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "----------------------------------------------------------------------------------------------------\n", - "AOC2021/input5.txt ➜ 9249 chars, 500 lines; first 7 lines:\n", - "----------------------------------------------------------------------------------------------------\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 500 lines:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "409,872 -> 409,963\n", "149,412 -> 281,280\n", "435,281 -> 435,362\n", "52,208 -> 969,208\n", "427,265 -> 884,265\n", "779,741 -> 779,738\n", - "949,41 -> 13,977\n", - "----------------------------------------------------------------------------------------------------\n", - "parse(5) ➜ 500 entries:\n", - "----------------------------------------------------------------------------------------------------\n", - "((409, 872, 409, 963), (149, 412, 281, 280), (435, 281, 435, 362), (52 ... 13), (919, 123, 88, 954))\n", - "----------------------------------------------------------------------------------------------------\n" + "...\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Parsed representation ➜ 500 tuples:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "(409, 872, 409, 963)\n", + "(149, 412, 281, 280)\n", + "(435, 281, 435, 362)\n", + "(52, 208, 969, 208)\n", + "(427, 265, 884, 265)\n", + "(779, 741, 779, 738)\n", + "...\n" ] } ], @@ -823,23 +625,20 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 1**: Consider only horizontal and vertical lines. At how many points do at least two lines overlap?" + "- **Part 1**: Consider only horizontal and vertical lines. **At how many points do at least two lines overlap?**" ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 16, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.041 seconds for correct answer: 7,436\n" + ] } ], "source": [ @@ -858,18 +657,14 @@ " 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)" + "answer(5.1, 7436, lambda: overlaps(in5))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: Consider all of the lines (including diagonals, which are all at ±45°). At how many points do at least two lines overlap?" + "- **Part 2**: Consider all of the lines (including diagonals, which are all at ±45°). **At how many points do at least two lines overlap?**" ] }, { @@ -881,18 +676,16 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 17, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.051 seconds for correct answer: 7,436\n", + "0.076 seconds for correct answer: 21,104\n" + ] } ], "source": [ @@ -916,8 +709,8 @@ "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)" + "answer(5.1, 7436, lambda: overlaps(in5, diagonal=False)) # Make sure it still works\n", + "answer(5.2, 21104, lambda: overlaps(in5, diagonal=True))" ] }, { @@ -926,27 +719,34 @@ "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." + "- **Input**: The input is comma-separated integers, each the age of a lanternfish (according to its internal timer). \n", + "\n", + "Over time, the lanternfish age and reproduce: Each day, their timer decrements by one. The day after it reaches 0 it is reset to 6 and a new lanternfish is born with an internal timer of 8." ] }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "----------------------------------------------------------------------------------------------------\n", - "AOC2021/input6.txt ➜ 600 chars, 1 lines; first 1 lines:\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, ... 5,5,1,3,1,4,2,3,3,1,4,1,1\n", - "----------------------------------------------------------------------------------------------------\n", - "parse(6) ➜ 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, ... , 4, 2, 3, 3, 1, 4, 1, 1)\n", - "----------------------------------------------------------------------------------------------------\n" + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 1 line:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "5,4,3,5,1,1,2,1,2,1,3,2,3,4,5,1,2,4,3,2,5,1,4,2,1,1,2,5,4,4,4,1,5,4,5,2,1,2,5,5,4,1,3,1,4,2,4,2, ...\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Parsed representation ➜ 300 ints:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "5\n", + "4\n", + "3\n", + "5\n", + "1\n", + "1\n", + "...\n" ] } ], @@ -958,31 +758,28 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 1**: Find a way to simulate lanternfish. How many lanternfish would there be after 80 days?\n", + "- **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." + "Instead, I'll use a `Counter` of fish, and process all the fish of each age group together, all at once. That way each update will be *O*(9) = *O*(1), not *O*(*n*). I have a hunch that Part 2 will involve a ton-o'-fish." ] }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 19, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.000 seconds for correct answer: 350,917\n" + ] } ], "source": [ - "Fish = Counter # Represent a school of fish as a Counter of their timer-ages\n", + "Fish = Counter # Represent a school of fish as a Counter of their internal timers\n", "\n", "def simulate(fish, days=1) -> Fish:\n", " \"\"\"Simulate the aging and birth of fish over `days`.\"\"\"\n", @@ -996,37 +793,35 @@ " \n", "assert simulate(Fish((3, 4, 3, 1, 2))) == Fish((2, 3, 2, 0, 1))\n", "assert simulate(Fish((2, 3, 2, 0, 1))) == Fish((1, 2, 1, 6, 0, 8))\n", + "assert Fish((1, 1, 1, 6, 8, 6)) == {1: 3, 6: 2, 8: 1}\n", "\n", - "answer(6.1, total(simulate(Fish(in6), 80)), 350917)" + "answer(6.1, 350917, lambda: total(simulate(Fish(in6), 80)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: How many lanternfish would there be after 256 days?\n", + "- **Part 2**: **How many lanternfish would there be after 256 days?**\n", "\n", - "My hunch was right, so part 2 is simple:" + "My hunch was right, so part 2 is straightforward:" ] }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 20, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.001 seconds for correct answer: 1,592,918,715,629\n" + ] } ], "source": [ - "answer(6.2, total(simulate(Fish(in6), 256)), 1_592_918_715_629)" + "answer(6.2, 1_592_918_715_629, lambda: total(simulate(Fish(in6), 256)))" ] }, { @@ -1049,22 +844,27 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "----------------------------------------------------------------------------------------------------\n", - "AOC2021/input7.txt ➜ 3887 chars, 1 lines; first 1 lines:\n", - "----------------------------------------------------------------------------------------------------\n", - "1101,1,29,67,1102,0,1,65,1008,65,35,66,1005,66,28,1,67,65,20,4,0,1001, ... 684,51,1186,1801,627,1379\n", - "----------------------------------------------------------------------------------------------------\n", - "parse(7) ➜ 1000 entries:\n", - "----------------------------------------------------------------------------------------------------\n", - "(1101, 1, 29, 67, 1102, 0, 1, 65, 1008, 65, 35, 66, 1005, 66, 28, 1, 6 ... 1, 1186, 1801, 627, 1379)\n", - "----------------------------------------------------------------------------------------------------\n" + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 1 line:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "1101,1,29,67,1102,0,1,65,1008,65,35,66,1005,66,28,1,67,65,20,4,0,1001,65,1,65,1106,0,8,99,35,67, ...\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Parsed representation ➜ 1000 ints:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "1101\n", + "1\n", + "29\n", + "67\n", + "1102\n", + "0\n", + "...\n" ] } ], @@ -1076,58 +876,54 @@ "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", + "The idea is that if you can get the crabs to all align in one horizontal position, they can save you from a giant whale by opening up an escape route to a cave.\n", "\n", - "- **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.)" + "\n", + "\n", + "- **Part 1**: Determine the horizontal position that the crabs can align to using the least fuel possible. (Each unit of horizontal travel costs one unit of fuel.) **How much fuel must they spend to align to that position?**" ] }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 22, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.000 seconds for correct answer: 352,707\n" + ] } ], "source": [ "def fuel_cost(positions) -> int:\n", " \"\"\"How much fuel does it cost to get everyone to the best alignment point?\"\"\"\n", " # I happen to know that the best alignment point is the median\n", - " align = median(positions)\n", + " align = int(median(positions))\n", " return sum(abs(p - align) for p in positions)\n", "\n", - "answer(7.1, fuel_cost(in7), 352707)" + "answer(7.1, 352707, lambda: fuel_cost(in7))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: Determine the horizontal position that the crabs can align to using the least fuel possible so they can make you an escape route! 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.) " + "- **Part 2**: Determine the horizontal position that the crabs can align to using the least fuel possible so they can make you an escape route! (Now for each crab the first unit of travel costs 1, the second 2, the third 3, and so on.) **How much fuel must they spend to align to that position?**" ] }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 23, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.361 seconds for correct answer: 95,519,693\n" + ] } ], "source": [ @@ -1136,14 +932,14 @@ " 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", + " for align in cover(*positions))\n", "\n", "def burn_rate2(p, align) -> int:\n", " \"\"\"The first step costs 1, the second 2, etc. (i.e. triangular numbers).\"\"\"\n", " steps = abs(p - align)\n", " return steps * (steps + 1) // 2\n", "\n", - "answer(7.2, fuel_cost2(in7), 95519693)" + "answer(7.2, 95519693, lambda: fuel_cost2(in7))" ] }, { @@ -1157,7 +953,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 24, "metadata": {}, "outputs": [ { @@ -1166,7 +962,7 @@ "490.543" ] }, - "execution_count": 28, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } @@ -1185,7 +981,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 25, "metadata": {}, "outputs": [ { @@ -1194,7 +990,7 @@ "{490: 95519693, 491: 95519725, 490.543: 95519083.0}" ] }, - "execution_count": 29, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } @@ -1215,7 +1011,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 26, "metadata": {}, "outputs": [ { @@ -1224,7 +1020,7 @@ "[376.0, 490.543]" ] }, - "execution_count": 30, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" }, @@ -1249,48 +1045,46 @@ "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", + "- **Input**: Each item 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." + "I'll split on the `|` and then extract atoms from both sides:" ] }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 27, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "----------------------------------------------------------------------------------------------------\n", - "AOC2021/input8.txt ➜ 16614 chars, 200 lines; first 7 lines:\n", - "----------------------------------------------------------------------------------------------------\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 200 lines:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "daegb gadbcf cgefda edcfagb dfg acefbd fdgab fg bdcfa fcgb | cdfgba fgbc dbfac gfadbc\n", "bdfc dcbegf bf egfbcda gebad cfgaed bfe edfgc aegfcb gebdf | fb fb bcdfaeg fcgdeb\n", "cebdgaf bfcd gceab bf bfcea gceafd ecdfa fegdab bfcade fba | dfcb dagfbe fbaged bfa\n", "efabcg aegcdb fgaed fac dgafbc becf eadcgbf aegfc fc cagbe | ecgfa agdef eagfc gdceab\n", "fcdae cdeabf fga gf gabfde cgadb gadebfc cgfe aegcdf afgcd | fbgadce gadefb fag bafegd\n", "gecadbf bgc dacgf gaecbf cbeda dbfg bgdca bg bafcgd gdacef | cdgfa fceabg dgfb dgabc\n", - "fbecdga gcdbea cegab fc cafe cfg ebgdf cbgfe afbgec bagcdf | feac acegb bfagce gcafbe\n", - "----------------------------------------------------------------------------------------------------\n", - "parse(8) ➜ 200 entries:\n", - "----------------------------------------------------------------------------------------------------\n", - "((('daegb', 'gadbcf', 'cgefda', 'edcfagb', 'dfg', 'acefbd', 'fdgab', ' ... cdg', 'agecb', 'acbeg')))\n", - "----------------------------------------------------------------------------------------------------\n" + "...\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Parsed representation ➜ 200 tuples:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "(('daegb', 'gadbcf', 'cgefda', 'edcfagb', 'dfg', 'acefbd', 'fdgab', 'fg', 'bdcfa', 'fcgb'), ('cd ...\n", + "(('bdfc', 'dcbegf', 'bf', 'egfbcda', 'gebad', 'cfgaed', 'bfe', 'edfgc', 'aegfcb', 'gebdf'), ('fb ...\n", + "(('cebdgaf', 'bfcd', 'gceab', 'bf', 'bfcea', 'gceafd', 'ecdfa', 'fegdab', 'bfcade', 'fba'), ('df ...\n", + "(('efabcg', 'aegcdb', 'fgaed', 'fac', 'dgafbc', 'becf', 'eadcgbf', 'aegfc', 'fc', 'cagbe'), ('ec ...\n", + "(('fcdae', 'cdeabf', 'fga', 'gf', 'gabfde', 'cgadb', 'gadebfc', 'cgfe', 'aegcdf', 'afgcd'), ('fb ...\n", + "(('gecadbf', 'bgc', 'dacgf', 'gaecbf', 'cbeda', 'dbfg', 'bgdca', 'bg', 'bafcgd', 'gdacef'), ('cd ...\n", + "...\n" ] } ], @@ -1300,7 +1094,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 28, "metadata": {}, "outputs": [], "source": [ @@ -1312,51 +1106,59 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "Each pattern and output value represents a digit on a [7-segment display](https://en.wikipedia.org/wiki/Seven-segment_display), with each letter a–g representing one of the 7 segments that is turned on in that digit. The mapping of letters to segments differs for each input item, but is consistent across all the digits within each item. Here's one mapping:\n", + "\n", + " aaaa\n", + " b c\n", + " b c\n", + " dddd\n", + " e f\n", + " e f\n", + " gggg\n", + " \n", "\n", "\n", - "- **Part 1**: In the output values, how many times do digits 1, 4, 7, or 8 appear?\n", + "- **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?*" + "That's the same as asking *how many output values have a length of 2, 4, 3, or 7 segments?*" ] }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 29, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.000 seconds for correct answer: 493\n" + ] } ], "source": [ - "def count1478(entries) -> int:\n", - " \"\"\"How many of the rhs digits in the entries are a 1, 4, 7, or 8?\"\"\"\n", + "def lengths2437(data):\n", + " \"\"\"Count the output values with lengths 2, 4, 3, 7.\"\"\"\n", " return quantify(len(value) in (2, 4, 3, 7) \n", - " for (lhs, rhs) in entries for value in rhs)\n", + " for (lhs, rhs) in data \n", + " for value in rhs)\n", "\n", - "answer(8.1, count1478(in8), 493)" + "answer(8.1, 493, lambda: lengths2437(in8))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: For each entry, determine all of the wire/segment connections and decode the four-digit output values. What do you get if you add up all of the output values?" + "- **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", + "Part 2 is *tricky*. The first output value `'cdfgba'` has 6 segments, so it could be either a 0, 6, or 9. To figure out which one it is I could do some fancy constraint satisfaction. That sounds hard. Or I could exhaustively try all permutations of the ways the 7 letters can map to the 7 segments. That sounds easy! Here's my plan:\n", + "- Make a list of the 7! = 5,040 possible `str.maketrans` translators that permute `'abcdefg'`.\n", "- Decode an entry by trying all translators and keeping the one that maps all of the ten lhs patterns to a valid digit. `decode` then applies the translator to the four rhs values, concatenates them, and converts the result into an `int`.\n", " - Note that `get_digit` must *sort* the translated letters to get a key that can be looked up in `segment_map`.\n", "- Finally, sum up the decoding of each entry." @@ -1364,18 +1166,15 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 30, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 34, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.599 seconds for correct answer: 1,010,460\n" + ] } ], "source": [ @@ -1396,7 +1195,7 @@ " 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)" + "answer(8.2, 1010460, lambda: sum(map(decode, in8)))" ] }, { @@ -1412,58 +1211,60 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 31, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "----------------------------------------------------------------------------------------------------\n", - "AOC2021/input9.txt ➜ 10100 chars, 100 lines; first 7 lines:\n", - "----------------------------------------------------------------------------------------------------\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 100 lines:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "9897656789865467895698765469899988672134598894345689864101378965457932349943210987654789653198789434\n", "8789542499996878954329984398789976561012987789245678953212567892345791998899329899765678969997668912\n", "7678943978987989965998993297649875432129876567956789864487678991056899877778939769886789998766457899\n", "4578999868998996899867894976532986543299876476897899987569899989167898766567898654998898998655345678\n", "2456987657679535679756799988643498657987654345789978899789998878998919954349997543219967987543237889\n", "1234896545568986798645678999754989767898765456998769759899987765789329863238898659301256798793156891\n", - "2346789432379997987434689489899879898919876567899954346998796434678997642127789798512345989989247892\n", - "----------------------------------------------------------------------------------------------------\n", - "parse(9) ➜ 100 entries:\n", - "----------------------------------------------------------------------------------------------------\n", - "((9, 8, 9, 7, 6, 5, 6, 7, 8, 9, 8, 6, 5, 4, 6, 7, 8, 9, 5, 6, 9, 8, 7, ... 6, 7, 9, 7, 6, 8, 7, 9))\n", - "----------------------------------------------------------------------------------------------------\n" + "...\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Parsed representation ➜ 100 tuples:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "(9, 8, 9, 7, 6, 5, 6, 7, 8, 9, 8, 6, 5, 4, 6, 7, 8, 9, 5, 6, 9, 8, 7, 6, 5, 4, 6, 9, 8, 9, 9, 9, ...\n", + "(8, 7, 8, 9, 5, 4, 2, 4, 9, 9, 9, 9, 6, 8, 7, 8, 9, 5, 4, 3, 2, 9, 9, 8, 4, 3, 9, 8, 7, 8, 9, 9, ...\n", + "(7, 6, 7, 8, 9, 4, 3, 9, 7, 8, 9, 8, 7, 9, 8, 9, 9, 6, 5, 9, 9, 8, 9, 9, 3, 2, 9, 7, 6, 4, 9, 8, ...\n", + "(4, 5, 7, 8, 9, 9, 9, 8, 6, 8, 9, 9, 8, 9, 9, 6, 8, 9, 9, 8, 6, 7, 8, 9, 4, 9, 7, 6, 5, 3, 2, 9, ...\n", + "(2, 4, 5, 6, 9, 8, 7, 6, 5, 7, 6, 7, 9, 5, 3, 5, 6, 7, 9, 7, 5, 6, 7, 9, 9, 9, 8, 8, 6, 4, 3, 4, ...\n", + "(1, 2, 3, 4, 8, 9, 6, 5, 4, 5, 5, 6, 8, 9, 8, 6, 7, 9, 8, 6, 4, 5, 6, 7, 8, 9, 9, 9, 7, 5, 4, 9, ...\n", + "...\n" ] } ], "source": [ - "in9 = Grid(rows=parse(9, digits))" + "in9 = Grid(parse(9, digits))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 1**: Find all of the *low points* on your heightmap. What is the sum of the risk levels of all low points on your heightmap?\n", + "- **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": 36, + "execution_count": 32, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 36, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.015 seconds for correct answer: 607\n" + ] } ], "source": [ @@ -1476,39 +1277,36 @@ " \"\"\"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)" + "answer(9.1, 607, lambda: total_risk(in9))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: What do you get if you multiply together the sizes of the three largest basins?\n", + "- **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." + "With that assumption, I can associate each location with its low point using a [flood fill](https://en.wikipedia.org/wiki/Flood_fill) that starts from each low point. I can then get the sizes of the three largest (most common) basins and multiply them together." ] }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 33, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 37, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.032 seconds for correct answer: 900,864\n" + ] } ], "source": [ "def find_basins(grid) -> Dict[Point, Point]:\n", - " \"\"\"Compute `basins` as a map of {point: low_point} for each point in grid.\"\"\"\n", + " \"\"\"Compute `basins` as a dict of {point: low_point_of_point's_basin} for each point in grid.\"\"\"\n", " basins = {} # A dict mapping each non-9 location to its low point.\n", " def flood_fill(p, low):\n", " \"\"\"Spread from p in all directions until hitting a 9;\n", @@ -1521,55 +1319,23 @@ " 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)" + "answer(9.2, 900864, lambda: \n", + " prod(count for low_point, count in \n", + " Counter(find_basins(in9).values()).most_common(3)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 3**: Verification and Visualization\n", + "- **Part 3**: Visualization\n", "\n", - "I want to check that the set of low points is the same as the set of basins I identified:" + "I'll make a plot to display height 9 locations in yellow and height 0 locations in deep purple, with a gradient in between:" ] }, { "cell_type": "code", - "execution_count": 38, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "249" - ] - }, - "execution_count": 38, - "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 also 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": 39, + "execution_count": 34, "metadata": {}, "outputs": [ { @@ -1586,31 +1352,31 @@ } ], "source": [ - "def show(in9, low=None):\n", + "def show_heights(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.scatter(*T(in9), marker='s', s=10, c=C, cmap=plt.get_cmap('plasma'))\n", + " if low: plt.plot(*T(low_points(in9)), low, markersize=4)\n", " plt.axis('square'); plt.axis('off')\n", " \n", - "show(in9)" + "show_heights(in9)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We can optionally display the low points. Here I'll display them as black diamonds:" + "We can optionally display the low points. Here I'll display them as red diamonds:" ] }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 35, "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAi0AAAIuCAYAAABzfTjcAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAgAElEQVR4nOy9cXAc1YHu+50eaTwzrZEsSxBh8bxxkpJ9kwdkr70hBVupW2wtvg8b6qWWxdRCvQCuwO7L5QakemCbusuyt7DNVsneLXZvhVC2c7cgZWdJ8QoC7+HUUvv2ERVwxbLAbp5RbUKWwqCsZcvyqGfksWb6/aEeuafVYx3pnDndPfP9/lH6m5H69OnTrQ7+6Rvhui4IIYQQQuKOFfUACCGEEEJk4EMLIYQQQhIBH1oIIYQQkgj40EIIIYSQRMCHFkIIIYQkAj60EEIIISQR8KGFEEIIIYmADy2EEEIISQR8aCGEEEJIIuBDCyGEEEISAR9aCCGEEJII+NBCCCGEkETAhxZCCCGEJAI+tBBCCCEkEfChhRBCCCGJgA8thBBCCEkEfGghhBBCSCLgQwshhBBCEgEfWgghhBCSCPjQQgghhJBEwIcWQgghhCQCPrQQQgghJBHwoYUQQgghiYAPLYQQQghJBHxoIYQQQkgi4EMLIYQQQhIBH1oIIYQQkgj40EIIIYSQRMCHFkLI8ggxBCFehhBDWt5HWoOozne77ZcsIlzXjWTHH975n84DyPuiwht/twW+rLBr8t7u17++3/++wk1v7uk+u/f2umzdvhe6Z8pfqMt60r/srv6PTXWZ9Vsfds//4Ma6rOOen3UHxlLYdOwvQ/c7Yh2vy0arO7u3dfygLntt/h6lsYQdm8r4Dg8crcsazWnYmGWPI2zMsu8LmwPZsaiMOex9us+H7NyH/TzZuZI9Ntn3ha2hXan9s4eqT9o2inCQc7vhbJ65sHE8+L0HxMHZB/G4nYWDEnKuDWfz4c8dqXvfrsl7tV+rUV1vYec3bP7Cvld2LHE6tiX3usq9W88ie7IHc+Icsrg5/YDzzoWDXWH3RNl7k9Q1veajredhn7RRFA5yeNh6zDlc2dMVto+wn7fqOfj1fVsd2CezKIoSbDyNJ5zd7nCXyj1W5VzK3jtlf57MWJw/+Ay+7cV9wCBR/peWfMh2PuR1ZsyYRZUJMXSo+qTdhSJScNGFogAwZk1cXPK+2gOLBRdZ733d85PxOA5mWrNBdzIPYKwHcyIFF2tRwonyM7b3XyCatl9v3Y11oSi89YhD1Sebvl9vHY9lURQL69vBg3i86fuNYebfDr5uBP7zECHkcozaKMLCwn+R9b72Zh49s+R9tQcW//uuLxw3N1JijPurxwCgN+Wd79qDC4DRZu7XW3e9/nVmo9j0/XrruG6/WThN3y9ZCh9aCCGXY8RBDlUIAKh9nZ57qm/J+0qwl7zvrfxOcyMlxvi+dScATFe8812BwDlkAWCkmfv11t20f505yDV9v946rttvCXbT90uWEuVDSyFkuxDyeuyzQXcST1T+HFe7nzlRjyUs656fxO9O/wV65uM5Pmb6M2viInLfnIT1YVntnLvuxMPWY84scqhAYBY5F8AN1aHOJe97Gk84tQeXkve+8x0DsZqXZmdXuKec+9w/Q7/7aeRjaWZ2SgwUANwwg4xbe2C5Of2AA9edaOZ+vXV3wyxyrrce8bD1WNP3663jG0rIubUHlqfxRNP3W/vf1pmC0/U3P4N1ptC0fUhm/u3g60boML3DGqd+Nbgk+/m/5ZZkH/1qYEn2j69vqdu+CcDk/3ZLXdZzDChcd7E+A+CJRHXZF//ju0v2Mfj5U0uyL19ZXJJNpd63flh+BmtRwlB1Igexd+iO1NG697w2Lz+W7qH6DAA8QXmRTQBeefsLddlNAP7JKi353v82/YF1whvfTWcmchCPDW25sDHwrj3Iv9dZH/0WYP/wqvrsHuDci9fURet+Czj9y/pztA7A3z14T/343gx/3/mJq5ZkYWMJQ/Z91X/ur9u2fiv8e3WfD9n1HLwWNgFI/ck/LHmf9ZWpJVnYuvr/fut/tbb8/HtIzc8h89rZHCpiqHr0hvqf1WAO/nhuT2APO3Gw9GxVTPSi+mgV4sDaIq6Zncj/j01LvvePLvxltTqxFpVHK6h677u9/IXAz7s3dL9h5yhsbYTN1Qsb/ktdtmsyfF7C5i/s/F7xhcklWdj9YAlCDP3vsHM2ivif8CEeFo9ZQPg5//vv7ajbvume8Ouj8if/vv4b98nfS9Z+84Ml+w07tm/84U8CSXANLPA/V7P1getO/B+p/cVvV4/b37fuRH9loAoAPyr+aeA77wldV7d//F8DWfjaWJK57oT44OpidXfVFk/14uDQs1VgT+g+wn6e7P1+4+cDczXpTvxf/fuKXyv8yH4rvxNXdPRWAeC3/8M7S45jy98/G8jC51RqnQoxlE+vyVnlMvIf/z/4+Hd+z1oX+tPk72thay1sLMFrsPuHITu9p8FgmgRFXJVMiKET5WfstSghBRc9mBMAxjxJLbbjWyJRRjU+Zk0551t+/j07NT8HCy5SlYVzjsmS0j6qQ50ovjiA6qa0reN9rZbVRE3TgmjU2SfiKvvx1EM4JQaM7re6KW0XXxxAdajT6H5nOq6yf9r7XZzvMHS8/1rOAxizyheEgAurfAEb/vbHUa4r/3bwdSPQaVFjtPZAAKD2tdeT1OJA6PhCJErSOozWHliAS0KsdexXUY6p5QkTNU0IoqS1EX9xGgB6RW0bgFUuA228rui0qGUjtX/TBYDKQj79rLXTkfjeyMY3d2BdXMbHTH82Ukll3Jow6LUwTVd3/gbPeROzt/N3OFgqiLpYEDUjHx+zZGbuf77CATBda1NzAVTT6SjXlX87+LoRIiuXaxkW/jPdGIBeANMAbvDkrHgQ5fgW9j0KYCRWc2KSKOYg7mvSBJx30iqYWlcJuV9HJuIG2gsBoOCJpLVsRU2KJhoIw/axLXV0fNCdzN9fPYZnrZ2Zw5U9E7JNj7KtsWENjrJtnIc/d2S8e34yf33hON7O35G5fWrvhEqLqmxL6Qv9+2Z3CNtOu0WURW57RojNH+78znjwfSqttmFjUWkaVWmTDGsB3bJmePYEcvZalDCDzPZ1QmweEcfq5qBRa6fsOQpdLzu/M95ZOJe/8t03cPqrN2Y2vvr8hO6GXRPtratupa5vat1x85ph550LB7tkz/mqG1Ndt/uAOJi5Fc9bL+HuvimxfnwUiFULskojuMp9TXfzq+wcyGYq7cEq16/UvLhu97kHtmVyr79vFW+6tq/alx9fB0jvQ+o43tq7dU7YJ9NuUZRFbsdP+vc5t0/t7Qp+rydy1/3eXrfvBTbiBl6PfXZKDODx1EP4RFwVS/nwfMcAftr7Xcx0GBqfEEM7zu6z0+5CKVnaXWhH7Syci9W8NDVrIEH3u58aGcvF/Fqc+sYOlLt7Y7kmm5VF1dRa+9+nxaB9RDyCKbG+aftg1n5ZtS9vz/7+jaj25bXvI1uaygMYS7sLbb9pt4gdZ/c1umb828GfbQSKuKQZjNYeWIBLMuiV774R6aAMEypB34bnIh1UqxNVUyshSeVLH78CBCTytBtfiZwiLrNmZCNlkXOD7ainv3pjO8mgoRL0y7irnebAeOZJ8NPefKMCgRlkKMQyY9Yg+8WGW5ZI5GXRUCL3bwd/thHiJ+ImRAYiy0ApkXMQFZx3QlZGgq6ZyETcgDwHAIWp1PtWUFzcljpaJy6uRM7VLWDKyq+ykp2KJBaV3Cc9vuv3jWdLU/kvffwKfrHhlszX3zs4oVvUDDteWXlOZQ5kJcURcWy83/00fxuew8u4K7PbHZ6QFfl0S7K6f56KpKhVfg27BhsIsSrCpOyYw8Ynu8Z1y6Uq15bKPlT+mCDsfbrnReW+plt4VplnlT8cWDIW1+1+87rhzBc/ftX6lw3b+0rZ/vGbgCXf6zVI1/3e7rjnZ0ZF3MgeWhAQeAbdyXytDj/YLuu1Lfq/JxIZitnKslK2Hx9s+hYAtJUM6s+mxHocwSNAG89BFNlpMWh78x75WJgxS0JWzF5he/fr5b4Xy2RNJTYi7v3VY4h5uywhhBBCIiQ2Iu6z1k4n5u2yzJgxY8aMWTtn/u3g60aIl4ibIBmIEEIIIWaJ7KElrBH3y1cW4WtvdW6f2tul0oirIsTqbp0My3RLXSr70C38aZXEViBlymay86ci5+qWGWXPr4lzpLsxVXYdhP082f2qSKi614Hua1BlbZgQ5GXbjePUHqy7NVv37ygT5y24j/x7dZ+qvbgPGCQ2Im5tu9beCoqLzJgxY8aMWdwyLJM1ldiIuIQQQgghlyM2Ii6WSj5xE5CYMWPGjBmzds7828HXjRAvEZcQHbBVmZD4027Xabsdb5OI7KElIM8BQGHj5+s+9lq5UVO3pGhCVtUtBes+Dt0yqKwkJttGvCu1f/ZQ9UnbRhEOcm43nM0j4th48H2yUpxuuU9FijPRbqwiyQZarguvzd+jNC+6rwWVVuWw7zVxXaqsId3yv4qQvbS1+M6t52GftFEUDnJ42HrMOVzZ0yX7RxYq9yHZ61yrDL/mo61zwj6ZdouiLHL4ybq9zu1Te7tkf2/Jisy67+PBtVG47iJ824vvg0Gi/OehoMCTD2RRy0bMkpYJMXSo+qTdhSJScNGFogAw1u9+Go/xMWPGDN71ONaFovCuUxyqPml7/yUi8vHpzqyJi3kAY2m3KGqfoLzj7L6kHq9/O/i6ESjiklZi1EYR/o9YB9B7G56LdFCEkEt412Ov/zq1UQQW/umk5cg8egYIHG/abd3jbTYUcZm1UjbiIOf6P2IdwPTLuIutysyYxSTzrsdp/3XqIOcCGInD+HRncwfWLTneskjs8fq3g68bgSJuu9KqUhhblaOlVdcV0Uu7XafNON42vdYiK5cLiEUAUNjy98/ClzWUxHQLcCZkRhP7lf149Y9uuWt2Q3qNbZXLqKbT21NCbJ65sHE8+L2yMqOs1KpbBg2V08Sx8X730/xteA4v467Mbnd4QqVBOU4NolE1g8pKe1vWDM+eQM5eixJmkNm+TojN21JH69ZVIzlX95yqzLOsXKoib5pokdYtPKuMZcn8uW73AXEwcyuet17C3X1TYv34KKB93ctKrbKt2as+v67bXfjg6kxm91lr7qm+vupQ53gPsPrfH2s+8ovMOx5O7XcOV/ZIN8ivVti1vjIF3/bi+GCQ2DXihmzHRUBqjUyIoYUHlgsQAKzyBQFgzJq4mK8OdUY/Pg3ZlFiPI3gEYKuyuUyIodoDSwouejAnAIwNupP5U2Ig+vExi112Wgza3nUa+VhMZNVNabv4ovq1UBN7u7Ag9l4SmfcOQRwzcWxYJmsqFHHbj1GrXPb+dRW1r72eLEbIahmtPbAAqH3tvb96LNJBEdJqhIm9rSwyB6GI237ZSDWdXjSZvK/TniwWh/ExS2Y2MoOMW/EegysL+fSz1k6uK2bMNGZhYq9Bkdm/HXzdCBRx25F2k+CSStJEO5V1lbRjJa1F0tZfG9/DI3toCUg/AJZ87HVD+SssU2kzNSHP6f4IclU51zpTyOdefx/Fm6511j7zWpfscchKbGHvi6o11sRHwuuWtF/o3ze74+w+O+0WURY5N+M6m1+/fl+d1BrH47AmLuYzj57B3IF1Tv6aT0LXlWw7qqxYrluwDWv2lRUcVQRv3eK27M9Tkf9l788q41O5F0vN1Vt7tzqwT2ZRFCXYeBpPOLvd4S7Zc2mi8Tjsew+Ig7O34nn7JdyNKbG+YdO3zvtu99Bn8G0vvg8GYSNum2bVvjxmf/9GVPvylFXjlgkxVHtg8YqoBICxbGkqHuO7TFYd6kTxxQFUN6Wl1lW7taMyi1fmXVNjWU9qzcLBg3g8EevvtBi0j4hHMCXWm9yvfzv4uhEo4hISP0ZrDyzApWbfL338SqSDagbt1o5K4oV3TdWtvywcgOsvtlDEZcYsftlIWSxt9v3FhltaTmptt3ZUZvHKvGuqbv2VuP4ul/m3g68bgSJu1CRNAGtEqxxHXGgn0S5ux5rEtZzEMceFuK0/clkiK5cLE3HtH14FX6Ysoql8PL1uISxM2jsgDs4+iC47Cwcl5LbbQmw+/Lkj48H3qQidups3w4TEXan9s4dg2zaKcJDb3i3E5hFxbDz4PpWPkzch7MapGXnmwsZxn9SayV/zyUScjkPrfhu0hcqec1nBNmztLpFuK/duPYvsyR7MiXPI7rh5zbDzzoWDXWFyron2VqnjfWvv1jlhn0y7RVEWuR0/6d/n3D61V1okjWrdy57fprcHu273C/37Ml8r/Mh6K7+z73zHwPiuBm21uuV1E7K0Snt6cK6u+MIkfNuL74NBKOJGlQkx9CAet7NwvH9HXZAtu+cn4zG+FRzHoeqTdheK8ERKAWDMEyyjH1+Cs5VKrUnOam2hXitzJGMZdCfzAMZ6MCdScLEWJZwoPxNrKbMmkqbdBZE07Rax4+y+WI85jtlMx1X2T3u/i/MdA5GPJeaZfzv4uhEo4kbHaO2BBbgkW15fOB7poFbBqI2l0qgnWBKSGLz23l5/q+9alIAYS5lhImnapchMWheKuNFlIyUslS3fzt+RNNlyxAk5Dk+wjMP4mDGTyrz23mmvzRcVCMwgE2spM0wkLQuKpMyalvm3g68bgSJulLSKANYqx0FIEtdyEsdMyCqJTMQNiEUAUKj+cz98WcMmWRXxTvbnybZYrrqF8c093Yc/d2S8e34yf33hON7O35G5fWrvhEoTpW4pU7r99vp949nSVP5LH7+CX2y4JfP19w62rjSq2AwqOz4TImTY+ZVtflVp4lVpAVWZF6n17LrdB8TBzK143noJd/dNifXjo4D0HKisg1ULk67b/eZ1w5kvfvyq9S8btveVsv3jNwGR/YGB7vOme92rrFPdkrHsHw6ozIHKPTF4zQx+/hR824vjg0Eie2jB8kJP3ASkpmTnOwbw097vAkCiZctSth8fbPoWkPDjYMbstBi0j+CRWIxFNitmr7C96y/ysTBr+QzLZE2FIi4hhBBCEgFFXGbMmDFjxoyZTObfDr5uhPYUcdutPbLdjpeowfVCZOFaIYaJzGkJSD8AUDg/Ud+Iu27fC3rbLufv6d6yZnj2BHL2WpQwg8z2dQ1aaGUbdlfbLNhIrtK9jzevG57d0pG1U/NzqKQy2zuF2Pzhzu+MB9+nIm+aEP5U5jSqJt6wLE7zp9JurCJgyoqBYXOlco5UpH7d16/u9ReJdPvr+7Y6sE9mURQl2DueFged3e5waBOvbhk5qnZelXuJ7H1D97pXaZUPnvONn1/aiHvTm3vYiBt4XV8mxNCJ8jP2WpSQgosezCWzhVY2E2Joy8+/Z6fm52DBRaqycLydhXPxGB+zeGVsN2YmmXn3zLEsFpp4s3DwIB5nE2/rZ/7t4OtGaDcRd7T2wAKg9jWJLbSyjNYeWIBLbbVXvvtGpIMisYXtxkQK755Z18SbhQOwiZc0mXYTcUdmkHErXnuk13yZxBZa2WykksosttV69tL06a/e2KrHy0wtY7sxM6nMu2fWNfGWwCbeNsj828HXjdB+Im67tUe22/ESNeK+Xih+xoe4rxXSksSqEbdw3UX4shW1y8oKddtSR8cH3cn8/dVjeNbamTlc2TMh2wJqorlURfQKFe+O3jCOyVLeOvYrVHf+RqZjz7sTugU9E8cRtg6iauOUHZ/smFXEO5XxybY0y86zSkuzlFzv3rn1LLInezAnziG74+Y1w847Fw52qaxJlTmVXeOy50OlFVj38UqtF9ftPvfAtkzu9fet4k3X9lX78uPrDDXxqsyB7PhkM5VrVfYeEfY7SrcAHDaW4H7/ySrBtw1g4Q9cYJC2bMQ9JQbweOohoF3aWweyqD7074B2OV5mSlkcW5o9GXisB3Oi9unLJ8rP2BCHhnBhY+Tja9es2pe3Z3//xliMhZmxDMtkTaXdRFxCSALxZOBev0S/FiWA4ichbUW7ibjMmDFLYObJwNOePI8KBGaQofjJjJnZzL8dfN0I7SfiEhI1lElXR6uLn1wXZCVEtV4iXqeROS0BYQgACvn3OuHLVvQR2rIipKx0K9tcaqJZVfdHpEfV5KkijoWdD9kWZN3HodKsekAcnH0QXXYWDkrIbbcbNDLrFit1C3om5mqpiOt270rtz3y7etz6vnVn3ykxMP4aYGRNqlyDUnPw1t7QdllZCVrleFXWgaysGpXoL9s4q/I7QPe1KnVfq9yrXUqX2e9U6n2r1ijv3y8M0l6NuMyYRZkJMfQgHrezcLwyrmJrNzI3IftEXGU/nnoIp8RA5GPRlWVLU3mwXZaZZDboLrQRL5XSm7teBt3JvL9RPrBfY1DEJcQco7UHFuBS42wLNzITCb708SsA22WJJPdXjwERSOn3V48h2CgfhQxPEZcZM3PZSCmkcbaFG5mZSWS/2HAL22WZSWfPWjsjkdKftXY69Y3ydfs1RrQiLsUz0gzivK5aXSYlq4PrIlrifM8II6r1EoN1GpmIWy8kLohnf3ThL6u49G9oDSUxWdlIt3BlQro10SapImrKzrPKz1u1zPjmnu4ta4Zna6LYDDLb1wmxeVvqaJ3o2kjO1X3eQtsuQxpnZdefbBunijircr3JzlWcGo9jIze7bnfhg6szmd1nrbmn+vqqQ53jPYD0vUl2bajcw2TvQypt2LqvQanfAb++T1qCVjkfKmttyVhct/uAOJi5Fc9bL+HuvimxfnwUULpmZOTcf0odRa1R3i/DwyDRPLQIMVR7YPGLZ9WJtagOddbeFRvxiVlCMiGGag8sKbjowZwAMDboTuY9cTMWY45j4yyz6LPqprRdfDE+67QdMk+CD5GgR4bwuSORj+9y2WkxaB/BI8b362uUD75uhKicliVCYhYOMo+eiWg4pEUYDYpiAHo9cY0QQurwJHhK0AkiqoeWJUJiCTl37sA6ConMVLKRelEMADDtiWtxGB8zZsxilHkSPCVo+cy/HXzdCNGJuDEQekgLwnVFyMpo02bVwDh4z0gIkYm4fiHxrfzOvvMdA+Pf+sGNwKV/I1OWAGXlSBX5UEXGkxWFo5JuVYRJlTbJsPFJt6iKY+P97qf52/AcXsZdmd3u8IRsC7IJ4VRFvlaZKxU5V7dEKSt5mtiHyjybmD/ZltdV3yPWfLT1POyTNorCQW7Hw6n9zuHKntBmVa0tr+6d0vvV/TtgyfXhut0v9O/LfK3wI6v2u2gXIP37Q3cjrso9Qve6Ct4nf/5vOfi2Ae9+CoNE2ohbExLPdyy2W8ZGcmKW3GxKrMcR8QhOi0GKrsyYNcisiYt5AGNdKIoUXHShiEPVJ5verNrvfhrJfi+XzXRcZQd+FxnZbwIz/3bwdSOwEZcQQtoQ7w8f6iRUG0WgyRLqbXgukv2S1oCNuMyYMWPWhpn3hw91EqpjQEJ9GXdFsl9mWjL/dvB1I0TbiEsIaQ5xkRxJvGnjZlWSTCITcQMyFAAUvnrTO/BlK2p1VGlwNCGrSoukmqVRFTlXVrqVHbPu9kzdjce6W0Vlj1elmTbs2F7o3ze7Q9h22i2iLHLbM0Jsfv36fePB98nKubJrPCqxV+UalBXzdR+H7sbUVY+vQbOqyj6krssG8quKRK4iyIftV/Z7dZ832eOI4pr56Fd1zg/g3WNhkMgeWrC80BM3AYkZs/hnQgzVHlgsuEi7RQFgLFuaypey/dGPj1nssqiaVWvyq+n9MlPOsEzWVCjiEtJajNYeWADUvvZ+6eNXIh0UIYTogCIuM2atlY2URX3bNIDpX2y4ha3AzJgxU83828HXjUARl+iB4md8oORICGlRIntoCUg/AFAY+OtX4ctWJBbJSkkmxMBVNzO+uad7W8cP6rLX5u+RFl1VJLFVN1tWd3bvSu2fPVR90rZRhIOc2w1HWvzULbapSKMmBEzdYmWj1s7Owrn8le++gdNfvdHZ+OrzXbrnWUWYjEq6DbtmVNak7qZWEyJ4VE3aYedDdj3rllBlz4eKSK8yp7obdnX+UcmpXw3Ctw145xIGoYjLTC0TYugQbLsLCx5FFyh+xiG7mF+LU9/YAQBsBWbGjJnODMtkTYUiLlFl1AbFT0IIIc2HIi4z1WzEAcVPZsyYMWuDzL8dfN0IrSXitooMmrTjoPi5QNLOW7sRp/MTp7GQ1qEN1lVkDy0BOQgACvn3OuHLViTjfXTLXbMb/vbHtlUuo5pOu6nyhc0zFzbWyaArkaF0y4KyTbJb1gzPnig/Y69FCTPIuOtQ2rwtdbTuOBrJuSrNr6ofO9/vfpq/Dc/hZdzl7HaHu2SbaSNpAVX83rA5ffO64dktP/+enZqfQyWVcTsrJe3rT3at6RYIZUVw3c3DWq+3X9+31YF9MouiKMHG03jC2e0Od+kW1aUkz+N/tfViKnsyVZkTlY4M3vnyHzpff+9g6Fh0t+7qbgnX/YcSsnOq+z6usiZNtJNL/bw1H209D/ukjaJwkMPD1mPO4cqeLp3Xlve17vf2un0vGBVxo/znoaDAkw9k8nKQEEMLDywXIODCKl8QAMa8j15f+c+LKhNiqPbAkoKLHswJAGOD7mQ8xneZbEqsxxHxCE6LwfYTP4UYqj2wWHCRqswlc/21aNY9P5kHMJZFUVhwkYWDB/G47f2/UqNj6SycywMYS1XmhAUXqfk5bPn59yIZC7PWybx7zVgXiiK18AcROFR9shnryr8dfN0IrSLijlrlsmdVoPa11/vo9SQxWntgAVD72nt/9VikgyLLMlp7YAEuycgJXH8tyfWF4wDQ6z8/WTjAwn9GN8qV776xZCyp+blIxkJaB+9eU7eubBSBFlxXrSLijlTT6cV/6PK+Tnsfvb6anxdVNjKDjFvxHrsqC/n0s9bOpB1Hu2UjlVRmUUZO8Ppryezt/B0OgGm/LF5CzgUwYnosp796owNgunavqkKgkspEMhZmrZN595q6Ne40Z437t4OvG6F1RNxWkUFb5TjajVY/b0kX/OJ0fuI0FtI6tMm6iqxcLkzE9b7WspWJXhc2jlsTF/OZR89g7sC6TP6aTyZ0i1QmGjpHxLFxn9Sa2e0OT7yIpn0AACAASURBVIS9z0S7p6yEKtvOa6IxVXfDpLRQF7L+ktiU2bDxGHat8Xh7txCbR8Sx8eD7VFqadc/BkuvNdbtf6N+X+VrhR9Zb+Z195zsGxncBRhqyl8yB63Z/dMtdmSv+8WfWv/3mb/ddzK8d3wRoF+Rl70OybbBxWrth75M9Xt3twWHrXkWal53TsDV+QBzM3IrnrZdwd9+UWD8+Cmg9R97Xut/bHff8jI24gW1pYag61IniiwNAwltAp8R6eB8Vn+jjaLesVdZfXdag8bjf/TQ/JdZHP74VZDMdV9k/7f1uLMZS7u61vcbiyMfCrHWy02LQ9n53NHO/WCZrKq0i4hJCmkNo4/FteC7SQRFC2pNWEXGZMWPWnCy08fhl3EXJmBmz9sv828HXjdA6Ii4hpDm0ieDXFiRdqF4pJo633eY0YmLViOt9rWX6GwNX0Cra7I/4XomcG/Y+E4KZynHISrcqwrPK+GS/N+x9uteaiVZg2eNt1NwcReOxyhpSkTJl50X3etYtvy453rf2bp0T9sm0WxRlkcNP1u11bp/a2yU7L7LHpnJP1Dqn+38cerwq15Fs07Ls9aFyf1GZv9U2AHtf635vW7/1IRtxA68zY8Ys4qytG49bIMuWpvIAxtLuQitw2i1ix9l9LdvEa50pNP1449S0bDDzbwdfNwJFXEIIaXG+9PErQKAxNe22ZmMqAORefx9o8vHGqWm5naCIy4wZM2Ytnv1iwy1LGlPLIppWYBNZ8aZrm368cWpaNpj5t4OvG4EiLiGmobhHoqDdhGoTx9tucxoDIiuXCwg+AFAoXHcRvkx/E6WiEBtV06NuiU1346zusaict7BG17BMVpRTafENy968bnh2S0fWTs3PoZLKbO8UYvOHO78zHtyHiqCnci5V5HAVuTTsHOk+HypzKruGdP/hgGxLs9TPc93uN68bznzx41etf9mwva+U7R+/qUFjqglBvpH0HZznVUvuK2geXnWjcIOm5bDvVWks133ty44luA/va93v7Z70L9mIG9iOi4DEjJlaJsRQ7YHFggtU5gSAsc7CufzF/Nrox8es5bNi9gr7g03fisVYTGQmmofj1LRsKMMyWVOhiEuIOUYXH1hwqV32ynffiHRQhBCSFCjiMmNmLhuppDKL7bKeTTZ9+qs3sl2WGTNmScj828HXjUARl7QecRZdKe6FE+dzBsR/fIS0CZE9tATkIAAoOH/wGXyZsY8+lxXCVJojZWUyFTlXRXo0IRCqiL2yguiWNcOzJ8rP2GtRwgwy7jqUNm9LHa0TXV+bv0dJrFRtq7UmLuYzj57B3IF1Tv6aT7p0y3i6z6XsOVr1mNd8tPU87JM2isJBDg9bjzmHK3tCm1p1S99SWYPx6Za5dbfp6l4vuv+wIewa3Nbxg7qs0bWqu+1c9h4rOwcmzpvulnWZ4wj8jl4cCwzCRlxmrZMJMVR7YEnBRQ8WRNdBdzIe4/O+Voc6UXxxANVN6bZvl7UmLuYBjHWhKFJw0YUiDlWfjE2raNzHx4yZ4cy/HXzdCBRxSSsxWntgAVD72nt/9VikgyKNyTx6Bgi0itqIT1Nr3MdHSLtBEZdZK2UjM8i4FU90rSzk089aOym6xjSbO7BuSauoE6NW0biPjxkzw5l/O/i6ESjirpRWEfJa5TiCUHRNHnE/Z3EfXyvRqvelKGmxOY2sXC4gJQFAYe03P4Ava4poKCsvhQlhW9YMz55AriZ5bl8nxOaZCxvHg/tVkcRUJF7Z75U9Dt1tvyrHKy3ypY6OD7qT+furx/CstTNzuLJnQraNU7fgbUKO1H0cuts4pcbsut0HxMHMrXjeegl3902J9eOjQHxaqV23u/DB1ZnM7rPW3FN9fdWhzvEeQHtjr0qrt4qYrzKnWqXbyr1bzyJ7sgdz4hyyO25eM+y8c+Fgl+4xq1xvKn88oVt8l5r7X9+31YF9MouiKMHe8bQ46Ox2h0Mlcpl11T20VMTtuOdnFHEDr8cjayB5eqJe9ONrt+O4THZKDODx1EP4RFzV9qJrUrLTYtA+Ih7BlFgf+VjCsuqmtF18cQDVoc7Ix9KKmSfLj/VgTqTgYi1KOFF+hsKzQtY9vzCnWRRF7ROoH8TjqnPq3w6+bgSKuPKESp6eqJckWuU4CCEtgifL9/rvS2tRAig8r5rrC8eBgESehQMkfE4p4spnoZKnJ+rFYXyyWascBzNmzFok82T5ae9+hAoEZpCh8KyQvZ2/Y4lEXlKXyP3bwdeNQBF3JbSKkNcqx0EIaR14X9JPC85pZCJuQPoBgMIX/+O78GXKEmCYnKYijs1c2DjuazPN5K/5ZEKlxVL248tlm3jDstDv1XwcsoKe1hbVFci5uoVOFTlSRcaTXqcKkqdsg2jTBcIGrZ0q137YWHQ3N5tYV7LXlu71p/KHDVLXYAPhWXfTbVQt0pGsDdftPvfAtkzu9fet4k3X9lX78uPrViCRB8difWUKvu3FMcMgkT20YHmhJzZCkz+rtZkCSLTk2SrHwYwZs9bJasJzHMbSKlm1L2/P/v6NOveBZbKmQhGXEEIIIYmAIi4zZsyYMWPGTCbzbwdfN0K0Im6LNfUREgm8jloXnttwOC9tS2QPLW9eNzy75effs1Pzc6h0ZPDOl//Q6f135Sou/RvZitokVYQmE2Jg09skG3yEu6yEqlsMlJ0/3R/XrnLedM+BCRl5y5rh2VpZ4Awy7jqUQtuNw/Yhexy6m0ZlpVHdTbKy50ilXVZFCg5rMz0P+6SNonCQw8PWY87hyp7QNlMTcq7K9aH1nr3/x1sr6TUnrXJZVNNpfPw7v+dsfPX5Lt0SuW7p1oSwK3t/We05Klx3Eb7txffBINH885AQQ7UHFgsuUvNz2PLz79mdhXOxkZeYMYt91gbtxu2a1dpMu1AUKbjoQhGHqk+2fUOsdaaQBzBmlS8IARdW+QI2/O2P235eDGb+7eDrRojKaRmtPbAsDGLhweXKd9+IaDiEJBK2G7coYW2mNopAwttMVcm9/j4A9ApvWwCwymWgzeelnYjqoWWkksq4/qa+Sirjnv7qjWxlZcZMPmO7cYtmYW2mjnqbaeKz4k3XOgCma1KDC6CaTrf9vBjM/NvB140QnYjbgk19xomTjBansbQTzbiOWvVcJu24orxHxnmuWv13R5znPgZE14i78zvjnYVz+SvffQP/9pu/3Xcxv3b8ir23A5f+jczYx6vLSntRtV2GiXdb1gzPnkCuJmBuXyfE5m2po3UC5mvz9ygJZrLj25XaP3sItm2jCAe57d1CbB4Rx+rGshIpWHfbqooYKCtq6m7ilZ4XhXbjsGN7oX/f7A5h22m3iLLIbc8Isfn16/eNB9+nW6Bu+rV1/K+2OrBPZlEUJdg7nhYHnd3ucKjUKrtedB/Hkvc1aDOVlS1XLRSv+WjrxVT2ZKoyJyodmR3vXDfsfP29g10q4rHKfXzJ9zaYF5X1p1uQl33fkmvwrb2h61R2rmRl5NWeo3e+8W34thfG/OaeNhBxF8hfzK/FqW/swMX8WmCp5BM3ASk+WQMB0/t4d+NjOVR90u5CEZ4wKACM9bufxmOu2iCrtRtXN6XV2o2FGNpxdp+ddouw4CLtLpzLbGkqVse70swT/MeyKIraJ90+iMcTIW/W2kyrfXkj+/Uk7rFUZU74/0gibnNlel5MZN51Fvd16t8Ovm4ENuImk1AB0/t4d+NjsVGsk6oB9N6G56IYC1FjtPbAAlw6l1/6+JVIB6WKJ/jXSa1ZOADlzSV4Endv8I8kwLlqOt51xnW6DGzETWYWKmB6H+9ufCwOcnVSNYDpl3EXZdDkZSNlsfRc/mLDLYk+l57gXye1lii1hmaexL0outb+SAKcq6Zn3nUW93Xq3w6+boRoG3HJ6omTjBansURJKwh0rXouW/W4mgHnKjo498sSmYgbkH6AS09stUxZ5DPRLGhCzg2TBbeljo4PupP5+6vH8Ky1M3O4smciTAaVbbZUkYJHxLHxfvfT/G14Di/jrsxud3jCRBus7PypNGXKthGHidFhzbSx/2j76/eNZ0tT+S99/Ap+seGWzNffOzihci2EZbJtzipzsGT9uW73C/37Ml8r/Mh6K7+z73zHwPguwEhLs4qoKXsutQqirtv90S13Za74x59ZtT+S2ATolYybIGSr3Dd0C/yrHp/rdhc+uDqT2X3Wmnuqr6861Dne02DuZdeLzvG9sOG/wLcNeL8DYJDIHlqwvNATpWyUiOyUGMDjqYcAQE3A1JBNifU4gkdiMRbjmRBDtQeWYDNtdagz+vGtMCtl+/HBpm8BLXYuZzqusn/a+91YjCXuWbm71z71jR2xGEu7ZdVNabv44kAsxtIgwzJZU6GIS4g6bKYlhBADUMRlxkw9YzMtM2bM2iHzbwdfNwJFXEJ0QIFOnlYQlglpVyK+fiN7aAlIPwCWfOy1smyku71Qt0ymJL+GiIsqTZS6G3vDfp6KQKgiwJk4HzPlL5z3NdM6+Ws+6TIhKZqQc7WKqWs+2noW2ZM9mBPnkMXN6Qecdy7It63qbptWkS11S7cm1rjKutJ9vCZapGVFelkRXGVNmjhvun+XBc/H6V9PWw/icTsLByXYeBpPOLvd4S4YJNJG3JDtOMlGzJitKNPWTNuiWa1ttQdzIgUXa1HCifIzcWv8ZMaMWUjWPT+Zrz2whDT2GoMiLiHECLW2Vb+wvBYlgI2fhMSe6wvHUXtgAaJr7KWIy4wZMyNZrW3VE5VRgcAM2LbKjFkSsrfzdzilQPu5r7HXGBRxdUCxkBA5KCy3J3G6R8ZpLEkjBtdvZA8tAekHAAqVP/n38GUrEgNNtFiGZVvWDM/WPnF5Bhl3HUqhTai6BcKoGhxlxxf281TOR9g+ohJTVdZLVOtUdl50jy9McNyV2j/77epx+/vWnTglBpoiQqq0y6q0/ao0bqus8TjJoLLytawQKzsvUtL8r++LTASXPR9R3Q9k1uRHvxpA9/xk/vrCcbyV34nzHQPGG3Ep4qpkQgzVHliCTaixGB8zZjHMPhFX2Y+nHsIpMRD5WJg1N4uTfN09PxmbsSQ4y5/vGMBPe7+L8x0DwdeNQBFXDTahEkJIA+IkX19fOB6bsZDVQxFXLWMTKjNmzJg1yOIkX7+dvyM2Y0lw5t8Ovm4EiriqxEBMIgEo2hESH+J0j4zTWMiqiOxTngPSDwAUTv1qEL6sYfuorIRqQnrcljo6PuhO5u+vHsOz1s7M4cqeCVmpS1aEDDs2leNVEe9kpUKVfai0Au9K7Z89BNu2UYSD3PZuIULF6CjaJHdN3mvkvMnKgrLnV0XuCzuXsk2osgKmynlTuR/Irnvdzdcq90QT87JkzK7bvSu1P/Pt6nHr+9adfafEwPhrgPZGXKl5dt3uwgdXZzK7z1pzT/X1VYc6x3sA6WPT3b4c1e+t1f7uGfjrV+HbXvx5MEhkDy1YKvA02o6LgNQwOyUG8HjqIQBgE2qUmRBDh2DbXSjCgosuFBfF6OpQZ/TjY8asTbOafB2HsVQ3pe3iiwOxGEtCMyyTNRWKuKSVGLW9BxYAta8UowkhpEWgiMuslbIRJ9DYCIrRzJgxY6Yr828HXzcCRVzSWsRNtKMUTFTg+iGkjsgeWgKiEgAUXnn7C/BlK/oYcRXpUffHfqvsw0SLqm6BK04y44h1/Hy/+2n+NjyHl3GXs9sd7tIteYYdb9g+DoiDs5c+xj3n2nA2v379vvHgfk1Idrplad3txrqlW9lrUEWqlh2z7PW75Oe9tXfredgnbRSFgxweth5zDlf2GFnPJpqRVaRbFdlcd9NtWCZ7jlQE6igastd+8wP4thffB4OwEZdZy2VTYj2OiEdwWgxGJ0YLMVT/Me4LUnC2NBWruWIWz8xbJ2NdKIrUglSOQ9Un2d7KLOrMvx183QgUcQlpDqPBj3EH0Pulj1+JdFAkGXjrpNe/fmwUAba3kjaHIi4zZs3JRoIf4w5g+hcbbqEUzGzZzFsn0/714yDH9lZmUWf+7eDrRkiGiEsZjSSRuEnBRI643G+4fghZQmTlcgExCwAK/2SV4MsKr83f071lzfDsCeTstShhBpnt64TYvC11dDz4vjDRS7dMpvIR87olMd0toCrSsop0GzZXuptzZSVFFRkvVLK7ft94tjSV/9LHr+AXG27JfP29gxMmGjBVGlhlf56JMes+R1Lrec1HW88ie7IHc+IcsjtuXjPsvHPhYFfYHwQ0vfXZdbsPiIOZW/G89RLu7psS68dHASMCq4lzrrvFV0VCVbm3yzYZy/5RiYm27tWuF+cPPoNve/F7YZB4i7hCDJ0oP2PXPkm5B3MCwNigOxkXKYkZs8tmpWw/Ptj0LRSzV7AtOeaZNXExD2CsB3Oi9gnAJ8rPRCq/nhaD9hHxCKbEeqP7ZcasQebfDr5uhLiLuKO1BxYAta+991ePRTooQkjr4TUn9/rvN2tRAii/EhIb4i7ijswg41Y8Gc37SPHpZ62dlBmZMWOmNfOak6e9+wwqEJhBhvIrM2aX/rd/O/i6EeIv4iZVRouLzEfI5eA6rSep9xsSf3itaSEyETcgLwFA4ef/loMvW5CSLmwctyYu5jOPnsHcgXWZ/DWfTMg2Vupu1JQVs17o3ze7Q9h22i2iLHLbM0JsnrmwcTy4D91NvCY+Sl1WzpUVYsPeFybFqQiiuiVjlaZME+3GYfto3NjbVWvs3W4Lsfnw546MB9+nsjZ0i+9NX8+u21344OpMZvdZa+6pvr7qUOd4D6AkdKpcv7JN37rbkmX/mED3OZf9eSbEY63SbeXeUMHbRGuxzpbcwnUX4dtefB8MEtlDC5YXehZFoOpQJ7yPEo+/zCjEUO2BxYKLtLvQhGpNXMxXhzqjHx8zZt46rT2w+Bt7u+cn8+c7BqIfX4RZdVPa9u43kY+FWfIz7w9HQgTvQ0O4sDHy8a0iwzJZU4m7iJtERmsPLMClJlRP8iMkLoQ29l5fOB7poAhpNbw/HKHgrYm4i7jB9yUhGymLpU2onuQXh/ExY1ZAg8bet/N3cJ0yY6Yx8/5wpFUEb/928HUjxF/ETSKU+RpDGS0+cJ0SYgZea9qI7KElIP0Al57YallDKclEO6CK1DpT/sJ5nzzs5K/5pEv2OGRFKt3toypCbJiwFian7Urtnz1UfdK2UYSDnNsNZ/OIODYefF/YPOuWN00IorL70N1uHDaW0Mber+8/72vsdb7+3sEu2blXOUdxaik10SKte/2p3Otk14usSB+VbK6z5bXRHMje72X/CKTwwdWzmd1n7bmn+lAd6kxEa3Ew877W/d6miLt0O04CknSWKHnYRCbE0CHYdhcWfJ8uT/zsdz/Ne22f8RtzG2S1xl5wnTJj1tSshQRvLJM1FYq4xBSjNpYKyrfhuUgHRQghJDlQxGVmKhtxQsTPl3EXxU9mzJgxS0bm3w6+bgSKuOQSzZZkKaMRIDoZmxI4IYknsoeWgAgEAIXqP/fDl61IcJTNdAtcUX0kvG7haqEd9fFaO6prwwlt8ZX9eY1EyH730/xteA4v4y5ntzvcJSv36RYXdbZENkMglG091b3WdM/zEvnVvXPredgnbRSFgxweth5zDlf2hDaDqoiQJvbbSG4OZrobmVXO76rnr7pTqeU67H1xun5lf3+onDfdTbeyrcUqknYcRdwo/3koKPDkA1nUslH7ZEIM1R5Y/O2o1sRF7fudEutxRDyC02KQ4mebZf3up3kAY10oitSCjI1D1Sdt77+AtNx+mTFrwcy/HXzdCBRxCdCgHZUtvkQnnnTd619nNopAk5tBo9ovIUQ/FHGZFdCgHZUtvsx0Zp50Pe1fZw5yTW8GjWq/zJi1YObfDr5uBIq4ZAFKssQEUa0zrm91KDKTGBBZuVxA+gEuPbHVsoatkyrtlFEJnSrtnrJyn2wzY2h2YeO4r8U3k7/mkwlZcUzlY+dlz5uKTKbSFBw29yYaSWUlOxMNoipztWRNum534YOrM5ndZ625p/r6qkOd4z2AkhwZ9r2y+1WRWnW37sapKXhbxw/qstcq9249i+zJHsyJc8juuHnNsPPOhYNdS943f0/oz4uqFVjljydMtO7qPm/NbjK2vjIF3/bi8cIgFHGZLWa1Ft/qpjQlWWZNy2rNoNWhzrbYb9KzQXcyD2CsB3Oi9gnFJ8rPUGRuz8y/HXzdCBRxCSGENOT+6jEA6E15InPtwQUUmUkEUMRlxowZM2YNs2etnQ6A6Yq3XYHADDIUmdsz828HXzcCRVyyctpNyDNxvK2yj3anVee4HUXmuJ/LuI+vSUQm4gakHwAonP7lAHxZQylJVl7S3USpW2bUKso1EOBk5VxZmezcA9tmuzNpW1wow13Tud0SYvP80RvGg+9TkXNl5TkT8vVCU3BXrSl4uy1EaFOwSvty2D4Of+5I3T4aCXWy63nLmuHZE8jZa1HCDDLb1wmxeVvqaN0+Xpu/x4icq9L2a6IJVfY4wlp3ZWVVlTWpchyyzaphIvOu1P7Mt6vHre9bd/adEgPjrwGh9xzZc65yj1CR+qWu1TUf+RuUdzyc2u8cruzpkhWtmy7d1jc8X3Z8OuXc1J/8A3zbi8cLg1DEZSafCTHU/dd/Z4u5MoQLiLmLAsAYJkvxGJ/uzERTcIN9dM9Pat3HifIz9lqUkIKLHswJAGOeYKlnH22e1Vp3W1lW/URcZT+eeginxMCKvzdJmXd9x7ZBOeKGZ/928HUjUMQlK2FUXCh7FV2ofe21jv0qqvE0GxNNwaH7uL5wXOs+ag8sAGpfez3Bkmig1rpLWTX5eNd3bBuU273hmSIus5VkI+6azkULyvs6Xd35G63anGuiKTh0H2/n79C6jxlk3Iq3D0+onPYES137aOus1rpLWTX5mXd9x7ZBOeKGZ/928HUjUMRNMlGIWO0m5Jk43lbZR7vDOW4d4n4u4z6+JhKrRty//94O+LIVfcx5s5sAVyLKqXyv7LHJipXaRUjJ5lwVgVD254XNi1YZr0FTsMrHycvOqW6ZcVvq6PigO5m/v3oMz1o7M4creyZMnCPd50NFNFRpKA47jqVypLysqn2dahZYpcTjBnMvKwDrvidqba923e4D4mDmVjxvvYS7+6bE+vFRQEkYl517qXud63a/ed1w5osfv2r9y4btfaVs//hNgPY/UoljI25kDy1YXuiJRHJKRCbEUO2BJShWepJcU8dSa84F0BbNuSaO18Q+TokBPJ56qKn7aPesJqvGYSzM1LLTYtA+gkdiMZawrJi9wv5g07eiGAuWyZoKRdxkQrGSEEJI20ERN5kZxUpmzJgxY2Y6828HXzdCckXcNm0DXKSNRaxlafe1QQghLUrsG3HD5K9dqf2zh2DbNopwkNveLcTmEXFsPPg+FTlX5WPidYu9oY24IWKliqSouylY98+TbQqWXRsmPmJepb1VZU51N8TqPpcq10JUTdCy17TKNagyL7qvVRX5NeznqYxFd0u4iWvGxHmTlWllxywjfXtf635vm27ETZ6IK8TQIdh2F4qwFtoABYCxfvfT/JRYf/nvbbGMYiXXBjNmzJgZzrBM1lSSKOKO2t4vJeBSg6jXEkjaG64NQghpYZIo4o44IQ2iXkvgct/LrLUzrg1mzJgxa17m3w6+boRkiriUUEkjuDZaG0rWpBFcG21BZA8tATkIAAqF6y7Cl11WwOx3P83fhufwMu5ydrvDXSrSre722zg1QsrKWrrFY5X9qsq5wbWhW4gNm4NIWjsNCYSyTby6hcQlYmXl3q1nkT3ZgzlxDlncnH7AeefCwS4Tcq7KmjQh65u4fk3cT2X3K7s2ohLVo2pUV3mfzPd6X+t+b5sWcaP856GgwJMPZJeVg6bEehwRj+C0GKSEyqwu49povWzQncwDGOvBnKh9gvKJ8jO29/+uIx8fs+gyrg2jmX87+LoRkijiEkLaDK/tudffAr0WJWDhnwNIG8O10V4kUcRlxoxZm2Ve2/O01/6MCgRmkHEBjMRhfMyiy7g2jGb+7eDrRkimiEuSAcU4zoFOKFmTRnBttA2RlcsFZCMAKPzi//5N+LKGEpZKM6PKR73LCn8qY1ER1nTLryrS3pvXDc9u6cjaqfk5VFKZ7Z1CbP5w53fGg+8zISSqfK9sc2TYmjwgDs4+iC47Cwcl5LbbQmyeubBxPLgP3VKrSvtosz/afiXHseTnuW73AXEwcyuet17C3X1TYv34KKD9HqF7XZl4n+x9TffaMHH9hu13yfl13e7CB1dnMrvPWnNP9fVVhzrHewDtcyo7L7r/UEK3kL3aNR74Y5nF98EgyWvEZRb/TIih2gOLBReozAkAY52Fc/mL+bXRj89EJsRQ7YHFgous185rTVzMV4c6ox9fQrPTYtA+gkdiMRZm8cqqm9J28cWBWIylxTMskzUVirikGYwuPrDgUjPtle++EemgDDNae2ABLs1B5tEzkQ6KEEKSDEVcZs3IRiqpzGIzrWdNTZ/+6o3t1Ew7Ugpp5507sK6d5oAZM2atlfm3g68bgSLuSqFYKQfFOM4BIYRoJlaNuNV/7ocva9guq1u6lZWctqwZnj1RfsZeixJmkHHXobT58OeO1ImVzWimlRXCVKRC3S2RH975n853Fs7lr3z3DZz+6o3Oxlef79J9PmS/V0VGVpFVDw8cPd89P5m/vnAcb+fvcG6f2ttlQsZTESFlm2TDjsNEE6qK4Cjb3qoiaZtoMpa99lWuN9ljU5k/lWs/7FzKniMTTcFh15HKH09EtdaC+7W+MgXf9uL7YBCKuLKZEEMnkLPXooQUXPRgQS7tnp/Mn++g/BWWXcyvxalv7ACAtm2mPd8xgJ/2fhdo4zlgxoxZS2VYJmsqFHHlGa09sACofe29vnA80kERQggh7QJFXPlsZAYZt+KJlV774vTb+TsoVjJjxowZs3bI/NvB141AEXclUKwkhBBCIiOygJzctAAAIABJREFUh5aACAQAhfx7nfBll5U8/Znqx6Gv5HutiYv5zKNnMHdgnZO/5hNpsVK3iCbb1GqiSdGEoKcqBcucDxUhUUUY193YqyJlyrY+m2h+DctURGYT7dAq8qtuETzsfXE6l7qboGXPke5zHjansteRiliuMs+rPd7uoc/g2158HwxCEXeFWXWoE17rIsVKZsyYMWPWbhmWyZoKRVxCCCGEJAKKuMyYMWPGjBkzmcy/HXzdCBRxCSH1sPW5veD51g/ntGlE9tASEJUAoPCj4p/ClzUUi1SaX1WEOt3SmW6BVff4TIizumXksHOpIrbpljJlj01F5FNpXw5rfZ65sHE8+D6Vc6lbItd9P1BpR9W9hlSagqUE0cq9W88ie7IHc+Icsrg5/YDzzoWDXSottCr3EhPt1brlXNk5DbumVVpydf/Rhsz8XfGFSfi2F98Hg0T5z0NBgScfyKKWjZgxa69MiKHaA4u/9dmauBiP8THTmg26k3kAYz2YEym4WIsSTpSfsb3/ShD5+JKYtcGc+reDrxuBIi4hpEZo63Pm0TORDoo0h/urxwCg13++16IELPyzBlkFnNPmQxGXGTNmtf8d2vo8d2AdW59bMHvW2ukAmPbOMyoQmEHGBTASh/ElMWuDOfVvB183QrQiLmUlQhoTxfXB1uf2Im7nuxV+J8RtTluMyMrltqwZnq19avI5ZHfcvGbYeb3wf1Zx6d/ImiL8qchaumVB2fHJSry6myjj1N6q8pH1sq2TKm2muqVb//Uxg8z2dUJs3pY6Oh58n+yxSZ/LCxvHfa3Pmfw1n0yoCOOyDcW614HKGpcVXU0I3rrF9yVyrut270rtz3y7etz6vnVn3ykxMP4aoLSuVn3e1ny0tZJec9Iql0U1nd7x8S13ORtffb5LVpyVlZZVzkfYzwub0xf692W+VviR9VZ+Z9/5joHxXYCRRtxmC96//R/egW978XthkGj+eSgg/NVkJQp/zJihoRDrSX5NH0ut9bm6Kc3W5zbIPhFX2Y+nHsIpMRDZWLx7/5hVviAEXFjlC9jwtz9OrMA603GV/dPe7+J8R3Rz2qTMvx183QhROS1LhL+1KIHCHyEAGgixnuRHSMvh3ft7hbctAFjlMkCBlQSI6qElIPwtyEoU/pgxQwENhFhP8ovD+Jgx05p59/7pmmHpAqim060ksLZK5t8Ovm6E6ERcykqkRivId7rh9UHajWased5b9BPxnEYm4o6IY+P97qf52/AcXsLdfVNi/fgfl78AXPo3sqZ83LiKYKbyUfQqHycv22ioW4g10fj50S13zW5Ir7GtchnVdHp7SojNZ/f83njwfbJys5Qo10CA0z0HsucjbHxhQmzY+1TWkAmhXfZ6092gLCsAq7TLqlzTJo437BzJHpvsPUyrrO+63ece2JbJvf6+Vbzp2r5qX358HSA9B0vGd/yvts4J+2TaLYqyyO34Sf8+5/apvV2y60/3Gpf9PSN7/epeLzJr45z4pXUItm2jCAe5HQ+n9juHK3u6YJBIG3GnxHocEY9gSqwHlko+cROQmOnOhBja8Lc/tq3yBXjy3UID65lCPMYXcUYhllm7ZdW+vD37+zei2pdX+nmdhXN5AGNptygsuEi7Rew4uy+xYm8csn730/yh6pN2F4pIwUUXijhUfbI2p8ZgIy6JklGrXIZfvgPQm3v9/cgGRAhJPle++wYA9FqezF57cAHF3lVzG56DjSL8c2rD/JyyEZdZlNlINZ12/fIdgOniTddSOGXGjNmqs9NfvdEBMF31/q9QFQJlkaPYq5C9jLscBznXP6cOFufUGNE24kYF5az4QOGUENIMeG/RTwzmNDIRNyAMAUAh/14nfFlThLoD4uDsg+iys3BQQm67LcTmw587Mh58n6xQZ6KpVaVBVEUKDhtfWHurUmvs20Pj+NdyXvzFabj/+YqM9fsfTcgeh8qYTbRT6pYtVeRIE62xKhJ02HHoFopVpFuVNaRyjky0nuoWy01cC1Jz4Lrdb143nPnix69a/7Jhe18p2z9+0wrEXln5Vfb8qrSiRyXSB9fLK+IYgn9AMwoYbcSN7KEF9Q8sl9vWJxYJMVR7YLHgIouiADDWPT+Z95oLm7NfZpfPfiMN9+AgAFA4ZcaMmbasmL3C/mDTt2IxllbJpsR6HMEjwfcZo91E3NHaAwuA2tfe6wvHIx0UIYQQQpan3UTckVJAJAIw/Xb+DoqfzJgxY8aM2eUz/3bwdSO0n4gbA5Eo8SRRZE7imEl8iGr9cN0SUkdkDy0BYQ0ACrd//F/hyxqKRaoftd09P5m/vnAcb+fvaNiQqFt+lZUUZY9DtiVXVnaTlQ+3rBmerX0C8Qwy7jqUNm9LHR0Pvs+EoCc7V7Jj1t1mqtJkrLvxWHbMKi2gugVW2QZRFQFdasyVe7eeRfZkD+bEOWRxc/oB550LB7vCjk3rmNd8tNWBfTKLoijBxtN4wtntDneZEGJV1pDufegWTlX2K9ucqzJXKvfxZsu5b/zdFvi2Ae9ahUEibcQN2TYiFp3vGMBPe7+LmY6rKH6uJBNiqPbLPwUXPZgTAMYG3cl4jK9VxswsNpm3TsZ6MCdqn0Z/ovxM05tVrYmLeQBjWSw0umbh4EE8zkZXZlFn/u3g60ZoNxGXqDFa++UPoPa19/7qsUgHtQxJHDOJCd466fWvn7UoAU1uAc08egYINLpm4TR9v4TEnXYTcZmpZSMzyLgVT2SuLOTTz1o74ywyJ3HMzGKSeetk2ls3qEBgBpmmN6vOHVi3pNG1BDa6Mos8828HXzdCtCIuJbPkzUESReYkjrlVSNr6DiOq9cN1S8gSIiuXq2+mtXc8LQ46f3ThL6u49G9kRhr+VtIIqVt03bJmePYEcjVBdPs6IUIFURVJUXZ80mJl6uj4oDuZv796DM9aOzOHK3smVMRKWZlMqc1UcswqzaWya1J2ncoKhLqbS2WvmVUIrDtuXjPcUGAN+3kmmkGlrlXX7d6V2p/5dvW49X3rzr5TYmD8NUBapF+1rO+63S/078t8rfAj6638zr7zHQPju1bQ6CqbycqlKudD5Z6tu+HZhNSqex+y16ru6yO4j1O/GoRvG/DuBzBINA8tS5ppFySz6sRaVIc6a++Km4CkNxNiqPbAEhRET4l4t/OeEgN4PPUQkKAG2ySOOclZY4H10BBSRyMf30qzT8RVtrd+jO53puMq+6e93zW+X2bMLpNhmaypROW0LGmmzcKpyWftAgVR0rJEJbASQlqbqB5aljTTlpBzPfmsRtwEJN0ZBVFmLZtFJbAyY8asqZl/O/i6EaITcSmZcQ5Ia8P1TQjRTGQPLTPlL5y3Ji7mM4+ewdxTfagOdRby73UCl/6NrCmClG55SUUa3dbxg/M+QdQ5XNnTpfJx9yrNoLJjNtHGqdp4HByz7JzqFv7Cxix7HLobmXXLkbJzvyu1f/bb1eP29607cUoMNGyNlT0fusVjFZFedsxxajiVXaeya1Ll2HTL+robe3U33arcY2XnSkVklhGyT/9yAL7txX3AIJH99RCAfHWoE8UXl0inwe04CUjaMwqizFo5i0pgZcaMWdMyLJM1FTbiEkIIISQRJLYR15q4iNw3J2F9WHYu9z5mzJgxYxZtZn1YdnLfnIQ1cTHysTBTyvzbwdeNEG0j7mqh4EcIIcmA92uikUhFXNT/e1jB/uFV8GXhktjxv9p6MZU9marMCQsuXAACOFN4fzBT3ZSueSEr+hhxFSlOVoCTbRUNe5/Kx92rtCtG1Qipst+wuQr7XpXjlV1XsudSpVFYtxypImqqSNCyx2ZiDcW9HVV3u6zsOVrN9WF9WHby156aq0L0WXBRhcAscm43nM0j4th4cL9hY9Z9L4lTO69Us3SDxmjd15HMmK2vTMG3vfg+GCTKfx4KCjz5QNZIBBqtPbAA8FpO0JvZfdaW+F5mzJgxY2Yo8+7LdZ9WbaMosFAyGPn4mK04828HXzdCEkXckUpHBv5iOgDTc0/1RTooQggh9Xj35bpPq3aQAxZKBglZMckTcV134p0v/6FTe3CppDIugBuqQ51xEZWYMWPGjBkA7758wyxybmXhn4bwsPWY4zktkY8vjtnV7mfOE5U/x6A7GflYQjL/dvB1IyRTxAVa4yPvCSGkHeD9Wg5Ky8sSKxE3rBFXVpqKkzSq0k6pIkxGJb/qPjbd8qHuj2uX3a+smCor3ulu6IxK/NR9bYWNOSqh3UQmK5avpJkbgfWnsq50t1Lrbt1VkVp1N2T799sz/5lz+5nH5ipAXwqXPq9rHUqbt6WO1knLK5Fzda61wnUX4dtefB8MEmkjruR2XAQkZsyYMWPGrCnZ1wo/sgFkU952Ci56MBdHaRnLZE0liSIuIYQQ0lK8ld8JANMVT1quQOAcsgCl5TqSJ+Iyi2VmnSmg629+ButMgQ3Fq8gG3Uk8UflzXO1+xvljZjSLufjZNtn5joECgBtmkHFrDyw3px+Im7R8qY2+vt3YGMkVcUl8oDymBuePRAXXXvyIs7Qcg/US2UNLQEoCgMLAX78KX9ZQctItGqo055oYi8o+dMtkS+Zl/4+3VtJrTlrlC0IAiw3F5+6/OVPtyy82FJuSc00Ik7IfYy8lQv76vq1nkT3ZgzmRgosKgBRwZpe1L/OJuGpx/l6bv0dadI1K0lYRhVUE4LDvVREwTUieJgTq5dpvr3BPObsxEip+frjzO3XiZ6O1ptK4HXZ9qKxdFfE47LzpFtVNtEg3835gfVh2Ll57NnfpXnVpvZh8cEliIy6zeGWjVrksvGbixYbi3Ovvs6FYcv5qNwEA8CS83m9Xj3P+mDU1uxXP2wB6EyB+MotBltl91q6/V9WtF2NQxCWqjFTTadT+e533dbp407WRDShhjJxDFn75DsD09607Ix0UaX1ewt0AxU8iydxTfQjeq6JYLxRxmallrjvx8e/8nlNNr4ELgWp6zUJDcV8+HuOLe+a6EzenH3BqN4MZLDQ8nxID8RhfQrOe+c+c353+C3TPN08uNbGPZmZTYn0B8Rc/tWcUj1eXVYc6C/57VWC9GIMiLtFDnOWxJMD504cJWTAGQqI22mnttdJ5i4qI10tkDy0BiQgACoOfPwVftqKPolf52HnZj/OOSnqUbVc00YAZNpY4CbGy8xcnyS7sfKh87LwJ6dZEw67sPPvn72r3M+dwda9Sq+hy8rr1YdnJX3tKeh+671cqDcXLzV9tzLJrTeV6UxF7V3OvW+na0N2ArnLPVjkfOu8Hd+T+GL7txbmCQdiIy4wZs5bJPIG5qa2imd1nk9Jcyszw2miTDMtkTYUiLiGkZfAE5qbKpXNP9TV9H0Q/JtYGaT4Ucb2v2dIUrvnwvyNXOt0WjaRsYGXWipknMDdVLq0OdTZ9H8z0ZybWRhtk/u3g60agiAu0n5zVbsdL2g8TsmA7CaytBM9boonsoSUgwAFA4f47/l/4shWJRasWJt/au9WBfTKLorDgogoBC672RtdI2lbDxE/3TukG1mZLXc3IVARq3WtNVryTbcAM+3myx6FbZFZpv9UtKYbNn0qrqO7rXGUN6ZZzdTewyl5vKvOne72o/CGC7oZn2ePQPaerPbbCdRfh2158HwzCRlxgtPbAAgDe11ZudGUDKzNmzJgxW03m3w6+bgSKuMBICTaqnpzlfW3lRlc2sBJCCEkkkYq4/e6nuM/9M/S7nwJLJR8zYpHrTjyNJ5zag0sJudZudGUDKzNmscysMwWn629+ButMIfKxMGPWIPNvB183QnQibtxk0HaTs9rteAmJM3G7HxISU6J5aBFi6Dzsk10++XUWOffn195fLGavWJRBVVv/dMuWJqRbFckzbB+yDay6RTSV+ZN9n6ygLCsL6m6YDHtf2D7CZEHd7cu6z9FyrbEr3a/s8YbtV3erqG7pcVlx9rOi07H3H+dcoE9g4Z+oK6mM21kpbZ65sHE8+L26JU/ZuZJtLVa5flW+V+WeKCs3mxCyTdxPV3sNWl+Zgm978efBIFH989CoHZBfbRTFFz9+lTIoM2bM2iqzjv+rDaBX1LbhIlVhUyuzWGb+7eDrRojqoWXEQa5OfnWQw79s2B7RcAghJBqqd34eAKb998NKRwZgUyshS4jmocV1Jx62HnNmkUNl4Z+G8LD1mFPK9sdFNmLGjBkzM9lAtgDghkoq49YeWN758h+yqZVZHDP/dvB1I0TbiEsZlBBCFuD9kJBliVUj7pevLMKXNRTCdAuduttCTezXxPeqSMEqkp3KnMpKhbLv0z1/snKkrFQo2wqssg+VZloTIrNuIVv3/UX3daTSPCwrN4eNxUSrbVyaX1cyf1GNT/cfgcicj+6hz+DbXpwDGKTD5M4CLCf0xE1AYsaMGTNmzNo9wzJZU2EjLiGEEEISQaSNuCHbcZGNmLVYli1N4ZoP/ztypdNO1GOJKuMcMItTlj4/7Qz+/U/QWTgX+VhMZNaHZSf3zUlYExcjH4tC5t8Ovm6EaEVcQkzAtlHOAYkX7bYe2+14m0hkTktA5AOAwsbPT8KXrUieU2m/lZXdVMaiW9bSvV8VWVC2pVRWTFWR8Za0/Vbu3XoW2ZO1T7auAH0pYGxXan/mE3HVYvuyaiuwbiFR6xpa89FWB/bJ7KUG6j4L7ljhg6sz1U1p+7Lf2yCTbVqWlXNV5GaVdaX7mtH9vSZavXXPy3JrPH1+2tkI+BuA+yqpzMlOITZ/uPM7dQ3AK5GRVe5rusVo//0vVzrtfB2Y8647VCH6yiJ3MiNEaOOxbolXp7w+8Nevwre9uA8YJMp/HgoKPPlAFrVsxKw1stHaAwsApBby3m9Xj7dT+/JoNtBADaA3s/tsO80Bs5hkV/zjz9qqAdhreu/1X39pt5jU4/VvB183AkVc0uqM1D7RGkDt6/T3rTsjHZRhRmqfYg6g9nV67qm+SAdF2pN/+83fBtqoAdhreq873rLIAS16vM2GIi6zpmWxED9dd+Lm9ANO7cFlBhkXwA2nxECs5qqpmetOPI0nnNqDSwk5F8AN1aFO7fsddCfxROXPcbX7GWVfZqHZxfzaAtqoAdhrer+hLHJu7YHlJ+v2Rnq81pmC0/U3P4N1prDS7/VvB183AkVc0hziJp6xbbT5cxC3c07iTbtdk3E53oRfp5E9tASkHwAoDH7+FHxZQyFRRUqSFa50f7S9iTHLNpyqCFxS7ai/vi8ofsKCe+aFviczMx2X5NdGc6oiWof9PJWmVhMfCR+nls1Vj2XNR0HhGSngzC5r3xLhOezaUlm7cRLVdZ9L3a2ncWrS1v0HFSqt1Cot0rLzJ9u4rbudfIkU/P4hvxSMEnKuDWfz4c8dqZOCd03eG8tGXIq4zJqRhYqfXyv8iOJn62YUnpkxi3kWJgVnsSIp2L8dfN0IFHFJMwgVP9/K74x0UKSpUHgmJOaEScEl2ECCpGCKuMz0Zw3Ez/MdbSS/tltG4bmlshZpb2UWyGpScAkLUnAJNp7GEyuRgv3bwdeNQBGXNI+4iGfEHDznySfhoiaRIMHXaWQPLQFhCAAKV3yhvhF3Jc2HsvKcrHAVJm/Kyrmm5arafmXHoiILrlrOVZwr3VK17rZkE+KsCXlYt1yqsg5MtMGqNIjqbuuOSqb1Z9aHZSd/7ak5rzl68b+arUNp84g4VidqNqPdWHfTbdj7dN/vVY5Dt2it+48Ygms88Dt6cR8wSGQ1/lhe6ImNvMSMGTNm7ZB5LclZT6RGCi560LpttcxWlWGZrKlQxCWEEAIA8FqSp/1C9TlkgQSJmqS1oYh7mazf/RT3uX+GK9xTbPdkxoxZy2deS/INM8i4tQeWm9MPtGxbbe1/X+Gecu5z/wz97qeRjyXmmX87+LoRKOI2gjIaIaRdSbCouWJ4r08UkT20BIQhACik/uQf4MtWJJjJSrdhctUSCcu9c+t52Ce7Ao2uBzCaOS0GF9s9VZtVVaRbqeOo7jTSFirbZtpMgXClme72zLA5NXFsKu2jKg3PJlp8dUutspmKWK7SmKoiFMuuA93XoOw9zETD7mrunT3znzm3n3msriF2Fjm3G06oeBy2DnRfH7r/uENni7T1lSn4the/FwZhI254NmqHNLreiufZ7smMGTNmLZJ5Ld11DbH2yhpi2y3zbwdfNwJF3HBGHOSWNLq+hLsjHRQhhBB9eC3ddQ2xDnIAxePYQhE3LHPdiYetx5xZ5FDx/nMhgBumxPp4jI8ZM2ZGs575z5zfnf4LdM9PRj4WZvoyr6X7hlnkXO9ej4etx4yJxwlsHvZvB183AkXcy9FOMhohJByKmq1PFPd6rqtVEdlDS0DwAYCC8wd1H3ut3N6qW3SNgzhWG0uYdLut4wd12Wvz90jLpbpFUlnRS1YQ1T33KrJlWCYrPcZVSLzc8eqWblVEet0S73LHUWuI9YuaJeRcG87mw587UidqmmrIVmn6NiFaq9zDTIj0so24uud0uXV1OQHYxB82yJzzwnUX4dte/F4YhI24zJgxY9YgqzXE+kXNLEVNZopZ2LpKkACMZbKmQhGXEEIaUGuI9YuaJdgARU2iQNi6ogAsB0XcFssG3Uk8UflzXO1+xhZfZswUs1pDbAk5t/bA8jSeaPmG2KiyBIqpq8pq6yoqAVgh828HXzcCRdxWgmIXIc2BUn7zacf7F9fVionsoSUgAgFAwf7hVfBlTWl11N3gaEKilMqO/9XWs8ie7MGcSMFFBUAKOPNC35OZmY6rFlt8V9LkqSLjqbTLygqisvvVLTeHfe9qZDw0ab2oHK+sgG7iWgg7DlnpNirhVGWuZI9Ddk5VWnxVxPLVXFtXu585h6t75ypAXwoLH9Q4g4y7DqXNMxc2jsvsV3fzcFTrSrdgq/JHJcH3/WlmP3zbgHcuYRA24rZONlp7YAEA76Ple73GxziMjxkzZsxCs29Xj9sAer37FlJw0YO5pIip7ZT5t4OvG4Eibuswcg5Z+D9SHsC01/hICCGx5fvWnQAw7b9/nUMWoJhKAlDEbZXMdSduTj/g1B5cZpBxAdzgNT5GP74EZ60sN/e7n+I+989whXtqxcdmTVxE7puTsD4st9y8tEKWpBbfU2KhmXYGGbf2wHJz+oEkiKmxzzSvA/928HUjUMRtNSh26aWV5UCVY2vleWkFknp+eP/SS1LXwWWI7KElIP0AWNK2V+hJ/1JJRItKfIqqvTVMGjUhW8oer+6mTBUpU6rp9q29oXLzLmtf5hNxSW5+bf4eaRlPdv6a3pZcuTf02ArvD2aqm9KLx9aT/qX094bNi4pwqiIP6272TVJD9hXuKWc3RkLbVsNafFXasFXmT+U6l722dO9D9+8elT9sUGlzlpWbg/P3T1YJvm3Au85hEIq4zJg1zkLlZk8ajMP4VLLQY/OaOlf1vS0yL4nPbsXzNoDehLatMtOUeddyr+Y2Z/928HUjUMQlpDGhcrMnDSad0GPzmjpX9b0tMi+J5yXcDbBtte1p1TbnRIi4nYVzGPz7nyB9fprCHzNzWQO52ZMGox+fStbg2LymzlV9r+55iZNIGqexLJdNifUFJLNt1UjWbq27mtucF/8wYdCdDL5uhPiLuC0oEpGE0cpyoMqxNXNe4nTdx2ksK6GV1+1qSeq5VEHnOojB/EX20BIQiwCgcH6ivhF33f4fb72Yyp5MVeaEBRcuAAGc+eh/+YNMubt3UfhrRivh/9/e+cfGcR34/Tu7JLU/RFEUpUQyVcOKUZlwKqVpFNvgBbnCB8ttDf9hxI2Eg3Gw7DpykT+uCtHWlpC2yuFkJwdebLgX3OXgOD7AhuUkPcDQ6eA7wDi4cWqnFi5Wm4TcGnZiRCZjiqLE5XBpkruvf3BmPTs7az7yvX1vfnw//8jz3SXnzZs3y4H54XdU5DSVTPfj1WXbW00cm0oLraxQJyvoyUq8uuVmW6KmzlbMSDm3g3S7KdFwatHtOfmzyHbUO/PPvBner+x6lh2LrNQ65rzQMpZOa1Jl/cl+rtlqv5W93mTbg1U+n3W37uqWaVU+73U3w2/mOt8rptw/afxR6SMB/6P5M3njEncRd9y/YQHg/WYOg7t+9hqFP2bMUprlzv46Nu2olFrTk7F1Vy17qHG23Crgt8yfMeIu4o7VewoIikQA5j747BesDooQ0j0aR28AYtKOSqk1PbB1V43v5o4iLODbmL94i7hCVC7c/LDr37jU82vC30r/duuSE7PuZmluoWW2Tra7WEVM2lEptaYnY+uuWnbJ2V0NCvih+TNG/EVcgEJZ1oiB7EViQJyu+ziNhajBc6mG5fmLVSNu4+c7Ecg6Sl26pSRZWVV3667ulldZUdPE49V1N7XKtq3qbkFWaWCNmmcVOVelQVl3C6iJ76ciQau0EctKyypNvLpaT/2v1S3dqgjjutdLN9uD8TFrSEU8lv1aW9fvZtfal0v/BYFtAGzEtS4bMbOesW2VGTNmzOKZBbfDrxsh7iIuyR5sWyWEEBJJvEXc9vcxS3uW5hZaZsxSluVmq+7WH7yG3GzV+ljSkpVqM+6ByWdRrF22PpaILLgdft0IyRBxSfagLEdIvKEwrx/O6br02NpxSPABgGr/W70IZF1pIJSV+0w8rl23nCYr05poD1aR4u7s+f488s/42e++DES+T1Z0VRGeZQVHFblZZcwm1pXs8ZpYa7LSvO79yr7PVpup7Nzr+n652aq7HVgSwJADQABDjb4tE3nHGbny6JfelNmviXUa9T7d7cG6BOpSbca9DQi2Lw8tO6WJguNIN/aqzIvMcVQ/s4LAdnMsMIi1mxasL/TETUBixowZM2YASq9cLAMoei3lcADklpfZLquQ3fje+TKAYrB9uU/Esn0Z62RdhSIuIYSQDbF4+0EAmPPlAgGg0dcHsF1207x9/V1AqH152WH7cphMirhsW2WWhCxXWUHpnmnkJpe5TpnFKmsM9VcBjDb6tggBB431DJdjAAAgAElEQVS+LXjv977EdlmFrFbcWQUwuuyUhH/Dcm7HybjN6UefS5WV8OtGyJ6IS9GJJAGuU5IEKMzrJ85zGoPPpVg14oYknw213+puW9XdSmiinVJW8rTV4qu7sVL2HJl4TLzs+ZCa+y3vHlrJFyf8J5wLAA4wW704XGjc1Ndcpxs5NlmBWrZpNCrTLQGqiK4qcqTuNmzdknuc2o11z73udlkT17TKcehuzZa9fjfzhya5yWVXHLxa2opFxxOFsYCS2AZ3xOSNS9Yacdm2yiwJ2bh/wwLA+w03BguPXOE6ZcaMmZWs8MiVctm7YQHWROEymqKwMbIm4rJtlSSBMf/J5gD8f+eWvjlkdVCEkOyy9M0huCi1fC65MC8KZ0vEZdsqsyRkQlQu3Pyw69+41PNr67Sxvzce42PGjFliMl0Nu439vdUTuVPuAkqor/1qCCdyp9zMOC1WibPoRIgP1ykhRIVuiLOWP5es3bSEJCIAqM5X9iCQdeUR5CqPu9ctM6rIuboFW9k5jZo/FfHTVhOlypij9qGyXxOCo+yYTcjScRJddV+rKutA5bPElnCv8j7ZdmOV9Sy7Xky0Jau8bzNro1SbcW+7+O1gwy6WnZIoCHek8dP9b4b3IfNz4W9++ikEtgHv2odB2IjLjBkzZsyYpSzrYsMu1sm6StZEXEIIIST1pLVhN1siruWsWLuMA5PPolSbSXTDaRIbhdkuqz/bKd7HA+Jb2CUupX5O++bn3OFXz6G3ejUT+zWRDaxOuXfMPYltq9PWx5LGrEsNu8Ht8OtGyKaIa4MYNAlqIYnHkcQxx50szamtY03zHKf52OJGyoR+azctIekHAKrl51tF3I08gjwqkxUDdbfftn2/N04eWnLKE33ioybBHMTs6wdPFBaLu8rB/ZoQ/mTFT9lG4aimVt1NwSrtsi7KE0W0zr3KmDfz2HloWFe620xlz0ebdCuOHppHeWJraE4fx3hhxhluzqmqnGti3a8nHg+sTrn3zp5qkRn9FtCnP/m9Fpnxwelj2tZubnLZ7T94aakODOWBZkXDDtRGxpwXWva7kT8c0C3nyu53PUG0hpIowx155dYzLcfWqb1VZcy650X2/Ko0lst+Znf78/SH138dgW3AO+cwSNYacW1l4/4NC7AmRAEY9ESpOIxP+jiiGoVj3tQ6XkT73Md8zHHPxsPNmAAG78ZzqZvTW6ovlgEMdmgB7dp+vfU56F1jyENgAEtd36+JzPvca5nTooE5ZaYlC26HXzcCRVwzjC07pbaGU0+UShKRjcIxb2odq6HMdlm9jIWbMQHMvYT7rA6qG7zRfwQIyYwmWkC99TkXvNauotj1/ZogShCtoQyk4NhI96GIayITonJux0nXv3FZdkoCwKgnStkf3waOI6pRONZNrUJUnsJp179xqaEU/zHHPROiEmrGFABGLzvXxWN8GrP5nt1VAKMLKImIFtCu7ddbn6PXUBD+DcvhvuNd3+9Gs83ItL4gWsOaIFpDGU/hdOyOjVlkFtwOv24EirgmSYsQlcTjSOKY406W5tTWscZ5jlVl2jgfG4kt1m5aQpInAFRfXPwGAllXmkt1S10qLZYmmlBVBK6oMau0B6vIbirHGzU+FSlOpVFYd5upbmkviY24tubAVpuzrf2uJyj7Mu21D/e9Gf5a2aZb3WtDt5xrQohVaQDW3fYbHvPMO7sR2G4eGwzCRlxmzJgxY7ahzBOUi5RpM5lhnayrUMQlhBCyIaIEZcq0xARWRVy/WXVYNCWuuMhGzJgxY8asQ+YLypRpM5cFt8OvG8GeiMtGREIISTaUaYlh7Ny0OM7+1mbVtT+f7b24YzHcUmpCwjLRNCorCps4XhWZTFa6NZHJHkeUuKgixcnOlYqsaksqlBVTVd6nMvdRc6pbolQRK3XLryrHoVtoNyGR2xL4TcjcJlqfdZ/z8PtCrfXN98Egtn49FGpWXWt7ZEspM2bMmDFjFtssuB1+3Qi2blramlWvogi2lBJCCCGkE3ZuWkLNqn7bI1tKmaUt2ynexwPiW9glLrm2x8KMGTMzWak24x6YfBbF2mXrY9GcBbfDrxvBbiMuJS6SZiibE5I9eN13FWs3LSHBB0Cb5NOVhljdYpvKPmy1Osq+T0XusyXZqUjQWvfx2I8OzaM8sdV7GnIDDnIQs49jvDDjDDdl805yrqyYKttWq3LOZQVC2TZTlWsh1KRdfXn1fu1yrqx8rfuaVmmWttWarfL9ZOfehGyuIv8H35ebXHb7D15qaQpedkqiINyRV24909IU3KkVPU7nPPz9qp9ZQWC7+f1gEJs9LVFCT1xkI2bMVLPxsnfDAqw1hgIYvBvPUTZnxiylmffHJIPB675PpKopOLgdft0IbMQlpDuMuVh7qjcA/9+5l3Cf1UERQrqH98ckLU3By04JYFOwNqw24kZs25ONpmvIPfFLYGqRwiQz9UyIyoncKXcBJdThYAElAWD0snOdtfHlKiso3TON3ORyYtf4XjHlhlq0YzW+qCw3ueyW7plGrrJifSzMupt5f0wyuuysNQUvOyWc23EyTU3Bwe3w60awK+LGBYpTpFvERTZPwxpP4jEkccxEnbhc9ynE2k1LSAQCgOp8pVXE3YgEuGmBa8u7h5ac8kSfaBUmXz94orBY3NUUJm9//dFYPQ5dRbiyJb/KippxkpFl3ycrxMo24qqIi22yav3YodYGaiAPzFYvDhdkGqhlx6K73TM4f7vEJfcRjC3VgaE80GzR3oHayJ35Z1oEx5dX71daGyrCbpSUKTtm2fViqzVWt/xqog1WtiFbpaladsyyArDuz9ioY4vKZD6fc5++jMB2c3wwCEVcYNy/YQE+EiZvfO88hUlmaclCDdQAgMEkNVB7AvOgN/ZmizZiLDj6UmaSxsyM2TpZcDv8uhEo4gJjy067MPn29XdZHRQhGmlroAYwl6QGak9gngu3aCPGgqMvZSZpzITEHYq4QlTO7Tjp+jcuy86aMFkr7oyL+JSqrLd6FcOvnkPf/FxiZdDEZaEG6msoCACjSWqg9gTm0WsoiGCLdpwFR1/KtDVmCsDy2cDqlHvH3JPYtpocwVslUzje4Hb4dSNQxPWhONV9KCXaJQ1rPInHYGPMvNbkydpcJfx4rd20hCQiAKjOvLMbgazjI751y6W6Gxx1i1SyY1H5Wt0tw21jOftnh1byxYl8fcnJQUAAcIDZq185XGgM9TdlUNWmUVuym6zcZ6I1NkrojGqS1S2Cy14fsmtXVoTU3fCsIlvKzr1si+9mxrJRAdjW9WFCkF9vrfXNz7n7/vb5lgbbGkqiDHfk6U9+r2WuOknQuj87df/cihLag8e7gJLY1uF4w+dy91+dR2C7ORYYhCIuM1PZuH/DAsD7LT8GS69cTIwMyoxZEjIKwPLZrp+91tZgW0SqGmxbMl9oDx5veWPHG9wOv24EirjEFGP1nkKb8Lx4+0GrgyIkbVAAlueDz34BCDXY1lAGUjpXvtAePF4XyWrspYjLzEwmROXCzQ+7/o1LPe/JoEP98RgfM2YpyWwLwEnKVvq3VwGM1rDWYFtDGU/hdGrnyhfaF1ASXlM3TuRObeR4g9vh141AEZeYJYkiJSFJhNeaPFmbqwQfr7WblpDgAwDVS78aRiDbkMyo+9HxKu+TlQBVhE6V95mYP9l2SpW2S5X50y10yo5FZe5lZTxbTcEqmYrkriL7qsy9rTbsqDFHrXHd8rWJzzDdrdm6PyNkPw9kx6z7c1elJVdGfO9/qxeB7eZYYJAekzsLsZ7QExt5iRkzZsyYMWPWRlTWVSjiEkIIISQRUMRlltksV1lB6Z5p5CaXk93OO11D7olfAlOLWo4jNfPCzEpWqs24ByafRbF22fpYmGnPgtvh141AEZdkk4S3QjbRfRxpmRdiB64f0mViJeL++B8+h0BWfXD6mPTj2k3Ic7LCmqzEJiuERc1B1NfKzpUJQdSE8Bz1Pqn5E0cPzaM8sRVrT/duwEEOYvb1gycKi8VdzXbejTyK3oRM27aPx350qN63ZSK3/KHjAM2W4erF4ULjpr5y8GulxryB72fi2lJpAVURo1VaT2WzOB2brgbgj2tbHXNeaGlb3chnkwl5WEUYl/3slB2L7j8gkR2zzPdr/HwnAtvN7weDsBGXWRaz8bJ3wwKstUICGLzxvfNJa+cdzy0vO167cLNl2GtEjcP3Y5ahTEPbKrP4Z8Ht8OtGoIhLssiYi1JbO+/b199ldVCbYKzR1wf//5V6/855jahx+H4kQ6ShbZXEn0SIuDvF+3hAfAu7xCWKgYGsWLuMA5PPolSb4bxsJBOiciJ3yl1ACV4rpAAwWivujMf4NnAc7/3el9xG3xYIOGj0bVlrGV5rRLX//ZhlKtPQtsos/llwO/y6EeIv4lLsiobzok6CWyFb0H0caZkXYgeuH9JFrN20hCQsAKj+4oMSAllHYfJxjBdmnOGmGDjeOGKk0dVE063Uo+3rxw5dQXFiAEtOHgJ1AHlg9sHcmcJvnD3NeXl59X5piVd3S6msFKfyPtnzJjvPKmKgbkFZpWVYt/hpolla5RrU3cqqIrXqXru65VLdn3+yLbSyc6+ynmU/h1SOzURjuYmfZZudv/nKHgS2m+ODQeIu4kYKk57wFf4+cRGVTGTj/g0LAHiPoB98qHE26/PCjBkzZsy6lwW3w68bIe4ibqQw6QlfWWbsKooIPnoewNx3c0etDooQQgjpJvEWcTsIk57wFf4+2cmEqBzuO+76Ny7XUBAARi85u+MxPmbMmDHLcJbiVuDgdvh1I8RfxAUodnWC80IIIfGCfyTRVazdtIQEJACovvur3QhkGxImVVoOVTIVUU72EfOyc6Ai3eqW3VTmQKWdV6U5V/caUpkrlWZV3eOT3W/cZXgpyX31fu3CbpzFyo3sV/fa0N3CbeIze73P3W61AptYVzLzMvNOy8/o5lhgkB6TOwuxntATNwGJGTNmzJgx65h5fyRSTHkrMNbJukrcRVxCCCEkEbAVuPvEW8Rtfx8zZsyYacv2iin3dP0JDItp62NhlvwsA63Awe3w60ZIhohLCCG6oTBJugX/SKJrWLtpCQk+ANokn46ykW4RTbeYqiKxqTx2XlZS1C2nmWiJlD1eWy2gugXlqK81IW/KZiqyoAlhcr21NrA65d47e2qpDgzlgWZ1wA7URu7MP9MiTHZqltYtUepek1H7kN2v7GdJnMRtlWOTbfZVaTZXacTVLSjLHkc423fDNALbza+FQSjiMmPGLHPZLdUXywCKXps08hAYwFLahElmzLqRYZ2sq1DEJYRkjjf6jwDAXLBV+iqKAIVJQmINRVxmzJhlLpvv2V0FMHoNBeHfsBzuO54mYZIZM+VsYHXKvWPuSWxbbYrqwfeEv8YIFHEJIdmFwiQh0cRUVLd20xKSoQCg2vj5TgSyrjxG3FYLo4qwqyLyqYjHukW0qPZRE4KjyrmU/VpZaTRODZi6WkDxMefNhLSse01GfW3UWHRLtybmRWX+ZGV4Ew27Jj4nTfwhguycqsyV7Jhlmn2/g28szjjDZXxEdbxxxKiIa/PXQ2GBpz+U2ZaNmDFjxowZs8xlXrPvYLjZ18sR8bXGoIhLCCGEkCadmn293CoUcZkZzYbFNE7Xn8BeMeXaHgszZsyAUm3GPTD5LIq1y9bHojvLzVbdrT94DbnZqvWxJCnr1Ozr5Yj4WmNQxCXmiKnYRUhmSfM1meZjM0UMRXVrNy0hYQj46I7Nz5Qfr65belQRn3SLdypNqFFzoNKaKCU41o8duoLixACWnDwE6gDywOyDuTOF3zh7ys33rd4v3RCru+lWt4y3nuzmz5WtVmCV1l1ZgVV2ramM2cT5UBGjVdauynlb71rtJFtugzsy5rzQ0go83jgi3d5q4lpd75yXajPubRe/3XJsNZREGe7ItQ/3vSmzD1kJX/bzytYfDshK5DKf7Td/YhGB7eb7YBCKuMxMZeP+DQsAeE2kgw81zpYlvpYZM2aas06yJVLQCnzje+fbjq2YkmOznAW3w68bgSIuMcXYVRQRbCAFMPfd3FGrgyIkq3SSLZGCVuC3r78LCB1bDWUgBceWdSjiMjOTCVE53Hfc9W9crqEgAIxecnbHY3xdyoq1yzgw+SxKtZnUiceUqpOddZIt09AKXCvurAIYraEk/BuWp3A6FcdmOQtuh183AkVcYpYYil1dI80iYJqPLWuk+ZpM87FllFiJuP1v9SKQbUiO1C09yopPJuRDlQZgFdFQZcy6G3HjLt22jfm3D0iLx1FzoFsOV2lpbsu2vKt0bCrrT/e5lL1+VVptY71ON9BuHPX9VKTbqM8D3W21uteabLN01NfKvs/WGpJZL/tumEZgu/k+GIQiLjNm3cnSLB6n+diYMWPWOQtuh183AkVcQrpDmsXjNB8bISTGUMTVkG1bncYdc09iYDWeQuJO8T4eEN/CLnEpluNLZZZm8TjNx8bMWLZXTLmn609gWExbHwsz6Sy4HX7dCBRxVYm7kBj38aWdNIuAaT420l34uUQ2SaxEXO9fP+soOUWJVLLip9am0S3vHnJRnihi0fFbF3MQs9WLw4XGTX1NIVFV0Nu0dCuOHppHeWJraHyPY7wQeLx4dbxxxIjUpSL8yc5f1D50H9umW4E7iKkqLc0m2kdlBXQT4rtuYVK2uVSlpVT2M0xlncruV1amVRnzetLtXjHlPt04uVQHhvJA8//W7UBt5M78My1ttapyrkqLr+7zobKGZEVrlesjah/hbNen2kXcHWd+SBE39Hqcs3H/hgVYa10EMFh45EpchMTxcsT4Qo8Xj9ucMmPGLMWZJ2wPegI38hAYwBLbapORBbfDrxuBIq4aYzWUEWxdBDC39M0hq4MKMOai1Da+ODxenBCSTTxhey4ocl9FEWBbLZEg9SJuV1s7hag8hdOuf+NSQ0kAGG3s742HNCVE5UTulLuAErzGSwFgNPR48biJXsoZxWNmcc/65ufc4VfPobd61fpYTGeesD16DQXh37Ac7jvOtloNWW5y2S3dM41cZaVb+whuh183QrpFXFOyV9yFxLiPTycU/Ejc4RpdI0ufSybIyLqydtMSkpIAoDpf2YNA1pVG0h8O/XHhWs9HrZ0beey8icfJJ1GO1N0+Kns+ZJtao8RoW02jKiK4inQre2wq61722HQ3yapcg6YbhXOTy27/wUtLAhhysPYr23q+IHrrtZHJI19tkVBVW6llJWPZ76cisMoK8rrPuYn1p/JzQeV4g585A6tT7r2zp5YacIb8P7pYQElsgzty7cN9b4a/drPn1/u35ed2z/2vUcQNvb7ZLLK185bqi5RQ05tFnvMYidHMMp55a3HQ8bZzEMjXKaEyU8u8n2uDwT+6KGOxG+squB1+3QhpFnEjWzvf6D9idVCkq0Se8xiJ0STjeGtxLijH13sKACVUooD3c61lXbkoASlcV+kVcTu0ds73sLUztVmHcx4bMZpZ5jNvLY7W8wXh37BcuPlhSqjMlDLv59roAkrC+6MLnMid6sa6Cm6HXzdCukVcgLJXFuE5J3GHa5R0gwysK2s3LSERCPjojs3PqrnPTypJj7KNpCryoYrEJrtflSZFlUfM626SlRUDbTW/qsi5Jsascs5lheJNS9AdZF8TkqxuoV13E6rKfmVbXk3ItFHfT/ec6v5jB5XjVRHLVc6vyhqS/cOLzV5H3r8tP7cH+t4xKuL2mNxZiPWEnq7LS8yYMWPGjBmzDWVYJ+sqaRZxCSGEEJIi0iviMmPGLDnZ1KKbe+KXwHTN/lhinpVqM+6ByWdRrF22PhZmmcuC2+HXjZB+EZcQEm8y0uSpBc4VyTjWnJaQ4AMA1fLzrY24nURNWTlXdwOriiyoW4TUPRYTj2tXESt1z7NuuU9FnpOVtGWPTVbGU7k+tK2NqUW3B2g2xApgSBR6J3KOM9L46f43w1+rW1bVvQ50X0dtbbpAsPV0aNkpTRQcZ+SVW8+0zJXq8eqWw2Wvc9l1b+JzSKU5XFaCjpPUL/P9+t/qRWC7+T4YhCIuM2bMrGW5s78uAyj6DbEOAHy4wobYiMxr0y0GW0/7RFdaT5kx+7gM62RdhSIuIcQajaM3AMCc/0tqAUBs6QNS2OSpSlSb7rKTztZTQjpBEZcZs4xk21anccfckxhYnXJtj6WZ7C5WAYyKQq8QDiAKfZj/g3+p3OSZRlnVb9NddkrCv2E5t+Mk23SZmcyC2+HXjUARl5AsEHeBU2eTZ9yPVZUMtJ4S0glrNy1RIm5I8lFuKY2SplRaInXLoCoipG7ZzUTjrO6vlV0Hsg2xJuQ+3etFqgm6fuzQFRQn/Cdg1wHkgdnHMV6YcYbLwa9VWZO620w3M1el2ox728VvB2VVLDslURDuyLUP970Z/n6615pKo7AJud6GvLmR96nI5ir71b0P2fZv3X+M0e1G5m37pxDYbu4XBrH566GwwNMfymzLRsyYpSUb929YACC/lg/ejefKEl+bqOzG986XAQxSVmXGrCtZcDv8uhEo4hKSfsb8J18D8P+dewn3WR1UN3j7+rsAyqqEpBarIm6usoLSPdPIVVaAdsknbgKSclasXcaByWdRqs3ER4TUlDXP5eRyYo6tt3oVw6+eQ9/8XGLGvKlMiMrhvuOuf+NyDQUBYPSyc108xqcxqxV3VkFZNfXZLnHJfUB8CzvF+4neR5yy3OSyG/p5HPW+4Hb4dSPYE3HTLsuFSfPxJvHYkjhmVbIkcGbpWLOGiWs3a58PCTpeOzctjrPfRXmiiEXHl+VqKInGxe2LjZv6mmKgqlxqQoCL+to2mfG3DxyaR3lia+B4cxCRIqQJ8VOnmLXjsR8dWskXJ/L1JScHAQHAAWarF4cL4XMpK47pFmLbhDpxVPp8mJCMVaQ9WTk37o3RKtebbqlaRfTXPae65XpZ+VX3NairmXZgdcq9d/ZUi2i9gJLYBnfk6U9+r0W0fnD6mPR1KbuPKJlb9nzIrhfZBmpdEnnf/Jy772+fb7ZSN+Cgni+I3nptZPLIV1uOd9enphHYbn4/GMTWr4fG/RuWtUEIFLHoeI2PPnETkFSy8XLoeJEeEXLcv2EBvEZTYDDm5zLN54MZs9Rmt1RfbBOty9ArWpvYR5yyXT97rQxg0G+lzkEgX1/qdLzB7fD3NoKtm5axGsoIynI1lP3GxzQy5qLUcrxIjwg5Vu8ptB1bzM9lms8HIanljf4jQEi0dqFXtDaxjzjxwWe/AISOt95TAGJ6vHZuWoSoPIXTrn/jUkMZT+G06zU++sROVNp0JkTlRO6Uu4AS6t7/akRaREghKhduftj1b1zq+TXJM9bnMs3ngxmzDWZ983Pu8Kvn0Fu9an0s62XzPburAEYXUBLetYsTuVNaRWsT+/D/e2B1yr1j7klsW53W8v02k630b68CGK3nC8K/Yblw88Odjje4Hf7eRrDbiJs1WS7Nx5vEY0vimAnRSYIEzBZMXLvd3kfc5j4hn4fWblpCAhIAVO99748QyJQbF3U3BsZd2rPVdqnyaHYVOVe3rKrSRiw7FltybtT7dAvtKteRyvtkx6fSJKtbgtbdfL2Z85GbXHb7D16KFDBVhFMT4rbsPKusU9lG3M1cl1Gybw0lUe4gFJv4wwuZ8xaHRtwekzsLsZ7QExtRiRkzZszSlnmyfDEoYKKzgBmLMacl82TfYviPUZCMucc6WVdhIy4hhGQQT5ZPjICZJqJk3xrKAOd+Xaw24kZsWxe9mDFjxmyzWZJaVD1ZXlbAjMWY05L5sm8Na83N/h+jJGDug9vh141gV8QlhJC0EDexUpaECJiphHO/YazdtIREIACo9r/Vi0Cm3KRoQl6Kel9UJitr6RbWVBqAu9nC6M+BieZSWbGym02eQGdRU/ccmGg9NSFkq8jmKteg7Hm7s+f7zWyvmHKfbpxcqgNDeaD5jKcdqI3cmX+mRax8efV+JVlV9+eBynmTnVMVyVhFcpf9/FP5ow0T0rzuP7zYrDSf+/RlBLabXwuDUMRlxowZM8XsocbZMoBi3tvOQ2AAlFqZpTLDOllXoYhLCCGKfDd3FADm6p5YWYeDqygCFCsJ0QpFXGbMmDFTzC45a2LlNRSEf8NyuO94EsRKZsw2kgW3w68bgSIuIYTogmIlIV0lViJu+fk9CGQbEg11N2DqFrNUHjtvopnWRIulrGBmS0w1Id0G5U1gTcqUlXNNNBmbaIyWXQe6G4Vl173s16qcX91t2CprN2ofsutZ5dhUpH6VP4qQbX3WLa+rSMu6z/lmf75VP7OCwHbzfTAIRVxmzJgxY8aMmWyGdbKuQhGXEEIIIYmAIu4Gs1xlBaV7ppGbXHZtjyWJWbF2GQcmn0WpNpPZ+RsW0zhdfwJ7xVRm5yCc5War7tYfvIbcbNX6WJhFZ0lq+2XWtSy4HX7dCBRxN0JSGy/jAuePcxAF5yT+8ByRmGDtpiUk+ABAtfHznQhkHeUllXbAKPFJSqirHzt0BcWJASw5eQjUAeSB2erF4ULjpr5ycB+6G3ZNyIcqQpjUPL9x8tCSU57oE4uO/yj2HMTs6pl/XsCeUnP+VBuPZeU03Q2TUZnsGnowd6bwG2dPcw5eXr1fu2gtKymaaFENipoDq1PuvbOnlhpwhvx1UUNJlOGOPP3J77U0yT44fczI9RH1tSrNuSrSre5GXFlB1Fbbb5w+/1SafXV/NukWrTf7By4XvvgQAtvNeYFBbP56KCzw9Icy27JROBv3f9gAgNd8Oeg93j0O44t7Nu7fsABrj2IHMJg7++sszV/kGvLaVOMwPuPZLdUXywAGg+uiiEU2ycYo89bnINt+mYW2w68bgSKuPGNXUUSw8RLAnPd4d7I+Y8tOCcFHsQOYaxy9weaYTBO5hrw21UzyRv8RAJgLrosaygCbZGMD235JnKCIK5sJUTncd9z1fyPJauEAAAxhSURBVOhcQ0EAGPUe725/fBqyrkqyQlTO7Tjp+jcuy05JABjF7mKs5qCrWYc15LWp2h+fhWy+Z61JtoaS8G9YnsJpNsnGKGPbL7PAfwe3w68bgSLuRklr46Up0S6t87cROAftcE7iD88RiQHWblpCwhoAVG/+xCIC2YYe0627qVVFspNtG1R5lLqK/CUryb5+8ERhsbirKYiqPiJdpRVTdyuwypyqvC+qMdWEaKj7mlFpoJa93lQalHWvA9nGXpXj0H0+VBqKVdp+TcjDsud3Pcn4445DRaA28RkW9b6o44ga82bPxzcKjyGw3dwHDEIRl1k/OkiyN753PrOCKDNmzJgxa8uC2+HXjUARlwAdJNm3r7/L6qAIIYSQIFZF3J3ifYQaFuMiG2Ur6yDJ1oo74zE+ZsyYMdOU7RVT7un6ExgW09bHojsz0Foc9XPbKPZEXDYsxg+KdoSQNJPmnzsmji0G82fnKc+Os38e5YmtaIqfQwsoTXzH+dPFGWe4KX6aesy5SkOs7sZelYZEle/3ym2PzePWM372u7cDsRI6VR71riK2ycqMcW/31NmKqTpmlfOrW3BUOTaVc6SyXmT/SEBXa/HH7SPqfSrty7Ljk10bbe3LQLDZd+gaChM7HCey2TfqeFU+E3XL4VHHFmiWHlpAaWKb44xMHvlqy7Ft9mdUbnLZFSiXwj+3tznOiMkbF1u/Hhovo1X8LGPRuRvPUfxkxowZM2baM799OY3NvlHN0mXNzdKFR66Uo35ue/swhq2bljEXreKnixJewn2WhkMIISTN+O3LaWz2jWqWdlECNB7b0jeHEPVzW+c+ZLBz0yJE5UTulLuAEupwsIASTuROuZed62IjNFnNfr0M52uXgF8t62+mZZaJrKvtxsxilxkQMBOf+e3LaWz29Y9tASUR/Jmq89ga+3urUT+3TTstdhtxKX62EwPRiSQcrqFswfO9MdL8c8fEsVmev1g14t51yzsIZMoCoW7BUUXKlGoLPftnkc207/7r3y8sbxtsCsqq8pes9KiyDxXpcUPycGi9yIrbuiVA3eLdpqXbDbQb6xZEZdeaiqipsg8T61T3WGTkyP6Dl4ICJhZQEtvgjlz7cN+bMvvQ3VpsS7iXlYdlW6ll/5BD97Wv+3hlW9tl1nj1MysIbDePDQZhI268sshm2l0/e42CMjPZjO3GGcoKj1zpuoDJjFngv4Pb4deNwEbceBHZTPvBZ79gdVAkUbDdOEMsfXMI6LKASUicsNqIG7FtXWiymnVopl3p3x6P8SUk27Y6jTvmnsTA6lT2JFS2G2cqa+zvraLLAiYzZoH/Dm6HXzeCXRGXRJNmUazbUEpcg2soW/B8k4xg7aYlJBYBQHX4hksIZF0RA1UETBOPFpeVv3SPRXfLq6ygpyKhtglmv33gkIvyRBGtEmr14nChcVNfObhfFelWRYoz0Tgr+yh6lXVl67pUkchttelGvS9KBo1T07LstWBizCrXpW7RWvcaUjleqc/EDrL0Zs9l7tOXEdhujhkGoYjLLE3ZeBHtEqonK8ZhfMyYMWOW5Cy4HX7dCBRxSZoYq6HcJqF6siIhhJCEQxGXmdEsV1lB6Z5p5Ca70PYrROUpnHb9G5ca1iRUT1bs+rExYyab7RVT7un6ExgW09bHwozZBrLgdvh1I1DEJeYwJclSSiRxhrI4IZvG2k1LSPABgOp8ZQ8CWVdk0ChRSbdoqPK4cd3Cruwj3HWPRUWSVRFO49REGSVbysq5uptztTY3WxQwo85HVKYyf7ol4+A62Cum3KcbJ5fqwFAeaw/tu4aC2IHayOSRr7Y02HaaZ93iZ9TxqmSy41P5LFYRrU38IYIJ4V72+tDZfO392/Jzm4247a8zS0dGSZZZ5rOHGmfLAAbz3nYeAgNYYoMts6Rkwe3w60agiEtMQUmWZJ7v5o4CwFzduw78pw2DDbaESJFJETfTjakfk1GS1Z8Ni2mcrj+BvSI5a62r6yDj2SVndxXA6DUUhH/DcrjveFcabHOTy27pnmnkKitGjo1Z+rKINRR8T/hrjJA9EZcSXDSUZPWTxLWWxDEnkW5fBzyPRJWYriFrNy0h6QcAqo2f70Qg25D4JCVRvnEyUgZ9/eCJwmJxV1MGVX28ukqrrYr8tWmRdAOSrKxMpiJB654X3U28Ue9rk27rxw5dQXFiAEtOHgJ1AHlg9sHcmcJvnD3NOe2GnLvpudryrvT1YULOlW38VJGgTRxH1H5VmkvXE6hzk8tu/8FLSw04Q/55XEBJbIM7cu3DfS2ybzcaYmWlW5V1b+s6V1kHsvKw7mPbjMzdaQ19B99YnHGGgx5idbxxhCJu6HWdWaQMeuN757Mug1KS1Z+N+zcsAOCJl4OeiBmH8UWOmddH8jPvuh0MnscyFin7MpPOOq2hu/Fc8LMg/H2MkDURN1IGffv6u6wOKgZQktXP2FUUERQuAcx5ImZc4fWRArzrdi54Hl2UAMq+RJJOa+gl3Gd1XEDWRNwOMmituNOe+PTrZThfuwT8yqL0mFFJtquZEJXDfcdd/8blGgoCwKgnYtofX4cxx+762GSW5cZZ77odXUBJ1Nf+tz5O5E51RfbVndmSh/vm59zhV8+ht3rV6H6jsjgI1J3W0GXnuuDXhb+PEXpM79Cn+pmVtqwfl9d93wCA8vN7Wt90P7DrU9NtX+s9NbqFrzz6k8bK7Ch6X7mIldsPLuIvXq588fu/E3rXo+h/q7c1+nzUUSDyffX/9i9aszPAzDu7W6IdAH767+7Kfe4Xf4786hLq/+u3pdyXnf3zj36p7X2e69Mk9/no/e7+q/Nt44uag3se+JtQcgz//sP/3mhUtqP+n+toPL59EQcWKv3/+6a2fUTN/dW/PtA65s9H73ffDe3nKGquos6593TRFqLeJ0vU99t+z/9py6LW1bb9U23ZP2sU27Kd9YON/5j7Or7SeAF/mTuy+HT90cqXdz/T9r67bnmnLZv+g3/Tsj3wgvy8yM7VhS8+1JLd/jqw65ODjfOrJ3Fr9Sx+2v/lxXsvn6zgtsfavp8ssmOJWs9Rcx+1hv5vrtYaOM7+P0GxNIAl/NPG/8PhvuO5yPcB+PE/fK5l+yZEn1/Z44haz1/58v8MfeWRyOOIumZu/Ff/2Ja5v986vgEAP7z+6y3Zg+JY5TvOny7ejefKL+E+bBfXNRD1vunodRB1bLJrKGofv/ig1HYcUedtBcVSCUtYPn8Fh/uO5y58KL9eoj6bvMLSJjsQ8VnsOPv35oulfH0J/2Tmf+DCzQ/nAP3rIOpnQNtc/dbZL1Au5bAEcX4OX8udyj1db9tlx7FEjTnqc03qXIpHK3+388ziLdUXy2/0H8Foz+7GvhvaP6tMkzWnBQD6G0P9WPi3v4PGUH/5497X1cxx9n/uF39ezq8uIQeBfH3JAfCT3GzV2u8zG/t7sfjXuxGQb63/bjXp2SVnN/5r/j8gIN/GanxR2XzPbvz94B/iWk9yxuwHw2K6H8BPfJ9oO2r4u+W/KHt/CWF9fCazGWe4/D3nP+Gyc531sayX2TpvvdWr/QB+kq8vOTkI5FeX8Llf/LmV9bJtdW0OtmLRyUNgKxbx7cYfW12713r2lP9+8A8x37Pbz4LvCX+NEbLmtMSJcf+GBfhIeiy9ctHqoAhJMl9pvAAAgx8J0Gs/ALEmoZKYYuu8feIffwyEhNP86lLX9xvFrdWzbWMpY9HKWOIMb1rsMVbvKbRJj4u3H7Q6KEKSDBtnk4mt8/bBZ78AhITTek+h6/uN4o3+I21joUDdTrZE3DhlQlQu3Pyw69+41PNromZjqD8e42PGLIGZycZZZvoyW+dtpX97FcBoPV8Q/g3LhZsftrJe5nvW5iDmAnVwO/y6EbLXiBs3stQQS4gpeF0lE1vnLU7rJU5jiSG8aSGEEEJIIqDTQgghhJBEwJsWQgghhCQC3rQQQgghJBHwpoUQQgghiYA3LYQQQghJBLxpIYQQQkgi4E0LIYQQQhIBb1oIIYQQkgh400IIIYSQRMCbFkIIIYQkAt60EEIIISQR8KaFEEIIIYmANy2EEEIISQS8aSGEEEJIIuBNCyGEEEISAW9aCCGEEJIIeNNCCCGEkETAmxZCCCGEJALetBBCCCEkEfCmhRBCCCGJgDcthBBCCEkEvGkhhBBCSCLgTQshhBBCEgFvWgghhBCSCHjTQgghhJBEwJsWQgghhCQC3rQQQgghJBH8f5XDVb2zfQy1AAAAAElFTkSuQmCC\n", "text/plain": [ "
" ] @@ -1622,7 +1388,7 @@ } ], "source": [ - "show(in9, 'kD')" + "show_heights(in9, low='rD')" ] }, { @@ -1640,33 +1406,28 @@ "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" + "- **Input**: Each item in the input is a string of opening and closing brackets: `[({<` and `>})]`.\n" ] }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 36, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "----------------------------------------------------------------------------------------------------\n", - "AOC2021/input10.txt ➜ 10196 chars, 102 lines; first 7 lines:\n", - "----------------------------------------------------------------------------------------------------\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 102 lines:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "[(([{<{(<{{[({{}{}}{[]()})<{{}()}>]}}(([{{{}[]}[[]()]}[<{}[]]{()()}]](({{}{}}{{}()}))){[{({}())[[\n", "<(({[<([{({[{{<>()}}[{<>()}({}{})]]<{<()<>>{[]()}}(((){}>[[][]])>}([{<[]{}>(<>[])}]))<[[[[[][]\n", - "(<<(<{{{{<<<[(()<>){()<>}][[()()]]>{<{[]{}}<<>()>>}>{(<{<>}([]{})><(<> ... []{})(()()))<<()[]>{{}[]}\n", + "(<<(<{{{{<<<[(()<>){()<>}][[()()]]>{<{[]{}}<<>()>>}>{(<{<>}([]{})><(<>())<(){}>>)<(([]{})(()())) ...\n", "[[[[<[{[(<{{{({}<>)((){})}((()())[()()])}}><[([((){})]<[()[]]{{}<>}>)[[{[]<>}][([]{})[{}()]]]]>)<{(<\n", "[<(<[[((<{((<<<>[]>><<<>{}>>){<[{}<>][<>[]]><<<>()>[(){}]>})[<{[{}<>][(){}]}<[[]<>][{}[]])>{([<>[]][\n", "(([[[[<([[{([{<>()}{()<>}][((){})]){[{[]<>}({}<>)][(<><>)[()[]]]}}<{{({}{}){[]{}}}<{<><>}({}{})>}>\n", - "{{{[<(<([<{({{[]()}[{}()]}{<()<>>(()<>)})}><<[{<()()>(()[])}<<<>[]]>][<{()}{<><>}>({{}[]})]>>](\n", - "----------------------------------------------------------------------------------------------------\n", - "parse(10) ➜ 102 entries:\n", - "----------------------------------------------------------------------------------------------------\n", - "('[(([{<{(<{{[({{}{}}{[]()})<{{}()}>]}}(([{{{}[]}[[]()]}[<{}[]]{()()}] ... []}>][<({}<>)>]]>)[[[((')\n", - "----------------------------------------------------------------------------------------------------\n" + "...\n" ] } ], @@ -1678,28 +1439,24 @@ "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", + "Ideally, the brackets are balanced, but items might be *corrupted* (an extra closing bracket of the wrong kind appears in the wrong place) or *incomplete* (one or more closing brackets are missing from the end).\n", " \n", - "- **Part 1**: Find the first illegal character in each corrupted line of the navigation subsystem. What is the total syntax error score for those errors?\n", - "\n", + "- **Part 1**: Find the first illegal character in each corrupted line of the navigation subsystem. **What is the total syntax error score for those errors?**\n", "\n", "The instructions for Part 1 say *Some of the lines aren't corrupted, just incomplete; you can ignore these lines for now.* That suggests we will not ignore the incomplete lines in Part 2. So I'll define `analyze_syntax` to return a tuple of two values: an error score for use in Part 1, and the missing characters for an incomplete line, which I suspect will be used in Part 2." ] }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 37, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 42, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.001 seconds for correct answer: 367,059\n" + ] } ], "source": [ @@ -1718,32 +1475,29 @@ " 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)" + "answer(10.1, 367059, lambda: sum(analyze_syntax(line)[0] for line in in10))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: Find the completion string for each incomplete line, score the completion strings, and sort the scores. What is the middle score?\n", + "- **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": 43, + "execution_count": 38, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 43, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.001 seconds for correct answer: 1,952,146,692\n" + ] } ], "source": [ @@ -1759,7 +1513,7 @@ " if e == 0)\n", " return median(scores)\n", "\n", - "answer(10.2, median_completion_score(in10), 1_952_146_692)" + "answer(10.2, 1_952_146_692, lambda: median_completion_score(in10))" ] }, { @@ -1773,58 +1527,60 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 39, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "----------------------------------------------------------------------------------------------------\n", - "AOC2021/input11.txt ➜ 110 chars, 10 lines; first 7 lines:\n", - "----------------------------------------------------------------------------------------------------\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 10 lines:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "1224346384\n", "5621128587\n", "6388426546\n", "1556247756\n", "1451811573\n", "1832388122\n", - "2748545647\n", - "----------------------------------------------------------------------------------------------------\n", - "parse(11) ➜ 10 entries:\n", - "----------------------------------------------------------------------------------------------------\n", - "((1, 2, 2, 4, 3, 4, 6, 3, 8, 4), (5, 6, 2, 1, 1, 2, 8, 5, 8, 7), (6, 3 ... 2, 4, 8, 7, 6, 6, 2, 7))\n", - "----------------------------------------------------------------------------------------------------\n" + "...\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Parsed representation ➜ 10 tuples:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "(1, 2, 2, 4, 3, 4, 6, 3, 8, 4)\n", + "(5, 6, 2, 1, 1, 2, 8, 5, 8, 7)\n", + "(6, 3, 8, 8, 4, 2, 6, 5, 4, 6)\n", + "(1, 5, 5, 6, 2, 4, 7, 7, 5, 6)\n", + "(1, 4, 5, 1, 8, 1, 1, 5, 7, 3)\n", + "(1, 8, 3, 2, 3, 8, 8, 1, 2, 2)\n", + "...\n" ] } ], "source": [ - "in11 = Grid(rows=parse(11, digits), neighbors=neighbors8)" + "in11 = Grid(parse(11, digits), directions8)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 1**: Given the starting energy levels of the dumbo octopuses in your cavern, simulate 100 steps. How many total flashes are there after 100 steps?\n", + "- **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": 45, + "execution_count": 40, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 45, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.008 seconds for correct answer: 1,591\n" + ] } ], "source": [ @@ -1851,32 +1607,29 @@ " grid[p2] += 1\n", " check_flash(grid, p2, flashers)\n", " \n", - "answer(11.1, simulate_flashes(in11, 100), 1591)" + "answer(11.1, 1591, lambda: simulate_flashes(in11, 100))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: If you can calculate the exact moments when the octopuses will all flash simultaneously, you should be able to navigate through the cavern. What is the first step during which all octopuses flash?\n", + "- **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": 46, + "execution_count": 41, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 46, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.027 seconds for correct answer: 314\n" + ] } ], "source": [ @@ -1894,7 +1647,7 @@ " if len(flashers) == len(grid):\n", " return step\n", " \n", - "answer(11.2, simulate_flashes2(in11), 314)" + "answer(11.2, 314, lambda: simulate_flashes2(in11))" ] }, { @@ -1910,33 +1663,38 @@ "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." + "- **Input**: Each item in the input is a connection between two caves. Big caves are written in uppercase, small caves in lowercase. `start` and `end` are two special caves with the obvious meaning." ] }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 42, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "----------------------------------------------------------------------------------------------------\n", - "AOC2021/input12.txt ➜ 144 chars, 22 lines; first 7 lines:\n", - "----------------------------------------------------------------------------------------------------\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 22 lines:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "xx-xh\n", "vx-qc\n", "cu-wf\n", "ny-LO\n", "cu-DR\n", "start-xx\n", - "LO-vx\n", - "----------------------------------------------------------------------------------------------------\n", - "parse(12) ➜ 22 entries:\n", - "----------------------------------------------------------------------------------------------------\n", - "(['xx', 'xh'], ['vx', 'qc'], ['cu', 'wf'], ['ny', 'LO'], ['cu', 'DR'], ... xh', 'DR'], ['cu', 'xh'])\n", - "----------------------------------------------------------------------------------------------------\n" + "...\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Parsed representation ➜ 22 tuples:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "('xx', 'xh')\n", + "('vx', 'qc')\n", + "('cu', 'wf')\n", + "('ny', 'LO')\n", + "('cu', 'DR')\n", + "('start', 'xx')\n", + "...\n" ] } ], @@ -1948,7 +1706,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 1**: How many paths through this cave system are there that visit small caves at most once?\n", + "- **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", @@ -1958,18 +1716,15 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 43, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 48, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.013 seconds for correct answer: 4,167\n" + ] } ], "source": [ @@ -1987,36 +1742,33 @@ "\n", "neighbors = multimap(in12, symmetric=True)\n", " \n", - "answer(12.1, quantify(search_paths(['start'], neighbors)), 4167)" + "answer(12.1, 4167, lambda: quantify(search_paths(['start'], neighbors)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: After reviewing the available paths, you realize you might have time to visit a single small cave twice. However, the caves named `start` and `end` can only be visited exactly once each. Given these new rules, how many paths through this cave system are there?\n", + "- **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." + "At first I felt bad that I would again have to copy/paste/edit the code for Part 1. I felt better when I realized that the revised function `search_paths2` would have need to call the original `search_paths`: once a path returns to a small cave for the second time, the remainder of the search should be under the `search_paths` rules, not the `search_paths2` rules." ] }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 44, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 49, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.317 seconds for correct answer: 98,441\n" + ] } ], "source": [ - "def search_paths2(path, neighbors):\n", + "def search_paths2(path, neighbors) -> Iterable[Path]:\n", " \"\"\"Find all paths that start with `path` and lead to 'end' using `neighbors`.\n", " Small caves can only be visited once, except one of them may be visited twice.\"\"\"\n", " if path[-1] == 'end':\n", @@ -2028,7 +1780,7 @@ " 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)" + "answer(12.2, 98441, lambda: quantify(search_paths2(['start'], neighbors)))" ] }, { @@ -2039,45 +1791,70 @@ "\n", "- **Input**: The input is a set of dots, e.g. \"`6,10`\", followed by an ordered list of fold instructions, e.g. \"`fold along y=7`\".\n", "\n", - "My `parse` command is not set up to parse two different sections, so I'll ask `parse` only to parse each line into a tuple of atoms. Then I'll further process the entries to get two variables:\n", + "My `parse` command is not set up to parse two different sections, so I'll ask `parse` only to parse each line into a tuple of atoms. Then I'll further process the items to get two variables:\n", "- `dots`: a set of `(x, y)` points, such as `(6, 10)`. \n", "- `folds`: a list of fold instructions such as `('fold', 'along', 'y', 7)`." ] }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 45, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "----------------------------------------------------------------------------------------------------\n", - "AOC2021/input13.txt ➜ 6424 chars, 789 lines; first 7 lines:\n", - "----------------------------------------------------------------------------------------------------\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 789 lines:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "103,224\n", "624,491\n", "808,688\n", "1076,130\n", "700,26\n", "55,794\n", - "119,724\n", - "----------------------------------------------------------------------------------------------------\n", - "parse(13) ➜ 789 entries:\n", - "----------------------------------------------------------------------------------------------------\n", - "((103, 224), (624, 491), (808, 688), (1076, 130), (700, 26), (55, 794) ... 'fold', 'along', 'y', 6))\n", - "----------------------------------------------------------------------------------------------------\n" + "...\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Parsed representation ➜ 789 tuples:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "(103, 224)\n", + "(624, 491)\n", + "(808, 688)\n", + "(1076, 130)\n", + "(700, 26)\n", + "(55, 794)\n", + "...\n" ] } ], "source": [ - "in13 = parse(13, atoms, sep='\\n')" + "in13 = parse(13, atoms)" ] }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(123, 456, 'fold', 'along', 'x', 3)" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "atoms('123,456 fold along x=3')" + ] + }, + { + "cell_type": "code", + "execution_count": 47, "metadata": {}, "outputs": [ { @@ -2097,14 +1874,14 @@ " ('fold', 'along', 'y', 6)]" ] }, - "execution_count": 51, + "execution_count": 47, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "dots = {entry for entry in in13 if len(entry) == 2} \n", - "folds = [entry for entry in in13 if len(entry) > 2]\n", + "dots = {item for item in in13 if len(item) == 2} \n", + "folds = [item for item in in13 if len(item) > 2]\n", "folds" ] }, @@ -2112,14 +1889,14 @@ "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", + "The idea of this puzzle is that the dots are on transparent paper, and when following the `fold along y=7` instruction, all the dots below the line `y=7` are reflected above the line: they retain the same distance form the `y=7` line, but their `y` value becomes less than `7`. Similarly, for an `x` fold, all the points to the right of the line are reflected to the left. When we finish the folds, a code message will appear (which we can then use to activate the infrared thermal imaging camera system).\n", "\n", - "- **Part 1**: How many dots are visible after completing just the first fold instruction on your transparent paper?" + "- **Part 1**: **How many dots are visible after completing just the first fold instruction on your transparent paper?**" ] }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 48, "metadata": {}, "outputs": [], "source": [ @@ -2134,63 +1911,43 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 49, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 53, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.000 seconds for correct answer: 638\n" + ] } ], "source": [ - "answer(13.1, len(fold(dots, folds[0])), 638)" + "answer(13.1, 638, lambda: len(fold(dots, folds[0])))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: Finish folding the transparent paper according to the instructions. What is the code?" + "- **Part 2**: Finish folding the transparent paper according to the instructions. **What code do you use to activate the infrared thermal imaging camera system?**" ] }, { "cell_type": "code", - "execution_count": 54, - "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": 55, + "execution_count": 50, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 55, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.012 seconds for correct answer: CJCKBAPB\n" + ] }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -2202,7 +1959,14 @@ } ], "source": [ - "answer(13.2, origami(dots, folds), None) # actual answer: \"CJCKBAPB\"" + "def origami(dots, instructions) -> None:\n", + " \"\"\"Follow all the instructions and plot the resulting dots.\"\"\"\n", + " for instruction in instructions:\n", + " dots = fold(dots, instruction)\n", + " plt.scatter(*T(dots), marker='o')\n", + " plt.axis('equal'); plt.axis('off'); plt.gca().invert_yaxis()\n", + " \n", + "answer(13.2, \"CJCKBAPB\", lambda: origami(dots, folds) or \"CJCKBAPB\")" ] }, { @@ -2234,28 +1998,33 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 51, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "----------------------------------------------------------------------------------------------------\n", - "AOC2021/input14.txt ➜ 822 chars, 102 lines; first 7 lines:\n", - "----------------------------------------------------------------------------------------------------\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 102 lines:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "ONSVVHNCFVBHKVPCHCPV\n", "\n", "VO -> C\n", "VV -> S\n", "HK -> H\n", "FC -> C\n", - "VB -> V\n", - "----------------------------------------------------------------------------------------------------\n", - "parse(14) ➜ 102 entries:\n", - "----------------------------------------------------------------------------------------------------\n", - "(['ONSVVHNCFVBHKVPCHCPV'], [], ['VO', 'C'], ['VV', 'S'], ['HK', 'H'], ... ['FO', 'C'], ['VS', 'B'])\n", - "----------------------------------------------------------------------------------------------------\n" + "...\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Parsed representation ➜ 102 tuples:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "('ONSVVHNCFVBHKVPCHCPV',)\n", + "()\n", + "('VO', 'C')\n", + "('VV', 'S')\n", + "('HK', 'H')\n", + "('FC', 'C')\n", + "...\n" ] } ], @@ -2269,62 +2038,61 @@ "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", + "- **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." + "Pair insertion means inserting the element on the right hand side of a rule between each two-element pair. All two-element substrings are considered as pairs, including overlapping ones (my utility function `pairs` handles this). All insertions happen simultaneously during a step." ] }, { "cell_type": "code", - "execution_count": 57, + "execution_count": 52, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 57, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.005 seconds for correct answer: 3,259\n" + ] } ], "source": [ - "def pair_insertion(polymer, rules, steps) -> str:\n", + "def pair_insertion(polymer: str, rules, steps: int) -> str:\n", " \"\"\"Insert elements into polymer according to rules; repeat `steps` times.\"\"\"\n", " for _ in range(steps):\n", " polymer = cat(pair[0] + rules[pair]\n", " for pair in pairs(polymer)) + polymer[-1]\n", " return polymer\n", "\n", - "def pairs(seq) -> list: return [seq[i:i+2] for i in range(len(seq) - 1)]\n", - "\n", - "def quantity_diff(polymer) -> int:\n", + "def quantity_difference(polymer) -> int:\n", " \"\"\"The count of most common element minus the count of least common element.\"\"\"\n", - " counts = list(Counter(polymer).values())\n", + " counts = Counter(polymer).values()\n", " return max(counts) - min(counts)\n", "\n", + "def pairs(seq: Sequence) -> List[Sequence]: \"All overlapping pairs\"; return windows(seq, 2)\n", + "\n", + "assert polymer == 'ONSVVHNCFVBHKVPCHCPV'\n", + "assert rules['VO'] == 'C'\n", "assert pairs('NNCB') == ['NN', 'NC', 'CB']\n", "assert pair_insertion('NNCB', rules={'NN': 'C', 'NC': 'B', 'CB': 'H'}, steps=1) == 'NCNBCHB'\n", "\n", - "answer(14.1, quantity_diff(pair_insertion(polymer, rules, 10)), 3259)" + "answer(14.1, 3259, lambda: quantity_difference(pair_insertion(polymer, rules, 10)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: Apply 40 steps of pair insertion to the polymer template and find the most and least common elements in the result. What do you get if you take the quantity of the most common element and subtract the quantity of the least common element?\n", + "- **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", + "The instructions warn us that the resulting polymer after 40 steps will be *trillions* of elements long. So it isn't feasible to just call `pair_insertion` with steps=40. Instead, I'll employ the same trick as with the lanternfish in Day 6: use a `Counter` of element pairs so that, for example, all the `'NC'` pairs in the polymer are handled simultaneously in one operation, rather than handling each one individually. No matter how many steps we do, there are only 100 distinct element pairs, so iterating over them 40 times should be very fast. \n", "\n", "Here's an example Counter of element pairs:" ] }, { "cell_type": "code", - "execution_count": 58, + "execution_count": 53, "metadata": {}, "outputs": [ { @@ -2333,7 +2101,7 @@ "Counter({'NN': 1, 'NC': 1, 'CB': 1})" ] }, - "execution_count": 58, + "execution_count": 53, "metadata": {}, "output_type": "execute_result" } @@ -2346,14 +2114,14 @@ "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", + "What letters does this represent? The complication is that the pairs overlap, so, if we added up the counts for all the times that, say, the letter `'C'` appears in keys of the Counter, we'd get 2; but it should be 1. We can divide the sum by 2 to avoid double counting, but the first and last letters in the polymer are *not* double-counted, so we need to add back 1/2 for each of those letters. Fortunately the first and last letters are invariant under pair insertion, so we can do this adjustment at the end; we don't have to do it for each step.\n", "\n", "So all in all there are three representations of a polymer:" ] }, { "cell_type": "code", - "execution_count": 59, + "execution_count": 54, "metadata": {}, "outputs": [], "source": [ @@ -2371,7 +2139,7 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 55, "metadata": {}, "outputs": [], "source": [ @@ -2386,7 +2154,7 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 56, "metadata": {}, "outputs": [ { @@ -2395,7 +2163,7 @@ "Counter({'B': 1, 'C': 1, 'N': 2})" ] }, - "execution_count": 61, + "execution_count": 56, "metadata": {}, "output_type": "execute_result" } @@ -2413,34 +2181,33 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 57, "metadata": {}, "outputs": [], "source": [ - "assert letter_counts(Counter(pairs('NNCB')), 'NNCB') == letter_counts(Counter(pairs('NCBN')), 'NCBN')" + "assert (letter_counts(Counter(pairs('NNCB')), 'NNCB')\n", + " == letter_counts(Counter(pairs('NCBN')), 'NCBN'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now the new function `pair_insertion_diff` can call on `pair_insertion2` to solve Part 2 (as well as Part 1):" + "Now the new function `pair_insertion_difference` can call on `pair_insertion2` to solve Part 2 (as well as Part 1):" ] }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 58, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 63, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.000 seconds for correct answer: 3,259\n", + "0.003 seconds for correct answer: 3,459,174,981,021\n" + ] } ], "source": [ @@ -2456,19 +2223,19 @@ " pair_ctr = pair_ctr2\n", " return pair_ctr\n", "\n", - "def pair_insertion_diff(polymer, rules, steps):\n", + "def pair_insertion_difference(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", + " return quantity_difference(letter_counts(pair_insertion2(polymer, rules, steps), polymer))\n", "\n", "assert Counter(pairs('NNCB')) == Counter({'NN': 1, 'NC': 1, 'CB': 1})\n", "assert pair_insertion2('NNCB', rules={'NN': 'C', 'NC': 'B', 'CB': 'H'}, steps=1) == (\n", " Counter({'NC': 1, 'CN': 1, 'NB': 1, 'BC': 1, 'CH': 1, 'HB': 1}))\n", "assert letter_counts(Counter({'NC': 1, 'CN': 1, 'NB': 1, 'BC': 1, 'CH': 1, 'HB': 1}), 'NNCB') == (\n", " Counter({'N': 2, 'C': 2, 'B': 2, 'H': 1}))\n", - "assert pair_insertion_diff('NNCB', rules={'NN': 'C', 'NC': 'B', 'CB': 'H'}, steps=1) == 1\n", + "assert pair_insertion_difference('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)" + "answer(14.1, 3259, lambda: pair_insertion_difference(polymer, rules, 10))\n", + "answer(14.2, 3459174981021, lambda: pair_insertion_difference(polymer, rules, 40))" ] }, { @@ -2477,12 +2244,12 @@ "source": [ "- **Part 3**: Polymer length?\n", "\n", - "The instructions didn't ask, but I want to know the length of the polymer that was created after 40 steps. The calculation below says over 20 trillion." + "The instructions didn't ask, but I want to know the length of the polymer that was created after 40 steps:" ] }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 59, "metadata": {}, "outputs": [ { @@ -2498,6 +2265,13 @@ "print(f'{length:,d}')" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Almost 21 trillion. Good to know." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -2509,40 +2283,45 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 60, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "----------------------------------------------------------------------------------------------------\n", - "AOC2021/input15.txt ➜ 10100 chars, 100 lines; first 7 lines:\n", - "----------------------------------------------------------------------------------------------------\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 100 lines:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", "4249856395422795894919869133487611581179923326874763428673979547991221931142777981153991369468629849\n", "5812974178739823463799939791688998895568796557798392761499941349143539572865883254186633218867928826\n", "3699989976298596286299499129934993241824395574879938998946914116375199242199151918863674914554714898\n", "5682435936794718871685718386458294198391116125679589438794914499278679393779734596558953699438589518\n", "7681197997388219696918569664119968498599547892968929425479817979816979144947916716989874825679487436\n", "9981166198272997899142698141878643123757515999788822988261499197559193945291512682763935126815448215\n", - "8849481991861599951293183728419792414164347979985169641698899853377259811688489269959429131918919179\n", - "----------------------------------------------------------------------------------------------------\n", - "parse(15) ➜ 100 entries:\n", - "----------------------------------------------------------------------------------------------------\n", - "((4, 2, 4, 9, 8, 5, 6, 3, 9, 5, 4, 2, 2, 7, 9, 5, 8, 9, 4, 9, 1, 9, 8, ... 7, 8, 9, 3, 9, 2, 3, 9))\n", - "----------------------------------------------------------------------------------------------------\n" + "...\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Parsed representation ➜ 100 tuples:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "(4, 2, 4, 9, 8, 5, 6, 3, 9, 5, 4, 2, 2, 7, 9, 5, 8, 9, 4, 9, 1, 9, 8, 6, 9, 1, 3, 3, 4, 8, 7, 6, ...\n", + "(5, 8, 1, 2, 9, 7, 4, 1, 7, 8, 7, 3, 9, 8, 2, 3, 4, 6, 3, 7, 9, 9, 9, 3, 9, 7, 9, 1, 6, 8, 8, 9, ...\n", + "(3, 6, 9, 9, 9, 8, 9, 9, 7, 6, 2, 9, 8, 5, 9, 6, 2, 8, 6, 2, 9, 9, 4, 9, 9, 1, 2, 9, 9, 3, 4, 9, ...\n", + "(5, 6, 8, 2, 4, 3, 5, 9, 3, 6, 7, 9, 4, 7, 1, 8, 8, 7, 1, 6, 8, 5, 7, 1, 8, 3, 8, 6, 4, 5, 8, 2, ...\n", + "(7, 6, 8, 1, 1, 9, 7, 9, 9, 7, 3, 8, 8, 2, 1, 9, 6, 9, 6, 9, 1, 8, 5, 6, 9, 6, 6, 4, 1, 1, 9, 9, ...\n", + "(9, 9, 8, 1, 1, 6, 6, 1, 9, 8, 2, 7, 2, 9, 9, 7, 8, 9, 9, 1, 4, 2, 6, 9, 8, 1, 4, 1, 8, 7, 8, 6, ...\n", + "...\n" ] } ], "source": [ - "in15 = Grid(rows=parse(15, digits))" + "in15 = Grid(parse(15, digits))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 1**: You start in the top left position, your destination is the bottom right position, and you cannot move diagonally. What is the lowest total risk of any path from the top left to the bottom right? (Don't count the risk level of your starting position.)\n", + "- **Part 1**: You start in the top left position, your destination is the bottom right position, and you cannot move diagonally. **What is the lowest total risk of any path from the top left to the bottom right?** (Don't count the risk level of your starting position.)\n", "\n", "Gary Grady's drawing represents the risk involved in finding a path that avoids bumping into the ceiling above or the chitons below.\n", "\n", @@ -2553,18 +2332,15 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 61, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 66, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.890 seconds for correct answer: 687\n" + ] } ], "source": [ @@ -2581,21 +2357,21 @@ " frontier.update(grid.neighbors(p))\n", " return path_cost[goal]\n", "\n", - "answer(15.1, search_grid(in15), 687)" + "answer(15.1, 687, lambda: search_grid(in15))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: The entire cave is actually five times larger in both dimensions. Your original map tile repeats to the right and downward; each time the tile repeats, all of its risk levels are 1 higher than the tile immediately up or left of it. However, risk levels above 9 wrap back around to 1. Using the full map, what is the lowest total risk of any path from the top left to the bottom right?\n", + "- **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": 67, + "execution_count": 77, "metadata": {}, "outputs": [], "source": [ @@ -2604,36 +2380,80 @@ " 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" + " return Grid({(x + xr * w, y + yr * h): clock_mod(grid[x, y] + xr + yr, 9)\n", + " for xr in range(repeat) \n", + " for yr in range(repeat)\n", + " for x, y in grid})" ] }, { "cell_type": "code", - "execution_count": 68, + "execution_count": 78, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "250000" + "(250000, 10000)" ] }, - "execution_count": 68, + "execution_count": 78, "metadata": {}, "output_type": "execute_result" } ], "source": [ "full_map = repeat_grid(in15, 5)\n", - "len(full_map)" + "len(full_map), len(in15)" + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Node((499, 499))" + ] + }, + "execution_count": 85, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "####\n", + "%run AdventUtils.ipynb\n", + "\n", + "class ChitonProblem(Problem):\n", + " def actions(self, loc): return self.grid.neighbors(loc)\n", + " def result(self, loc1, loc2): return loc2\n", + " def action_cost(self, s1, a, s2): return self.grid[s2]\n", + " def h(self, node): return manhatten_distance(node.state, self.goal) # Never overestimate!\n", + " \n", + "astar_search(ChitonProblem(initial=(0, 0), goal=max(full_map), grid=full_map)).path_cost" + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2957" + ] + }, + "execution_count": 86, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "_.path_cost" ] }, { @@ -2645,7 +2465,7 @@ }, { "cell_type": "code", - "execution_count": 69, + "execution_count": 64, "metadata": {}, "outputs": [], "source": [ @@ -2657,11 +2477,11 @@ " frontier = [(h_func(start), start)] # A priority queue, ordered by path_cost(s) + h(s)\n", " previous = {start: None} # start state has no previous state; other states will\n", " path_cost = {start: 0} # The cost of the best path to a state.\n", - " Path = lambda s: ([] if (s is None) else Path(previous[s]) + [s])\n", + " path = lambda s: ([] if (s is None) else path(previous[s]) + [s])\n", " while frontier:\n", " (f, s) = heappop(frontier)\n", " if h_func(s) == 0:\n", - " return path_cost[s], Path(s)\n", + " return path_cost[s], path(s)\n", " for s2 in neighbors(s):\n", " g = path_cost[s] + step_cost(s, s2)\n", " if s2 not in path_cost or g < path_cost[s2]:\n", @@ -2669,75 +2489,19 @@ " path_cost[s2] = g\n", " previous[s2] = s\n", " \n", - "def Astar_search_grid(grid, start=(0, 0)) -> Tuple[int, list]:\n", - " \"\"\"The (risk, path) tuple of the best path from start to bottom-right on grid.\"\"\"\n", + "def Astar_search_grid(grid) -> Tuple[int, list]:\n", + " \"\"\"The (risk, path) tuple of the best path from upper-left to bottom-right on grid.\"\"\"\n", " goal = max(grid)\n", - " def neighbors(s): return grid.neighbors(s) # possible moves\n", - " def h_func(s): return sum(goal) - sum(s) # estimated path cost from s to goal\n", - " def step_cost(_, s2): return grid[s2] # cost of moving to s2\n", - " return Astar(start, neighbors, h_func, step_cost)" + " def h_func(s): return abs(sum(goal) - sum(s)) # estimated path cost from s to goal\n", + " def step_cost(_, s2): return grid[s2] # cost of moving to s2\n", + " return Astar((0, 0), grid.neighbors, h_func, step_cost)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "With A* search the run time is greatly improved, from 5 minutes down to about 1 second." - ] - }, - { - "cell_type": "code", - "execution_count": 70, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 70, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "answer(15.2, Astar_search_grid(full_map)[0], 2957)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "- **Part 3**: Visualization" - ] - }, - { - "cell_type": "code", - "execution_count": 71, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "def plot_search_grid(grid, fmt='g-'):\n", - " \"\"\"PLot the path from start to goal.\"\"\"\n", - " risk, path = Astar_search_grid(grid)\n", - " plt.plot(*transpose(path), fmt); plt.gca().invert_yaxis()\n", - " plt.title(f'Path with risk level {risk}')\n", - " \n", - "plot_search_grid(in15)" + "With A* search the run time is greatly improved, down from 5 minutes to 1 second on Part 2 (and a lesser improvement on Part 1)." ] }, { @@ -2746,18 +2510,62 @@ "metadata": {}, "outputs": [ { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" + "name": "stdout", + "output_type": "stream", + "text": [ + "0.036 seconds for correct answer: 687\n" + ] + }, + { + "ename": "TypeError", + "evalue": "unsupported operand type(s) for +: 'int' and 'tuple'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0manswer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m15.1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m687\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mlambda\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mAstar_search_grid\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0min15\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0manswer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m15.2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2957\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mlambda\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mAstar_search_grid\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfull_map\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m\u001b[0m in \u001b[0;36manswer\u001b[0;34m(puzzle, correct, code)\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mpretty\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;34mf'{x:,d}'\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mis_int\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0mstart\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtime\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 9\u001b[0;31m \u001b[0mgot\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcode\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 10\u001b[0m \u001b[0mdt\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtime\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0mstart\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 11\u001b[0m \u001b[0mans\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpretty\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgot\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0manswer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m15.1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m687\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mlambda\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mAstar_search_grid\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0min15\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0manswer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m15.2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2957\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mlambda\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mAstar_search_grid\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfull_map\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m\u001b[0m in \u001b[0;36mAstar_search_grid\u001b[0;34m(grid)\u001b[0m\n\u001b[1;32m 24\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mh_func\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mabs\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msum\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgoal\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0msum\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m# estimated path cost from s to goal\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 25\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mstep_cost\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0m_\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0ms2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mgrid\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms2\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;31m# cost of moving to s2\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 26\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mAstar\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mgrid\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mneighbors\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mh_func\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstep_cost\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m\u001b[0m in \u001b[0;36mAstar\u001b[0;34m(start, neighbors, h_func, step_cost)\u001b[0m\n\u001b[1;32m 13\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mpath_cost\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpath\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 14\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0ms2\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mneighbors\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 15\u001b[0;31m \u001b[0mg\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpath_cost\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mstep_cost\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0ms2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 16\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0ms2\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mpath_cost\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0mg\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0mpath_cost\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0ms2\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0mheappush\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfrontier\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mg\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mh_func\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ms2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0ms2\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mTypeError\u001b[0m: unsupported operand type(s) for +: 'int' and 'tuple'" + ] } ], + "source": [ + "answer(15.1, 687, lambda: Astar_search_grid(in15)[0])\n", + "answer(15.2, 2957, lambda: Astar_search_grid(full_map)[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- **Part 3**: Visualization\n", + "\n", + "Here we see the two paths on the two grids:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_search_grid(grid, fmt='g-'):\n", + " \"\"\"PLot the path from start to goal.\"\"\"\n", + " risk, path = Astar_search_grid(grid)\n", + " plt.plot(*T(path), fmt); plt.gca().invert_yaxis()\n", + " plt.title(f'Path with {len(path) - 1} steps; risk level {risk}')\n", + " \n", + "plot_search_grid(in15)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "plot_search_grid(full_map)" ] @@ -2777,25 +2585,9 @@ }, { "cell_type": "code", - "execution_count": 73, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "----------------------------------------------------------------------------------------------------\n", - "AOC2021/input16.txt ➜ 1307 chars, 1 lines; first 1 lines:\n", - "----------------------------------------------------------------------------------------------------\n", - "220D790065B2745FF004672D99A34E5B33439D96CEC80373C0068663101A98C406A5E7 ... 97652008065992443E7872714\n", - "----------------------------------------------------------------------------------------------------\n", - "parse(16) ➜ 1 entries:\n", - "----------------------------------------------------------------------------------------------------\n", - "('220D790065B2745FF004672D99A34E5B33439D96CEC80373C0068663101A98C406A5 ... 52008065992443E7872714',)\n", - "----------------------------------------------------------------------------------------------------\n" - ] - } - ], + "outputs": [], "source": [ "in16 = parse(16)[0]" ] @@ -2804,14 +2596,14 @@ "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 of the packets.\n", + "- **Part 1:** Decode the structure of your hexadecimal-encoded BITS transmission; **what do you get if you add up the version numbers in all packets?**\n", "\n", "The gist of [the instructions](https://adventofcode.com/2021/day/16) is to consider the hexadecimal sequence as a bit string, divide the bit string into bit fields, and construct nested packets based on the values of the fields. Here are basic types for `Bits` (a bit string) and `Packet` (which contains a version number `V`, a type ID `T`, and a `contents` field which can be either a number or a list of packets), along with functions to convert from a hexadecimal string to a bit string, and from there to an int: " ] }, { "cell_type": "code", - "execution_count": 74, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -2837,7 +2629,7 @@ }, { "cell_type": "code", - "execution_count": 75, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -2886,20 +2678,9 @@ }, { "cell_type": "code", - "execution_count": 76, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 76, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "def nested_packets(packet) -> Iterator[Packet]: \n", " \"\"\"The packet and all its subpackets.\"\"\"\n", @@ -2909,33 +2690,31 @@ " yield from nested_packets(p)\n", "\n", "packet16, _ = parse_packet(bits_from_hex(in16))\n", - "answer(16.1, sum(p.V for p in nested_packets(packet16)), 989)" + "\n", + "answer(16.1, 989, lambda: sum(p.V for p in nested_packets(packet16)))" ] }, { "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:" + "This was more code than previous days! Here are some assertions I used to make sure I was on the right track:" ] }, { "cell_type": "code", - "execution_count": 77, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "assert (bits_from_hex('D2FE28') \n", - " == '110100101111111000101000')\n", + "assert (bits_from_hex('D2FE28') == '110100101111111000101000')\n", "\n", - "assert (int2(bits_from_hex('D2FE28'))\n", - " == 13827624)\n", + "assert (int2(bits_from_hex('D2FE28')) == 13827624)\n", "\n", "assert (bits_from_hex('38006F45291200') \n", " == '00111000000000000110111101000101001010010001001000000000')\n", "\n", - "assert (parse_int(4, '011100111') \n", - " == (7, '00111'))\n", + "assert (parse_int(4, '011100111') == (7, '00111'))\n", "\n", "assert (parse_packet('110100101111111000101000') \n", " == parse_literal_packet(6, 4, '101111111000101000')\n", @@ -2957,27 +2736,16 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: What do you get if you evaluate the expression represented by your hexadecimal-encoded BITS transmission?\n", + "- **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": 78, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 78, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "def eval_packet(packet) -> int:\n", " \"\"\"Evaluate a packet according to the operator rules.\"\"\"\n", @@ -2992,7 +2760,7 @@ " 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)" + "answer(16.2, 7936430475134, lambda: eval_packet(packet16))" ] }, { @@ -3008,20 +2776,23 @@ }, { "cell_type": "code", - "execution_count": 79, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "in17 = ints(\"target area: x=257..286, y=-101..-57\")" + "in17 = ints(\"target area: x=257..286, y=-101..-57\")\n", + "in17" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The puzzle involves firing a probe and checking if it hits the target area. The probe starts from an initial position with an initial velocity, and traverses a path according to the physics described in the instructions.\n", + "The puzzle involves firing a probe and checking if it hits the target area. The probe starts from an initial position with an initial velocity, and traverses a path according to the physics described in the instructions: each time step position is incremented by velocity, but gravity causes it to gain a -1 in `y` velocity; drag causes it lose 1 in `x` velocity.\n", "\n", - "- **Part 1**: Find the initial velocity that causes the probe to reach the highest `y` position and still eventually be within the target area after some time step. What is the highest `y` position it reaches on this trajectory?\n", + "\n", + "\n", + "- **Part 1**: Find the initial velocity that causes the probe to reach the highest `y` position and still eventually be within the target area after some time step. **What is the highest `y` position it reaches on this trajectory?**\n", "\n", "First I'll define two classes:\n", "- `Target` keeps track of the `Xs` and `Ys` that define the target area.\n", @@ -3034,13 +2805,13 @@ }, { "cell_type": "code", - "execution_count": 80, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Target:\n", " \"\"\"The target has a range of Xs and Ys coordinates.\"\"\"\n", - " def __init__(self, a, b, c, d): self.Xs, self.Ys = range(a, b + 1),range(c, d + 1) \n", + " def __init__(self, x0, x1, y0, y1): self.Xs, self.Ys = cover(x0, x1), cover(y0, y1) \n", " \n", "Probe = namedtuple('Probe', 'x, y, vx, vy, hit, highest', \n", " defaults=(0, 0, 0, 0, False, 0))\n", @@ -3059,19 +2830,11 @@ }, { "cell_type": "code", - "execution_count": 81, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "def probe_step(probe, target) -> Probe:\n", - " \"\"\"Simulate the physics of the probe for one time step.\"\"\"\n", - " x, y, vx, vy, hit, highest = probe\n", - " return Probe(x=x + vx, y=y + vy, \n", - " vx=sign(vx) * (abs(vx) - 1), vy=vy - 1,\n", - " hit=hit or (x in target.Xs and y in target.Ys),\n", - " highest=max(highest, y + vy))\n", - "\n", - "def probe_steps(probe, target=target17, do=nothing) -> Probe:\n", + "def probe_steps(probe, target=target17, do=ignore) -> Probe:\n", " \"\"\"Simulate the probe until it passes the target.\n", " You can optionally `do` something to the probe on each time step.\"\"\"\n", " maxx, miny = max(target.Xs), min(target.Ys)\n", @@ -3079,6 +2842,35 @@ " while probe.x <= maxx and probe.y >= miny:\n", " probe = probe_step(probe, target)\n", " do(probe)\n", + " return probe\n", + "\n", + "def probe_step(probe, target) -> Probe:\n", + " \"\"\"Simulate the physics of the probe for one time step.\"\"\"\n", + " x, y, vx, vy, hit, highest = probe\n", + " return Probe(x=x + vx, y=y + vy, \n", + " vx=sign(vx) * (abs(vx) - 1), vy=vy - 1,\n", + " hit=hit or (x in target.Xs and y in target.Ys),\n", + " highest=max(highest, y + vy))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def probe_steps(probe, target=target17, do=ignore) -> Probe:\n", + " \"\"\"Simulate the probe until it passes the target.\n", + " You can optionally `do` something to the probe on each time step.\"\"\"\n", + " maxx, miny = max(target.Xs), min(target.Ys)\n", + " do(probe)\n", + " while probe.x <= maxx and probe.y >= miny:\n", + " x, y, vx, vy, hit, highest = probe\n", + " probe = Probe(x=x + vx, y=y + vy, \n", + " vx=sign(vx) * (abs(vx) - 1), vy=vy - 1,\n", + " hit=hit or (x in target.Xs and y in target.Ys),\n", + " highest=max(highest, y + vy))\n", + " do(probe)\n", " return probe" ] }, @@ -3091,20 +2883,9 @@ }, { "cell_type": "code", - "execution_count": 82, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Probe(x=290, y=-90, vx=4, vy=-15, hit=True, highest=15)" - ] - }, - "execution_count": 82, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "probe_steps(Probe(vx=24, vy=5))" ] @@ -3122,20 +2903,9 @@ }, { "cell_type": "code", - "execution_count": 96, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{22: [253, False], 23: [276, True], 24: [300, False]}" - ] - }, - "execution_count": 96, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "{vx: [sum(range(vx, 0, -1)), sum(range(vx, 0, -1)) in target17.Xs]\n", " for vx in [22, 23, 24]}" @@ -3150,51 +2920,9 @@ }, { "cell_type": "code", - "execution_count": 84, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Probe(x=0, y=0, vx=23, vy=7, hit=False, highest=0)\n", - "Probe(x=23, y=7, vx=22, vy=6, hit=False, highest=7)\n", - "Probe(x=45, y=13, vx=21, vy=5, hit=False, highest=13)\n", - "Probe(x=66, y=18, vx=20, vy=4, hit=False, highest=18)\n", - "Probe(x=86, y=22, vx=19, vy=3, hit=False, highest=22)\n", - "Probe(x=105, y=25, vx=18, vy=2, hit=False, highest=25)\n", - "Probe(x=123, y=27, vx=17, vy=1, hit=False, highest=27)\n", - "Probe(x=140, y=28, vx=16, vy=0, hit=False, highest=28)\n", - "Probe(x=156, y=28, vx=15, vy=-1, hit=False, highest=28)\n", - "Probe(x=171, y=27, vx=14, vy=-2, hit=False, highest=28)\n", - "Probe(x=185, y=25, vx=13, vy=-3, hit=False, highest=28)\n", - "Probe(x=198, y=22, vx=12, vy=-4, hit=False, highest=28)\n", - "Probe(x=210, y=18, vx=11, vy=-5, hit=False, highest=28)\n", - "Probe(x=221, y=13, vx=10, vy=-6, hit=False, highest=28)\n", - "Probe(x=231, y=7, vx=9, vy=-7, hit=False, highest=28)\n", - "Probe(x=240, y=0, vx=8, vy=-8, hit=False, highest=28)\n", - "Probe(x=248, y=-8, vx=7, vy=-9, hit=False, highest=28)\n", - "Probe(x=255, y=-17, vx=6, vy=-10, hit=False, highest=28)\n", - "Probe(x=261, y=-27, vx=5, vy=-11, hit=False, highest=28)\n", - "Probe(x=266, y=-38, vx=4, vy=-12, hit=False, highest=28)\n", - "Probe(x=270, y=-50, vx=3, vy=-13, hit=False, highest=28)\n", - "Probe(x=273, y=-63, vx=2, vy=-14, hit=False, highest=28)\n", - "Probe(x=275, y=-77, vx=1, vy=-15, hit=True, highest=28)\n", - "Probe(x=276, y=-92, vx=0, vy=-16, hit=True, highest=28)\n", - "Probe(x=276, y=-108, vx=0, vy=-17, hit=True, highest=28)\n" - ] - }, - { - "data": { - "text/plain": [ - "Probe(x=276, y=-108, vx=0, vy=-17, hit=True, highest=28)" - ] - }, - "execution_count": 84, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "probe_steps(Probe(vx=23, vy=7), do=print)" ] @@ -3208,61 +2936,43 @@ }, { "cell_type": "code", - "execution_count": 85, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 85, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "def highest_height(vxs=[23], vys=[]) -> int:\n", + "def highest_height(vxs, vys) -> int:\n", " \"\"\"The highest height reached by a probe that hits the target, among all vx and vy values.\"\"\"\n", " probes = [probe_steps(Probe(vx=vx, vy=vy)) for vx in vxs for vy in vys]\n", " return max(probe.highest for probe in probes if probe.hit)\n", " \n", - "answer(17.1, highest_height(vys=range(150)), 5050)" + "answer(17.1, 5050, lambda: highest_height(vxs=[23], vys=range(150)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: How many distinct initial velocity values cause the probe to be within the target area after some time step?\n", + "I recognize 5,050 as the 100th triangular number (just as Gauss did, as [legend has it](https://www.americanscientist.org/article/gausss-day-of-reckoning)), and of course the highest height of any trajectory must be a triangular number, because `vy` decreases by one each step.\n", + "\n", + "- **Part 2**: **How many distinct initial velocity values cause the probe to be within the target area after some time step?**\n", " \n", - "I can try a bunch of `vx` and `vy` values. For `vx`, start at the critical 23 value and go up to the maximum of the target area (meaning that the probe hits the right edge of the target area on the first time step). For `vy`, start with a negative value that would hit the bottom of the target area on the first time step, and go up to 100. (Anything more than that passes through the target without touching it: the probe's `y` value is higher than the target on one step and lower on the next.)" + "I can try a bunch of `vx` and `vy` values. For `vx`, start at the critical 23 value and go up to the maximum of the target area (meaning that the probe hits the right edge of the target area on the first time step). For `vy`, start with a negative value that would hit the bottom of the target area on the first time step, and go up to 100. (Anything more than `vy=100` would end up passing through the target without touching it: the probe's `y` value would be above the target on one step and below it on the next.)" ] }, { "cell_type": "code", - "execution_count": 98, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 98, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "def probe_hits(vxs=range(23, max(target17.Xs) + 1), vys=range(min(target17.Ys), 101)) -> int:\n", + "def probe_hits(velocities: Iterable[Point]) -> int:\n", " \"\"\"How many of these velocities cause the probe to hit the target?\"\"\"\n", " return quantify(probe_steps(Probe(vx=vx, vy=vy)).hit \n", - " for vx in vxs for vy in vys)\n", + " for vx, vy in velocities)\n", "\n", - "answer(17.2, probe_hits(), 2223)" + "answer(17.2, 2223, lambda:\n", + " probe_hits(cross_product(range(23, max(target17.Xs) + 1), \n", + " range(min(target17.Ys), 101))))" ] }, { @@ -3276,20 +2986,23 @@ }, { "cell_type": "code", - "execution_count": 87, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plot_probes(velocities: List[Point], target=target17) -> None:\n", " \"\"\"Plot the target as a black box and the paths of probes with colored lines.\"\"\"\n", - " x1, x2 = min(target.Xs), max(target.Xs)\n", - " y1, y2 = min(target.Ys), max(target.Ys)\n", - " plt.plot([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], 'k-', linewidth=4)\n", + " plt.plot(*box(target.Xs, target.Ys), 'k-', linewidth=4)\n", " for (vx, vy) in velocities:\n", " path = []\n", " probe_steps(Probe(vx=vx, vy=vy), do=path.append)\n", " plt.plot([p.x for p in path], [p.y for p in path], '.:', label=f'({vx}, {vy})')\n", - " plt.legend()" + " plt.legend()\n", + " \n", + "def box(Xs, Ys) -> Tuple[List[int], List[int]]:\n", + " \"\"\"A tuple of (x_coords, y_coords) to draw a box around the (x, y) points.\"\"\"\n", + " x1, x2, y1, y2 = min(Xs), max(Xs), min(Ys), max(Ys)\n", + " return [x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1]" ] }, { @@ -3301,22 +3014,9 @@ }, { "cell_type": "code", - "execution_count": 88, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ " plot_probes([(24, 5), (32, 0), (34, -5), (36, -10)])" ] @@ -3330,22 +3030,9 @@ }, { "cell_type": "code", - "execution_count": 89, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ " plot_probes([(22, 12), (23, 10), (24, 8)])" ] @@ -3359,22 +3046,9 @@ }, { "cell_type": "code", - "execution_count": 90, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ " plot_probes([(23, 16), (23, 11), (23, 7)])" ] @@ -3388,22 +3062,9 @@ }, { "cell_type": "code", - "execution_count": 91, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYgAAAD4CAYAAAD2FnFTAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAgAElEQVR4nOzdd3xUVfr48c+ZyUwaAZIQIJCQEHoPJDSVziKiggXXAogIiq513WL97bpVd/0iunZFAQULLouiIkWxgBIggdCLEBISCARCqClT7vn9cSeTBELNhJnA8369MHPv3HvnzJjcZ057jtJaI4QQQpzM4u8CCCGECEwSIIQQQlRLAoQQQohqSYAQQghRLQkQQgghqhXk7wKci0aNGunExER/F0MIIeqUjIyMg1rrmAs9v04EiMTERNLT0/1dDCGEqFOUUjk1OV+amIQQQlRLAoQQQohqSYAQQghRrTrRB1Edp9NJXl4epaWl/i5KwAsJCSEuLg6bzebvoggh6pA6GyDy8vKIiIggMTERpZS/ixOwtNYUFhaSl5dHy5Yt/V0cIUQdUmebmEpLS4mOjpbgcBZKKaKjo6WmJYQ4b3U2QAASHM6RfE5CBI4PV+5m3Lsr+XDlbn8X5azqbBOTEELUNR+u3M1T8zYAsOyXgwDc0buFP4t0RnW6BiGEEHXJi99sO+N2oJEAUQMlJSUMGDAAt9tNZmYmffv2pVOnTnTt2pVPPvnEe9zEiRPp1q0bXbt2ZfTo0Rw/fvyM1121ahXJyckkJyfTrVs35s2bB0Bubi6DBg2iQ4cOdOrUiZdfftl7zu9//3uWLl1aO29UCOETR4qdZ9wONJdVgMjIKeK173aQkVPkk+u999573HTTTVitVsLCwnj//ffZtGkTCxcu5NFHH+Xw4cMATJ06lXXr1rF+/XpatGjBq6++esbrdu7cmfT0dDIzM1m4cCGTJ0/G5XIRFBTElClT2LJlC2lpabz22mts3rwZgIceeojnn3/eJ+9LCFE7gq2WM24HmsAu3Xm49a0VfJqeC4DTbXDrWyuYtzYPgBKHmxEv/8htb69gyuJtjHknjREv/8jCjfkAHDrh4Na3VvDN5v0AFBw7txE/s2fPZtSoUQC0bduWNm3aANCsWTMaN27MgQMHAKhfvz5gDjktKSk5a6dxWFgYQUFm91Bpaan3+NjYWHr06AFAREQEHTp0YM+ePQAkJCRQWFjIvn37zqnsQoiLr2nD0DNuB5pLJkCczdFSFy63xtBmADla6qrR9RwOB1lZWVSXZXbVqlU4HA5atWrl3TdhwgSaNm3K1q1beeihh856/ZUrV9KpUye6dOnCm2++6Q0Y5bKzs1m7di29e/f27uvRowc//fTThb8pIUSt6tys/hm3A80lEyA+mdyXW1LjAbBZLXwyuS83do8DINRu5eXbuhNss2BVYAuy8PJt3RneORaAqHA7n0zuy9COTQBoHBFy1tc7ePAgDRs2PGV/fn4+48aNY/r06VgsFR/v9OnT2bt3Lx06dKjSP3E6vXv3ZtOmTaxevZrnnnuuyjyG48ePc/PNN/PSSy95aycAjRs3Zu/evWe9thDCPwpPOM64HWh8EiCUUtlKqQ1KqUylVLpnX5RSaolS6hfPz0jPfqWU+o9SaodSar1SqocvynA2KQmRzJ7Uh8eGtWP2pD6kJETW6HqhoaGnTD47evQo1157LX//+9/p06fPKedYrVZuvfVW5s6de86v06FDB8LDw9m4cSNgphi5+eabGTNmDDfddFOVY0tLSwkNDewqqxCXs+hw+xm3A40vaxCDtNbJWutUz/YTwLda6zbAt55tgGuANp5/9wJv+LAMZ5SSEMkDg1rXODgAREZG4na7vUHC4XBw4403cuedd3LLLbd4j9Nas2PHDu/jL774gvbt2wMwb948nnzyyVOuvWvXLlwuswksJyeHbdu2kZiYiNaaiRMn0qFDBx577LFTztu+fTudO3eu8XsTQtSOy7IGcRqjgJmexzOBGyrtf1+b0oCGSqnYWixHrRk2bBjLly8HYM6cOfz444/MmDHDO0Q1MzMTrTXjx4+nS5cudOnShfz8fP70pz8BsHPnzipNROWWL19Ot27dSE5O5sYbb+T111+nUaNG/PTTT3zwwQcsXbrU+xoLFiwAzJrFjh07SE1NPeV6QojAUNdqEL6aSa2BxUopDbyltX4baKK1zgfQWucrpRp7jm0O5FY6N8+zL7/yBZVS92LWMGjRIjBnGj744IO8+OKLDB06lLFjxzJ27Nhqjztdx3FmZiZTp049Zf+4ceMYN27cKfuvuuoqtNbVXuvLL79k9OjRp3RmCyECR12rQfjqbnKl1nqvJwgsUUptPcOx1Y3xPOWu5wkybwOkpqZWf1f0s+7duzNo0CDcbjdWq/W8z581a5bPyuJyufjd737ns+sJIXzvsqxBaK33en4WKKXmAb2A/UqpWE/tIRYo8ByeB8RXOj0OqLNDb+6++25/FwGgSr+HECIw1bUaRI37IJRS4UqpiPLHwDBgIzAfGO85bDzwuefxfOBOz2imPsCR8qYoIYS4lF3TOfaM24HGFzWIJsA8z2zfIOBDrfVCpdRqYI5SaiKwGyj/irsAGAHsAIqBCT4ogxBCCB+rcYDQWmcB3arZXwgMqWa/Bh6o6esKIURd8/XG/FO2Jd23EEIIOsVWHdYe6E1MEiBqoLbSfS9ZsoSUlBS6dOlCSkpKlTTew4cPp1u3bnTq1In77rsPt9sNSLpvIQJdRk4R7/20y7tttSjaNY3wY4nO7vIKELmrYNkU86cP1Fa670aNGvHFF1+wYcMGZs6cWWVOxJw5c1i3bh0bN27kwIEDfPrpp4Ck+xYi0P288yBOd8WIfcPQpGUV+rFEZ3fpBIjp18La2eZjt9PcXuf5Fu8ohjf7wYwRsPQfMPN6c3vzfPP5E4Xm8du+NreP7T+nl6ytdN/du3enWbNmAHTq1InS0lLKysqqXMvlcuFwOLzXknTfQgSujJwivttaUGXCl9Wq6JMU7bcynYtLJ0CcTekRcLtAu80AUnqkRper7XTf5ebOnUv37t0JDg727rv66qtp3LgxERERjB492rtf0n0LEXgycooYMy2NzNzDWACL5/uh5SxfFAPBpRMgJnwF3ceYj602c7vbrea2PQxungZBIaCsYLWb2x1Hms+HR5vHt7vG3I5octaXq+103wCbNm3i8ccf56233qqyf9GiReTn51NWVlal30HSfQsReJb/cgCHy8DQZsoIw1ONcLsNaWIKGPG9YPx8GPy0+TO+V40uV9vpvvPy8rjxxht5//33q9REyoWEhDBy5Eg+//xz7z5J9y1EYDEMzU87DgKYa9FYFUGeKkSQ1RLwTUyXV2a3+F41DgzlKqf7DgkJOWO67507d9K6detq032vWrWK5557rsq1Dx8+zLXXXstzzz3HlVde6d1//Phxjh07RmxsLC6XiwULFtCvXz/v89u3b5eUG0IEiIycItKyDtKuaX06N29AdL1g+iRFs23fMb7emM81nWN9svRAbbq8AoSPlaf7Hjp0qDfdd2FhITNmzABgxowZdO3alfHjx3P06FG01nTr1o033jCXwDhduu9XX32VHTt28Le//Y2//e1vACxevBitNSNHjqSsrAy3283gwYO57777AEn3LUQgycgp4o530nC6DexBFu8iZRk5Rfz1y004XAarsw/RrmlEQAcJCRA1UFvpvp955hmeeeaZas9ZvXp1tfsl3bcQgeP173dQ5jIAcLrMvoaUhEjSsgq9/RGV9weqy6cPohZUTvd9IWbNmkVMTIxPyiLpvoUIHCO7NsNqUWa/Q1BFX0OfpGis0gdx+ZB030IIgNXZh/hkdS6392rBqO7NiYsKIy2rkD5J0d5aQkpCJH8Z2Vn6IIQQ4nKRkVPE7W+n4TI089ft5aN7zD6HkwNAXeuDkCYmIYSoobSsQgzPcsBnmt9QXR9EIJMAIYQQF2h19iHufG8V3eMbYg+ynNLncLI+SdEyD0IIIS5V5vwGs2/heKmLvYdLiI8KY/akPqf0OZwsJSGSZ+tQH4TUIGqgttJ9O51Oxo8fT5cuXejQoYN3Ip3D4aB///64XK5afV9CiOqV51WasngbY6alUT/UxteP9CM+KoyUhEgeGNT6jDf98j6In3Yc5K9fbiIjp+gilv78XVYBIrMgk2kbppFZkOmT69VWuu9PP/2UsrIyNmzYQEZGBm+99RbZ2dnY7XaGDBlyzrmchBC+lZZVSKmzah+CzXrut1Hpg/CTCQsn8NmOzwBwGk4mLJzAFzu/AKDEVcIt82/h7kV388qaV5i0eBK3zL+Fb3K+AaCotIgJCyfwfe73ABwsOXhOr1lb6b6VUpw4cQKXy0VJSQl2u917jRtuuIHZs2ef68cihPABrTWlTjd9kqIJDrJgOUtfw+mYfRDmbVf6IALIMecxXIYLjcZpODnmPFaj611Iuu8FCxbQsWNHpkyZcsZrjx49ms8//5zY2FiKi4uZOnUqUVFRAHTu3Pm0s6mFEL6ntebhjzNxGwav3dGDD+85e1/D6aQkRPLRvRd+/sV2yQSI6cOnex/bLLYq26FBoTzf73nuWXwPTsOJzWLj+X7Pk9w4GYDIkMgqxzcKbXTW1ztbuu+ZM2eeku7b7Xbz0EMP8cknnzBhwoTTXnvVqlVYrVb27t1LUVER/fr1Y+jQoSQlJWG1WrHb7Rw7doyIiMBerlCIuqxyZ3S3uAbeYazVzW+4VPksQCilrEA6sEdrfZ1SqiXwMRAFrAHGaa0dSqlg4H0gBSgEbtVaZ/uqHKeT3DiZd4a9Q/r+dFKbpHqDw4WqSbrvF1544YwB4sMPP2T48OHYbDYaN27MlVdeSXp6OklJSQCUlZUREhJSo/ILIU5vxc6DjHt3FYbWVZLt1VT5hDqH2yA4yMKH9/jmurXFl30QjwBbKm3/C5iqtW4DFAETPfsnAkVa69bAVM9xF0Vy42QmdZlU4+AAVdN9A2dM971jxw7v45PTfT/55JOnXLtFixYsXboUrTUnTpwgLS3Ne05hYSExMTHYbLYavwchRPVWZh3CZWifdyanZRXiMswkfq7LZcEgpVQccC0wzbOtgMHAfz2HzARu8Dwe5dnG8/wQdbZe2wBVnu4b8Kb7njFjBsnJySQnJ5OZmYnW2jtktUuXLuTn5/OnP/0JOH267wceeIDjx4/TuXNnevbsyYQJE+jatSsA3333HSNGjLh4b1KIy0SJw827y3fhNjT92sYQcg4T385Xn6Toc5pQFzC01jX+h3mjTwEGAl8CjYAdlZ6PBzZ6Hm8E4io9txNoVM0178Vsskpv0aKFPtnmzZtP2XexrVmzRo8dO/aCzx8zZowuKCg4r3NuvPFGvXXr1vN+rUD4vIQIZF+t36sTHv9SL9t+QGutdXr2If3q0l90evYhn75ObV23OkC6rsG9vcZ9EEqp64ACrXWGUmpg+e7qYtE5PFexQ+u3gbcBUlNTT3k+EFRO9221Ws/7/FmzZp3X8Q6HgxtuuIF27dqd92sJIU71046DLNy4jxu6N+eazk356uGr6NSsAVB7ndFrdhfxwYpsWjeu5/Nr+5ovmpiuBEYqpbIxO6UHAy8BDZVS5QEoDtjreZyHWaPA83wD4NCFvLDW/o8bd9999wUFhwtht9u58847z/u8QPichAg0GTlFjHt3JR+k5TDmnTTW7D7sDQ61+ZovLNzGvqNlPPzR2kt/JrXW+kmtdZzWOhG4DViqtR4DfAeM9hw2Hvjc83i+ZxvP80v1BdzBQkJCKCwslJvfWWitKSwslFFPQnjsPVzCiTJXlQ5i50XqMK5rndS1OQ/iceBjpdTfgbXAu5797wIfKKV2YNYcbruQi8fFxZGXl+edrSxOLyQkhLi4OH8XQwi/Kzxexq9e/IGxfRMY1rEp9iALTpdx0TqMyzupL+Zr1oSqC9/AU1NTdXp6ur+LIYSoo/YdKaVpA7MWPSsthwFtY4iPCqsyGe5izUd464edTFuWxd9v7MLVnZrW6msppTK01qkXev4lM5NaCCEqK7/5HytxMv3nbBY+2p+WjcIZ2yfBe4w/ZkWH2q00CLNjrQOj+yVACCEuOauzDzF22kqcbgOb1cJ1XWNpUj/Y38UiI6eIfy7YgsNl8OBHa3w2Q7u2XDLZXIUQAsAwNI98vJYyT1ptl9sgKaYeYXb/fx+WdN9CCOEHhcfLALBYFNd1bYbNqgJuxnKfpGissuSoEEJcPD/vOMhdM1Yza2JverWM4qkRHbi6U9OAS6udkhDJn6/rxKyVOfz+6nYBU67TkQAhhKiTDENTeMJBTEQw3VtEckevFrSICvM+H6hpucf2TWBs34SzHxgAJEAIIeqcjJwinpi7Hreh+eaxAYTarTw7spO/i3VO/DG09kJJgBBC1Bm5h4rZf7SUse+upMxpEGRVrNldRGpilL+Ldk4ycoq47e0VON36slsPQgghas2GvCMMnvI97/20C4fLQGM2M63cdUGp3PwiLasQt2FOTq4LqTYkQAghAlaJw82W/KMAdGpWn4cGt+Gm7nF1a02FSuraehCSakMIEbDueT+djXuO8MMfBmEPqvg+W5fa8U92McsuqTaEEJcE88Z5kBCbldt7tSDMHsSDg1pT6nRXCQ4QuCOUzsXKrIO88X0WSY3C/V2Us5IAIYTwu4ycIsZMS/POMs49VMyzIzvTLb6hv4vmUxk5Rbz07Q4cLoNHP8mkcf2QgA500gchhPCrHQXHmfHzLm9wsCiICvd/3qTakJZViMtdd9aDkAAhhPCrl7/9hWW/HPR23tqDLFzZupG/i1Ur6lontTQxCSEuqsLjZbz63Q7u7Z9EbINQnh7RAZu1I9mFxXW24/lcpSRE8sDA1rz+/U6m3NIt4N+nBAghxEVV7HDz8apcusY14Mbucd6FfKLrBQf8DdMX+rWNoeBYGb2TAn9ynwxzFULUumnLssg9VMxfRnUG4HCxg4Zhdj+X6tInw1yFEAFp1a5CVmcX0ScpmgPHy9h7pBS3obFa1GUdHOrSHA4JEEIIn/t0dS5/mLsei6fTedbE3qReE/hNKrVNcjEJIS5LDpfBviOlAOQdLgbwrpxWl/Il1SbJxSSEuCzd/k4aD3+8Fq01/ds2JsRWd4ZzXiyX3TBXpVQI8CMQ7Lnef7XWf1ZKtQQ+BqKANcA4rbVDKRUMvA+kAIXArVrr7JqWQwhxcTndBos37WdEl6YopbinX0tCbFbAHM45e1KfOtPWfrHUtc/FFzWIMmCw1robkAwMV0r1Af4FTNVatwGKgIme4ycCRVrr1sBUz3FCiDrm6437eODDNSzfcRCA4Z1jGdiuMUqZay6nJETywKDWAX8TvNjyiop5+dtfaFQv8Dvqa1yD0OY42eOeTZvnnwYGA3d49s8EngXeAEZ5HgP8F3hVKaV0XRhvK8RlKiOniBU7D1LmMuga15BfdWzCiM5NaXB3L666RGc91xaHyyA5viFZB0+QEB3YCft8MopJKWUFMoDWwGvATuCw1trlOSQPaO553BzIBdBau5RSR4Bo4OBJ17wXuBegRYsWviimEOICVE6kpzX0SYriVx2bEGS1MKBtjL+LV6dk5BTx/z7fiMNlcP+sw8yedBmMYtJau7XWyUAc0AvoUN1hnp/qDM9VvubbWutUrXVqTIz8EgrhD99tLeDhj9ZUSaR3VRupMVyotKxC72fpdF1mo5i01oeB74E+QEOlVHkNJQ7Y63mcB8QDeJ5vAMgYOCECRKnTTYnDDUBwkIVQmxWbtfLIGwkQF6pPUjRWi/kdOcga+KOYahwglFIxSqmGnsehwFBgC/AdMNpz2Hjgc8/j+Z5tPM8vlf4HIQLDkWIn/f/9He/9tAuAvq2iWfLYAD68pw+PDWsX8E0igS4lIZLnb+pK36Qo3hqXEvCfZY1zMSmlumJ2QlsxA84crfVflVJJVAxzXQuM1VqXeYbFfgB0x6w53Ka1zjrTa0guJiFqz7FSJxvyjnCFp7P5xcXb6Nc2hp6JMvO5rqtpLiZJ1ifEZe6JueuZv24vK58aQkSIzd/FueQZq6fDls9RHUehUifU6mtJsj4hxHn5YVsBb/ywk7uuSGR451juH9iKO3q3kOBQm3JXQfYyKNiK2jDHHJaT9R2gIPUuPxfu9CRACHGZ0FqzZvdh7v0ggzKXQUZOER/fG9hrItdp5UEhNBoWPg6uUnO4pgalQGsoXvYKYRIghBD+9NcvNnO01EnLRuE4PWsiG4YmLatQAkRtyEmD968HtwssFjAM71Oq0kB/Z+kJPxTu3EmyPiEuUbsOVtx86oUEERESRJ+WUXUqWVydsn4OfP6gWXPI+QncDsAwg4PFQvnttnK3r9Gki1+Keq6kBiHEJWj+ur08/NFavnjwKrrENeCxX7X1PleXksUFpPKmo2YpYA+D+F7mvnmTQRuw4b8w/HkICjZrEFa7uV1SiCrYit4wxwwSCiK7Xuvvd3NGEiCEuAQYhmbp1gKi6tnp0SKSQe1ieOKa9iQ0Cjvl2JSESAkMF0Jr2DgPPv+NWTtQCpQFnthtBoxybgeUFML4L839if3MIALw5aOApw8CYF/mRX8b50MChBCXAJeheeazjfRsGUWPFpFEhNi4b0ArfxerbstdBTuXmjf4xCshczZ8/oAZFLQBWCH5NrDYzGOswWZwsNorgkJ5YPBS3lxDqtJ/A5UECCHqoIycImb+nM3ewyV8el9fc1nPSb1JiD61xiDOk+GGPRkw4zpwl4F1Ctz1FSQNgisfgZVvgdtpBoLu48AaZAaC8fNPrTGcrGm3KonnVNNuF+UtXSgJEELUIUdKnGzNP8r46asocxqg4LttBQxu34TWjev5u3h1T3l/QmI/aNwR3uoPqRPMmoDhSUZtuM1j+v0OfvVXaH9d9YGg2hpDVXvy82iqwarArWFffp43zXUgkgAhRB2RdeA417+ynP5tY8zU25j5bbbkH2Nw+yb+Ll7dk7sK3htuNhcFhZg1gKQBENUKwhuZNYTKTUblziEQnM62o3aaYXZnWDzbEiCEEBdk275j7D1SwqB2jWnZKJyxfRNo36Q+320rwOkyZKjq+Up7A/ZtgBteN2sB2gC0GQiyl8F1UyuOPZcmo/PUrr4DjZk23a0V7eo7fHLd2iIBQogA9tcvN5F/uJSBbWNQSvHkNeZSKy2iw2So6tnkroI178PRfBjzqTkXofQonDhgzk1I7GfWHKqrJUCNagqn0zx5GMbalzAMJ1htNE8e5tPr+5oECCECyJrdRUxZvI03xqZQP8TG32/oQsNQm3ed53IyVPU0Du2CjBnmzf6TseAqNfdv/xraXwsDH6849lw7ln1Mo1C66ozqQCUBQgg/K3W6cbgN6ofYsFst5BWVsLuwmM7NG9CyUWCvWew3lSerHcmFFn2gURs4cRBWvGr+dDswEx9Z4cBWM0CcrBZqCWeUvQyL4TTnQbhd5nu4mK9/niRACOEHGTlFpGUV0j2+Ib+dk8mo5OY8NaIDnZs34LvfDcRiqQNfL/3BMGDNDPj6CXOUkdVm1hKGPgtX/Raa94A/7jIDwsa5p28+8pfQaCpWWDY824FLAoQQF9nXG/J5+OO1uA2NPcjCr1PjGdqhYhSSBIeTHD8Ax/Ihtqu5vegZc34CgBvo+yBcac5QxmKFkPp+az46G3OYq8KqNIZWAT/MVZL1CXGRTVu+C6dbexeub1I/hF4tZfU2clfBsimQswIKd1bsnzMO5j9kPrZYPHmOQsymI6sdOo6qvkE/vpc5dyFAggPACndHXFhxa4ULKyvcHf1dpDOSGoQQtSzrwHGe+N8G/jKyEx1i63P/wFY8MHsNLrcMU/XauRQ+usPTbwDYQuDx3eYs5SF/BltoxbEpd0Lj9gFXOzgXnZvXR60F5Wlm6ty8vp9LdGYSIISoBSUON4eKHTRvGEp0eDBFJxwcOFZGh1gY2qEJH95zmWdULV8fwWKBzI/gs/s9GewMM9dRm6s9cxSAhL6nnn+xO5d9pH3pOrRyowA7Bu1L1wFD/V2s05ImJiF8TGvNja//xFP/2wBAgzAbi3/bn/5tY7zHpCRE8sCg1pdncMhfB1PaQc5yczuuJySPNZuLlNVMetfnfgiy+7ectSGxHwQFo7GY7y9QOs9PQ2oQQvjAzgPHmZ+5l0eHtkEpxSND2hBdL9j7/MnzGC4L3qGo3eGHF6DbbZAy3kxl0bI/2D25oxq1hhteNZuO6mCz0XmJ70Vmpyc4umYunQaOo1GAv08JEEJcIK21mVPHoli96xBv/rCTUcnNSIqpxzVdYv1dPP/65E7Y9pWZdMhqh9huFf0IwfVg9LunnlNHm43OS+4qum18HmV1wPI/QavuAf2ea9zEpJSKV0p9p5TaopTapJR6xLM/Sim1RCn1i+dnpGe/Ukr9Rym1Qym1XinVo6ZlEOJiysgp4t8LtzLo/77ni/V7Abihe3NWPDmEpJjLNKPq2tnw7V8rtgt/MbOgarfZ8dx2GHT9tf/KFyiyl6HcZSgMcDmqLjQUgHzRB+ECfqe17gD0AR5QSnUEngC+1Vq3Ab71bANcA7Tx/LsXeMMHZRCi1h0udvDJ6t2MmZbGmz/sZPehYvKPmKkcQmxWosIvwTbzcuVDUHNXmdt56VUDwr71kPVDxYLL179cdShqgLe1XyxbQ7rh0BYMDQ5tYWvIJb4ehNY6H8j3PD6mlNoCNAdGAQM9h80Evgce9+x/X2utgTSlVEOlVKznOkIErIc+Wsu63MM4XAZGeU5/Q5/9xLoudxXMvB5cZeY6y+O/MDuaV74FvSZDRBO4+p/mJLVyATpRzd827jlKy0qryG3cc5T2Pf1YoLPw6SgmpVQi0B1YCTQpv+l7fjb2HNYcyK10Wp5n38nXulcpla6USj9w4IAviynEOdm89ygPzF7D8TJz4Zg/Xt2eZ0d2wh5kwaq4tOcwOEth+2I4sse8ybs8eY3K02InjzFTWkR4ZoBXDg7lAnCimr/1tW7GihuLAisGfa2b/V2kM/JZJ7VSqh4wF3hUa330DKM2qnvilK9hWuu3gbcBUlNTL4OvaSIQlDrdON0GESE2ylxuVu4qZEfBcZLjG9IlrpDIB6oAACAASURBVAFd4hqQEB1+ac5hOJRl/oxKMlNif3gLDPu7Z2imvWKZzcR+YJelTS9EebpvDCeWoMsk3bdSyoYZHGZrrf/n2b2/vOlIKRULFHj25wHxlU6PA/b6ohxC1MSJMhcD/+97bkmJ44/D29O9RSQ/PzEEe1DVinadT7VdPvw0rhdEJkDDFubN/60B0OlGGPkfaBgPdy2A5inmrObxX0hzkS/E96K0/9OwaR62HndgCfDPssYBQplVhXeBLVrrFys9NR8YDzzv+fl5pf0PKqU+BnoDR6T/QfjL5r1H2bDnMLf2bEF4cBATrkwkNaEiL9LJwaHOq5LSQkOTTnDfcjMr6s3TzJTZ5RKvrHh8OQxBvRhyV2H/8XlwO7B8+2eI6xHQn6svahBXAuOADUqpTM++pzADwxyl1ERgN3CL57kFwAhgB1AMTPBBGYQ4Z1pr78S1j1bt5ov1exmV3JwQm5XfDGzt59L5mOGu6B9Y9LS5mI7bYQ4/VRZo0rXi2LZX+6WIl5XsZVi1A6UMtNt56a8HobVeTvX9CgBDqjleAw/U9HWFOF8ZOUV8tnYP32zZzwcTe9G6cQSPDG3D74e1I8RWTSdrXbfhv/DVY/BwJoRFQeshZlPSmplmmmyrHVLv8ncpLy+J/dDKitIG2hKECvDhvzKTWlzycg8Vs3Z3EX+cux6Hy0wAl55TROvGETSqlA6jzstfbya9G/kfs+8gujV0uqli2c1Wg81/XUZLf4KfZBhteMPxGAPIYJGrL7812pDi70KdgQQIcUlzuQ1ueuNnGoTaqsxfKDzu8HfRaiZ3FfyyGHavhF73QMeRUL85hEaC2xyWS7NkaPbSqedKf4LfpGUVcsgdSr4lmhJtJS2rMKAHPEiAEJecRZv2sWjjPqb8uhtBVgsv/robJQ43D3+8FqerDq/BoDV8/zy4SmDl256+BMNcZrPjSAiPhru+9HcpxRkMqZfNRPs/sOHCiY3sel2AwO33kgAhLgn5R0qIqRdMkNVCwbEyNucf5XCxk8hwO/3amGm2Z0+qg2swbPkSju6B3pPN9RKyl4OzuFJHs2eJTVEnmOtBuFBoLLhkPQghatv6vMNc9a/vWLJ5PwB39GrB14/0I/Kk3Eh1Yg2Ggq2w6p2K7e1fw6q3K3IcjZ8P1/yr0toJkueoTpH1IISoXVprPsvcQ3CQlRFdYunUrAEPD25D1/iGAFgtdWDthfLJarHJUFIEHUaaN4ztC+Hbv5gT1sIbwdXPmesmlGcmsFglz1FdVsfWg1BaB34Wi9TUVJ2enu7vYgg/K3W6vcNRb3z9JyLD7Lx3VwBnOquO1pD5oTn81O00b/huhzlTuWV/KD5kHhNeB/tIxNnlrkLPuM78fx4UjBr/Ra0GeKVUhtY69ULPlxqECFgZOUXePoONe47w6nc7+PEPgwi1W3nnzlSiwupIeu3SI2Ym1HqN4eAv8PlvMKcOaTCA1LshwTNrOSzqDBcSdV72MnA7UGh0+XoQAVyLkD4IEZBWZhVy29srmLJ4G2OmpWGzKq7tEkup0w1Ao3rBWAK1KUlrMyiAmQX1xY6w3DPctFEbGPikmTa7vA+h2+3VZ0MVl5ytId0o00G4tKJUB13660EI4UvlaTCWbNmP0202fzpdBkXFTp4d2cnPpTuDyiktZlwH9nAYM8fsVxj+PDTtbD6nFAx8wpywJn0Il51vjyfyreNp+li2sFJ3YPDxRNr7u1BnIDUIETCe/N96/jx/EwDXdI4N3HUXTl5d7du/wet9K0YaJd8OnW+uOL7HOGjWveo1ZK2Ey1KfpGiaWw/T0ZJNhMUZWL/X1ZAahPCbUqebVbsO0b+tOU+hXnAQNqv5nSUlIZKP7gnAeQu5q2DGtZ5ORk8a7KadzW2306wxdB/r71KKAJVi+YXu9tewGE5GWDOxWAYCgfslQQKE8Jt3l+/i/xZv44ffD6JFdBhPX9uxyvMBs+7C/s2w+BkY/pynk9GTyqI8G2e/35nDUoU4m+xlKG32oynDJZ3UQpTbd6SU+2dlsGrXIQB+nRrPrIm9iY8K9XPJTlJSBAv+CFnfm9vBEXB4Nxzf753oJJPUxAVJ7Idb2XFpC9pqC/jfH6lBiFpV7HBRcLSMxEbhNAi1sXXfMfKPlAAQExFMTEQAZFPVGlZPg3pNzJxG9nqw+TNz6c2kgebqag9Vmocjk9TEhYrvRcm1r+Le8F9Cr7iH4AD//ZEAIWrVbW+nYbNamHv/FYTarXz72AD/DU8tn72c2A8MFxzLNzuTlYKMmRDTzgwQVhs8tuX0Q08lG6qogYgjv0D2Qki91d9FOSsJEMJnMnKKmLcmj71HSnjnzp5YLYrf/qotEcEVv2Z+Cw5bv4JP7zKHo1rtEN8TinLM9RKUMrOghjasOF7mJYjakLsK908vobCg592PtUFcQH/ZkAAhauxIsZNNe49w98zVlDkNNPD1xnyu69qMQe0a+6dQbifkroQWV4DFAmlvetZhxvzZrAfc/nFFjqPKwUGIWrInczFNXE6sysDlcrAnczHNAzhASCe1qJHcQ8X0fu4bpv+cjcNlBgergpzC4otfmKP5UHbcfLxpnjkcNX+tud37vqqdy+2vNSezCXERrXB3xIUVt1Y4sbLC3fHsJ/mRBAhx3uav28snq3cDEBcZyuT+rRjhj4ltbldFQNi/CV5sD9sWmNuth8Kts6BRO3O7w7Uw/ksY/LTZyRzA39rEpatl90FM0yOxKs0Txm9o2X2Qv4t0RpLNVZyTo6VO6ofYAJg0czWHi5389/4rqhxTOblerc1fKE9p4SyFqR0hdaJ50zcMSHvdrBlEtayd1xbCBxZl7uL79Tu5tk9nrmrbtFZfq6bZXH1Sg1BKvaeUKlBKbay0L0optUQp9YvnZ6Rnv1JK/UcptUMptV4p1cMXZRC158OVu+nzz285dMJsw/+/W7oxZ3LfU47z+YI8J6e0+PA2mDvJfGwLgT6/gURPFlSLBa54UIKDCHghhZuI3DaHVmXb/F2Us/JVJ/UM4FXg/Ur7ngC+1Vo/r5R6wrP9OHAN0MbzrzfwhuenCBAnylzMSc9lQNsYkmLq0TMxkjv7Jnqfb3gx0mznroLp15jDUYNCzWahFr3N9Bbl+v++9sshhC/lrqL/z3fT3+aAzz+HhrW7HkRN+aQGobX+ETh00u5RwEzP45nADZX2v69NaUBDpVSsL8ohaqbMZaYAKHG6eW7BVu8Snm2aRPDENe2JCq/lwLB9Ecy83kyRnb3MbE4Cc9RR9jK46rfQ5/7aLYMQtSl7GcqzHoQqT9USwGqzk7qJ1jofwPOzfLxjcyC30nF5nn1VKKXuVUqlK6XSDxw4UIvFFAD3fZDBwx+ZI34a1Qtm6e8HMHlAq9p90QPbYd79cNjz62C4wXGiUkqLEElpIS4tif0wgoIxkFQbp1PdTKlTesq11m8Db4PZSV3bhbrcLNm0n0/Sd3P/QLPPoFfLqCr/E+Iiw3z/oqVHzJQWSQOheQqgYfvX0PXXZjqL9iPMf2BuS0oLcamJ78WcDq+ye80SrhwwiisD/Pe6NgPEfqVUrNY639OEVODZnwfEVzouDthbi+UQHoZhhoC1uYe5f3YGLkOz7JeDfHhPH+6+qhY6dw0DtnpmKLfsD5Yg+OHf5s/mKdCoLfxhp6S0EJeNjJwiZq0t4ipl4bdLi3kjqSgwMhafRm02Mc0Hxnsejwc+r7T/Ts9opj7AkfKmKFF7cg8VM+TFH1i8eR9pWYUYnuHNLrdBWlahj15kFXz9BKx809xWCpb8CVa9Y27bw+F32+DKRyqel5QW4jKSllXIUFbwRNBH9HKv993fXi3xSQ1CKfURMBBopJTKA/4MPA/MUUpNBHYDt3gOXwCMAHYAxcAEX5RBnCoz9zDHSp30axNDs4ahtGsSQUSIjT5J0diDLDhdRs0ntZUdh4LN5uOZI8FVAigzlUV8Lxg3DxrEVRwvKS3EZWxIvWwSrF/i0ooXbG+SU28A0NrfxTotnwQIrfXtp3lqSDXHauABX7yuOJVhaG9CvGfnb8LQmn5tYrBaFG+OS/EeN3vSBa7WpjUU7oToVmYN4PvnzBpCv8cqch0pS8VCKDIvQQiv9qXrMJQLCxoLbtqXrgOG+rtYpyWpNi4h/83IY+D/fU+p0xwe+n+3dGP2pOqnmJzXpDZHsTn0FGDdx/BqChzcbm73GA9jPoWWA8zRRjLqSIjTS+yHstrQKFRQ4I9ikgBRhzndBgs25HPgWBkAzRuG0i2+IUdLnQC0blyPCE96jPNWPgdh30b4d0vYscTcThoI174I4eY60sS0haQBkNDXHHUkuY6EOL34XqgrHzXnQdzwRsD/nUgupjpIa41SiqwDxxk85QeeubYDk/ol+ebijhPwVn/oPg6uetRMm/3Ns5A8BpoEduZJIeqCnQVH+fmXAkZ2b0GDWs5KUNNcTLIeRB2iteaRjzNpGGbjr6M6kxRTj7n39yU5/gKGyVVeXW31uxDSAEb82xxplDQQoj0dZ1YbXP0PX74NIS5reRt+JH/pfIrrj6FBlwH+Ls4ZSYAIcN9vK2Demj3ceUUiKQmRNG0QUmWFtpSEqPO/6MKnKoaiWu3m5LTQSkHm2ik1LLUQolq5q+j30wT625zozz6Dhl8GdDOTBIgAVN7st2b3YSbNTMdlaBZu2seH9/ThqREdzv+Cu36EtbPhhjfMrKcFm0FXynPUpBP0+50P34EQojp7MhfT1O3EojRul5N8WVFOnI8dBce45uVl3rUVLmhCW1E2LPkznPAcf2wf5PwER/eY24OeMjOkyogjIS6qFe6OOLDh0hacBAX8inJSgwgAG/KO4DIMureIpFnDUBqE2nC4jXOf0FZ2HDZ/BnE9IaYdFBfCilfN9Bath0Dnm6HLLRXrL8f3kjxHQvhBy+6DuDP9aXrqzWSoTvxRVpSruUtxFFP5SCTD0Aya8j0tosL4YOKpcxaqXaXNMCBvtbnGcrNkKD4EL7SCQU+bayQYBjiOQ0j9i/yuhBBns2H1D5RmfkpQv0fp3r52Z1HLKKY66P0V2XyyOpcvHrwKi0Xx2h09iI+qPntqiuUXUoKWQVkXyG8KsV3NmsCcO825B7fMgLAoeDAdojxDXS0WCQ5CBKguJ1ZA/kfgHkIgp9kACRAXRanTzcKN+/hVxyaEBwcRUy+YNo3rcazMRYNQG52bNzj1JMMwm40++40nhYWGyER4eK0ZIG7/sCIggJn6QggR2HJXYSybCoaBmncfqn7zgG7ilU7qWlTefLdp7xEe/SSTxZv3AXBNl1heuq07DUJPmuVceqTi8aInYd5kMziUjzhqVSm1VfOUqkNThRCBL3sZuB1YMMxJqJfxinKXLafbYNy7K5n6zS8A9GgRyZzJfRnV7aSF8wzD/Aewfg78K7FidbWut8JVvzMnqikrWIPNhXWE32UWZDJtwzQyCzL9do2anO+L8osLVCkXE7Ki3KWvvBO5VUw4Wpu1A5vVQlxkGDH1zGn0Sil6tTxpQtu+DTDrZrjxLWg1yKwR9P+D+UsD0LyH+a/1YBlt5COZBZmk708ntUkqyY2TT3n+hPMEJa4SGoU2AiD7SDYnXCfoFN0JgNX7VrPhwAbeWPcGDrcDq8XKhE4TeKjHQwDM3T4Xl+Hi1va3AvDexvewKivjO5nLoryU8RIhQSH0ie3DPYvvodRdilVZmTF8BsmNk3n252eJDY9lcrfJADyz/BkSGyQyqcskAJ5c9iTto9rTLaab9/wgSxDTr55OcuNk/pH2D5IbJ3Nt0rUA/GfNf0hunEz/uP4AzNw0k1BrKC+kv+At/9+v+jsjWo5Aa82uo7toFNqI+vZT+6/O9tmJcxTfi71dHqDJulfI6fcirQL8b1pqEDWQnn2IMdPSmLJ4Gw98uJZHP8mk2OEC4LmbujCub2LFwWXHYPoISH/P3I5KMm/65Z3J0a3M+QkRTau+SHwvcxJbgP8i1YaTv+k63U4OlR7yNt3tOb6Hn/f87D1+bcFaZm6a6d3+Kusrnln+jPdaExZO4OU1L3PP4nvILMjknyv/yfC5w73H/yPtH4xdMNa7/Vrmazzx4xPe7dlbZjNz80wcbgcGBk7DyWc7P/M+/+3ub1mUs8i7vf7AetYdWOfdzj+Rz/7i/aTvT8fhSY1uaIP0/eYIvWJXMWXuMu/xp2w7iyl1lVY53224veen708n+2i29/g52+ewZv8a7/aU9CkszFlYpfzzfpkHQJm7jFGfjeLTbZ8CcMxxjO7vd+fDLR+SWZDJpMWTeHnNy9y96G4yCzI57jjOK2tfYeuhrQCUukrJLMjkSFlFM6nUVE6VkVPE4PRetCmZyYglUWTkFPm7SGckNYgLtDr7EHdPX43DZWBosKC568qWhNkrfaRfPwFh0TDgDxAcASENweYZrWQPh9Hv+qfwteRs3zKLncXsPb6XFvVbYLfayTmaQ9reNK5rdR3htnBW5a/iv9v/yzN9nyHrcBYTFk3AZbgItgYzbdg01hSsYWrGVFbesZIwWxhf7vySVzNfZc3YNdisNn7e+zNvrnuTcR3HYVEW9hfvZ1PhJsC8ebo9fTlOw0n6/nR6Nu1JZEhFP84NrW/gyuZXercndZlEiavEu/1076fZeHAjf/zxjzgNJ0GWIP7V71/e518f+nqV9/vSoJeqbP+r/7+8n5PdasdpOLFZbKQ2MUch/rv/v6sc/+LAF6tsvzz45TOeP2/UvCrHL79teZXttDvS2FS4id8c+A1Ow4lVWRnbwQyIVmXl3/3/TdvItt7t8Z3G0yG6A+n703G6zQzB5QEpMiSSaRumkdQgifZR7ck7lse4r8fxwoAXGJ44nPk75vP0T0+jUARbg/lnv3+y/sB6RrcdTUL9BIqdxRSVFdEkrAlBliDv+7rUaylpWYU4XAYasyk6LaswoJcclQBxjtyG5vttBTSpH0Ln5g1IiA6jdZN62Pam05NNZKhO3GbPh28+gaHPmiedKKiYnAbmyKM6zGW4OFhykPr2+oTZwjhYcpDvcr+jf/P+5J/IZ+KiiTgMB3aLnXevfpcTzhM8vuxx3h32Lu2i2rFszzJ+/8Pv+d/I/9Emsg0bD27k7yv/Tq/YXrRs0JKisiI2H9pMsbPYvKEbVW/oVzS7gqd6P4XVs0zp9a2up3dsbyzKrAhP7DyRiZ0nojA/87s7383dne8GILVJKsHW4Co31eTGyfwq4Vfe99crtmotrV1UuyrbMWExDGoxiHeGvVOjG1ly4+QaXeNCzw+zhdGzac9qz7VZbVzT8poqxz6a8igACnVKQEqon8DacWsxtNmHFlsvljeGvkG7SPMz++Ww2f+m0TgNJ8vzlvNl1pcMaTGEhPoJpO9P54FvH2DWiFl0i+nGnG1z+MfKf4AGu9XO1IFTiYuIIz4i3vv/+1LQJymaO2zfM1p9yxv6ZvokXeHvIp2RTJQ7C6fbwGa1UOp00+e5bxnWsQn/Ht3NfDLjfYwvfwvaAKsdS/sRULQLJi015yIEKK016w6sIy0/jQbBDRgUP4im4U0pKi1i+qbpXJ1wNZ0adWL30d08+v2jPJbyGFc1v4pNBzdx21e38crgVxgYP5B1B9YxdsFYXhvyGtuLtvPKmlcwMLBg4aEeDzEofhAfb/2Y8Z3GExcRx74T+8gsyKRvs740CG5AsbOYYlcxkcGRp9wEMgsyuWfxPd6b0jvD3qnxt8rL4RtqbTnfz666/3/dYrqh0ViUhX0n9vHz3p8Z0mIIDYIb8P+W/z9vc51VWekf15/vcr9j8c2Lia0Xy897fuaHvB94pMcjhNnCWJm/kvUH1tOzac+69f8ydxXG9BEow4m2BmO5q3aT9clEuVr01y82sy7vMHPvv4IQm5VPx7Qkaf8SKEsym4y2f41Fm30OGE5o3BFume7TMpzLH6ahDSzKgqENVuxdQdPwprRq2AqH28E/V/6TAXEDGNRiEEfKjnDN/67hhlY38On2T71t0XnH8vhDzz/gNJzM2jyLVg1a0alRJ8JsYcTXiyfEGgJAXEQcf+77Z9pEtgGgQ1QHvhn9DVEhUdS31z/lW2arhq14us/T3nI2DW/K8JYVbf5htjDCbNVPEKzpt+zTXbNO3UwCyPl+dqf7/1deu2sa3pSb2tzkPf7mtjezMHuh9/fnuqTrGNJiCE3CmwCQdSSLBbsW8IeefyCzIJPJSybj1m5v82O4LRxDG1VqfQH5hSB7GUob5qdguCqW5g1QEiAqKThayry1e5h4VUuCrBY6NQ6mw/GtuA41JygqgTaWfFj8JDRuC62HQu/7YedSczyz1W6urOYD5Wk4MgsyuXvR3bgNN3arnXeGvcOCXQtoF9mOm9veDMDATwZyXdJ1/L7n71EoHlr6EGM7jOWx1MewWWws37OcVg3NSXQR9giuS7qOE84T3uBgwYLdYo62igmNIWNsBsrTLNYotJG33RugQXADRrcd7d22W+3eP2C5oYuTnc//v7P9/oztOJYxHcaglCJ9f7q3actluEjfn872Q9tZd2Adi0abgwReXfsq7218r8rfTkD8LiX2w61sYGgsQTaUDHMNbG5D4zIMgoOsrNl9mFkLf+CKyCN06dqDmzuEwte/hfi/mKurtegLj6wzZzQDJPWH8V+c1zDUrCNZlLnK6BBtpu1+b+N72C12xnY0OwvHfDWGZvWa8cKAF8zOQcPsHCxvh99SuIVwW7j3ere2u9V7LaUUM4fP9N60lVJ8c8s33mMtysJTvZ8isyCTBbsWeL+tDYgf4D2+JuSGLmribL8/5b+f1fUnXZ1wNfuL93uP/Srrqyp/O6+sfYUbWt/A9a2ur903cTbxvQga8S/ImG6OWgzg2gNc5gHi0AkHN/3nOx7sFcHoIVcwpF00w+o/i2XX9dC1B9RvBhO+hmbdzROstorg4PGTxcnPIQa/CraTDCzJWcL+E/u9N/xnf36WguIC7wiXf678J2WuMj4Y8QEAGfszCA0KZSzm8cMSh9EwuCFg/iHYLXZchsv7h1A+Jr7c/cn3V9nuEtPlrO+7Nr7tC3GxnO73N75+vPeY5/o9x6TFk7x/O/tP7GfjwY3eAPGnn/7E0ISh1LfXv/h/B2FRcLzAzLwc4PzWSa2UGg68DFiBaVrr5093rC87qX/cfoCiQwcZ1cf81p3zQj8ahtlo8MBS84Dti9BRrVCNzCRa24u280vRL97JRx9u+ZCf9v7Ea0NeI7Mgk/ELx2NogxBrCO8Me4dPt3/K+gPr+eLGLwD4YPMHFJUW8XCPhwHYeHAjSinv5KuzCch2VCHqgJP/dlyGiyBLEEcdR7njqzsYEDeAOdvm4HA7UErxfL/nq/SR1ZaNe44wf91e7hvQiqhwWZP6FEopK/Aa8CsgD1itlJqvtd7s69f6/IdprM76mp5J1zBqwCSKFjxO8+KlOFO3YQsK5sDA2/mgcB2Pe6qrM537eGXh06TdkUaQJYhF2YuYtmEawxOHY7VYzRTd2kBrbU5Q8sTX8iagZ694liBV8bGO6ziuSnk6N+p8XuWXZhshLszJfzvl8y3q2+vz5Y1f8s76d7x9cWhYsXcFw1sOp6C4gN1Hd2NRFtYUrPH5l7NNK5dgX7OYLaGjuHLQtT67bm3wVxNTL2CH1joLQCn1MTAK8GmA+PyHafwl6yWcCr7O2gbAkW6JPLM7giXH82naMJFdDWJYuHMT95UdoVFoIzpEdWBsh7HeiVC3t7+dm9rc5B1rf3v727m9/e2ApwnopJE7NovttOURQgSOnk17ev9+gyxBjGw9EoD5O+fz8pqXzX4OtxOb1ca0YdN8EiS2rv6GG9ZNxmZ14fj+f2yt9zHtew6t8XVri78CRHMgt9J2HlBltRyl1L3AvQAtWrS4oBfJyF6ESwFK4UKTkb2Ie2+aStOWfYgIjwFgdJvR3NL2Fu85vWJ7VZkwVZ6XpzrSli9E3XW6v9/b299O9pFsvtj5BQYGZe4y/rrir8wdObfGAzmKNi+lDW4sCoK0m6LNS0ECxCmq+5SrdIZord8G3gazD+JCXiQl8WoWZG3BhSZIQ0rLq4mLiCMuIq6iIDJyR4jLVnV/v+G2cEa3Hc2i7EU4DScKRfuo9t57xYxNMyhxltC3Wd/z/tuP7DgYR9Y72LQLJ0FEdhzss/dSG/wVIPKA+ErbccBeX7/IqAHmiJ+M7EWktLzauy2EEGdyutrFkuwlTEmfgkLx3sb3znt+RfueQ5mT9ya5axbTd/Aorgjg2gP4L0CsBtoopVoCe4DbgDtq44VGDZgkgUEIcd6qq11kH83GgsWbDffrXV9z3Hmcq5pfdc7X/XWPWHCVQWqKr4vsc35JGKS1dgEPAouALcAcrfUmf5RFCCHOVXnHtlVZsVls5BzN4allT1HsLD739OYlh2H/JqiUyj1Q+W2inNZ6AbDAX68vhBDn6+Smp47RHdl1ZBfbi7Zzz+J7KHOXYbfazzjqadtxO9lBg+hdsJuGDS9sAM7FErgpR4UQIgAlN05mUpdJJDdOxm610y6qHen70ylzl6HRONwO7yJOp8hdReuvxzBk3zvUn3Mz5K66uIU/TxIghBCihsrzQ1mwYLfaSW2SytqCtac2OWUvw+p2EISBxe0087gFsMs6F5MQQvjCyU1Pxx3H+c23vwEg2BpcMdopsR+GxYoyDLQlCItkcxVCiEtf5VFPL2W8hPZM7SpPw5PcOJkMow3/cvw/UvUmMlyd+KPRhkAeyyRNTEII4WMD4wcSYg3xjnZKjjEDR1pWIatdrXndPYrV7takZRX6uaRnJjUIIYTwscpNTmXuMp5c9iTXt7qeJtHd6W/bzAS+4E9Mpk9StL+LekYSIIQQohaUNznN3T6XgpIC3t3wLnarnT8PGEuHdS5eHZ5M14RIfxfzjCRACCFELSoqKwKNd/b1ekc+hFzBFUGH/F20s5I+CCGEqEXlywJYsACaoRs+YPiBd2n46WiZKDGjwQAACJpJREFUByGEEJez8v6I61pdh9KahWF2NgYH1Yl5EBIghBDiNJYsWUJSUhJKqRr9696kO1PvmorL6WZuRDiTmjZmpcVC3zseP+u5SUlJLFmyxC/vXwKEEEKcxuTJk9m1a5dPrhXePhxtURhK4cDCAzvCSMtzn/W8Xbt2MXnyZJ+U4XxJgBBCiNPwVXAAOLH1BNql0RoMLGxbe/bgUBvlOB8SIIQQ4iIo2VnCrn/vIqjITZvjDppE1/N3kc5KAoQQQpwjrXWN/k3/3yKMhnZ2RgQRemcYH6/78bTHBgIJEEIIcZEszvoZrQy0ApSbxVk/+7tIZyQBQgghLpKRkdHYMEBr7NpgZKSk2hBCCAGMshzBebCIuRHh3Fd0lAGtj/i7SGckNQghhLhYEvvR2q0ZUlxCA4sVZD0IIYQQAJnBdibGNsVpOLFbbUwLtlP9ytWBQWoQQghxkaTvT8epXWgFTsN9+rWrA4QECCGEuEhSm6RiU1aUBpsliNQmqf4u0hnVKEAopW5RSm1SShlKqdSTnntSKbVDKfX/27u/GLnKMo7j35/rbqvQUJG21rboVKqxJrrUDaFRuFCkf24WDIl7JRcaEoVYLryoaWLwggs1emFCpBRq0BhLRZtuYrWAoiYa2y6ybbdsFhYoYW3drTH80cTi0seL826dLGdmujs7e+YMv08ymXPec3bO8+xztu+c9z0zHZO0pap9a2obl7SzmeObmZVJ78peHvrE3Xztnat48IbvXvwvSttVs3MQI8Dngd3VjZI2AgPAx4D3A09I+nDafB/wOWACOCZpMCKeaTIOM7OWk9T0a9zwhc1c3ftufrRjB38eHFmAqFqnqSuIiBiNiLGcTf3Avog4HxEvAuPAdekxHhEvRMQbwL60r5lZx9u87Rpe3/oax9/3d/5zS7B52zVFh1RXq+Yg1gAvV61PpLZa7W8h6Q5JQ5KGzp0716IwzcwWzweuvYwLABLTytYvRaVSaWlctTTsICQ9IWkk51HvnX/edVjUaX9rY8QDEdEXEX0rVqxoFKaZWdt76el/0x3QFUF3ZOuNVCoVdu/e3XC/Vmg4BxERN83jdSeAdVXra4EzablWu5lZW2nFl+Yd/MODPHX6MJ+sbKH/0JcX/PUXUquGmAaBAUlLJFWADcBR4BiwQVJFUg/ZRPZgi2IwM2s7E1ec5yDPsvTqdY13Llizt7neKmkC2Az8StJhgIg4BewHngF+A9wZEW9GxDRwF3AYGAX2p33NzDre8NQwe0/uJQh2/WkXw1PDRYdUV1O3uUbEAeBAjW33AvfmtB8CDjVzXDOzMhqaHGL6wjRBMH1hmqHJobb+LIQ/SW1mtkj6VvXR09VDl7rofkd323+S2l/WZ2a2SHpX9rLn5j0MTQ7Rt6qvra8ewB2Emdmi6l3Z2/YdwwwPMZmZWS53EGZmlssdhJmZ5XIHYWZmudxBmJlZLncQZmaWS634MqqFJukc8FITL3EV8I8FCqddOKdy6MScoDPz6sScPhIRy+b7w6X4HERENPV935KGIqK9P7I4R86pHDoxJ+jMvDo1p2Z+3kNMZmaWyx2EmZnlert0EA8UHUALOKdy6MScoDPzck6zlGKS2szMFt/b5QrCzMzmyB2EmZnl6ugOQtJWSWOSxiXtLDqe+ZJ0WtJJScMzt61JulLS45KeS8/vKTrORiTtlTQlaaSqLTcPZX6QandC0qbiIq+tRk73SPpbqtewpO1V276RchqTtKWYqOuTtE7Sk5JGJZ2StCO1l7ZWdXIqba0kLZV0VNLxlNO3UntF0pFUp0ck9aT2JWl9PG3/YMODRERHPoAu4HlgPdADHAc2Fh3XPHM5DVw1q+07wM60vBP4dtFxXkIeNwKbgJFGeQDbgV8DAq4HjhQd/xxyugf4es6+G9N5uASopPOzq+gccuJcDWxKy8uAZ1Pspa1VnZxKW6v0+748LXcDR9Lvfz8wkNrvB76Slr8K3J+WB4BHGh2jk68grgPGI+KFiHgD2Af0FxzTQuoHHk7LDwO3FBjLJYmIPwL/nNVcK49+4MeR+QuwXNLqxYn00tXIqZZ+YF9EnI+IF4FxsvO0rUTE2Yj4a1p+HRgF1lDiWtXJqZa2r1X6ff8rrXanRwCfAR5N7bPrNFO/R4HPSlK9Y3RyB7EGeLlqfYL6J0Q7C+AxSU9JuiO1rYqIs5Cd/MDKwqJrTq08yl6/u9Jwy96q4b/S5ZSGIa4le3faEbWalROUuFaSuiQNA1PA42RXOq9ExHTapTruizml7a8C7633+p3cQeT1jGW9p/dTEbEJ2AbcKenGogNaBGWu3w+BDwG9wFnge6m9VDlJuhz4BXB3RLxWb9ectrbMKyenUtcqIt6MiF5gLdkVzkfzdkvPc86pkzuICWBd1fpa4ExBsTQlIs6k5yngANmJMDlzGZ+ep4qLsCm18iht/SJiMv3hXgD28P+hidLkJKmb7B/Sn0bEL1NzqWuVl1Mn1AogIl4Bfk82B7Fc0sz37FXHfTGntP0KGgyPdnIHcQzYkGb0e8gmZQYLjmnOJF0madnMMnAzMEKWy+1pt9uBg8VE2LRaeQwCX0x3yFwPvDozvNHuZo2/30pWL8hyGkh3k1SADcDRxY6vkTQu/RAwGhHfr9pU2lrVyqnMtZK0QtLytPwu4CayuZUngdvSbrPrNFO/24DfRZqxrqnomfgWz/JvJ7tb4XlgV9HxzDOH9WR3UxwHTs3kQTZ2+FvgufR8ZdGxXkIuPyO7jP8v2buZL9XKg+xy+L5Uu5NAX9HxzyGnn6SYT6Q/ytVV++9KOY0B24qOv0ZOnyYbejgBDKfH9jLXqk5Opa0V8HHg6RT7CPDN1L6erDMbB34OLEntS9P6eNq+vtEx/FUbZmaWq5OHmMzMrAnuIMzMLJc7CDMzy+UOwszMcrmDMDOzXO4gzMwslzsIMzPL9T9BN1yTpOPzKgAAAABJRU5ErkJggg==\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ " plot_probes([(23, 32), (23, 23), (23, 8)])" ] @@ -3417,22 +3078,9 @@ }, { "cell_type": "code", - "execution_count": 92, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ " plot_probes([(23, 100)])" ] @@ -3446,22 +3094,9 @@ }, { "cell_type": "code", - "execution_count": 93, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ " plot_probes([(23, 100)]); plt.axis('square');" ] @@ -3477,8 +3112,1480 @@ " - Now if a time step has 10 `vx` and 12 `vy` intersects, then that's 120 hits. \n", " - The number of simulations we have to do is the sum of the lengths of `vxs` and `vys`, not their product.\n", " - This technique could have cut the number of simulations from about 50,000 to about 500. \n", - " - But 50,000 is a small number and the code runs in under a second, so we don't need a re-implementation" + " - But 50,000 is a small number and the code runs in well under a second, so we don't need a re-implementation" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 18](https://adventofcode.com/2021/day/18): Snailfish\n", + "\n", + "- **Input**: The input is the math homework for some snailfish: each item is a *snailfish number*. A snailfish number is either a regular number (a non-negative integer) or a bracketed pair of two snailfish numbers separated by a comma. \n", + "\n", + "For now I'll leave each line of the input as a string:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "in18 = parse(18)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- **Part 1**: Add up all of the snailfish numbers in the order they appear. **What is the magnitude of the final sum?**\n", + "\n", + "From [the Day 18 instructions](https://adventofcode.com/2021/day/18), I determined that I will need to do the following:\n", + "- **Add** two snailfish numbers.\n", + "- **Reduce** a snailfish number to simplified form.\n", + "- **Split** a regular number that is inside a snailfish number.\n", + "- **Explode** a deeply-nested pair inside a snailfish number.\n", + "- Compute the **magnitude** of a snailfish number.\n", + "\n", + "What representation of snailfish numbers can best handle these operations? I considered three candidates:\n", + "1. **String**: a string in the same format as the input file. I could manipulate them with regular expressions.
But as my friend Jamie Zawinski [famously said](http://regex.info/blog/2006-09-15/247), \"now I have two problems.\"\n", + "2. **Tree**: a nested tree of pairs, each a Python list of two elements. The input string can be directly converted into this form with `functools.literal_eval`. But **explode** needs to find the \"previous\" and \"next\" numbers–that would require walking up and down the tree, or maintaining next/previous pointers. Sounds complicated.\n", + "3. **Flat list**: A flat linear list of regular numbers, with annotations giving the nesting level of each number. \n", + "\n", + "I decided to go with the flat list. The split and explode operations should be easy. Finding the previous and next number is trivial. However, for the **magnitude** calculation I think I will want to convert to the nested tree form. \n", + "\n", + "I will define these three data types:\n", + "- `Snum`: a *snailfish number*. Implemented as a mutable flat list of `Num` objects. \n", + "- `Num`: a *regular number*. A mutable object with two fields: `.n`, the number itself, and `.level`, the nesting level. \n", + "- `Tree`: a *snailfish number* represented by either an `int` or a list of two `Tree` elements.\n", + "\n", + "For example, the snailfish number `[[7,[8,9]],10]` would be represented as:\n", + "\n", + " Snum([Num(7, level=2), Num(8, level=3), Num(9, level=3), Num(10, level=1)])\n", + "\n", + "Here are the three type definitions:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Snum(list): \n", + " \"\"\"An Snum is a list of Num components.\"\"\"\n", + "\n", + "@dataclass()\n", + "class Num:\n", + " \"\"\"A \"regular number\" within an Snum, annotated with its nesting level.\"\"\"\n", + " n: int\n", + " level: int\n", + " \n", + "Tree = Union[int, List['Tree']] # An int like `1` or a list like `[2,3]` or `[1,[2,3]]`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Below are the four main operations. (I wanted to be sure I got the instructions right, so I copied them mostly verbatim into the docstrings.) Note that `split` and `explode` mutate their argument, and return `True` if they changed the argument. That simplifies the control flow in `snum_reduce`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def snum_add(left: Snum, right: Snum) -> Snum:\n", + " \"\"\"To add two snailfish numbers, form a pair from the left and right \n", + " parameters of the addition operator. Snailfish numbers must always be reduced.\"\"\"\n", + " snum = Snum(Num(x.n, x.level + 1) for x in left + right)\n", + " return snum_reduce(snum)\n", + "\n", + "def snum_reduce(snum) -> Snum:\n", + " \"\"\"Mutate snum until it is in reduced form.\n", + " To reduce a snailfish number, you must repeatedly do the first action \n", + " that applies to the snailfish number: (1) explode (2) split.\"\"\"\n", + " while explode(snum) or split(snum):\n", + " continue\n", + " return snum\n", + "\n", + "def split(snum) -> bool:\n", + " \"\"\"If any regular number is 10 or greater, the leftmost such regular number splits.\n", + " Mutate the snum and return True if there was a split; False if not.\n", + " To split a regular number, replace it with a pair; the left element of the pair \n", + " should be the regular number divided by two and rounded down, while the right element \n", + " of the pair should be the regular number divided by two and rounded up. \n", + " For example, 10 becomes [5,5], 11 becomes [5,6], 12 becomes [6,6], and so on.\"\"\"\n", + " i = first(i for i, s in enumerate(snum) if s.n >= 10)\n", + " if i is None: \n", + " return False\n", + " else: # The number to split is snum[i]\n", + " level = snum[i].level\n", + " L, R = snum[i].n // 2, (snum[i].n + 1) // 2\n", + " snum[i] = Num(L, level + 1)\n", + " snum.insert(i + 1, Num(R, level + 1))\n", + " return True\n", + " \n", + "def explode(snum) -> bool:\n", + " \"\"\"If any pair is nested inside four pairs, the leftmost such pair explodes.\n", + " Mutate the snum and return True if there was an explode; False if not.\n", + " To explode a pair, the pair's left value is added to the first regular number \n", + " to the left of the exploding pair (if any), and the pair's right value is added \n", + " to the first regular number to the right of the exploding pair (if any). \n", + " Exploding pairs will always consist of two regular numbers. \n", + " Then, the entire exploding pair is replaced with the regular number 0.\"\"\"\n", + " i = first(i for i, s in enumerate(snum) if s.level > 4)\n", + " if i is None:\n", + " return False\n", + " else: # the exploding pair is: [snum[i], snum[i + 1]]\n", + " if i - 1 >= 0: # The pair's left value is added to the number to the left\n", + " snum[i - 1].n += snum[i].n\n", + " if i + 2 < len(snum): # The pair's right value is added to the number to the right\n", + " snum[i + 2].n += snum[i + 1].n\n", + " snum[i:i+2] = [Num(0, snum[i].level - 1)] # Replace the pair with a `0`\n", + " return True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here are routines to convert from a string into an `Snum`, and from an `Snum` to a `Tree`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def snum_from_str(snum_str: str) -> Snum:\n", + " \"\"\"Convert a string representing a snailfish number into an Snum (a list of Nums).\"\"\"\n", + " level = 0\n", + " result = []\n", + " # Break the string into pieces, e.g. '[[7,8],10]' => '[[', '7', ',', '8', '],', '10', ']'\n", + " for piece in re.split(r'(\\d+)', snum_str):\n", + " if piece[0] in '0123456789':\n", + " result.append(Num(int(piece), level))\n", + " else:\n", + " level += piece.count('[') - piece.count(']')\n", + " return result\n", + " \n", + "def tree_from_snum(snum) -> Tree:\n", + " \"\"\"Convert an Snum into a nested tree of two-element pairs.\"\"\"\n", + " q = deque(snum)\n", + " def grab(level):\n", + " return (q.popleft().n if q[0].level == level \n", + " else [grab(level + 1), grab(level + 1)])\n", + " return grab(level=0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, the **magnitude**. I can't see an easy way to directly compute it on an `Snum`, so I'll convert to a `Tree` first." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def snum_magnitude(snum) -> int: \n", + " \"\"\"The magnitude of a pair is 3 times the magnitude of its left element \n", + " plus 2 times the magnitude of its right element. \n", + " The magnitude of a regular number is just that number.\"\"\"\n", + " def mag(tree): return tree if is_int(tree) else 3 * mag(tree[0]) + 2 * mag(tree[1])\n", + " return mag(tree_from_snum(snum))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now the puzzle solution:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "homework = mapt(snum_from_str, in18)\n", + "\n", + "answer(18.1, 4457, lambda: snum_magnitude(functools.reduce(snum_add, homework)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Great! It worked the first time! That justifies my choice of a flat list as the easiest representation for `Snum`.\n", + "\n", + "- **Part 2**: **What is the largest magnitude of any sum of two different snailfish numbers from the homework assignment?**\n", + "\n", + "My implementation is fast enough that I can try all possibilities:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "answer(18.2, 4784, lambda:\n", + " max(snum_magnitude(snum_add(L, R)) \n", + " for L in homework \n", + " for R in homework if L != R))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 19](https://adventofcode.com/2021/day/19): Beacon Scanner \n", + "\n", + "- **Input**: The input is a sequence of *scanner reports*, where each report is a list of the relative three-dimensional distances to beacons that the scanner can see.\n", + "\n", + "I'll `parse` the input into sections, then split each section into lines, ignoring the first line with the scanner number, and parsing the remaining lines of each section into a sequence of 3D points, and collecting them into an object of type `Scanner`, which I define as a subclass of `set`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Scanner(set):\n", + " \"\"\"A Scanner is a set of points representing the beacons that the scanner can see.\"\"\"\n", + " \n", + "in19 = parse(19, lambda section: Scanner(mapt(ints, section.splitlines()[1:])), '\\n\\n')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- **Part 1**: Unfortunately, while each scanner can report the positions of all detected beacons relative to itself, the scanners do not know their own position. You'll need to determine the positions of the beacons and scanners yourself. Unfortunately, there's a second problem: the scanners also don't know their rotation or facing direction. Assemble the full map of beacons. **How many beacons are there?**\n", + "\n", + "The first thing I want to do is figure out how important efficiency is. `parse` said there are only 36 scanners. On average, how many beacons does each scanner see?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mean(map(len, in19))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Not too bad; I was worried there could be thousands of beacons. There are:\n", + " - *S* = 36 scanners\n", + " - *B* = 26 mean beacons per scanner\n", + " - 24 orientations of each scanner (per the instructions)\n", + "\n", + "At first I was confused: I thought that if there are 3! permutations of `(x, y, z)`, and 23 ways to have or not have a minus sign on each dimension, then there should be 6 × 8 = 48 different orientations. But the instructions say 24. I decided that if you don't permute (x, y, z), and just negate the x component, that's equivalent to a mirror image, which we don't want. You can get a mirroring with a minus sign on any of the three components, or by reversing the order of the components (e.g. `(x, y, z)` going to `(z, y, x)`) but not if you just rotate the order (e.g. `(x, y, z)` going to `(z, x, y)`). Two mirrorings reverse things back to normal, so the 24 valid orientations are those with a rotation and an even number of minus signs, or a reversal in the ordering and an odd number of minus signs. \n", + "\n", + "Since I'm going to be applying these transforms often, I will compile them into functions. I'll do that by constructing strings and then applying `eval` to each string." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Point3D = Tuple[int, int, int] # A 3D Point, like (1, 2, -3).\n", + "\n", + "def transformers(signs, orders) -> List[Callable]: \n", + " \"\"\"Strings that define 3D transformations of signs and variable ordering.\"\"\"\n", + " return [transformer(f'({a}{x}, {b}{y}, {c}{z})')\n", + " for a,b,c in signs for x,y,z in orders]\n", + "\n", + "def transformer(transform: str) -> Callable:\n", + " \"\"\"Turn this transformer string into a callable function.\"\"\"\n", + " fn = eval('lambda points: {' + transform + ' for x, y, z in points}')\n", + " fn.__name__ = 'Orient' + transform\n", + " return fn\n", + " \n", + "\n", + "orient_fns = (transformers([' ', '-- ', ' --', '- -'], ['xyz', 'yzx', 'zxy']) + \n", + " transformers(['---', '- ', ' - ', ' -'], ['zyx', 'xzy', 'yxz']))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here's a test applying the 24 orientation functions to a tiny set `B` of 2 beacons:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert len(orient_fns) == 24\n", + "\n", + "B = {(1, 2, 3), (-4, -5, -6)}\n", + "\n", + "assert [(fn.__name__, fn(B)) for fn in orient_fns] == [\n", + " ('Orient( x, y, z)', {(-4, -5, -6), (1, 2, 3)}),\n", + " ('Orient( y, z, x)', {(-5, -6, -4), (2, 3, 1)}),\n", + " ('Orient( z, x, y)', {(-6, -4, -5), (3, 1, 2)}),\n", + " ('Orient(-x, -y, z)', {(-1, -2, 3), (4, 5, -6)}),\n", + " ('Orient(-y, -z, x)', {(-2, -3, 1), (5, 6, -4)}),\n", + " ('Orient(-z, -x, y)', {(-3, -1, 2), (6, 4, -5)}),\n", + " ('Orient( x, -y, -z)', {(-4, 5, 6), (1, -2, -3)}),\n", + " ('Orient( y, -z, -x)', {(-5, 6, 4), (2, -3, -1)}),\n", + " ('Orient( z, -x, -y)', {(-6, 4, 5), (3, -1, -2)}),\n", + " ('Orient(-x, y, -z)', {(-1, 2, -3), (4, -5, 6)}),\n", + " ('Orient(-y, z, -x)', {(-2, 3, -1), (5, -6, 4)}),\n", + " ('Orient(-z, x, -y)', {(-3, 1, -2), (6, -4, 5)}),\n", + " ('Orient(-z, -y, -x)', {(-3, -2, -1), (6, 5, 4)}),\n", + " ('Orient(-x, -z, -y)', {(-1, -3, -2), (4, 6, 5)}),\n", + " ('Orient(-y, -x, -z)', {(-2, -1, -3), (5, 4, 6)}),\n", + " ('Orient(-z, y, x)', {(-3, 2, 1), (6, -5, -4)}),\n", + " ('Orient(-x, z, y)', {(-1, 3, 2), (4, -6, -5)}),\n", + " ('Orient(-y, x, z)', {(-2, 1, 3), (5, -4, -6)}),\n", + " ('Orient( z, -y, x)', {(-6, 5, -4), (3, -2, 1)}),\n", + " ('Orient( x, -z, y)', {(-4, 6, -5), (1, -3, 2)}),\n", + " ('Orient( y, -x, z)', {(-5, 4, -6), (2, -1, 3)}),\n", + " ('Orient( z, y, -x)', {(-6, -5, 4), (3, 2, -1)}),\n", + " ('Orient( x, z, -y)', {(-4, -6, 5), (1, 3, -2)}),\n", + " ('Orient( y, x, -z)', {(-5, -4, 6), (2, 1, -3)})\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "That looks good. The next step is to find pairs of scanners that can see the same beacons. The instructions say that every scanner has at least one other scanner with which it shares at least 12 detected beacons. How can I find the matches, and do it efficiently? Some options:\n", + "1. For every pair of scanners, I could apply every orientation transformation to *one of them, then try every alignment of a point in that transformation to a point in the other scanner, then count the number of matching points with that alignment, and see if the count exceeds 12. Total complexity: 24 *S*2 *B*3 operations.\n", + "2. For every scanner, compute the distance between each pair of points. (This set of distances will be invariant under orientation transformations, so we've saved a factor of 24.) Now compare distance sets between pairs of scanners and see which ones exceed 12×11/2 = 66 matches. Total complexity: *S* *B*2 + *S*2*B*2 operations. (Note: we actually store squared distances, not distances, to avoid possible round-off error and to save the runtime cost of computing square roots.)\n", + "\n", + "Option 2 it is! The algorithm will be:\n", + "\n", + " keep track of lists of `aligned` and `unaligned` scanners\n", + " add the first unaligned scanner to the list of aligned scanners\n", + " whenever an aligned scanner is added:\n", + " check to see if each unaligned can scanner can be aligned with it\n", + " while there are unaligned scanners:\n", + " pop off one unaligned scanner S\n", + " find an unaligned scanner R that c\n", + " \n", + "**NOTE: I started below, but did not complete the solution.**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def align_scanners(scanners, k=12) -> Set[Point3D]:\n", + " \"\"\"Align all scanners\"\"\"\n", + " for S in scanners:\n", + " S.distances2 = {distance2(p, p2) for p, p2 in combinations(S, 2)}\n", + " aligned, unaligned = [], scanners\n", + " return align_scanner(unaligned.pop(), aligned, unaligned, k)\n", + "\n", + "def add_aligned_scanner(S, aligned, unaligned, k) -> List[Scanner]:\n", + " \"\"\"Add S to aligned; recursively align other scanners with S; return aligned.\"\"\"\n", + " aligned.append(S)\n", + " unaligned.remove(S)\n", + " for R in unaligned.copy():\n", + " if R not in aligned and len(S.distances2 & R.distances2) >= k * (k - 1) // 2:\n", + " add_aligned_scanner(align(R, S), aligned, unaligned, k)\n", + " \n", + "def distance2(p: Point3D, p2: Point3D) -> int:\n", + " \"\"\"Squared distance between two 3D points.\"\"\"\n", + " return (p[0]-p2[0]) ** 2 + (p[1]-p2[1]) ** 2 + (p[2]-p2[2]) ** 2\n", + "\n", + "def align(R: Scanner, S: Scanner, k=12) -> Scanner:\n", + " \"Orient and offset Scanner R to align with Scanner S; return the new version of R.\"\n", + " for orient in orient_fns:\n", + " R2 = orient(R)\n", + " for offset in offsets(S1, A):\n", + " S2 = Scanner(add3D(s, offset) for s in S)\n", + " if len(S2 & A) >= k:\n", + " S2.distances2 = S.distances2\n", + " return S2\n", + " raise ValueError(\"Can't align scanners\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we can find two scanners that are alignable, we have to actually align them:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def offsets(S, A) -> Iterator[Point3D]:\n", + " \"\"\"All offsets that would make some point in S align with some point in A.\"\"\"\n", + " return (minus3D(a, s) for s in S for a in A)\n", + "\n", + "def minus3D(A: Point3D, B: Point3D) -> Point3D:\n", + " return (A[0] - B[0], A[1] - B[1], A[2] - B[2])\n", + "\n", + "def add3D(A: Point3D, B: Point3D) -> Point3D:\n", + " return (A[0] + B[0], A[1] + B[1], A[2] + B[2])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 20](https://adventofcode.com/2021/day/20): Trench Map\n", + "\n", + "- **Input**: The input is in two sections: first, a 512-character string describing an image enhancement algorithm; second a grid of pixels depicting an image.\n", + "\n", + "I'll parse the file into two sections, calling the first segment `rules` and the second `image`. Then I'll convert the `image` to a `Grid`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "rules, image = in20 = parse(20, sep='\\n\\n')\n", + "image = Grid(image.splitlines())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- **Part 1**: Start with the original input image and apply the image enhancement algorithm twice, being careful to account for the infinite size of the images. **How many pixels are lit in the resulting image?**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**It is a truth universally acknowledged,** that an Eric Wastl in possession of an Advent of Code, must be in want of a [Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) puzzle. This is it! The \"image enhancement algorithm\" defines how pixels turn on and off. I've done Life before, e.g. [here](Life.ipynb) and [here](https://adventofcode.com/2020/day/17). This should be easy! \n", + "\n", + "**Unfortunately**, there's a twist: spontaneous generation. I was *shocked* to find `rules[0] == '#'` in my enhancement rules. (That wasn't the case for the example in the instructions.) This means that every dark pixel in the middle of empty dark space will spontaneously become light pixel in the next generation, resulting in an infinite number of light pixels.\n", + "\n", + "\n", + "\n", + "**Fortunately**, `rules[255] == '.'`, which means that almost all of the infinite number of light pixels will become dark again in the following generation. It's as if we convert between a positive image and a negative on each enhancement.\n", + "\n", + "Therefore, my strategy will be:\n", + "- In generation 0 (and every even generation) we will have a finite set of *light* pixels. Use them to generate a finite set of *dark* pixels.\n", + "- In generation 1 (and every odd generation), we will have a finite set of *dark* pixels. Use them to generate a finite set of *light* pixels.\n", + "- Generating a dark pixel requires at least one neighboring light pixel, and generating a light pixel requires at least one neighboring dark pixel. So to know what pixels to consider as candidates for the next generation, I can use the current set of pixels and their neighbors. So I won't need the `Grid`, just the `set` of light pixels in it. I'll create a subclass of `set` called `Pixels` which is just like `set` but keeps track of the `.color` of pixels, light or dark." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "light = '#'\n", + "dark = '.'\n", + "\n", + "class Pixels(set):\n", + " \"\"\"A set of pixel Points; self.color says if they are light or dark.\"\"\"\n", + " def __init__(self, pixels, color=light):\n", + " self.update(pixels)\n", + " self.color = color\n", + "\n", + "def enhance(pixels, n=1, rules=rules) -> Pixels:\n", + " \"\"\"The set of pixels that result from applying enhancement rules `n` times.\"\"\"\n", + " for generation in range(n):\n", + " candidate_pixels = set(flatten(map(neighborhood, pixels)))\n", + " negative = dark if pixels.color == light else light\n", + " pixels = Pixels((p for p in candidate_pixels\n", + " if rules[pixel_sum(p, pixels)] == negative),\n", + " negative)\n", + " return pixels\n", + "\n", + "def pixel_sum(point, pixels):\n", + " \"\"\"The sum of the 9 pixels in the neighborhood. If `pixels.color == light`,\n", + " add up the neighborhood points that are in `pixels`; if not,\n", + " add up the neighborhood points that are not in `pixels`.\"\"\"\n", + " return sum((256 >> i) * ((p in pixels) == (pixels.color == light))\n", + " for i, p in enumerate(neighborhood(point)))\n", + "\n", + "def neighborhood(point) -> List[Point]:\n", + " \"The nine points surrounding `point` (including `point` itself).\"\n", + " (x, y) = point\n", + " return [(x-1, y-1), (x, y-1), (x+1, y-1), \n", + " (x-1, y), (x, y), (x+1, y), \n", + " (x-1, y+1), (x, y+1), (x+1, y+1)]\n", + "\n", + "pixels = Pixels(p for p in image if image[p] == light) # pixels in my image" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "answer(20.1, 5437, lambda: len(enhance(pixels, 2)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- **Part 2**: Start again with the original input image and apply the image enhancement algorithm 50 times. **How many pixels are lit in the resulting image?**\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "answer(20.2, 19340, lambda: len(enhance(pixels, 50)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 21](https://adventofcode.com/2021/day/21): Dirac Dice\n", + "\n", + "\n", + "We're playing a two-player game with the following rules: On each turn a player rolls the die three times, adds them up, and moves forward that many spaces on a circular board consisting of spaces marked 1 to 10; space 10 wraps around to 1. The player increases their score by the space they land on; first player to 1000 points wins.\n", + "\n", + "- **Input**: My input is the text \"`Player 1 starting position: 5 \\ Player 2 starting position: 6`\".\n", + "\n", + "I won't bother to read this from the file; I'll just translate it into this statement:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "start_positions = (5, 6)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- **Part 1**: Play a practice game using the deterministic 100-sided die. The moment either player wins, **what do you get if you multiply the score of the losing player by the number of times the die was rolled during the game?**\n", + "\n", + "I'll implement a deterministic die as an iterator: `cycle(range(1, 101)` returns 1 first, then 2, etc. up to 100, then goes back to 1. My function `play_dice` keeps track of positions and scores for both players and returns the product of the number of dice rolled times the loser's score." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def play_dice(die: Iterator, positions=start_positions, target=1000) -> int:\n", + " \"\"\"Play the dice game with the given die until someone scores `target`.\n", + " Return total_number_of_dice_rolled * loser_score.\"\"\"\n", + " positions = list(positions)\n", + " scores = [0, 0]\n", + " for turn in count_from(1):\n", + " player = (turn - 1) % 2\n", + " roll = next(die) + next(die) + next(die)\n", + " positions[player] = clock_mod(positions[player] + roll, 10)\n", + " scores[player] += positions[player]\n", + " if scores[player] >= 1000:\n", + " return 3 * turn * min(scores)\n", + " \n", + "answer(21.1, 1002474, lambda: play_dice(die=cycle(range(1, 101))))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- **Part 2**: This time we play with a quantum die. Using your given starting positions, determine every possible outcome. **Find the player that wins in more universes; in how many universes does that player win?**\n", + "\n", + "A three-sided quantum die splits the universe into three copies every time it is rolled: one where the outcome of the roll was 1, one where it was 2, and one where it was 3. We need to track all these universes. But for this game a player only needs 21 points to win; not 1000.\n", + "\n", + "The instructions warn us that there will be *trillions* of universes, so I'm concerned about efficiency. I must avoid enumerating all possibile universes one by one. I should either use a `Counter` of game states (as with the lanternfish in Day 6), or use dynamic programming (which I can implement with caching on a recursive function). \n", + "\n", + "I decided to go with the recursive function. I'll define `play_dice2(pos1, pos2, score1, score2, target)` as a function that returns a tuple of (number of wins, number of losses) for player 1, given the positions of the two players, the scores of the two players, and the target winning score, under the assumption that it is player 1's turn to roll. Note that in the recursive call to `play_dice2` it is player 2's turn, so the arguments and return values are swapped. I did it this way, rather than have two-element sequences for the positions and scores, because this way cuts in half the number of different game states I have to track." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dice3_rolls = mapt(sum, cross_product([1, 2, 3], repeat=3))\n", + "\n", + "@cache\n", + "def play_dice2(pos1, pos2, score1, score2, target) -> Tuple[int, int]:\n", + " \"\"\"The number of (winning universes, losing universes) for player 1,\n", + " given that player 1 is at `pos1` with `score1` points (and likewise for player 2).\"\"\"\n", + " wins, losses = 0, 0\n", + " if score2 >= target: # Player 2 has won; record a loss for player 1\n", + " losses += 1 \n", + " else: # Player 1 takes their turn; then count wins and loses for the game\n", + " for roll in dice3_rolls:\n", + " newpos1 = clock_mod(pos1 + roll, 10)\n", + " roll_losses, roll_wins = play_dice2(pos2, newpos1, score2, score1 + newpos1, target)\n", + " wins += roll_wins\n", + " losses += roll_losses\n", + " return wins, losses\n", + "\n", + "answer(21.2, 919758187195363, lambda: max(play_dice2(*start_positions, 0, 0, 21)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "I had some ideas to make this more efficient (such as iterating over a Counter of `dice3_rolls`) but it is fast enough as is.\n", + "\n", + "How many different game states did we explore?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "play_dice2.cache_info()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Only 24,841 distinct game states." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 22](https://adventofcode.com/2021/day/22): Reactor Reboot\n", + "\n", + "- **Input**: The input is a list of steps for rebooting the ship's reactor, such as \"`on x=10..12,y=10..12,z=10..12`\".\n", + "\n", + "I'll parse each line into a tuple consisting of either `\"on\"` or `\"off\"` followed by three ranges (using `cover` because the high end is inclusive):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Step = Tuple[str, range, range, range]\n", + "\n", + "def parse_reboot_step(line: str) -> Step:\n", + " \"\"\"Parse a line into a reboot step description.\"\"\"\n", + " x1, x2, y1, y2, z1, z2 = ints(line)\n", + " return (line.split()[0], cover(x1, x2), cover(y1, y2), cover(z1, z2))\n", + "\n", + "in22 = parse(22, parse_reboot_step)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The intent of each step is to turn on or off all the individual cubes in the reactor core that are within the *cuboid* specified by the three ragnes. So a 3x3x3 cuboid specifies 27 cubes.\n", + "\n", + "- **Part 1**: Execute the reboot steps. Afterward, considering only cubes in the region x=-50..50,y=-50..50,z=-50..50, **how many cubes are on?**\n", + "\n", + "[I have a bad feeling about this](https://www.youtube.com/watch?v=S74rvpc6W60). The example in the instructions has a small enough number of cubes that we can just brute-force enumerate them. But I suspect in Part 2 there will be trillions of cubes and brute force willl no longer work. [It's a trap!](https://www.youtube.com/watch?v=4F4qzPbcFiA) \n", + "\n", + "Still, I'll go ahead and do the brute-force enumeration for Part 1: for each step that describes a cuboid that falls within the region, form the set of all the cubes in the cuboid, and add or subtract them from a running set of `cubes`. Each cube is denoted by a single `(x, y, z)` triple of integer coordinates. Note that it is guaranteed that every step describes a cuboid that is either entirely within or entirely outside the position range -50..50 on all three dimensions, so I only need to check one of the three dimensions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def count_cubes(steps, region=cover(-50, 50)) -> int:\n", + " \"\"\"Follow `steps` to turn on or off those cubes that are in `region`, and count 'on' cubes.\"\"\"\n", + " cubes = set()\n", + " for (flip, xs, ys, zs) in steps:\n", + " if xs.start < region.start or xs.stop > region.stop:\n", + " pass # step is outside the region\n", + " elif flip == \"on\":\n", + " cubes |= set(cross_product(xs, ys, zs))\n", + " else:\n", + " cubes -= set(cross_product(xs, ys, zs))\n", + " return len(cubes) \n", + "\n", + "answer(22.1, 533863, lambda: count_cubes(in22))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- **Part 2**: Starting again with all cubes off, execute all reboot steps. Afterward, considering all cubes, **how many cubes are on?**\n", + "\n", + "Well, I was *wrong*. There aren't going to be *trillions* of cubes; there are going to be *quadrillions*. \n", + "\n", + "I will need a way to handle steps without explicitly enumerating the cubes. If no cuboids intersected, it would be easy: for each \"on\" cuboid, multiply the lengths of the three sides to get the volume, then add up the volumes. But the intersections complicate things.\n", + "\n", + "I'll start with some [exploratory data analysis](https://en.wikipedia.org/wiki/Exploratory_data_analysis) to answer some questions.\n", + "\n", + "**Question**: how many of the steps are \"on\"?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Counter(step[0] for step in in22)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Answer**: There are 420 steps, of which about 3/4 are \"on\" steps.\n", + "\n", + "**Question**: How big are the cuboids? " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "volumes = [len(x) * len(y) * len(z) / 1e12 for (on, x, y, z) in in22]\n", + "plt.hist(volumes, rwidth=0.8, bins=36, \n", + " label=f'cuboid volume in trillions (mean {mean(volumes):.1f})')\n", + "plt.legend();" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Answer**: An average cuboid consists of 7.6 trillion cubes. A big one has 35 trillion, but most are under 10 trillion. \n", + "\n", + "**Question**: How long are the sides of the cuboids?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lengths = [[len(step[i]) for step in in22] for i in (1,2,3)]\n", + "plt.hist(lengths, bins=25, label=list('xyz'))\n", + "plt.legend();" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Answer**: The histogram above breaks out the lengths of the sides of the cuboids by x, y, and z dimensions. It looks like a roughly normal distribution (although noisy) in each dimension with mean around 20,000, except that we can see a spike just above length 0 for the 20 small cuboids that are used in Part 1 (plus a few other cuboids that happen to have small ranges in one of the `x` or `z` dimensions, but not `y`).\n", + "\n", + "**Question**: Can I visualize the actual cuboids?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure(figsize=(6, 6)) # Make it square\n", + "for (on, xs, ys, zs) in in22:\n", + " plt.plot(*box(xs, ys), '-')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Answer**: The plot above gives a 2D projection into the XY plane. (You could also project to XZ or YZ planes, but I don't think that would be very different. To do 3D you'd need animation. A 3D VR headset would help.) \n", + "\n", + "We can also look more closely at a sample of every tenth cuboid, this time in the YZ plane:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure(figsize=(6, 6))\n", + "for (on, xs, ys, zs) in in22[::10]:\n", + " plt.plot(*box(ys, zs), '-')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Question**: What's a good strategy for counting cubes?\n", + "\n", + "**Answer**: You can see in the plots above that when two rectangles intersect, they can leave non-rectangular pieces (and in 3 dimensions, non-cuboid pieces). That's the difficulty. The key is to break these pieces up into smaller pieces that are all cuboids. The algorithm will be:\n", + "\n", + "1. Find all the **split points** in the first dimension–values of `x` that start or end a cuboid. Create a list of **bins**, one covering the range between each pair of adjacent split points.\n", + "\n", + "2. Assign each step to the bin(s) it overlaps. For example (in 2 dimensions), suppose that step 1 is a 7x4 \"on\" step and step 2 is a 6x4 \"off\" step, and that they span the locations shown below left. When we split on `x` we get the three bins shown below right (all 1s in the left bin, all 2s in the right bin, and both 1 and 2 in the middle bin):\n", + "\n", + " 1111111 1111 111\n", + " 1111111 1111 111\n", + " 1111222222 1111 222 222\n", + " 1111222222 1111 222 222\n", + " 222222 222 222\n", + " 222222 222 222\n", + "\n", + "\n", + "3. Recurse if there are more dimensions to split on. In the example, we need to split on `y`. The two outside bins aren't split (because they only have one step), but the middle bin is split into 3 bins. Notice that each bin is a rectangle (and in 3 dimensions, each bin would be a cuboid). \n", + "\n", + " 111\n", + " 1111 111\n", + " 1111\n", + " 1111 222 222\n", + " 1111 222 222\n", + " 222\n", + " 222 222\n", + " 222\n", + " \n", + "\n", + "4. For each bin that is \"on\", multiply their side lengths to get their volume, and add up the volumes to get the total number of \"on\" cubes. In the example, the \"on\" bins (with step 1) are one 4x4 cuboid and one 3x2 cuboid, giving a total of 22 cubes.\n", + "\n", + "For the real input data, I would probably end up with a million or so bins. Adding up the volumes of a million bins will be faster than enumerating a quadrillion cubes.\n", + " \n", + "Below is the implementation. For the `Bin` data type, I'll have a field for the dimension we are splitting on; one for the range of values covered in that dimension by the bin; and one for a list of `kids`: At first it holds a list of all the steps that at least partially overlap the bin, but then (for the `x` and `y` dimensions but not `z`) that list of steps will be replaced by a list of bins." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "@dataclass\n", + "class Bin:\n", + " dim: int # 1, 2, or 3 for x, y, or z dimension\n", + " range: range # locations that are covered by this bin, on the given dimension\n", + " kids: Union[List[Step], List['Bin']] # Steps that overlap this bin (may be converted to bins)\n", + "\n", + "def bins_from_steps(steps, dims=(1, 2, 3)) -> List[Bin]:\n", + " \"\"\"Convert the steps to a list of bins.\"\"\"\n", + " # (1) Find all the split points in the first dimension. Create a list of bins.\n", + " dim, *more_dims = dims\n", + " splits = sorted(set(flatten((step[dim].start, step[dim].stop) for step in steps)))\n", + " bins = [Bin(dim, range(*pair), []) for pair in pairs(splits)]\n", + " # (2) Assign each step to the bin(s) it overlaps.\n", + " for bin in bins:\n", + " bin.kids = [step for step in steps if overlaps(bin.range, step[dim])]\n", + " # (3) Recurse if there are more dimensions\n", + " if more_dims:\n", + " for bin in bins:\n", + " bin.kids = bins_from_steps(bin.kids, more_dims)\n", + " return bins\n", + "\n", + "def overlaps(A: range, B: range) -> bool:\n", + " \"\"\"Do these ranges overlap?\"\"\"\n", + " return (B.start <= A.start < B.stop) or (A.start <= B.start < A.stop)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here is the example, showing the three bins at the `x` level, and five at the `y` level:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "steps2 = [('on', range(0, 7), range(0, 4)), \n", + " ('off', range(4, 9), range(2, 6))]\n", + "\n", + "assert (bins_from_steps(steps2, (1, 2)) == \n", + " [Bin(1, range(0, 4), \n", + " [Bin(2, range(0, 4), [('on', range(0, 7), range(0, 4))])]), \n", + " Bin(1, range(4, 7), \n", + " [Bin(2, range(0, 2), [('on', range(0, 7), range(0, 4))]), \n", + " Bin(2, range(2, 4), [('on', range(0, 7), range(0, 4)), \n", + " ('off', range(4, 9), range(2, 6))]), \n", + " Bin(2, range(4, 6), [('off', range(4, 9), range(2, 6))])]), \n", + " Bin(1, range(7, 9), \n", + " [Bin(2, range(2, 6), [('off', range(4, 9), range(2, 6))])])])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The test passes! The only thing that remains is to count up the number of cubes in the \"on\" cuboids. I'll define `count_cubes2` so that it can handle Part 1 as well as Part 2:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def count_cubes2(steps, region=None) -> int:\n", + " \"\"\"Follow `steps` to turn on or off cubes, and count 'on' cubes.\"\"\"\n", + " if region:\n", + " steps = [s for s in steps if s[1].start >= region.start and s[1].stop <= region.stop]\n", + " return sum(count_cubes_in_bin(bin) for bin in bins_from_steps(steps))\n", + "\n", + "def count_cubes_in_bin(bin) -> int:\n", + " \"\"\"How many \"on\" cubes are in this bin? Recurse down to `dim == 3`, keeping track of \n", + " the lengths of all three sides, and return the product for bins that are \"on\".\"\"\"\n", + " N = len(bin.range)\n", + " if bin.dim < 3:\n", + " return N * sum(map(count_cubes_in_bin, bin.kids))\n", + " elif bin.kids and (bin.kids[-1][0] == 'on'):\n", + " return N\n", + " else:\n", + " return 0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's go ahead and solve the complete puzzle without any more tests:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "answer(22.1, 533863, lambda: count_cubes2(in22, region=cover(-50, 50)))\n", + "answer(22.2, 1261885414840992, lambda: count_cubes2(in22))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We got the right answers. Part 1 runs 50 times faster; Part 2 takes over 5 seconds. One idea to make it faster: I compare every step to every bin. This could probably be speeded up with a binary search. I chose not to do that because binary searches are notorious for off-by-one errors; doubly so when searching over ranges of numbers, not a single number." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 23](https://adventofcode.com/2021/day/23): Amphipod\n", + "\n", + "**NOTE: I started below, but did not complete the solution.** This should be an easy application of A-star search to a grid problem, but I didn't find the time to complete it, because of holiday season obligations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class HGrid(Grid):\n", + " \"\"\"A hashable grid; can be used as a key in dicts.\"\"\"\n", + " def __hash__(self): return hash(tuple(sorted(self.items())))\n", + " \n", + "in23 = HGrid(parse(23))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "amphipods = A, B, C, D = 'ABCD' # The four kinds of creature\n", + "amphi_costs= {A:1, B:10, C:100, D:1000} # Cost of each step\n", + "goal_cols = {A: 3, B: 5, C: 7, D: 9} # Column each Amphipod type aspires to\n", + "hall_row = 1 # Row where the hall spaces are\n", + "side_rows = {2, 3} # Rows every Amphipod aspires to\n", + "side_rooms = {(goal_cols[L], r) for r in side_rows for L in amphipods} # All the spaces off of the hallway\n", + "hallway = {(c, hall_row) for c in range(1, 12)} # Hallway spaces\n", + "hall_stops = {(c, r) for (c, r) in hallway if c not in goal_cols.values()} # Hallway spaces where you're allowed to stop\n", + "\n", + "@dataclass\n", + "class Move:\n", + " start: Point\n", + " end: Point\n", + " what: Char\n", + " \n", + "def extract_state(grid, kinds):\n", + " \"\"\"The parts of the grid of a certain kind.\"\"\"\n", + " return {loc: grid[loc] for loc in grid if grid[loc] in kinds}\n", + "\n", + "def clear(loc, dest, state):\n", + " \"\"\"Is the path from `loc` to `dest` clear of Amphipods in state?\"\"\"\n", + " Δx = sign(X_(dest) - X_(loc))\n", + " Δy = sign(Y_(dest) - Y_(loc))\n", + " return all((X_(loc) + i * Δx, Y_(loc) + i * Δy) not in state\n", + " for i in range(1, distance(loc, dest) + 1))\n", + "\n", + "def amphi_moves(state) -> Iterable[Move]:\n", + " \"\"\"All the moves that can be made from a state.\"\"\"\n", + " for loc in state:\n", + " # If you're in a side room and you have a clear path to the doorway ...\n", + " if loc in side_rooms and clear(loc, (X_(loc), hall_row), state):\n", + " # ... then you can move to any hall stop that you have a clear path to\n", + " for hall in hall_stops:\n", + " if clear((X_(loc), hall_row), hall, state):\n", + " yield Move(loc, hall, state[loc])\n", + " # If you're in the hallway and you have a clear path to your doorway ... \n", + " goal_col = goal_cols[state[loc]]\n", + " if loc in hallway and clear(loc, (goal_col, hall_row), state):\n", + " # ... then you can move to the top empty space in your side room if no foreign occupants\n", + " occupants = {L for (x, y), L in state.items() if x == goal_col}\n", + " if not occupants or occupants == {state[loc]}:\n", + " top_row = max(y for y in side_rows if (goal_col, y) not in state)\n", + " yield Move(loc, (goal_col, top_row), state[loc])\n", + " \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Δc = 0\n", + "Δc" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from time import perf_counter as pfc\n", + "from heapq import heappop, heappush\n", + "\n", + "\n", + "def read_puzzle(filename):\n", + " with open(filename) as f:\n", + " return ''.join([c for c in f.read() if c in 'ABCD.'])\n", + "\n", + "\n", + "def can_leave_room(puzzle, room_pos):\n", + " for a in room_pos:\n", + " if puzzle[a] == '.': continue\n", + " return a\n", + "\n", + "\n", + "def blocked(a,b,puzzle):\n", + " step = 1 if a Dict[str, int]:\n", + " \"\"\"Run a program with an input string on the submarine's ALU,\n", + " and return a dict of the program variables (and {'error': op} if appropriate).\"\"\"\n", + " inputs = iter(inputstr)\n", + " M = dict(w=0, x=0, y=0, z=0) # Memory\n", + " for op, a, *rest in program:\n", + " b = M.get(rest[0], rest[0]) if rest else None\n", + " if op == 'inp': \n", + " M[a] = int(next(inputs))\n", + " elif op == 'add': \n", + " M[a] += b\n", + " elif op == 'mul': \n", + " M[a] *= b\n", + " elif op == 'div': \n", + " if b == 0: return {'error': 1, **M}\n", + " M[a] //= b\n", + " elif op == 'mod': \n", + " if M[a] < 0 or b <= 0: return {'error': 2, **M}\n", + " M[a] %= b\n", + " elif op == 'eql': \n", + " M[a] = int(M[a] == b)\n", + " return M\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can run a program:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "alu(program, '12345678901234')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's figure out where the input instructions are:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "{i for i, ins in enumerate(program) if ins[0] == 'inp'}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "They're every 18 instructions. Let's look at the program divided into 18-instruction segments:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def instr(i) -> str: return f'{i:3d}: ' + ' '.join(map(str, program[i])).ljust(9)\n", + "\n", + "for i in range(18):\n", + " print(\"| \".join(instr(i + 18 * j) for j in range(6)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Each column is similar, but there are different constants for instructions 4–5 and 15 in each column:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "{i: {instr(i + 18 * j)[5:].strip() for j in range(6)} for i in range(18)}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's try every possible digit as the first input, and see what the program computes right up to the point where it is ready to read the second digit:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "{d: alu(in24[:18], d) for d in '0123456789'}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It looks like after processing one digit, *d*, we have:\n", + "- w = *d*\n", + "- x = 1\n", + "- y = z = *d* + 6\n", + " \n", + "Let's try it for every two-digit input:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def inputs(n) -> List[str]: return [str(i).zfill(n) for i in range(10 ** n)]\n", + "\n", + "{i: alu(in24[:36], i) for i in inputs(2)}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "So, *w* is the last digit of the input; *x* is always 1; *y* is 6 more than the last digit, and *z* is 162 + 26 times the first digit plus the last digit.\n", + "\n", + "**I was unable to figure out was going on.**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "{i: alu(in24, i) for i in ['10000000000000', \n", + " '12345678901234', \n", + " '11111111111111',\n", + " '23456789876543']}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 25](): Sea Cucumber\n", + "\n", + "- **Input**: The input is a map of east-moving (`>`) and south-moving (`v`) sea cucumbers.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "in25 = Grid(parse(25))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- **Part 1**: Every step, the sea cucumbers in the east-facing herd attempt to move forward one location, then the sea cucumbers in the south-facing herd attempt to move forward one location. When a herd moves forward, every sea cucumber in the herd first simultaneously considers whether there is a sea cucumber in the adjacent location it's facing (even another sea cucumber facing the same direction), and then every sea cucumber facing an empty location simultaneously moves into that location. **Find somewhere safe to land your submarine. What is the first step on which no sea cucumbers move?**\n", + "\n", + "Since the herd moves simultaneously, I won't directly update the grid; rather I will keep track of the herd of east-goers (as a set of points) and the herd of south-goers, and update each set on each step. (After extracting the two herds from the grid, it is only used for its width and height)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def move_cukes(grid) -> int:\n", + " \"\"\"Move sea cucumbers according to rules until they stop moving.\n", + " Return the number of steps it took.\"\"\"\n", + " east = {p for p in grid if grid[p] == '>'}\n", + " south = {p for p in grid if grid[p] == 'v'}\n", + " for step in count_from(1):\n", + " new_east = set(move_herd(east, south, 1, 0, grid.width, grid.height))\n", + " new_south = set(move_herd(south, new_east, 0, 1, grid.width, grid.height))\n", + " if east == new_east and south == new_south:\n", + " return step\n", + " east, south = new_east, new_south\n", + "\n", + "def move_herd(herd: Set[Point], other_herd, dx, dy, width, height) -> Iterator[Point]:\n", + " \"\"\"The new positions of the herd; they each move by (dx, dy) if that space is open.\"\"\"\n", + " for (x, y) in herd:\n", + " p2 = ((x + dx) % width, (y + dy) % height)\n", + " yield p2 if (p2 not in herd and p2 not in other_herd) else (x, y)\n", + " \n", + "answer(25.1, 424, lambda: move_cukes(in25))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Unfortunately for me, Part 2 of Day 25 just asked if you had solved all the other puzzles. I hadn't." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Submarine Captain's Log, Supplemental\n", + "\n", + "Here is a summary of the results:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "answers" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "I didn't complete days **19, 23, and 24**. 19 is a bit tedious, but shouldn't be conceptually hard. 23 is easy; just need to apply standard graph-search ideas. For 24, I have no idea. I need to reverse engineer the program to understand what it is actually doing. \n", + "\n", + "We see that only the following puzzles took more than a second of run time:\n", + "- **15.2 Chiton**: just over 1 second for search through the 500x500 grid \n", + "- **20.2 Trench Map**: 4 seconds for enhancing the 100x100 grid for 50 generations\n", + "- **22.2 Reactor reboot**: 5 seconds for counting a quadrillion cubes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/ipynb/Advent-2022.ipynb b/ipynb/Advent-2022.ipynb new file mode 100644 index 0000000..e4754b2 --- /dev/null +++ b/ipynb/Advent-2022.ipynb @@ -0,0 +1,936 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
Peter Norvig, December 2022
\n", + "\n", + "# Advent of Code 2022\n", + "\n", + "I'm doing Advent of Code (AoC) again this year.\n", + "\n", + "Happily for us all, [@GaryJGrady](https://twitter.com/GaryJGrady/) is drawing his cartoons again too! Below, Gary's elf makes preparations on the eve of AoC:\n", + "\n", + "\n", + "\n", + "I prepared by loading up my [**AdventUtils.ipynb**](AdventUtils.ipynb) notebook from last year:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'mapt' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m~/Google Drive/Python/AdventUtils.ipynb\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m## TESTS\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0mparse\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"hello\\nworld\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mshow\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;34m'hello'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'world'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mparse\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"123\\nabc7\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdigits\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mshow\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m3\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m7\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mtruncate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'hello world'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m99\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m'hello world'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/Google Drive/Python/AdventUtils.ipynb\u001b[0m in \u001b[0;36mparse\u001b[0;34m(day_or_text, parser, sep, show)\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0mtext\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mget_text\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mday_or_text\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[0mprint_parse_items\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Puzzle input'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtext\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msplitlines\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mshow\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'line'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 10\u001b[0;31m \u001b[0mrecords\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmapt\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mparser\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtext\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrstrip\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msplit\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msep\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 11\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mparser\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[0mprint_parse_items\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Parsed representation'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mrecords\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mshow\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34mf'{type(records[0]).__name__}'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mNameError\u001b[0m: name 'mapt' is not defined" + ] + } + ], + "source": [ + "%run AdventUtils.ipynb" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You might want to [take a look](AdventUtils.ipynb) to see how the `parse` and `answer` functions work, since they will be used for each day's puzzles. You'll really have to read [each day's puzzle description](https://adventofcode.com/2022/day/1). Each solution will have three parts:\n", + "- **Reading the Input**, e.g. for Day 1, `in1 = parse(1, ints, sep=paragraph)`. The function `parse` splits the input file for day 1 into records (by default each line is a record, but the `sep` keyword argument can be used to split by paragraph or other separators), and then applies a function (here `ints`, which returns a tuple of all integers in a string) to each record. `parse` prints the first few lines of the input file and the first few records of the parsed result.\n", + "- **Solving Part One**, e.g. `answer(1.1, ..., lambda: ...)`. The function `answer` takes three arguments:\n", + " 1. The puzzle we are answering, in the form *day*.*part*\n", + " 2. The correct answer as verified by AoC (recorded here so that if I modify and re-run the notebook, I can verify that it still works), \n", + " 3. A function to call to compute the answer. (It is passed as a function so we can time how long it takes to run.)\n", + "- **Solving Part Two**, e.g. `answer(1.2, ..., lambda: ...)`.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 1](https://adventofcode.com/2022/day/1): Calorie Counting\n", + "\n", + "There is a complex backstory involving food for the elves and calories, but computationally all we have to know is that the input is a sequence of paragraphs, where each paragraph contains some integers. My `parse` function knows how to handle that:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 2275 lines:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "15931\n", + "8782\n", + "16940\n", + "14614\n", + "\n", + "4829\n", + "...\n" + ] + }, + { + "ename": "NameError", + "evalue": "name 'mapt' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0min1\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mparse\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mints\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mparagraphs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m\u001b[0m in \u001b[0;36mparse\u001b[0;34m(day_or_text, parser, sep, show)\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0mtext\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mget_text\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mday_or_text\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[0mprint_parse_items\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Puzzle input'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtext\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msplitlines\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mshow\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'line'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 10\u001b[0;31m \u001b[0mrecords\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmapt\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mparser\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtext\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrstrip\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msplit\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msep\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 11\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mparser\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[0mprint_parse_items\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Parsed representation'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mrecords\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mshow\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34mf'{type(records[0]).__name__}'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mNameError\u001b[0m: name 'mapt' is not defined" + ] + } + ], + "source": [ + "in1 = parse(1, ints, paragraphs)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 1: Find the Elf carrying the most Calories. How many total Calories is that Elf carrying?\n", + "\n", + "Find the maximum sum among all the tuples:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "answer(1.1, 70116, lambda: max(sum(elf) for elf in in1))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 2: Find the top three Elves carrying the most Calories. How many Calories are those Elves carrying in total?\n", + "\n", + "Find the sum of the 3 biggest sums:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "answer(1.2, 206582, lambda: sum(sorted(sum(elf) for elf in in1)[-3:]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To be clear, here is exactly what I did to solve the day's puzzle:\n", + "\n", + "1. Typed and executed `in1 = parse(1, ints, paragraphs)` in a Jupyter Notebook cell, and examined the output. Looked good to me.\n", + "2. Solved Part 1: typed and executed `max(sum(elf) for elf in in1)` in a cell, and saw the output, `70116`.\n", + "3. Copy/pasted `70116` into the [AoC Day 1](https://adventofcode.com/2022/day/1) input box and submitted it.\n", + "4. Verified that AoC agreed the answer was correct. (On some other days, the first such submission was not correct.)\n", + "5. Typed and executed `answer(1.1, 70116, lambda: max(sum(elf) for elf in in1))` in a cell, for when I re-run the notebook.\n", + "6. Repeated steps 2–5 for Part 2." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 2](https://adventofcode.com/2022/day/2): Rock Paper Scissors \n", + "\n", + "The input is two one-letter strings per line indicating the two player's plays:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "in2 = parse(2, atoms)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 1: What would your total score be if everything goes exactly according to your strategy guide?\n", + "\n", + "One confusing aspect: there are multiple encodings. Rock/Paper/Scissors corresponds to A/B/C, and X/Y/Z, and scores of 1/2/3. I decided the least confusing approach would be to translate everything to 1/2/3:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "RPS = Rock, Paper, Scissors = 1, 2, 3\n", + "rps_winner = {Rock: Paper, Paper: Scissors, Scissors: Rock}\n", + "\n", + "def rps_score(you: int, me: int) -> int:\n", + " \"\"\"My score for a round is my play plus 3 for draw and 6 for win.\"\"\"\n", + " return me + (6 if rps_winner[you] == me else 3 if me == you else 0)\n", + " \n", + "answer(2.1, 13268, lambda: sum(rps_score('.ABC'.index(a), '.XYZ'.index(x)) for a, x in in2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 2: What would your total score be if everything goes exactly according to your strategy guide?\n", + "\n", + "In Part 2 the X/Y/Z does not mean that I should play rock/paper/scissors; rather it means that I should lose/draw/win:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "rps_loser = {rps_winner[x]: x for x in RPS} # Invert the dict\n", + "\n", + "def rps_score2(you: int, x: Char) -> int:\n", + " \"\"\"First letter means A=Rock/B=Paper/C=Scissors; second means X=lose/Y=draw/Z=win.\"\"\"\n", + " me = rps_loser[you] if x == 'X' else you if x == 'Y' else rps_winner[you]\n", + " return rps_score(you, me)\n", + "\n", + "answer(2.2, 15508, lambda: sum(rps_score2('.ABC'.index(a), x) for a, x in in2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 3](https://adventofcode.com/2022/day/3): Rucksack Reorganization\n", + "\n", + "Each line of input is just a string of letters; the simplest input to parse:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "in3 = parse(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 1: Find the item type that appears in both compartments of each rucksack. What is the sum of the priorities of those item types?\n", + "\n", + "The two \"compartments\" are the two halves of the string. Find the common item by set intersection. The function `the` makes sure there is exactly one letter in the interesection:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def common_item(rucksack: str) -> Char:\n", + " \"\"\"The one letter that appears in both left and right halves of the input string.\"\"\"\n", + " left, right = split_at(rucksack, len(rucksack) // 2)\n", + " return the(set(left) & set(right))\n", + "\n", + "priority = {c: i + 1 for i, c in enumerate(string.ascii_letters)}\n", + "\n", + "answer(3.1, 8401, lambda: sum(priority[common_item(rucksack)] for rucksack in in3))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 2: Find the item type that corresponds to the badges of each three-Elf group. What is the sum of the priorities of those item types?\n", + "\n", + "My utility function `batched(in3, 3)` (from the [itertools recipes](https://docs.python.org/3/library/itertools.html#itertools-recipes) groups a sequence into subsequences of length 3; then we find the intersection and get its priority:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "answer(3.2, 2641, lambda: sum(priority[the(intersection(group))] for group in batched(in3, 3)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 4](https://adventofcode.com/2022/day/4): Camp Cleanup\n", + "\n", + "Each input line corresponds to two ranges of integers, which I'll represent with a 4-tuple of endpoints:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "in4 = parse(4, positive_ints)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 1: In how many assignment pairs does one range fully contain the other?\n", + "\n", + "I could have turned each range into a set of integers and compared the sets, but I was concerned that a huge range would mean a huge set, so instead I directly compare the endpoints of the ranges:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def fully_contained(lo, hi, LO, HI) -> bool:\n", + " \"\"\"Is the range `lo-hi` fully contained in `LO-HI`, or vice-veresa?\"\"\"\n", + " return (lo <= LO <= HI <= hi) or (LO <= lo <= hi <= HI)\n", + "\n", + "answer(4.1, 477, lambda: quantify(fully_contained(*line) for line in in4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 2: In how many assignment pairs do the ranges overlap?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def overlaps(lo, hi, LO, HI) -> bool:\n", + " \"\"\"Do the two ranges have any overlap?\"\"\"\n", + " return (lo <= LO <= hi) or (LO <= lo <= HI)\n", + "\n", + "answer(4.2, 830, lambda: quantify(overlaps(*line) for line in in4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 5](https://adventofcode.com/2022/day/5): Supply Stacks\n", + "\n", + "My `parse` function is primarily intended for the case where every record is parsed the same way. In today's puzzle, the input has two sections (in two paragraphs), each of which should be parsed differently. The function `parse_sections` is designed to handle this case. It takes as input a list of parsers (in this case two of them), which will be applied in order to parse the corresponding section:\n", + "- The first section is a **diagram**, which is parsed by picking out the characters in each stack; that is, in columns 1 and every 4th column after. \n", + "- The second section is a list of **moves**, which can be parsed with `ints` to get a 3-tuple of numbers. \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "in5 = parse(5, parse_sections([lambda line: line[1::4], ints]), sep=paragraphs, show=12)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 1: After the rearrangement procedure completes, what crate ends up on top of each stack?\n", + "\n", + "Rearranging means repeatedly popping a crate from one stack and putting it on top of another stack, according to the move commands:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def rearrange(diagram, moves) -> str:\n", + " \"\"\"Given a diagram of crates in stacks, apply move commands.\n", + " Then return a string of the crates that are on top of each stack.\"\"\"\n", + " stacks = {int(row[-1]): [L for L in reversed(row[:-1]) if L != ' '] for row in T(diagram)}\n", + " for (n, source, dest) in moves:\n", + " for _ in range(n):\n", + " stacks[dest].append(stacks[source].pop())\n", + " return cat(stacks[i].pop() for i in stacks)\n", + "\n", + "answer(5.1, 'SHQWSRBDL', lambda: rearrange(*in5))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 2: After the rearrangement procedure completes, what crate ends up on top of each stack?\n", + "\n", + "In part 1, when *n* crates were moved with a model 9000 crane, it was done one-at-a-time, so the stack ends up reversed at its destination. In part 2 we have the more advanced model 9001 crane, which can lift all *n* crates at once, and place them down without reversing them. I'll rewrite `rearrange` to handle either way. I'll rerun part 1 to make sure the new function definition is backwards compatible." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def rearrange(diagram, moves, model=9000) -> str:\n", + " stacks = {int(row[-1]): [L for L in row[-1::-1] if L != ' '] for row in T(diagram)}\n", + " for (n, source, dest) in moves:\n", + " stacks[source], crates = split_at(stacks[source], -n)\n", + " if model == 9000: crates = crates[::-1]\n", + " stacks[dest].extend(crates)\n", + " return cat(stacks[i].pop() for i in stacks)\n", + "\n", + "answer(5.1, 'SHQWSRBDL', lambda: rearrange(*in5))\n", + "answer(5.2, 'CDTQZHBRS', lambda: rearrange(*in5, model=9001))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 6](https://adventofcode.com/2022/day/6): Tuning Trouble\n", + "\n", + "The input is a single line of characters:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "in6 = parse(6)[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 1: How many characters need to be processed before the first start-of-packet marker is detected?\n", + "\n", + "A start-of-packet marker is when there are *n* distinct characters in a row. I initially made a mistake: I read the instructions hastily and assumed they were asking for the *start* of the start-of-packet marker, not the *end* of it. When AoC told me I had the wrong answer, I went back and figured it out." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def first_marker(stream, n=4) -> int:\n", + " \"\"\"The number of characters read before the first start-of-packet marker is detected.\"\"\"\n", + " return first(i + n for i in range(len(stream)) if len(set(stream[i:i+n])) == n)\n", + "\n", + "answer(6.1, 1987, lambda: first_marker(in6, 4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 2: How many characters need to be processed before the first start-of-message marker is detected?\n", + "\n", + "Now we're looking for 14 distinct characters, not just 4." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "answer(6.2, 3059, lambda: first_marker(in6, 14))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 7](https://adventofcode.com/2022/day/7): No Space Left On Device \n", + "\n", + "The input is a sequence of shell commands; I'll make `parse` split each line into words:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "in7 = parse(7, str.split)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 1: Find all of the directories with a total size of at most 100000. What is the sum of the total sizes of those directories?\n", + "\n", + "I'll keep track of a stack of directories (as Unix/Linux does with the `pushd`, `popd`, and `dirs` commands). All I need to track is `cd` commands (which change the `dirs` stack) and file size listings. I can ignore `ls` command lines and \"`dir` *name*\" output. The `browse` command examines the lines of the transcript and returns a Counter of `{directory_name: total_size}`. From that Counter I can sum the directory sizes that are under 100,000. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def browse(transcript) -> Counter:\n", + " \"\"\"Return a Counter of {directory_name: total_size}, as revealed by the transcript of commands and output.\"\"\"\n", + " dirs = ['/'] # A stack of directories\n", + " sizes = Counter() # Mapping of directory name to total size\n", + " for tokens in transcript:\n", + " if tokens[0].isnumeric():\n", + " for dir in dirs: # All parent directories get credit for this file's size\n", + " sizes[dir] += int(tokens[0])\n", + " elif tokens[0] == '$' and tokens[1] == 'cd':\n", + " dir = tokens[2]\n", + " if dir == '/':\n", + " dirs = ['/']\n", + " elif dir == '..':\n", + " dirs.pop()\n", + " else:\n", + " dirs.append(dirs[-1] + dir + '/')\n", + " return sizes \n", + "\n", + "answer(7.1, 1232307, lambda: sum(v for v in browse(in7).values() if v <= 100_000))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 2: Find the smallest directory that, if deleted, would free up enough space on the filesystem to run the update. What is the total size of that directory?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def free_up(transcript, available=70_000_000, needed=30_000_000) -> int:\n", + " \"\"\"What is the size of the smallest directory you can delete to free up enough space?\"\"\"\n", + " sizes = browse(transcript)\n", + " unused = available - sizes['/']\n", + " return min(sizes[d] for d in sizes if unused + sizes[d] >= needed)\n", + "\n", + "answer(7.2, 7268994, lambda: free_up(in7))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 8](https://adventofcode.com/2022/day/8): Treetop Tree House\n", + "\n", + "The input is a grid of heights of trees; my `Grid` class handles this well:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "in8 = Grid(parse(8, digits))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 1: Consider your map; how many trees are visible from outside the grid?\n", + "\n", + "In the worst case this is *O*(*n*2), so I don't feel too bad about taking the brute force approach of considering every location in the grid, and checking if for **any** direction, **all** the points in that direction have a shorter tree:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def visible_from_outside(grid) -> int:\n", + " \"\"\"How many points on grid are visible from the outside?\n", + " Points such that, for some direction, all the points in that direction have a shorter tree.\"\"\"\n", + " return quantify(any(all(grid[p] < grid[loc] for p in go_in_direction(loc, dir, grid))\n", + " for dir in directions4)\n", + " for loc in grid)\n", + "\n", + "def go_in_direction(start, direction, grid) -> Iterable[Point]:\n", + " \"\"\"All the points in grid that are beyond `start` in `direction`.\"\"\"\n", + " (x, y), (dx, dy) = start, direction\n", + " while True:\n", + " (x, y) = (x + dx, y + dy)\n", + " if (x, y) not in grid:\n", + " return\n", + " yield (x, y)\n", + "\n", + "answer(8.1, 1829, lambda: visible_from_outside(in8))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 2: Consider each tree on your map. What is the highest scenic score possible for any tree?\n", + "\n", + "If I had chosen better abstraction for Part 1, perhaps I could re-use some \"visible\" function. As it is, I can only re-use `go_in_direction`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def scenic_score(loc, grid) -> int:\n", + " \"\"\"The product of the number of trees you can see in each of the 4 directions.\"\"\"\n", + " return prod(viewing_distance(loc, direction, grid) for direction in directions4)\n", + "\n", + "def viewing_distance(loc, direction, grid):\n", + " \"\"\"How many trees can you see from this location in this direction?\"\"\"\n", + " seen = 0\n", + " for seen, p in enumerate(go_in_direction(loc, direction, grid), 1):\n", + " if grid[p] >= grid[loc]:\n", + " break\n", + " return seen\n", + "\n", + "answer(8.2, 291840, lambda: max(scenic_score(loc, in8) for loc in in8))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 3: Exploration" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "*Note*: Up to now, I haven't worried about the efficiency of the code, since every day's code ran in about a millisecond. But today took 50 times longer, so I'm starting to get nervous. Maybe in the coming days I will need to be more aware of efficiency issues.\n", + "\n", + "I can plot the trees. Darker green means taller, and the red dot is the most scenic spot. We see that the taller growth is towards the center of the forest:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "square_plot([max(in8, key=lambda p: scenic_score(p, in8))], 'ro',\n", + " extra=lambda: plt.scatter(*T(in8), c=list(in8.values()), \n", + " cmap=plt.get_cmap('YlGn')))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 9](https://adventofcode.com/2022/day/9): Rope Bridge\n", + "\n", + "The input consists of command lines, which we can parse as one tuple of two atoms (a command name and an integer) per line:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "in9 = parse(9, atoms)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These are motion commands for the head of a rope; the tail (one knot away) must follow, so that it is always on or adjacent to the head's location.\n", + "\n", + "#### Part 1: Simulate your complete hypothetical series of motions. How many positions does the tail of the rope visit at least once?\n", + "\n", + "The rules for how the tail moves are a bit tricky, but otherwise the control flow is easy. I'll return the set of visited squares, in case I need it in part 2, but for this part I just need the size of the set. I provide for an optional starting position; this is arbitrary, but it makes it eeasier to follow the example in the puzzle description if I start at the same place they start at." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def move_rope(motions, start=(0, 4)) -> Set[Point]:\n", + " \"\"\"Move rope according to `motions`; return set of points visited by tail.\"\"\"\n", + " deltas = dict(R=East, L=West, U=North, D=South)\n", + " H = T = start # Head and Tail oof the rope\n", + " visited = {start}\n", + " for (op, n) in motions:\n", + " for _ in range(n):\n", + " H = add(H, deltas[op])\n", + " T = move_tail(T, H)\n", + " visited.add(T)\n", + " return visited\n", + "\n", + "def move_tail(T: Point, H: Point) -> Point:\n", + " \"\"\"Move tail to be close to head if it is not already adjacent.\"\"\"\n", + " dx, dy = sub(H, T)\n", + " if max(abs(dx), abs(dy)) > 1:\n", + " if dx: # Different column\n", + " T = add(T, (sign(dx), 0))\n", + " if dy: # Different row\n", + " T = add(T, (0, sign(dy)))\n", + " return T\n", + " \n", + "answer(9.1, 6236, lambda: len(move_rope(in9, (0, 4))))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 2: Simulate your complete series of motions on a larger rope with ten knots. How many positions does the tail of the rope visit at least once?\n", + "\n", + "I'll re-write `move_rope` to take an optional argument giving the number of knots in the rope. Then instead of just one `move_tail` per loop, I'll move all the non-head knots in the rope, each one to follow the one immediately in front of it. I'll show that the re-write is backwards compatible by repeating the two-knot solution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def move_rope(motions, start=(0, 4), knots=2) -> Set[Point]:\n", + " deltas = dict(R=East, L=West, U=North, D=South)\n", + " rope = [start] * knots\n", + " visited = {start}\n", + " for (op, n) in motions:\n", + " for _ in range(n):\n", + " rope[0] = add(rope[0], deltas[op])\n", + " for k in range(1, knots):\n", + " rope[k] = move_tail(rope[k], rope[k - 1])\n", + " visited.add(rope[-1])\n", + " return visited\n", + "\n", + "answer(9.1, 6236, lambda: len(move_rope(in9, (0, 4))))\n", + "answer(9.2, 2449, lambda: len(move_rope(in9, (0, 4), knots=10)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Part 3: Exploration\n", + "\n", + "Because I chose to return the set of visited points, I can plot the tail of the rope (for various size ropes):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "square_plot(move_rope(in9, knots=2), '.');" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "square_plot(move_rope(in9, knots=10), '.');" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "square_plot(move_rope(in9, knots=20), '.');" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 10](https://adventofcode.com/2022/day/10): Cathode-Ray Tube \n", + "\n", + "Another puzzle involving running an interpreter on a program. The program is a sequence of lines, each containing one or two atoms:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "in10 = parse(10, atoms)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We're never sure what we will need in Part 2, so I'll make the program interpreter output the cycle number and value of X for every cycle. For Part 1, we sum the product of these two for cycles in {20, 60, 100, ... 220}:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def run(program) -> Iterable[Tuple[int, int]]:\n", + " \"\"\"Execute the program, oputputing (cycle_number, X_register_value) on each cycle.\n", + " Remember that an `addx` instruction takes 2 cycles.\"\"\"\n", + " X = 1\n", + " cycle = 0\n", + " results = []\n", + " for (op, *args) in program:\n", + " cycle += 1\n", + " results.append((cycle, X))\n", + " if op == 'addx':\n", + " cycle += 1\n", + " results.append((cycle, X))\n", + " X += args[0]\n", + " return results\n", + "\n", + "answer(10.1, 12560, lambda: sum(c * X for c, X in run(in10) if c in range(20, 221, 40)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For Part 2 I'm glad I kept all the `(cycle, X)` pairs. I just need to map `cycle` to `(x, y)` positions on the screen, and plot the result. Then I'll use my eyeballs (not an OCR program) to determine what letters are indicated." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def render(program):\n", + " \"\"\"As the cycle number scans a 40-pixel wide CRT, turn on pixels\n", + " where register X and the scan position differ by 1 or less.x\"\"\"\n", + " points = []\n", + " for (c, X) in run(program):\n", + " x, y = (c - 1) % 40, (c - 1) // 40\n", + " if abs(X - x) <= 1:\n", + " points.append((x, y))\n", + " square_plot(points)\n", + " \n", + "answer(10.2, \"PLPAFBCL\", lambda: render(in10) or \"PLPAFBCL\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Summary\n", + "\n", + "The results so far, with run times:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "answers" + ] + }, + { + "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 +} diff --git a/ipynb/AdventUtils.ipynb b/ipynb/AdventUtils.ipynb new file mode 100644 index 0000000..e689030 --- /dev/null +++ b/ipynb/AdventUtils.ipynb @@ -0,0 +1,596 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
Peter Norvig
Decembers 2016–2021
\n", + "\n", + "# Advent of Code Utilities\n", + "\n", + "Stuff I might need for [Advent of Code](https://adventofcode.com). First, some imports that I have used in past AoC years:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from collections import Counter, defaultdict, namedtuple, deque, abc\n", + "from dataclasses import dataclass\n", + "from itertools import permutations, combinations, cycle, chain\n", + "from itertools import count as count_from, product as cross_product\n", + "from typing import *\n", + "from statistics import mean, median\n", + "from math import ceil, floor, factorial, gcd, log, log2, log10, sqrt, inf\n", + "from functools import reduce\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import time\n", + "import heapq\n", + "import string\n", + "import functools\n", + "import pathlib\n", + "import re" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Daily Input Parsing\n", + "\n", + "Each day's work will consist of three tasks, denoted by three sections in the notebook:\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", + " - Breaks the file into a sequence of *items* separated by `sep` (default newline).\n", + " - Applies `parser` to each item and returns the results as a tuple.\n", + " - Useful parser functions include `ints`, `digits`, `atoms`, `words`, and the built-ins `int` and `str`.\n", + " - Prints the first few input lines and output records. This is useful to me as a debugging tool, and to the reader.\n", + "- **Part 1**: Understand the day's instructions and:\n", + " - Write code to compute the answer to Part 1.\n", + " - Once I have computed the answer and submitted it to the AoC site to verify it is correct, I record it with the `answer` function.\n", + "- **Part 2**: Repeat the above steps for Part 2.\n", + "- Occasionally I'll introduce a **Part 3** where I explore beyond the official instructions.\n", + "\n", + "Here is `parse` and some helper functions for it:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "current_year = 2022 # Subdirectory name for input files\n", + "lines = '\\n' # For inputs where each record is a line\n", + "paragraphs = '\\n\\n' # For inputs where each record is a paragraph \n", + "\n", + "def parse(day_or_text:Union[int, str], parser:Callable=str, sep:Callable=lines, show=6) -> tuple:\n", + " \"\"\"Split the input text into items separated by `sep`, and apply `parser` to each.\n", + " The first argument is either the text itself, or the day number of a text file.\"\"\"\n", + " text = get_text(day_or_text)\n", + " print_parse_items('Puzzle input', text.splitlines(), show, 'line')\n", + " records = mapt(parser, text.rstrip().split(sep))\n", + " if parser != str:\n", + " print_parse_items('Parsed representation', records, show, f'{type(records[0]).__name__}')\n", + " return records\n", + "\n", + "def get_text(day_or_text:Union[int, str]) -> str:\n", + " \"\"\"The text used as input to the puzzle: either a string or the day number of a file.\"\"\"\n", + " if isinstance(day_or_text, int):\n", + " return pathlib.Path(f'AOC/{current_year}/input{day_or_text}.txt').read_text()\n", + " else:\n", + " return day_or_text\n", + "\n", + "def print_parse_items(source, items, show:int, name:str, sep=\"─\"*100):\n", + " \"\"\"Print verbose output from `parse` for lines or records.\"\"\"\n", + " if not show:\n", + " return\n", + " count = f'1 {name}' if len(items) == 1 else f'{len(items)} {name}s'\n", + " for line in (sep, f'{source} ➜ {count}:', sep, *items[:show]):\n", + " print(truncate(line))\n", + " if show < len(items):\n", + " print('...')\n", + " \n", + "def truncate(object, width=100) -> str:\n", + " \"\"\"Use elipsis to truncate `str(object)` to `width` characters, if necessary.\"\"\"\n", + " string = str(object)\n", + " return string if len(string) <= width else string[:width-4] + ' ...'\n", + "\n", + "def parse_sections(specs: Iterable) -> Callable:\n", + " \"\"\"Return a parser that uses the first spec to parse the first section, the second for second, etc.\n", + " Each spec is either parser or [parser, sep].\"\"\"\n", + " specs = ([spec] if callable(spec) else spec for spec in specs)\n", + " fns = ((lambda section: parse(section, *spec, show=0)) for spec in specs)\n", + " return lambda section: next(fns)(section)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "## Functions that can be used by `parse`\n", + "\n", + "Char = str # Intended as the type of a one-character string\n", + "Atom = Union[str, float, int] # The type of a string or number\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(r'-?[0-9]+', text))\n", + "\n", + "def positive_ints(text: str) -> Tuple[int]:\n", + " \"\"\"A tuple of all the integers in text, ignoring non-number characters.\"\"\"\n", + " return mapt(int, re.findall(r'[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(r'[0-9]', text))\n", + "\n", + "def words(text: str) -> Tuple[str]:\n", + " \"\"\"A tuple of all the alphabetic words in text, ignoring non-letters.\"\"\"\n", + " return tuple(re.findall(r'[a-zA-Z]+', text))\n", + "\n", + "def atoms(text: str) -> Tuple[Atom]:\n", + " \"\"\"A tuple of all the atoms (numbers or identifiers) in text. Skip punctuation.\"\"\"\n", + " return mapt(atom, re.findall(r'[+-]?\\d+\\.?\\d*|\\w+', text))\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 x.is_integer() else x\n", + " except ValueError:\n", + " return text\n", + " \n", + "def mapt(function: Callable, sequence) -> tuple:\n", + " \"\"\"`map`, with the result as a tuple.\"\"\"\n", + " return tuple(map(function, sequence))" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "## TESTS\n", + "\n", + "assert parse(\"hello\\nworld\", show=0) == ('hello', 'world')\n", + "assert parse(\"123\\nabc7\", digits, show=0) == ((1, 2, 3), (7,))\n", + "assert truncate('hello world', 99) == 'hello world'\n", + "assert truncate('hello world', 8) == 'hell ...'\n", + "\n", + "assert atoms('hello, cruel_world! 24-7') == ('hello', 'cruel_world', 24, -7)\n", + "assert words('hello, cruel_world! 24-7') == ('hello', 'cruel', 'world')\n", + "assert digits('hello, cruel_world! 24-7') == (2, 4, 7)\n", + "assert ints('hello, cruel_world! 24-7') == (24, -7)\n", + "assert positive_ints('hello, cruel_world! 24-7') == (24, 7)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Daily Answers\n", + "\n", + "Here is the `answer` function, which gives verification of a correct computation (or an error message for an incorrect computation), times how long the computation took, ans stores the result in the dict `answers`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# `answers` is a dict of {puzzle_number_id: message_about_results}\n", + "answers = {} \n", + "\n", + "def answer(puzzle, correct, code: callable) -> str:\n", + " \"\"\"Verify that calling `code` computes the `correct` answer for `puzzle`. \n", + " Record results in the dict `answers`. Prints execution time.\"\"\"\n", + " def pretty(x): return f'{x:,d}' if is_int(x) else truncate(x)\n", + " start = time.time()\n", + " got = code()\n", + " dt = time.time() - start\n", + " ans = pretty(got)\n", + " msg = f'{dt:5.3f} seconds for ' + (\n", + " f'correct answer: {ans}' if (got == correct) else\n", + " f'WRONG!! ANSWER: {ans}; EXPECTED {pretty(correct)}')\n", + " answers[puzzle] = msg\n", + " return msg" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Additional utility functions \n", + "\n", + "All of the following have been used in solutions to multiple puzzles in the past, so I pulled them all in here:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "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 x in numbers:\n", + " result *= x\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 minmax(numbers) -> Tuple[int, int]:\n", + " \"\"\"A tuple of the (minimum, maximum) of numbers.\"\"\"\n", + " numbers = list(numbers)\n", + " return min(numbers), max(numbers)\n", + "\n", + "def first(iterable) -> Optional[object]: \n", + " \"\"\"The first element in an iterable, or None.\"\"\"\n", + " return next(iter(iterable), None)\n", + "\n", + "def T(matrix: Sequence[Sequence]) -> List[Tuple]:\n", + " \"\"\"The transpose of a matrix: T([(1,2,3), (4,5,6)]) == [(1,4), (2,5), (3,6)]\"\"\"\n", + " return list(zip(*matrix))\n", + "\n", + "def cover(*integers) -> range:\n", + " \"\"\"A `range` that covers all the given integers, and any in between them.\n", + " cover(lo, hi) is a an inclusive (or closed) range, equal to range(lo, hi + 1).\"\"\"\n", + " return range(min(integers), max(integers) + 1)\n", + "\n", + "def the(sequence) -> object:\n", + " \"\"\"Return the one item in a sequence. Raise error if not exactly one.\"\"\"\n", + " items = list(sequence)\n", + " if not len(items) == 1:\n", + " raise ValueError(f'Expected exactly one item in the sequence {items}')\n", + " return items[0]\n", + "\n", + "def split_at(sequence, i) -> Tuple[Sequence, Sequence]:\n", + " \"\"\"The sequence split into two pieces: (before position i, and i-and-after).\"\"\"\n", + " return sequence[:i], sequence[i:]\n", + "\n", + "def batched(data, n) -> list:\n", + " \"Batch data into lists of length n. The last batch may be shorter.\"\n", + " # batched('ABCDEFG', 3) --> ABC DEF G\n", + " return [data[i:i+n] for i in range(0, len(data), n)]\n", + "\n", + "def sliding_window(sequence, n) -> Iterable[Sequence]:\n", + " \"\"\"All length-n subsequences of sequence.\"\"\"\n", + " return (sequence[i:i+n] for i in range(len(sequence) + 1 - n))\n", + "\n", + "def ignore(*args) -> None: \"Just return None.\"; return None\n", + "\n", + "def is_int(x) -> bool: \"Is x an int?\"; return isinstance(x, int) \n", + "\n", + "def sign(x) -> int: \"0, +1, or -1\"; return (0 if x == 0 else +1 if x > 0 else -1)\n", + "\n", + "def append(sequences) -> Sequence: \"Append sequences into a list\"; return list(flatten(sequences))\n", + "\n", + "def union(sets) -> set: \"Union of several sets\"; return set().union(*sets)\n", + "\n", + "def intersection(sets):\n", + " \"Intersection of several sets.\"\n", + " first, *rest = sets\n", + " return set(first).intersection(*rest)\n", + "\n", + "def square_plot(points, marker='o', size=12, extra=None, **kwds):\n", + " \"\"\"Plot `points` in a square of given `size`, with no axis labels.\n", + " Calls `extra()` to do more plt.* stuff if defined.\"\"\"\n", + " plt.figure(figsize=(size, size))\n", + " plt.plot(*T(points), marker, **kwds)\n", + " if extra: extra()\n", + " plt.axis('square'); plt.axis('off'); plt.gca().invert_yaxis()\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\n", + "\n", + "flatten = chain.from_iterable # Yield items from each sequence in turn\n", + "cat = ''.join\n", + "cache = functools.lru_cache(None)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "## TESTS\n", + "\n", + "assert quantify(words('This is a test'), str.islower) == 3\n", + "assert mapt(first, words('This is a test')) == ('T', 'i', 'a', 't')\n", + "assert multimap(((i % 3), i) for i in range(9)) == {0: [0, 3, 6], 1: [1, 4, 7], 2: [2, 5, 8]}\n", + "assert prod([2, 3, 5]) == 30\n", + "assert total(Counter('hello, world')) == 12\n", + "assert cover(3, 1, 4, 1, 5) == range(1, 6)\n", + "assert minmax([3, 1, 4, 1, 5, 9]) == (1, 9)\n", + "assert first('abc') == 'a'\n", + "assert T([(1, 2, 3), (4, 5, 6)]) == [(1, 4), (2, 5), (3, 6)]\n", + "assert the({1}) == 1\n", + "assert split_at('hello, world', 6) == ('hello,', ' world')\n", + "assert batched('abcdefghi', 3) == ['abc', 'def', 'ghi']\n", + "assert list(sliding_window('abcdefghi', 3)) == ['abc', 'bcd', 'cde', 'def', 'efg', 'fgh', 'ghi']\n", + "assert is_int(-42) and not is_int('one')\n", + "assert sign(-42) == -1 and sign(0) == 0 and sign(42) == +1\n", + "assert append(([1, 2], [3, 4], [5, 6])) == [1, 2, 3, 4, 5, 6]\n", + "assert union([{1, 2}, {3, 4}, {5, 6}]) == {1, 2, 3, 4, 5, 6}\n", + "assert intersection([{1, 2, 3}, {2, 3, 4}, {2, 4, 6, 8}]) == {2}\n", + "assert clock_mod(24, 12) == 12 and 24 % 12 == 0\n", + "assert list(flatten(['abc', 'def', '123'])) == ['a', 'b', 'c', 'd', 'e', 'f', '1', '2', '3']\n", + "assert cat(['hello', 'world']) == 'helloworld'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Points on a Grid\n", + "\n", + "Many puzzles seem to involve a two-dimensional rectangular grid with integer coordinates. First we'll define the two-dimensional `Point`, then the `Grid`." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "Point = Tuple[int, int] # (x, y) points on a grid\n", + "\n", + "def X_(point) -> int: \"X coordinate\"; return point[0]\n", + "def Y_(point) -> int: \"Y coordinate\"; return point[1]\n", + "\n", + "def distance(p: Point, q: Point) -> float:\n", + " \"\"\"Distance between two points.\"\"\"\n", + " dx, dy = abs(X_(p) - X_(q)), abs(Y_(p) - Y_(q))\n", + " return dx + dy if dx == 0 or dy == 0 else (dx ** 2 + dy ** 2) ** 0.5\n", + "\n", + "def manhatten_distance(p: Point, q: Point) -> int:\n", + " \"\"\"Distance along grid lines between two points.\"\"\"\n", + " return sum(abs(pi - qi) for pi, qi in zip(p, q))\n", + "\n", + "def add(p: Point, q: Point) -> Point:\n", + " \"\"\"Add two points.\"\"\"\n", + " return (X_(p) + X_(q), Y_(p) + Y_(q))\n", + "\n", + "def sub(p: Point, q: Point) -> Point:\n", + " \"\"\"Subtract point q from point p.\"\"\"\n", + " return (X_(p) - X_(q), Y_(p) - Y_(q))\n", + "\n", + "directions4 = North, South, East, West = ((0, -1), (0, 1), (1, 0), (-1, 0))\n", + "directions8 = directions4 + ((1, 1), (1, -1), (-1, 1), (-1, -1))" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "## TESTS\n", + "\n", + "p, q = (0, 3), (4, 0)\n", + "assert Y_(p) == 3 and X_(q) == 4\n", + "assert distance(p, q) == 5\n", + "assert manhatten_distance(p, q) == 7\n", + "assert add(p, q) == (4, 3)\n", + "assert sub(p, q) == (-4, 3)\n", + "assert add(North, South) == (0,0)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "class Grid(dict):\n", + " \"\"\"A 2D grid, implemented as a mapping of {(x, y): cell_contents}.\"\"\"\n", + " def __init__(self, mapping_or_rows, directions=directions4):\n", + " \"\"\"Initialize with either (e.g.) `Grid({(0, 0): 1, (1, 0): 2, ...})`, or\n", + " `Grid([(1, 2, 3), (4, 5, 6)]).\"\"\"\n", + " self.update(mapping_or_rows if isinstance(mapping_or_rows, abc.Mapping) else\n", + " {(x, y): val \n", + " for y, row in enumerate(mapping_or_rows) \n", + " for x, val in enumerate(row)})\n", + " self.width = max(map(X_, self)) + 1\n", + " self.height = max(map(Y_, self)) + 1\n", + " self.directions = directions\n", + " \n", + " def copy(self): return Grid(self, directions=self.directions)\n", + " \n", + " def neighbors(self, point) -> List[Point]:\n", + " \"\"\"Points on the grid that neighbor `point`.\"\"\"\n", + " return [add(point, Δ) for Δ in self.directions if add(point, Δ) in self]\n", + " \n", + " def to_rows(self, default='.') -> List[List[object]]:\n", + " \"\"\"The contents of the grid in a rectangular list of lists.\"\"\"\n", + " return [[self.get((x, y), default) for x in range(self.width)]\n", + " for y in range(self.height)]\n", + " \n", + " def to_picture(self, sep='', default='.') -> str:\n", + " \"\"\"The contents of the grid as a picture.\"\"\"\n", + " return '\\n'.join(map(cat, self.to_rows(default)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# A* Search\n", + "\n", + "Many puzzles involve searching over a branching tree of possibilities. For many puzzles, an ad-hoc solution is fine. But when there is a larger search space, it is useful to have a pre-defined efficient best-first search algorithm, and in particular an A* search, which incorporates a heuristic function to estimate the remaining distance to the goal. This is a somewhat heavy-weight approach, as it requires the solver to define a subclass of `SearchProblem`." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "def A_star_search(problem, h=None):\n", + " \"\"\"Search nodes with minimum f(n) = path_cost(n) + h(n) value first.\"\"\"\n", + " h = h or problem.h\n", + " return best_first_search(problem, f=lambda n: n.path_cost + h(n))\n", + "\n", + "def best_first_search(problem, f) -> 'Node':\n", + " \"Search nodes with minimum f(node) value first.\"\n", + " node = Node(problem.initial)\n", + " frontier = PriorityQueue([node], key=f)\n", + " reached = {problem.initial: node}\n", + " while frontier:\n", + " node = frontier.pop()\n", + " if problem.is_goal(node.state):\n", + " return node\n", + " for child in expand(problem, node):\n", + " s = child.state\n", + " if s not in reached or child.path_cost < reached[s].path_cost:\n", + " reached[s] = child\n", + " frontier.add(child)\n", + " return search_failure" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "class SearchProblem:\n", + " \"\"\"The abstract class for a search problem. A new domain subclasses this,\n", + " overriding `actions` and perhaps other methods.\n", + " The default heuristic is 0 and the default action cost is 1 for all states.\n", + " When you create an instance of a subclass, specify `initial`, and `goal` states \n", + " (or give an `is_goal` method) and perhaps other keyword args for the subclass.\"\"\"\n", + "\n", + " def __init__(self, initial=None, goal=None, **kwds): \n", + " self.__dict__.update(initial=initial, goal=goal, **kwds) \n", + " \n", + " def __str__(self):\n", + " return '{}({!r}, {!r})'.format(type(self).__name__, self.initial, self.goal)\n", + " \n", + " def actions(self, state): raise NotImplementedError\n", + " def result(self, state, action): return action # Simplest case: action is result state\n", + " def is_goal(self, state): return state == self.goal\n", + " def action_cost(self, s, a, s1): return 1\n", + " def h(self, node): return 0 # Never overestimate!\n", + " \n", + "class GridProblem(SearchProblem):\n", + " \"\"\"Problem for searching a grid from a start to a goal location.\n", + " A state is just an (x, y) location in the grid.\"\"\"\n", + " def actions(self, loc): return self.grid.neighbors(loc)\n", + " def result(self, loc1, loc2): return loc2\n", + " def action_cost(self, s1, a, s2): return self.grid[s2]\n", + " def h(self, node): return manhatten_distance(node.state, self.goal) \n", + "\n", + "class Node:\n", + " \"A Node in a search tree.\"\n", + " def __init__(self, state, parent=None, action=None, path_cost=0):\n", + " self.__dict__.update(state=state, parent=parent, action=action, path_cost=path_cost)\n", + "\n", + " def __repr__(self): return f'Node({self.state})'\n", + " def __len__(self): return 0 if self.parent is None else (1 + len(self.parent))\n", + " def __lt__(self, other): return self.path_cost < other.path_cost\n", + " \n", + "search_failure = Node('failure', path_cost=inf) # Indicates an algorithm couldn't find a solution.\n", + " \n", + "def expand(problem, node):\n", + " \"Expand a node, generating the children nodes.\"\n", + " s = node.state\n", + " for action in problem.actions(s):\n", + " s2 = problem.result(s, action)\n", + " cost = node.path_cost + problem.action_cost(s, action, s2)\n", + " yield Node(s2, node, action, cost)\n", + " \n", + "def path_actions(node):\n", + " \"The sequence of actions to get to this node.\"\n", + " if node.parent is None:\n", + " return [] \n", + " return path_actions(node.parent) + [node.action]\n", + "\n", + "def path_states(node):\n", + " \"The sequence of states to get to this node.\"\n", + " if node in (search_failure, None): \n", + " return []\n", + " return path_states(node.parent) + [node.state]" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "class PriorityQueue:\n", + " \"\"\"A queue in which the item with minimum key(item) is always popped first.\"\"\"\n", + "\n", + " def __init__(self, items=(), key=lambda x: x): \n", + " self.key = key\n", + " self.items = [] # a heap of (score, item) pairs\n", + " for item in items:\n", + " self.add(item)\n", + " \n", + " def add(self, item):\n", + " \"\"\"Add item to the queue.\"\"\"\n", + " pair = (self.key(item), item)\n", + " heapq.heappush(self.items, pair)\n", + "\n", + " def pop(self):\n", + " \"\"\"Pop and return the item with min f(item) value.\"\"\"\n", + " return heapq.heappop(self.items)[1]\n", + " \n", + " def top(self): return self.items[0][1]\n", + "\n", + " def __len__(self): return len(self.items)" + ] + } + ], + "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 +}