From c30614ec4587418fb264efb466cba58991029f16 Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Sat, 26 Dec 2020 00:47:12 -0800 Subject: [PATCH] Add files via upload --- ipynb/Advent-2020.ipynb | 1370 +++++++++++++++++++++++++++++---------- 1 file changed, 1012 insertions(+), 358 deletions(-) diff --git a/ipynb/Advent-2020.ipynb b/ipynb/Advent-2020.ipynb index 20c4a0f..50abe91 100644 --- a/ipynb/Advent-2020.ipynb +++ b/ipynb/Advent-2020.ipynb @@ -8,23 +8,20 @@ "\n", "# Advent of Code 2020\n", "\n", - "This year I return to [Advent of Code](https://adventofcode.com), as I did in [2016](Advent+of+Code), [17](Advent+2017), and [18](Advent-2018.ipynb). Thank you, [Eric Wastl](http://was.tl/)! This notebook describes each day's puzzle only briefly; you'll have to look at the [Advent of Code website](https://adventofcode.com/2020) if you want the full details. Each puzzle has a part 1 and a part 2.\n", + "This year I return to [Advent of Code](https://adventofcode.com), as I did in [2016](Advent+of+Code), [17](Advent+2017), and [18](Advent-2018.ipynb). Thank you, [**Eric Wastl**](http://was.tl/)! You'll have to look at the [Advent of Code website](https://adventofcode.com/2020) if you want the full description of each puzzle; this notebook gives only a brief summary. \n", "\n", - "For each day from 1 to 25, I'll write **four pieces of code** with the following format (and perhaps some auxiliary code). For example, on day 3:\n", - "- `in3: List[str] = data(3)`: the day's input data, parsed into an appropriate form (here, a list of string lines). Most days the data comes from a file, read via the function `data`, but some days the data is so small I just copy and paste it.\n", - "- `def day3_1(nums): ... `: a function that takes the day's data as input and returns the answer for part 1.\n", - "- `def day3_2(nums): ... `: a function that takes the day's data as input and returns the answer for part 2.\n", - "- `do(3)`: runs `day3_1(in3)`. I'll then use the result to hopefully unlock part 2 and define `day3_2`, which also gets run when I call `do(3)` again. Once I verify both answers, I'll change `do(3)` to `do(3, 167, 736527114)` to serve as a unit test.\n", + "For each day from 1 to 25, I'll write **four pieces of code**, each in a separate notebook cell. For example, on day 3:\n", + "- `in3: List[str] = data(3)`: the day's input data, parsed into an appropriate form (here, a list of string lines). Most days the data comes from a file, read via the function `data` (which has optional arguments to say how to split the data into sections/records and how to parse each one), but some days the data is so small I just copy and paste it.\n", + "- `def day3_1(nums): ... `: a function that takes the day's data as input and returns the answer for Part 1.\n", + "- `def day3_2(nums): ... `: a function that takes the day's data as input and returns the answer for Part 2.\n", + "- `do(3)`: executes the call `day3_1(in3)`. I'll then use the result to hopefully unlock part 2 and define `day3_2`, which also gets run when I call `do(3)` again. Once I verify both answers, I'll change `do(3)` to `do(3, 167, 736527114)` to serve as a unit test.\n", "\n", "# Day 0: Imports and Utility Functions\n", "\n", "Preparations prior to Day 1:\n", "- Some imports.\n", "- A way to read the day's data file and to print/check the output.\n", - "- Some utilities that are likely to be useful.\n", - "\n", - "Since I'm not even attempting to compete for speed, I'll take the time to use reasonable variable names (not single-letter names), and to give type annotations for most of the functions I define (but not the `day` functions, which all return `int`). Traditionally, a lot of problems are of the form \"How many of your input items have property P?\", for which I can do `quantify(inputs, P)` or \"What is the sum of the result of applying F to each input item?\", for which I can do `sum(map(F, inputs))`.\n", - "\n" + "- Some utilities that are likely to be useful." ] }, { @@ -35,14 +32,16 @@ "source": [ "from __future__ import annotations\n", "from collections import Counter, defaultdict, namedtuple, deque\n", - "from itertools import permutations, combinations, cycle, product, islice, chain\n", - "from functools import lru_cache\n", - "from typing import Dict, Tuple, Set, List, Iterator, Optional\n", - "from sys import maxsize\n", + "from itertools import permutations, combinations, product, chain\n", + "from functools import lru_cache, reduce\n", + "from typing import Dict, Tuple, Set, List, Iterator, Optional, Union, Sequence\n", + "from contextlib import contextmanager\n", "\n", - "import re\n", + "import operator\n", + "import math\n", "import ast\n", - "import operator" + "import sys\n", + "import re" ] }, { @@ -53,20 +52,23 @@ "source": [ "def data(day: int, parser=str, sep='\\n') -> list:\n", " \"Split the day's input file into sections separated by `sep`, and apply `parser` to each.\"\n", - " sections = open(f'data/advent2020/input{day}.txt').read().rstrip().split(sep)\n", - " return [parser(section) for section in sections]\n", + " with open(f'data/advent2020/input{day}.txt') as f:\n", + " sections = f.read().rstrip().split(sep)\n", + " return list(map(parser, sections))\n", " \n", - "def do(day, *answers) -> Dict[int, int]:\n", - " \"E.g., do(3) returns {1: day3_1(in3), 2: day3_2(in3)}. Verifies `answers` if given.\"\n", + "def do(day, *answers) -> List[int]:\n", + " \"E.g., do(3) returns [day3_1(in3), day3_2(in3)]. Verifies `answers` if given.\"\n", " g = globals()\n", - " got = {}\n", + " got = []\n", " for part in (1, 2):\n", " fname = f'day{day}_{part}'\n", " if fname in g: \n", - " got[part] = g[fname](g[f'in{day}'])\n", + " got.append(g[fname](g[f'in{day}']))\n", " if len(answers) >= part: \n", - " assert got[part] == answers[part - 1], (\n", - " f'{fname}(in{day}) got {got[part]}; expected {answers[part - 1]}')\n", + " assert got[-1] == answers[part - 1], (\n", + " f'{fname}(in{day}) got {got[-1]}; expected {answers[part - 1]}')\n", + " else:\n", + " got.append(None)\n", " return got" ] }, @@ -76,6 +78,13 @@ "metadata": {}, "outputs": [], "source": [ + "Number = Union[float, int]\n", + "Atom = Union[Number, str]\n", + "Char = str # Type used to indicate a single character\n", + "\n", + "cat = ''.join\n", + "flatten = chain.from_iterable\n", + "\n", "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", @@ -84,27 +93,61 @@ " \"Return first item in iterable, or default.\"\n", " return next(iter(iterable), default)\n", "\n", - "def multimap(items: Iterable[Tuple]) -> dict:\n", - " \"Given (key, val) pairs, return {key: [val, ....], ...}.\"\n", - " result = defaultdict(list)\n", - " for (key, val) in items:\n", - " result[key].append(val)\n", - " return result\n", - "\n", - "def prod(numbers) -> float: # Will be math.prod in Python 3.8, but I'm in 3.7\n", + "def prod(numbers) -> Number:\n", " \"The product of an iterable of numbers.\" \n", - " result = 1\n", - " for n in numbers:\n", - " result *= n\n", - " return result\n", + " return reduce(operator.mul, numbers, 1)\n", + "\n", + "def dot(A, B) -> Number: \n", + " \"The dot product of two vectors of numbers.\"\n", + " return sum(a * b for a, b in zip(A, B))\n", "\n", "def ints(text: str) -> Tuple[int]:\n", " \"Return a tuple of all the integers in text.\"\n", - " return tuple(map(int, re.findall('-?[0-9]+', text)))\n", + " return mapt(int, re.findall('-?[0-9]+', text))\n", "\n", - "cat = ''.join\n", - "flatten = chain.from_iterable\n", - "Char = str # Type used to indicate a single character" + "def lines(text: str) -> List[str]:\n", + " \"Split the text into a list of lines.\"\n", + " return text.strip().splitlines()\n", + "\n", + "def mapt(fn, *args): \n", + " \"Do map(fn, *args) and make the result a tuple.\"\n", + " return tuple(map(fn, *args))\n", + "\n", + "def atoms(text: str, ignore=r'', sep=None) -> Tuple[Union[int, str]]:\n", + " \"Parse text into atoms separated by sep, with regex ignored.\"\n", + " text = re.sub(ignore, '', text)\n", + " return mapt(atom, text.split(sep))\n", + "\n", + "def atom(text: str, types=(int, str)):\n", + " \"Parse text into one of the given types.\"\n", + " for typ in types:\n", + " try:\n", + " return typ(text)\n", + " except ValueError:\n", + " pass\n", + "\n", + "@contextmanager\n", + "def binding(**kwds):\n", + " \"Bind global variables within a context; revert to old values on exit.\"\n", + " old_values = {k: globals()[k] for k in kwds}\n", + " try:\n", + " globals().update(kwds)\n", + " yield # Stuff within the context gets run here.\n", + " finally:\n", + " globals().update(old_values)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Notes:\n", + "- Since I'm not even attempting to compete to be among the first 100 people to find a solution, I'll take the time to write docstrings; to use reasonable variable names (not single-letter names); and to give type annotations for most functions (but not the `day` functions, which all return `int`, except `day21_2`).\n", + "- Traditionally, a lot of AoC problems are solved by one of the following two forms:\n", + " - `quantify(inputs, P)`: How many of your input items have property P?\n", + " - `sum(map(F, inputs))`: What is the sum of the result of applying F to each input item?\n", + "- Some days I will re-use code that was defined on a previous day.\n", + "- I will give a few tests using `assert`, but far fewer test cases than if I was programming seriously." ] }, { @@ -162,7 +205,7 @@ { "data": { "text/plain": [ - "{1: 787776, 2: 262738554}" + "[787776, 262738554]" ] }, "execution_count": 7, @@ -236,7 +279,7 @@ { "data": { "text/plain": [ - "{1: 383, 2: 272}" + "[383, 272]" ] }, "execution_count": 11, @@ -262,7 +305,7 @@ " ..#.#...#.#\n", " .#...##..#.\n", " \n", - "where each `#` is a tree and the pattern in each row implicitly repeats to the right.\n", + "where each `#` is a tree and the pattern in each row implicitly repeats to the right. We'll call a list of row-strings a *picture*.\n", "\n", "1. Starting at the top-left corner of your map and following a slope of down 1 and right 3, how many trees would you encounter?\n", "2. What do you get if you multiply together the number of trees encountered on each of the slopes 1/1, 1/3, 1/5, 1/7, 2/1?\n" @@ -285,10 +328,10 @@ "metadata": {}, "outputs": [], "source": [ - "def day3_1(rows, dx=3, dy=1, tree='#'): \n", + "def day3_1(picture, dx=3, dy=1, tree='#'): \n", " \"How many trees are on the coordinates on the slope dy/dx?\"\n", " return quantify(row[(dx * y) % len(row)] == tree\n", - " for y, row in enumerate(rows[::dy]))" + " for y, row in enumerate(picture[::dy]))" ] }, { @@ -297,9 +340,9 @@ "metadata": {}, "outputs": [], "source": [ - "def day3_2(rows):\n", + "def day3_2(picture):\n", " \"What is the product of the number of trees on these five slopes?\"\n", - " def t(dx, dy): return day3_1(rows, dx, dy)\n", + " def t(dx, dy): return day3_1(picture, dx, dy)\n", " return t(1, 1) * t(3, 1) * t(5, 1) * t(7, 1) * t(1, 2) " ] }, @@ -311,7 +354,7 @@ { "data": { "text/plain": [ - "{1: 167, 2: 736527114}" + "[167, 736527114]" ] }, "execution_count": 15, @@ -334,8 +377,6 @@ " ecl:gry pid:860033327 eyr:2020 hcl:#fffffd\n", " byr:1937 iyr:2017 cid:147 hgt:183cm\n", "\n", - " iyr:2013 ecl:amb cid:350 eyr:2023 pid:028048884 hcl:#cfa07d byr:1929\n", - "\n", " hcl:#ae17e1 iyr:2013\n", " eyr:2024 ecl:brn pid:760753108 byr:1931 hgt:179cm\n", " \n", @@ -369,9 +410,7 @@ "source": [ "required_fields = {'byr', 'ecl', 'eyr', 'hcl', 'hgt', 'iyr', 'pid'}\n", "\n", - "def valid_passport(passport) -> bool: return required_fields.issubset(passport)\n", - "\n", - "def day4_1(passports): return quantify(passports, valid_passport) " + "def day4_1(passports): return quantify(passports, required_fields.issubset) " ] }, { @@ -394,9 +433,8 @@ " ecl (Eye Color) - exactly one of: amb blu brn gry grn hzl oth.\n", " pid (Passport ID) - a nine-digit number, including leading zeroes.\n", " cid (Country ID) - ignored, missing or not.'''\n", - " return (valid_passport(passport)\n", - " and all(field_validator[field](passport[field])\n", - " for field in required_fields))\n", + " return all(field in passport and field_validator[field](passport[field])\n", + " for field in required_fields)\n", "\n", "field_validator = dict(\n", " byr=lambda v: 1920 <= int(v) <= 2002,\n", @@ -417,7 +455,7 @@ { "data": { "text/plain": [ - "{1: 237, 2: 172}" + "[237, 172]" ] }, "execution_count": 19, @@ -435,7 +473,7 @@ "source": [ "# Day 5: Binary Boarding\n", "\n", - "The input is a list of boarding passes, such as `BFFFBBFRRR`. Each boarding pass corrsponds to a **seat ID** using an encoding where B and F stand for the back and front half of the remaining part of the plane; R and L stand for right and left half of a row. (The encoding is the same as substituting 0 for F or L, and 1 for B or R, and treating the result as a binary number.)\n", + "The input is a list of boarding passes, such as `'FBFBBFFRLR'`. Each boarding pass corrsponds to a *seat ID* using an encoding where B and F stand for the back and front half of the remaining part of the plane; R and L stand for right and left half of a row. (The encoding is the same as substituting 0 for F or L, and 1 for B or R, and treating the result as a binary number.)\n", "\n", "1. What is the highest seat ID on a boarding pass?\n", "- What is the one missing seat ID, between the minimum and maximum IDs, that is not on the list of boarding passes?" @@ -447,13 +485,15 @@ "metadata": {}, "outputs": [], "source": [ - "def seat_id(seat: str, table=str.maketrans('FLBR', '0011')) -> int:\n", + "ID = int # A type\n", + "\n", + "def seat_id(seat: str, table=str.maketrans('FLBR', '0011')) -> ID:\n", " \"Treat a seat description as a binary number; convert to int.\"\n", - " return int(seat.translate(table), base=2)\n", + " return ID(seat.translate(table), base=2)\n", "\n", "assert seat_id('FBFBBFFRLR') == 357\n", "\n", - "in5: List[int] = data(5, seat_id)" + "in5: List[ID] = data(5, seat_id)" ] }, { @@ -485,7 +525,7 @@ { "data": { "text/plain": [ - "{1: 906, 2: 519}" + "[906, 519]" ] }, "execution_count": 23, @@ -507,10 +547,6 @@ "\n", " abc\n", "\n", - " a\n", - " b\n", - " c\n", - "\n", " ab\n", " ac\n", " \n", @@ -524,9 +560,9 @@ "metadata": {}, "outputs": [], "source": [ - "in6: List[List[str]] = data(6, str.splitlines, '\\n\\n')\n", + "Group = List[str]\n", "\n", - "assert in6[1] == ['arke', 'qzr', 'plmgnr', 'uriq'] # A group is a list of strs" + "in6: List[Group] = data(6, lines, sep='\\n\\n')" ] }, { @@ -547,7 +583,7 @@ "metadata": {}, "outputs": [], "source": [ - "def day6_2(groups: List[List[str]]): \n", + "def day6_2(groups): \n", " \"For each group, compute the number of letters that EVERYONE got. Sum them.\"\n", " return sum(len(set.intersection(*map(set, group)))\n", " for group in groups)" @@ -561,7 +597,7 @@ { "data": { "text/plain": [ - "{1: 6530, 2: 3323}" + "[6530, 3323]" ] }, "execution_count": 27, @@ -583,7 +619,6 @@ "\n", " light red bags contain 1 bright white bag, 2 muted yellow bags.\n", " dark orange bags contain 3 bright white bags, 4 muted yellow bags.\n", - " bright white bags contain 1 shiny gold bag.\n", " \n", "1. How many bag colors must eventually contain at least one shiny gold bag?\n", "2. How many individual bags must be inside your single shiny gold bag?\n", @@ -626,14 +661,14 @@ "outputs": [], "source": [ "def day7_1(rules, target='shiny gold'):\n", - " \"How many colors of bags can contain the target color bag?\"\"\"\n", - " return quantify(contains(bag, target, rules) for bag in rules)\n", - "\n", - "def contains(bag, target, rules) -> bool:\n", - " \"Does this bag contain the target (perhaps recursively)?\"\n", - " contents = rules.get(bag, {})\n", - " return (target in contents\n", - " or any(contains(inner, target, rules) for inner in contents))" + " \"How many colors of bags can contain the target color bag?\"\n", + " @lru_cache(None)\n", + " def contains(bag, target) -> bool:\n", + " \"Does this bag contain the target (perhaps recursively)?\"\n", + " contents = rules.get(bag, {})\n", + " return (target in contents\n", + " or any(contains(inner, target) for inner in contents))\n", + " return quantify(contains(bag, target) for bag in rules)" ] }, { @@ -642,12 +677,12 @@ "metadata": {}, "outputs": [], "source": [ - "def day7_2(rules, target='shiny gold'): return num_contained_in(target, rules)\n", - "\n", - "def num_contained_in(target, rules) -> int:\n", + "def num_contained_in(rules, target='shiny gold') -> int:\n", " \"How many bags are contained (recursively) in the target bag?\"\n", - " return sum(n + n * num_contained_in(bag, rules) \n", - " for (bag, n) in rules[target].items() if n > 0)" + " return sum(n + n * num_contained_in(rules, inner) \n", + " for (inner, n) in rules[target].items() if n > 0)\n", + "\n", + "day7_2 = num_contained_in" ] }, { @@ -658,7 +693,7 @@ { "data": { "text/plain": [ - "{1: 103, 2: 1469}" + "[103, 1469]" ] }, "execution_count": 31, @@ -691,17 +726,19 @@ "Instruction = Tuple[str, int] # e.g. ('jmp', +4)\n", "Program = List[Instruction]\n", "\n", - "def parse_instruction(line: str) -> Instruction:\n", - " \"Parse a line of assembly code into an Instruction: an ('opcode', int) pair.\"\n", - " opcode, arg = line.split()\n", - " return opcode, int(arg)\n", - " \n", - "in8: Program = data(8, parse_instruction)\n", - " \n", + "in8: Program = data(8, atoms)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ "def day8_1(program):\n", - " \"Execte the program until it loops; then return accum.\"\n", + " \"Execute the program until it loops; then return accum.\"\n", " pc = accum = 0\n", - " executed = set()\n", + " executed = set() # Set of instruction addresses executed so far\n", " while True:\n", " if pc in executed:\n", " return accum\n", @@ -718,12 +755,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "I had to think about what to do for Part 2. Do I need to make a flow graph of where the loops are? That sounds hard. But I soon realized that I can just use brute force—try every alteration of an instruction (there are only $O(n)$ of them), and run each altered program to see if it terminates (that too takes only $O(n)$ time)." + "I had to think about what to do for Part 2. Do I need to make a flow graph of where the loops are? That sounds hard. But I soon realized that I can just use brute force—try every alteration of an instruction (there are only 638 instructions and at most one way to alter each instruction), and run each altered program to see if it terminates (that too takes no more than 638 steps each)." ] }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 34, "metadata": {}, "outputs": [], "source": [ @@ -742,7 +779,7 @@ "def run_program(program) -> Tuple[bool, int]:\n", " \"Run the program until it loops or terminates; return (terminates, accum)\"\n", " pc = accum = 0\n", - " executed = set()\n", + " executed = set() # Set of instruction addresses executed so far\n", " while 0 <= pc < len(program):\n", " if pc in executed:\n", " return False, accum # program loops\n", @@ -758,16 +795,16 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 35, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{1: 1521, 2: 1016}" + "[1521, 1016]" ] }, - "execution_count": 34, + "execution_count": 35, "metadata": {}, "output_type": "execute_result" } @@ -792,12 +829,19 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 36, + "metadata": {}, + "outputs": [], + "source": [ + "in9: List[int] = data(9, int)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, "metadata": {}, "outputs": [], "source": [ - "in9: List[int] = data(9, int)\n", - "\n", "def day9_1(nums, p=25):\n", " \"\"\"Find the first number in the list of numbers (after a preamble of p=25 numbers) \n", " which is not the sum of two of the p numbers before it.\"\"\"\n", @@ -809,7 +853,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 38, "metadata": {}, "outputs": [], "source": [ @@ -835,16 +879,16 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 39, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{1: 776203571, 2: 104800569}" + "[776203571, 104800569]" ] }, - "execution_count": 37, + "execution_count": 39, "metadata": {}, "output_type": "execute_result" } @@ -869,16 +913,23 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "in10: List[int] = data(10, int) # Input is the joltage of each adapter" + ] + }, + { + "cell_type": "code", + "execution_count": 41, "metadata": {}, "outputs": [], "source": [ - "in10: List[int] = data(10, int) # Input is the joltage of each adapter\n", - "\n", "def day10_1(jolts):\n", " \"\"\"Arrange the joltages in order; count the number of each size difference;\n", " return the product of 1- and 3-jolt differences.\"\"\"\n", - " jolts = [0, *sorted(jolts), max(jolts) + 3]\n", + " jolts = [0] + sorted(jolts) + [max(jolts) + 3]\n", " diffs = Counter(jolts[i + 1] - jolts[i] \n", " for i in range(len(jolts) - 1))\n", " assert {1, 2, 3}.issuperset(diffs)\n", @@ -887,7 +938,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 42, "metadata": {}, "outputs": [], "source": [ @@ -895,7 +946,7 @@ "\n", "@lru_cache(None)\n", "def arrangements(jolts, prev) -> int:\n", - " \"\"\"The number of ways to attach some of `jolts` to `prev`.\"\"\"\n", + " \"The number of arrangements that go from prev to the end of `jolts`.\"\n", " first, rest = jolts[0], jolts[1:]\n", " if first - prev > 3:\n", " return 0\n", @@ -912,16 +963,16 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 43, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{1: 2346, 2: 6044831973376}" + "[2346, 6044831973376]" ] }, - "execution_count": 40, + "execution_count": 43, "metadata": {}, "output_type": "execute_result" } @@ -949,12 +1000,12 @@ "1. Simulate your seating area by applying the seating rules repeatedly until no seats change state. How many seats end up occupied?\n", "2. Same problem, but with two rule changes:\n", " - When considering adjacency, if there is a *floor* cell in some direction, skip over that to the next visible seat in that direction. \n", - " - Empty a seat only when there are 5 occupied neighbors, not 4. " + " - A seat becomes empty only when there are 5 occupied neighbors, not 4. " ] }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 44, "metadata": {}, "outputs": [], "source": [ @@ -963,7 +1014,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 45, "metadata": {}, "outputs": [], "source": [ @@ -972,7 +1023,7 @@ "Contents = Char # The contents of each location is one of the above 4 characters\n", "\n", "class Layout(list):\n", - " \"\"\"A layout of seats (occupied or not) and floor space.\"\"\"\n", + " \"A layout of seats (occupied or not) and floor space.\"\n", " \n", " crowded = 4\n", " \n", @@ -1020,14 +1071,14 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 46, "metadata": {}, "outputs": [], "source": [ "def day11_2(seats): return Layout2(seats).run().count(occupied)\n", "\n", "class Layout2(Layout):\n", - " \"\"\"A layout of seats (occupied or not) and floor space, with new rules.\"\"\"\n", + " \"A layout of seats (occupied or not) and floor space, with new rules.\"\n", "\n", " crowded = 5\n", " \n", @@ -1037,7 +1088,7 @@ " \n", " def visible(self, x, dx, y, dy) -> Contents:\n", " \"The contents of the first visible seat in direction (dx, dy).\"\n", - " for i in range(1, maxsize):\n", + " for i in range(1, sys.maxsize):\n", " x += dx; y += dy\n", " if not (0 <= y < len(self) and 0 <= x < len(self[y])):\n", " return off\n", @@ -1047,30 +1098,22 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 47, "metadata": {}, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 9.62 s, sys: 30.7 ms, total: 9.65 s\n", - "Wall time: 9.7 s\n" - ] - }, { "data": { "text/plain": [ - "{1: 2299, 2: 2047}" + "[2299, 2047]" ] }, - "execution_count": 44, + "execution_count": 47, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "%time do(11, 2299, 2047)" + "do(11, 2299, 2047)" ] }, { @@ -1078,7 +1121,7 @@ "metadata": {}, "source": [ "I have to confess that I \"cheated\" here: after seeing the problem description for Part 2, I went back and refactored the code for Part 1 in two places:\n", - "- `Layout`: Introduced the `crowded` attribute; it had been an inline literal `4`. Also made `deltas` an attribute.\n", + "- `Layout`: Introduced the `crowded` attribute; it had been an inline literal `4`. Also made `deltas` an attribute, not a global constant.\n", "- `next_generation`: Changed `Layout(seats)` to `type(self)(seats)`.\n", "\n", "There was more refactoring and less reuse in Part 2 than I would have liked, but I don't feel like I made bad choices in Part 1." @@ -1100,7 +1143,7 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 48, "metadata": {}, "outputs": [], "source": [ @@ -1109,7 +1152,7 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 49, "metadata": {}, "outputs": [], "source": [ @@ -1141,7 +1184,7 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 50, "metadata": {}, "outputs": [], "source": [ @@ -1159,16 +1202,16 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 51, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{1: 439, 2: 12385}" + "[439, 12385]" ] }, - "execution_count": 48, + "execution_count": 51, "metadata": {}, "output_type": "execute_result" } @@ -1190,19 +1233,19 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 52, "metadata": {}, "outputs": [], "source": [ "x = 0\n", - "in13: Tuple[int] = (29,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,41,x,x,x,x,x,x,x,x,x,577,\n", - " x,x,x,x,x,x,x,x,x,x,x,x,13,17,x,x,x,x,19,x,x,x,23,x,x,x,x,x,x,x,601,x,x,x,x,x,x,\n", - " x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,37)" + "in13: Tuple[ID] = (29,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,41,x,x,x,x,x,x,x,x,x,577,\n", + " x,x,x,x,x,x,x,x,x,x,x,x,13,17,x,x,x,x,19,x,x,x,23,x,x,x,x,x,x,x,601,x,x,x,x,x,\n", + " x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,37)" ] }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 53, "metadata": {}, "outputs": [], "source": [ @@ -1226,7 +1269,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 54, "metadata": {}, "outputs": [], "source": [ @@ -1234,7 +1277,7 @@ " \"Find the time where all the buses arrive at the right offsets.\"\n", " schedule = {t: id for t, id in enumerate(ids) if id != x}\n", " step = schedule[0]\n", - " return first(t for t in range(0, maxsize, step) \n", + " return first(t for t in range(0, sys.maxsize, step) \n", " if all(wait(schedule[i], t) == i for i in schedule))\n", "\n", "assert day13_2((7,13,x,x,59,x,31,19)) == 1068781\n", @@ -1250,20 +1293,9 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 55, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{1: 174, 2: 780601154795940}" - ] - }, - "execution_count": 52, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "def day13_2(ids):\n", " \"Find the time where all the buses arrive at the right offsets.\"\n", @@ -1274,8 +1306,26 @@ " while wait(schedule[t], time + t):\n", " time += step\n", " step *= schedule[t]\n", - " return time\n", - "\n", + " return time" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[174, 780601154795940]" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ "do(13, 174, 780601154795940)" ] }, @@ -1290,12 +1340,12 @@ "1. Execute the initialization program. What is the sum of all values left in memory after it completes?\n", "2. Execute the initialization program using an emulator for a version 2 decoder chip. What is the sum of all values left in memory after it completes?\n", "\n", - "A *mask* is a bit string but with three possible values at each position, 01X. I could make it into two bitstrings, but I choose to leave it as a `str`." + "A *mask* is a bit string but with three possible values at each position, `01X`. I could make it into two bitstrings, but I choose to leave it as a `str`." ] }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 57, "metadata": {}, "outputs": [], "source": [ @@ -1311,7 +1361,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 58, "metadata": {}, "outputs": [], "source": [ @@ -1332,12 +1382,14 @@ "\n", "def bin36(i) -> str: return f'{i:036b}'\n", "\n", + "assert bin36(255 + 2 ** 20) == '000000000000000100000000000011111111'\n", + "\n", "def day14_1(program): return sum(run_docking(program).values())" ] }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 59, "metadata": {}, "outputs": [], "source": [ @@ -1345,7 +1397,7 @@ " \n", "def run_docking2(program) -> Memory:\n", " \"Execute the program using version 2 instructions and return memory.\"\n", - " mask = '0' * 36\n", + " mask = bin36(0)\n", " mem = defaultdict(int)\n", " for addr, val in program:\n", " if addr == 'mask':\n", @@ -1360,16 +1412,16 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 60, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{1: 11884151942312, 2: 2625449018811}" + "[11884151942312, 2625449018811]" ] }, - "execution_count": 56, + "execution_count": 60, "metadata": {}, "output_type": "execute_result" } @@ -1392,7 +1444,7 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": 61, "metadata": {}, "outputs": [], "source": [ @@ -1401,7 +1453,7 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": 62, "metadata": {}, "outputs": [], "source": [ @@ -1423,12 +1475,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Part 2 involves no changes, but looks for the 30 millionth number. If it had been 3 million, I'd think \"no problem!\" If it had been 30 billion, I'd think \"I need a more efficient solution!\" Can 30 million be done in 15 seconds, which [AoC's about page](https://adventofcode.com/2020/about) says is the maximum run time? I'm not sure; let's try it:" + "Part 2 involves no changes, but asks for the 30 millionth number. If it had been 3 million, I'd think \"no problem!\" If it had been 30 billion, I'd think \"I need a more efficient solution!\" As it is, I'll run it and see how long it takes:" ] }, { "cell_type": "code", - "execution_count": 59, + "execution_count": 63, "metadata": {}, "outputs": [], "source": [ @@ -1437,7 +1489,7 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 64, "metadata": { "scrolled": true }, @@ -1446,17 +1498,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 11.5 s, sys: 273 ms, total: 11.8 s\n", - "Wall time: 11.8 s\n" + "CPU times: user 7.31 s, sys: 114 ms, total: 7.42 s\n", + "Wall time: 7.42 s\n" ] }, { "data": { "text/plain": [ - "{1: 412, 2: 243}" + "[412, 243]" ] }, - "execution_count": 60, + "execution_count": 64, "metadata": {}, "output_type": "execute_result" } @@ -1465,12 +1517,32 @@ "%time do(15, 412, 243)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "That's reasonable; I won't bother trying to find a more efficient approach." + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Day 16: Ticket Translation\n", "\n", + "The input to this puzzle has three parts: some rules for valid fields; your ticket; and a set of nearby tickets. The puzzle is to figure out what field number corresponds to what field name (class, row, seat, etc.). The input looks like:\n", + "\n", + " class: 1-3 or 5-7\n", + " row: 6-11 or 33-44\n", + "\n", + " your ticket:\n", + " 7,1,14\n", + "\n", + " nearby tickets:\n", + " 7,3,47\n", + " 40,4,50\n", + "\n", + "\n", "1. Consider the validity of the nearby tickets you scanned. What is the sum of the values that are are not valid for any field?\n", "2. Discard invalid tickets. Use the remaining valid tickets to determine which field is which. Look for the six fields on your ticket that start with the word departure. What do you get if you multiply those six values together?\n", "\n", @@ -1479,7 +1551,7 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 65, "metadata": {}, "outputs": [], "source": [ @@ -1492,8 +1564,7 @@ " def __contains__(self, item): return any(item in s for s in self)\n", " \n", "def parse_ticket_sections(fieldstr: str, your: str, nearby: str) -> TicketData:\n", - " fields = dict(map(parse_ticket_line, fieldstr))\n", - " return TicketData(fields=fields, \n", + " return TicketData(fields=dict(map(parse_ticket_line, fieldstr)), \n", " your=Ticket(your[1]), \n", " nearby=[Ticket(line) for line in nearby[1:]])\n", "\n", @@ -1503,12 +1574,12 @@ " a, b, c, d = ints(line.replace('-', ' '))\n", " return field, Sets((range(a, b + 1), range(c, d + 1)))\n", "\n", - "in16 = parse_ticket_sections(*data(16, str.splitlines, sep='\\n\\n'))" + "in16 = parse_ticket_sections(*data(16, lines, sep='\\n\\n'))" ] }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 66, "metadata": {}, "outputs": [], "source": [ @@ -1533,7 +1604,7 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 67, "metadata": {}, "outputs": [], "source": [ @@ -1544,13 +1615,13 @@ " fields, your, nearby = ticket_data\n", " ranges = Sets(ticket_data.fields.values())\n", " valid = [t for t in nearby + [your] if valid_ticket(t, ranges)]\n", - " possible = [set(fields) for _ in range(len(your))]\n", - " while any(len(p) > 1 for p in possible):\n", + " possible = {i: set(fields) for i in range(len(your))}\n", + " while any(len(possible[i]) > 1 for i in possible):\n", " for field_name, i in invalid_fields(valid, fields):\n", " possible[i] -= {field_name}\n", " if len(possible[i]) == 1:\n", " eliminate_others(possible, i)\n", - " return {field: i for i, [field] in enumerate(possible)}\n", + " return {field: i for i, [field] in possible.items()}\n", "\n", "def invalid_fields(valid, fields) -> Iterable[Tuple[str, int]]:\n", " \"Yield (field_name, field_number) for all invalid fields.\"\n", @@ -1560,7 +1631,7 @@ "\n", "def eliminate_others(possible, i):\n", " \"Eliminate possible[i] from all other possible[j].\"\n", - " for j in range(len(possible)):\n", + " for j in possible:\n", " if j != i:\n", " possible[j] -= possible[i]\n", "\n", @@ -1573,22 +1644,22 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 68, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{1: 21071, 2: 3429967441937}" + "[21071, 3429967441937]" ] }, - "execution_count": 64, + "execution_count": 68, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "do(16)" + "do(16, 21071, 3429967441937)" ] }, { @@ -1597,16 +1668,19 @@ "source": [ "# Day 17: Conway Cubes\n", "\n", - "Now we are explicitly playing *Life*, but in three dimensions not two. I've coded this before; I'll adapt my [old version](Life.ipynb) to three dimensions. My implementation represents a generation as the set of active cell coordinates." + "Now we are explicitly playing *Life*, but in three dimensions, not two. I've coded this before; I'll adapt my [old version](Life.ipynb) to three dimensions (actually, make it `d` dimensions in general). My implementation represents a generation as the set of active cell coordinates.\n", + "\n", + "1. Starting with your given initial configuration, simulate six cycles. How many cubes are left in the active state after the sixth cycle?\n", + "2. Simulate six cycles in a 4-dimensional space. How many cubes are left in the active state after the sixth cycle?" ] }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 69, "metadata": {}, "outputs": [], "source": [ - "in17: Picture = '''\n", + "in17: Picture = lines('''\n", "##.#....\n", "...#...#\n", ".#.#.##.\n", @@ -1614,95 +1688,74 @@ ".###....\n", ".##.#...\n", "#.##..##\n", - "#.####..'''.strip().splitlines()" + "#.####..\n", + "''')" ] }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 70, "metadata": {}, "outputs": [], "source": [ - "Cell = tuple # of (x, y, z) int coordinates\n", + "Cell = Tuple[int,...]\n", "\n", - "def day17_1(picture, n=6):\n", + "def day17_1(picture, d=3, n=6):\n", " \"How many cells are active in the nth generation?\"\n", - " cells = parse_cells(picture)\n", + " return len(life(parse_cells(picture, d), n))\n", + "\n", + "def parse_cells(picture, d=3, active='#') -> Set[Cell]:\n", + " \"Convert a 2-d picture into a set of d-dimensional active cells.\"\n", + " return {(x, y, *(0,) * (d - 2))\n", + " for (y, row) in enumerate(picture)\n", + " for x, cell in enumerate(row) if cell is active}\n", + "\n", + "def life(cells, n) -> Set[Cell]:\n", + " \"Play n generations of Life.\"\n", " for g in range(n):\n", " cells = next_generation(cells)\n", - " return len(cells)\n", - "\n", - "def parse_cells(picture, active='#') -> Set[Cell]:\n", - " \"Convert a 2-d picture into a set of 3-d active cells.\"\n", - " return {(x, y, 0) for (y, row) in enumerate(picture)\n", - " for x, cell in enumerate(row) if cell is active}\n", + " return cells\n", "\n", "def next_generation(cells) -> Set[Cell]:\n", " \"\"\"The set of live cells in the next generation.\"\"\"\n", " return {cell for cell, count in neighbor_counts(cells).items()\n", " if count == 3 or (count == 2 and cell in cells)}\n", "\n", - "def neighbor_counts(cells) -> Dict[Cell, int]:\n", - " \"The number of live neighbors for each cell that has neighbors.\"\n", - " return Counter(flatten(map(neighbors, cells)))\n", + "@lru_cache()\n", + "def cell_deltas(d: int):\n", + " return set(filter(any, product((-1, 0, +1), repeat=d)))\n", "\n", - "deltas3d = set(product((-1, 0, +1), repeat=3)) - {(0, 0, 0)}\n", + "def neighbor_counts(cells) -> Dict[Cell, int]:\n", + " \"\"\"A Counter of the number of live neighbors for each cell.\"\"\"\n", + " return Counter(flatten(map(neighbors, cells)))\n", "\n", "def neighbors(cell) -> List[cell]:\n", " \"All adjacent neighbors of cell in three dimensions.\"\n", - " return [tuple(map(operator.add, cell, delta)) \n", - " for delta in deltas3d]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Part 2 asks us to move to 4 dimensions. I'll generalize the previous code to work in 3 or 4 dimensions:" + " return [tuple(map(operator.add, cell, delta))\n", + " for delta in cell_deltas(len(cell))]" ] }, { "cell_type": "code", - "execution_count": 67, + "execution_count": 71, "metadata": {}, "outputs": [], "source": [ - "def parse_cells(picture, d=3, active='#') -> Set[Cell]:\n", - " \"Convert a 2-d picture into a set of d-dimensional active cells.\"\n", - " return {(x, y, *(d - 2) * (0,) )\n", - " for (y, row) in enumerate(picture)\n", - " for x, cell in enumerate(row) if cell is active}\n", - "\n", - "def day17_1(picture): return day17_2(picture, n=6, d=3)\n", - "\n", - "def day17_2(picture, n=6, d=4):\n", - " \"How many cells are active in the nth generation in a d-dimensional world?\"\n", - " cells = parse_cells(picture, d=d)\n", - " for g in range(n):\n", - " cells = next_generation(cells)\n", - " return len(cells)\n", - "\n", - "deltas = [set(product((-1, 0, +1), repeat=d)) - {(0,) * d}\n", - " for d in range(5)]\n", - "\n", - "def neighbors(cell) -> List[cell]:\n", - " \"All adjacent neighbors of cell in all dimensions.\"\n", - " return [tuple(map(operator.add, cell, delta)) \n", - " for delta in deltas[len(cell)]]" + "def day17_2(picture): return day17_1(picture, d=4)" ] }, { "cell_type": "code", - "execution_count": 68, + "execution_count": 72, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{1: 291, 2: 1524}" + "[291, 1524]" ] }, - "execution_count": 68, + "execution_count": 72, "metadata": {}, "output_type": "execute_result" } @@ -1717,7 +1770,7 @@ "source": [ "# Day 18: Operation Order\n", "\n", - "At first I thought I could just apply `eval` to each line, but alas, the operation order is different. I could have used a parsing framework, but I decided to do it all from scratch.\n", + "At first I thought I could just apply `eval` to each line, but alas, the operation order is non-standard.\n", "\n", "1. All operations are done left-to-right. Evaluate the expression on each line of the homework; what is the sum of the resulting values?\n", "2. Addition is done before multiplication. What do you get if you add up the results of evaluating the homework problems using these new rules?" @@ -1725,7 +1778,7 @@ }, { "cell_type": "code", - "execution_count": 69, + "execution_count": 73, "metadata": {}, "outputs": [], "source": [ @@ -1738,7 +1791,7 @@ }, { "cell_type": "code", - "execution_count": 70, + "execution_count": 74, "metadata": {}, "outputs": [], "source": [ @@ -1758,7 +1811,7 @@ }, { "cell_type": "code", - "execution_count": 71, + "execution_count": 75, "metadata": {}, "outputs": [], "source": [ @@ -1766,28 +1819,27 @@ " \"Evaluate an expression under addition-first rules.\"\n", " if isinstance(expr, int):\n", " return expr\n", + " elif '*' in expr:\n", + " i = expr.index('*')\n", + " return evaluate2(expr[:i]) * evaluate2(expr[i + 1:])\n", " else:\n", - " expr = list(expr) # Make expr mutable\n", - " while '+' in expr: # Change [... 3, '+', 4 ...] to [... 7 ...]\n", - " i = expr.index('+')\n", - " expr[i-1:i+2] = [evaluate2(expr[i - 1]) + evaluate2(expr[i + 1])]\n", - " return prod(evaluate2(x) for x in expr if x not in operators)\n", + " return sum(evaluate2(x) for x in expr if x is not '+')\n", " \n", "def day18_2(exprs): return sum(map(evaluate2, exprs))" ] }, { "cell_type": "code", - "execution_count": 72, + "execution_count": 76, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{1: 3885386961962, 2: 112899558798666}" + "[3885386961962, 112899558798666]" ] }, - "execution_count": 72, + "execution_count": 76, "metadata": {}, "output_type": "execute_result" } @@ -1800,120 +1852,722 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Summary, and Timing\n", + "# Day 19\n", "\n", - "So far, two of the puzzles run slowly (but under the 15 second cap). All the other 16 puzzles combined run in under a second total." + "A grep-like pattern matcher, where a *message* is a sequence of characters (all `'a'` or `'b'`) and a *pattern* I will represent as a list of items, where each item can be a character, a rule number (which is associated with a pattern), or a *choice* of two or more patterns. The input has two sections: first \"rule number: pattern\" lines, then messages, one per line. \n", + "\n", + "I will define `match` to return the remaining string in the message if part or all of the message is matched, or `None` if it fails.\n", + "\n", + "1. How many messages completely match rule 0?\n", + "2. Two of the rules are wrong. After updating rules 8 and 11, how many messages completely match rule 0?" ] }, { "cell_type": "code", - "execution_count": 73, + "execution_count": 77, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "Message = str # A string we are trying to match, e.g. \"ababba\"\n", + "Choice = tuple # A choice of any of the elements, e.g. Choice(([5, 6], [7]))\n", + "Pattern = List[Union[Char, int, Choice]]\n", + "\n", + "def parse_messages(rules, messages) -> Tuple[Dict[int, Pattern], List[Message]]:\n", + " \"Return a dict of {rule_number: pattern} and a list of messages.\"\n", + " return dict(map(parse_rule, rules)), messages\n", + "\n", + "def parse_rule(line):\n", + " \"Parse '1: 2 3' => (1, [2, 3]); '4: 5, 6 | 7' => (4, Choice(([5, 6], [7]))).\"\n", + " n, *rhs = atoms(line.replace(':', ' ').replace('\"', ' '))\n", + " if '|' in rhs:\n", + " i = rhs.index('|')\n", + " rhs = [Choice((rhs[:i], rhs[i + 1:]))]\n", + " return n, rhs\n", + " \n", + "in19 = parse_messages(*data(19, lines, sep='\\n\\n'))" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "metadata": {}, + "outputs": [], + "source": [ + "def day19_1(inputs):\n", + " \"How many messages completely match rule 0?\"\n", + " rules, messages = inputs\n", + " return quantify(match(rules[0], msg, rules) == '' \n", + " for msg in messages)\n", + "\n", + "def match(pat, msg, rules) -> Optional[Message]:\n", + " \"If a prefix of msg matches pat, return remaining str; else None\"\n", + " if pat and not msg: # Failed to match whole pat\n", + " return None \n", + " elif not pat: # Matched whole pat\n", + " return msg \n", + " elif pat[0] == msg[0]: # Matched first char; continue\n", + " return match(pat[1:], msg[1:], rules)\n", + " elif isinstance(pat[0], int): # Look up the rule number \n", + " return match(rules[pat[0]] + pat[1:], msg, rules)\n", + " elif isinstance(pat[0], Choice): # Match any of the choices\n", + " for choice in pat[0]:\n", + " m = match(choice + pat[1:], msg, rules)\n", + " if m is not None:\n", + " return m\n", + " return None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For part 2, I coded the two changed rules by hand, taking care to avoid infinite left-recursion:" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "metadata": {}, + "outputs": [], + "source": [ + "def day19_2(inputs):\n", + " \"How many messages completely match rule 0, with new rules 8 and 11?\"\n", + " rules, messages = inputs\n", + " rules2 = {**rules, 8: [42, maybe(8)], 11: [42, maybe(11), 31]}\n", + " return day19_1((rules2, messages))\n", + " \n", + "def maybe(n): return Choice(([], [n]))" + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[190, 311]" + ] + }, + "execution_count": 80, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "do(19, 190, 311)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Day 20: Jurassic Jigsaw\n", + "\n", + "You are given a bunch of picture tiles, which can be put together to form a larger picture, where the edges of tiles match their neighbors.\n", + "\n", + "1. Assemble the tiles into an image. What do you get if you multiply together the IDs of the four corner tiles?\n", + "2. In the assembled image, how many `#` pixels are not part of a sea monster (which is a specific shape)?" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "metadata": {}, + "outputs": [], + "source": [ + "def jigsaw_tiles(sections: List[List[str]]) -> Dict[ID, Picture]:\n", + " \"Return a dict of {tile_id: tile_picture}.\"\n", + " return {first(ints(header)): tile\n", + " for (header, *tile) in sections}\n", + " \n", + "in20 = jigsaw_tiles(data(20, lines, sep='\\n\\n'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For Part 1, I can find the corners without knowing where all the other tiles go. It is guaranteed that \"the outermost edges won't line up with any other tiles,\" but all the inside edges will. We'll define `edge_count` to count how many times an edge appears on any tile (using a `canonical` orientation, because tiles might be flipped). Then the corner tiles are ones that have two edges that have an edge count of 1." + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "metadata": {}, + "outputs": [], + "source": [ + "Edge = str\n", + "\n", + "def day20_1(tiles: Dict[ID, Picture]):\n", + " \"The product of the ID's of the 4 corner tiles.\"\n", + " edge_count = Counter(canonical(e) for id in tiles for e in edges(tiles[id]))\n", + " is_outermost = lambda edge: edge_count[canonical(edge)] == 1\n", + " is_corner = lambda tile: quantify(edges(tile), is_outermost) == 2\n", + " corners = [id for id in tiles if is_corner(tiles[id])]\n", + " return prod(corners)\n", + " \n", + "def edges(tile) -> Iterable[Edge]:\n", + " \"The 4 edges of a tile.\"\n", + " for i in (0, -1): \n", + " yield tile[i] # top/bottom\n", + " yield cat(row[i] for row in tile) # left/right\n", + " \n", + "def canonical(edge) -> Edge: return min(edge, edge[::-1])" + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[15670959891893, None]" + ] + }, + "execution_count": 83, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "do(20, 15670959891893) " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Family holiday preparations kept me from doing **Part 2** on the night it was released, and unfortunately I didn't feel like coming back to it later: it seemed too tedious for too little reward. I thought it was inelegant that a solid block of `#` pixels would be considered a sea monster with waves. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Day 21: Allergen Assessment\n", + "\n", + "This is another Sudoku-like problem, similar to Day 16, involving ingredients and allergens. Crucially, each allergen is found in exactly one ingredient, but a food may not list every allergen. I can reuse the function `eliminate_others`, but the rest I need to define here. `day21_2` is the only `day` function that does not return an `int`.\n", + "\n", + "1. Determine which ingredients cannot possibly contain any of the allergens in your list. How many times do any of those ingredients appear?\n", + "2. What is your canonical dangerous ingredient list?" + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "metadata": {}, + "outputs": [], + "source": [ + "Ingredient = str\n", + "Allergen = str\n", + "Food = namedtuple('Food', 'I, A') # I for set of ingredients; A for set of allergens\n", + "\n", + "def parse_food(line) -> Food:\n", + " \"Parse 'xtc wkrp (contains fish, nuts)' => Food({'xtc', 'wkrp'}, {'fish', 'nuts'})\"\n", + " ingredients, allergens = line.split('(contains')\n", + " return Food(set(atoms(ingredients)), set(atoms(allergens, ignore='[,)]')))\n", + "\n", + "in21 = data(21, parse_food)" + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "metadata": {}, + "outputs": [], + "source": [ + "def day21_1(foods):\n", + " \"How many times does an ingredient with an allergen appear?\"\n", + " bad = bad_ingredients(foods)\n", + " allergens = set(flatten(bad.values()))\n", + " return sum(len(food.I - allergens) for food in foods)\n", + "\n", + "def bad_ingredients(foods) -> Dict[Allergen, Set[Ingredient]]:\n", + " \"A dict of {allergen: {set_of_ingredients_it_could_be}}; each set should have len 1.\"\n", + " all_I = set(flatten(food.I for food in foods))\n", + " all_A = set(flatten(food.A for food in foods))\n", + " possible = {a: set(all_I) for a in all_A}\n", + " while any(len(possible[a]) > 1 for a in possible):\n", + " for food in foods:\n", + " for a in food.A:\n", + " possible[a] &= food.I\n", + " if len(possible[a]) == 1:\n", + " eliminate_others(possible, a)\n", + " return possible" + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "metadata": {}, + "outputs": [], + "source": [ + "def day21_2(foods) -> str:\n", + " \"What are the bad ingredients, sorted by the allergen name that contains them?\"\n", + " bad = bad_ingredients(in21)\n", + " return ','.join(first(bad[a]) for a in sorted(bad))" + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[2282, 'vrzkz,zjsh,hphcb,mbdksj,vzzxl,ctmzsr,rkzqs,zmhnj']" + ] + }, + "execution_count": 87, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "do(21, 2282, 'vrzkz,zjsh,hphcb,mbdksj,vzzxl,ctmzsr,rkzqs,zmhnj')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Day 22: Crab Combat\n", + "\n", + "Part 1 is the card game *War* (here called *Combat*) with no ties. Part 2 is a more complex recursive version of the game.\n", + "\n", + "1. Play a game of Combat using the two decks you just dealt. What is the winning player's score?\n", + "2. Play a game of Recursive Combat. What is the winning player's score?\n", + "\n", + "Each player holds a *deal* of cards, which I will represent as a `deque` so I can deal from the top and add cards to the bottom." + ] + }, + { + "cell_type": "code", + "execution_count": 88, + "metadata": {}, + "outputs": [], + "source": [ + "Deal = Union[tuple, deque] # Cards are a tuple on input; a deque internally \n", + "Deals = Tuple[Deal, Deal] # Cards are split into two piles for the two players.\n", + "\n", + "in22: Deals = (\n", + " (12,40,50,4,24,15,22,43,18,21,2,42,27,36,6,31,35,20,32,1,41,14,9,44,8),\n", + " (30,10,47,29,13,11,49,7,25,37,33,48,16,5,45,19,17,26,46,23,34,39,28,3,38))" + ] + }, + { + "cell_type": "code", + "execution_count": 89, + "metadata": {}, + "outputs": [], + "source": [ + "def day22_1(deals): return combat_score(combat(deals))\n", + "\n", + "def combat(deals: Deals) -> Deals:\n", + " \"Given two deals, play Combat and return the final deals (one of which will be empty).\"\n", + " deals = mapt(deque, deals)\n", + " while all(deals):\n", + " topcards = mapt(deque.popleft, deals)\n", + " winner = 0 if topcards[0] > topcards[1] else 1\n", + " deals[winner].extend(sorted(topcards, reverse=True))\n", + " return deals\n", + "\n", + "def combat_score(deals: Deals) -> int:\n", + " \"The winner's cards, each multiplied by their reverse index number, and summed.\"\n", + " winning = deals[0] or deals[1]\n", + " return dot(winning, range(len(winning), 0, -1))" + ] + }, + { + "cell_type": "code", + "execution_count": 90, + "metadata": {}, + "outputs": [], + "source": [ + "def day22_2(deals): return combat_score(recursive_combat(deals))\n", + " \n", + "def recursive_combat(deals: Deals) -> Deals:\n", + " \"A game of Recursive Combat.\"\n", + " deals = mapt(deque, deals)\n", + " previously = set()\n", + " while all(deals):\n", + " if seen(deals, previously):\n", + " return (deals[0], ())\n", + " topcards = mapt(deque.popleft, deals)\n", + " if all(len(deals[p]) >= topcards[p] for p in (0, 1)):\n", + " deals2 = [tuple(deals[p])[:topcards[p]] for p in (0, 1)]\n", + " result = recursive_combat(deals2)\n", + " winner = 0 if result[0] else 1\n", + " else:\n", + " winner = 0 if topcards[0] > topcards[1] else 1\n", + " deals[winner].extend([topcards[winner], topcards[1 - winner]])\n", + " return deals\n", + "\n", + "def seen(deals, previously) -> bool:\n", + " \"Return True if we have seen this pair of deals previously; else just remember it.\"\n", + " hasht = mapt(tuple, deals)\n", + " if hasht in previously:\n", + " return True\n", + " else:\n", + " previously.add(hasht)\n", + " return False" + ] + }, + { + "cell_type": "code", + "execution_count": 91, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[31809, 32835]" + ] + }, + "execution_count": 91, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "do(22, 31809, 32835)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Day 23: Crab Cups\n", + "\n", + "A game involving moving around some cups that are labelled with positive integers.\n", + "\n", + "1. Using your labeling, simulate 100 moves. What are the labels on the cups after cup 1?\n", + "2. With 1 million cups and 10 million moves, determine which two cups will end up immediately clockwise of cup 1. What do you get if you multiply their labels together?" + ] + }, + { + "cell_type": "code", + "execution_count": 92, + "metadata": {}, + "outputs": [], + "source": [ + "in23 = '872495136'" + ] + }, + { + "cell_type": "code", + "execution_count": 93, + "metadata": {}, + "outputs": [], + "source": [ + "Cup = int # The label on a cup (not the index of the cup in the list of cups)\n", + "\n", + "def day23_1(cupstr: str, n=100):\n", + " \"Return the int representing the cups, in order, after cup 1; resulting from n moves.\"\n", + " cups = list(map(int, cupstr))\n", + " current = cups[0]\n", + " for i in range(n):\n", + " picked = pickup(cups, current)\n", + " dest = destination(cups, current)\n", + " place(cups, picked, dest)\n", + " current = clockwise(cups, current)\n", + " return after(1, cups)\n", + "\n", + "def pickup(cups, current) -> List[Cup]:\n", + " \"Return the 3 cups clockwise of current; remove them from cups.\"\n", + " i = cups.index(current)\n", + " picked, cups[i+1:i+4] = cups[i+1:i+4], []\n", + " extra = 3 - len(picked)\n", + " if extra:\n", + " picked += cups[:extra]\n", + " cups[:extra] = []\n", + " return picked\n", + " \n", + "def destination(cups, current) -> Cup:\n", + " \"The cup with label one less than current, or max(cups).\"\n", + " return max((c for c in cups if c < current), default=max(cups))\n", + "\n", + "def clockwise(cups, current) -> Cup:\n", + " \"The cup one clockwise of current.\"\n", + " return cups[(cups.index(current) + 1) % len(cups)]\n", + "\n", + "def place(cups, picked, dest):\n", + " \"Put `picked` after `dest`\"\n", + " i = cups.index(dest) + 1\n", + " cups[i:i] = picked\n", + "\n", + "def after(cup, cups) -> int:\n", + " \"All the cups after `cup`, in order.\"\n", + " i = cups.index(cup) + 1\n", + " string = cat(map(str, cups + cups))\n", + " return int(string[i:i+len(cups)])" + ] + }, + { + "cell_type": "code", + "execution_count": 94, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[278659341, None]" + ] + }, + "execution_count": 94, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "do(23, 278659341)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For Part 1, I aimed to be very explicit in my code, and the solution works fine for 9 cups and 100 moves (although it did end up being more lines of code than I wanted/expected). However, my approach would not be efficient enough to do Part 2; thus it was a poor choice. For now I'll skip Part 2; maybe I'll come back to it later. I had an idea for a representation where each entry in `cups` is either a `list` or a `range` of cups; that way we are only breaking up and/or shifting small lists, not million-element lists." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Day 24: Lobby Layout\n", + "\n", + "This puzzle involves a hexagonal grid. The input is a list of directions to follow to identify a destination hex tile that should be flipped from white to black. I recall that Amit Patel of Red Blob Games has [covered hex grids topic thoroughly](https://www.redblobgames.com/grids/hexagons/); I followed his recommendation to use a (pointy) **axial coordinate** system. \n", + "\n", + "1. Go through the renovation crew's list and determine which tiles they need to flip. After all of the instructions have been followed, how many tiles are left with the black side up?\n", + "2. Another version of *Life*, but on a hex grid, where a tile is live (black) if it was white and has two black neighbors, or it was black and has 1 or 2 black neighbors. How many tiles will be black after 100 days (generations)?" + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "metadata": {}, + "outputs": [], + "source": [ + "in24 = data(24)" + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "metadata": {}, + "outputs": [], + "source": [ + "def day24_1(lines: List[str]):\n", + " \"How many tiles are flipped an odd number of times?\"\n", + " counts = Counter(map(follow_hex, lines))\n", + " return quantify(counts[tile] % 2 for tile in counts)\n", + "\n", + "hexdirs = dict(e=(1, 0), w=(-1, 0), ne=(1, -1), sw=(-1, 1), se=(0, 1), nw=(0, -1))\n", + "\n", + "def parse_hex(line) -> List[str]: return re.findall('e|w|ne|sw|se|nw', line)\n", + "\n", + "def follow_hex(directions: str):\n", + " \"What (x, y) location do you end up at after following directions?\"\n", + " x, y = 0, 0\n", + " for dir in parse_hex(directions):\n", + " dx, dy = hexdirs[dir]\n", + " x += dx\n", + " y += dy\n", + " return (x, y)\n", + "\n", + "assert parse_hex('wsweesene') == ['w', 'sw', 'e', 'e', 'se', 'ne']\n", + "assert follow_hex('eeew') == (2, 0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "I'll use `binding` to temporarily redefine `next_generation` and `cell_deltas` to work with this problem; then call `life`." + ] + }, + { + "cell_type": "code", + "execution_count": 97, + "metadata": {}, + "outputs": [], + "source": [ + "def day24_2(lines: List[str], days=100):\n", + " \"How many tiles are black after 100 days of Life?\"\n", + " counts = Counter(map(follow_hex, lines))\n", + " blacks = {tile for tile in counts if counts[tile] % 2}\n", + " with binding(next_generation=next_generation24, \n", + " cell_deltas=cell_deltas24):\n", + " return len(life(blacks, 100))\n", + "\n", + "def next_generation24(cells) -> Set[Cell]:\n", + " \"The set of live cells in the next generation.\"\n", + " counts = neighbor_counts(cells)\n", + " return ({c for c in cells if counts[c] in (1, 2)} |\n", + " {c for c in counts if c not in cells and counts[c] == 2})\n", + "\n", + "@lru_cache()\n", + "def cell_deltas24(d) -> Iterable[Cell]:\n", + " \"The neighbors are the 6 surrounding hex squares.\"\n", + " return hexdirs.values()" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[420, 4206]" + ] + }, + "execution_count": 98, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "do(24, 420, 4206) " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Day 25: Combo Breaker\n", + "\n", + "This puzzle involves breaking a cryptographic protocol (whose details I won't describe here).\n", + "\n", + "1. What encryption key is the handshake trying to establish?\n", + "2. The last rule of AoC: there is no Part 2 for Day 25." + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "metadata": {}, + "outputs": [], + "source": [ + "in25 = 1965712, 19072108" + ] + }, + { + "cell_type": "code", + "execution_count": 100, + "metadata": {}, + "outputs": [], + "source": [ + "def transform(subj) -> Iterator[int]:\n", + " \"A stream of transformed values, according to the protocol.\"\n", + " val = 1\n", + " while True:\n", + " val = (val * subj) % 20201227\n", + " yield val\n", + "\n", + "def day25_1(keys):\n", + " \"Find the loopsize for the first key; transform the other key that number of times.\"\n", + " loopsize = first(i for i, val in enumerate(transform(7)) if val == keys[0]) \n", + " return first(val for i, val in enumerate(transform(keys[1])) if i == loopsize)" + ] + }, + { + "cell_type": "code", + "execution_count": 101, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[16881444, None]" + ] + }, + "execution_count": 101, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "do(25, 16881444)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Postmortem: Timing\n", + "\n", + "Advent of Code suggests that each day's puzzle should run in [15 seconds or less](https://adventofcode.com/2020/about).\n", + "I met that goal, with days 11 and 15 taking about 8 seconds each, days 22 and 25 about 2 seconds, and the average under a second per day. (However, I skipped Part 2 on days 20 and 23.)\n", + "\n", + "Here's a report, with stars in the first column indicating run times on a log scale: 0 stars for under 1/100 seconds up to 4 stars for over 10 seconds:" + ] + }, + { + "cell_type": "code", + "execution_count": 102, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 817 ms, sys: 5.42 ms, total: 823 ms\n", - "Wall time: 825 ms\n" + " Day Secs. Answers\n", + " === ===== =======\n", + " 1 0.000 [787776, 262738554]\n", + " 2 0.000 [383, 272]\n", + " 3 0.000 [167, 736527114]\n", + " 4 0.001 [237, 172]\n", + " 5 0.000 [906, 519]\n", + " 6 0.001 [6530, 3323]\n", + " 7 0.001 [103, 1469]\n", + " 8 0.008 [1521, 1016]\n", + " 9 0.003 [776203571, 104800569]\n", + " 10 0.000 [2346, 6044831973376]\n", + " *** 11 8.271 [2299, 2047]\n", + " 12 0.001 [439, 12385]\n", + " 13 0.000 [174, 780601154795940]\n", + " * 14 0.063 [11884151942312, 2625449018811]\n", + " *** 15 7.480 [412, 243]\n", + " ** 16 0.145 [21071, 3429967441937]\n", + " ** 17 0.273 [291, 1524]\n", + " 18 0.005 [3885386961962, 112899558798666]\n", + " ** 19 0.262 [190, 311]\n", + " 20 0.001 [15670959891893, None]\n", + " 21 0.001 [2282, 'vrzkz,zjsh,hphcb,mbdksj,vzzxl,ctmzsr,rkzqs,zmhnj']\n", + " *** 22 2.246 [31809, 32835]\n", + " 23 0.000 [278659341, None]\n", + " ** 24 0.724 [420, 4206]\n", + " *** 25 1.827 [16881444, None]\n", + "CPU times: user 21.3 s, sys: 62.1 ms, total: 21.3 s\n", + "Wall time: 21.3 s\n" ] - }, - { - "data": { - "text/plain": [ - "{1: {1: 787776, 2: 262738554},\n", - " 2: {1: 383, 2: 272},\n", - " 3: {1: 167, 2: 736527114},\n", - " 4: {1: 237, 2: 172},\n", - " 5: {1: 906, 2: 519},\n", - " 6: {1: 6530, 2: 3323},\n", - " 7: {1: 103, 2: 1469},\n", - " 8: {1: 1521, 2: 1016},\n", - " 9: {1: 776203571, 2: 104800569},\n", - " 10: {1: 2346, 2: 6044831973376},\n", - " 12: {1: 439, 2: 12385},\n", - " 13: {1: 174, 2: 780601154795940},\n", - " 14: {1: 11884151942312, 2: 2625449018811},\n", - " 16: {1: 21071, 2: 3429967441937},\n", - " 17: {1: 291, 2: 1524},\n", - " 18: {1: 3885386961962, 2: 112899558798666},\n", - " 19: {},\n", - " 20: {},\n", - " 21: {},\n", - " 22: {},\n", - " 23: {},\n", - " 24: {},\n", - " 25: {}}" - ] - }, - "execution_count": 73, - "metadata": {}, - "output_type": "execute_result" } ], "source": [ - "advent = set(range(1, 26))\n", - "slow = {11, 15}\n", + "import time\n", "\n", - "def doall(days): return {i: do(i) for i in days}\n", + "def timing(days=range(1, 26)):\n", + " \"Report on timing of `do(day)` for all days.\"\n", + " print(' Day Secs. Answers')\n", + " print(' === ===== =======') \n", + " for day in days:\n", + " t0 = time.time()\n", + " answers = do(day)\n", + " t = time.time() - t0\n", + " if answers != [None, None]:\n", + " stars = '*' * int(3 + math.log(t, 10))\n", + " print(f'{stars:>4} {day:2} {t:6.3f} {answers}')\n", "\n", - "%time doall(advent - slow) " - ] - }, - { - "cell_type": "code", - "execution_count": 74, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 10.6 s, sys: 41.2 ms, total: 10.6 s\n", - "Wall time: 10.7 s\n" - ] - }, - { - "data": { - "text/plain": [ - "{1: 2299, 2: 2047}" - ] - }, - "execution_count": 74, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%time do(11)" - ] - }, - { - "cell_type": "code", - "execution_count": 75, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 10.9 s, sys: 254 ms, total: 11.1 s\n", - "Wall time: 11.2 s\n" - ] - }, - { - "data": { - "text/plain": [ - "{1: 412, 2: 243}" - ] - }, - "execution_count": 75, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%time do(15)" + "%time timing()" ] } ], @@ -1933,7 +2587,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.7" + "version": "3.7.6" } }, "nbformat": 4,