From 3b92dfd2d6157ea5f06fb800e71c239c3c2adad2 Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Tue, 14 Dec 2021 14:23:12 -0800 Subject: [PATCH] Add files via upload --- ipynb/Advent-2021.ipynb | 571 +++++++++++++++++++++++++++++----------- 1 file changed, 412 insertions(+), 159 deletions(-) diff --git a/ipynb/Advent-2021.ipynb b/ipynb/Advent-2021.ipynb index c433853..2c515bc 100644 --- a/ipynb/Advent-2021.ipynb +++ b/ipynb/Advent-2021.ipynb @@ -8,9 +8,9 @@ "\n", "# Advent of Code 2021\n", "\n", - "I'm doing [Advent of Code](https://adventofcode.com/) (AoC) this year. I'm not competing for points\\, just for fun.\n", + "I'm doing [Advent of Code](https://adventofcode.com/2021) (AoC) this year. I'm not competing for points, just participating for fun.\n", "\n", - "To fully understand the puzzles, click on each day's link (e.g. [Day 1](https://adventofcode.com/2021/day/1)) and read the instructions; I give only a brief summary. \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 in part 2 (although I may edit the code for clarity, without changing the initial approach).\n", "\n", @@ -30,10 +30,9 @@ "from itertools import permutations, combinations, chain, count as count_from, product as cross_product\n", "from typing import *\n", "from statistics import mean, median\n", - "\n", + "from math import ceil\n", + "from functools import lru_cache\n", "import matplotlib.pyplot as plt\n", - "import functools\n", - "import math\n", "import re" ] }, @@ -48,7 +47,7 @@ " - 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**: Write code to compute the answer to Part 1, and submit the answer to the AoC site. Use the function `answer` to record the correct answer here in the notebook and serve as a regression test when the notebook is re-run.\n", + "- **Part 1**: Write code to compute the answer to Part 1, and submit the answer to the AoC site. Use the function `answer` to record the correct answer here in the notebook and serve as a regression test when the notebook is re-run. If there are non-trivial components in this code, I might provide unit tests for them using `assert`.\n", "- **Part 2**: Repeat coding and `answer` for Part 2.\n" ] }, @@ -59,19 +58,21 @@ "outputs": [], "source": [ "def answer(puzzle_number, got, expected) -> bool:\n", - " \"\"\"Verify the answer we got was expected.\"\"\"\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', show=6) -> tuple:\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", " text = open(f'AOC2021/input{day}.txt').read()\n", - " if show > 0:\n", - " lines = text.splitlines()[:show]\n", - " print(f'First {len(lines)} lines of Day {day} input:\\n{\"-\"*29}')\n", + " entries = mapt(parser, text.rstrip().split(sep))\n", + " if print_lines:\n", + " lines = text.splitlines()[:print_lines]\n", + " head = f'First {len(lines)} lines of Day {day} input (which is parsed into {len(entries)} entries)'\n", + " print(f'{head}:\\n{\"-\" * len(head)}')\n", " for line in lines:\n", " print(line if len(line) <= 100 else line[:100] + ' ...')\n", - " return mapt(parser, text.rstrip().split(sep))\n", + " return entries\n", "\n", "def ints(text: str) -> Tuple[int]:\n", " \"\"\"A tuple of all the integers in text, ignoring non-number characters.\"\"\"\n", @@ -82,15 +83,17 @@ " return mapt(int, re.findall('[0-9]', text))\n", "\n", "def words(text: str) -> List[str]:\n", - " \"\"\"A list of all the alphabetic words in text.\"\"\"\n", + " \"\"\"A list of all the alphabetic words in text, ignoring non-letters.\"\"\"\n", " return re.findall('[a-zA-Z]+', text)\n", "\n", + "Char = str # Intended as a one-character string\n", + "\n", "Atom = Union[float, int, str]\n", "\n", "def atoms(text: str, sep=None) -> Tuple[Atom]:\n", " \"\"\"A tuple of all the atoms (numbers or strs) in text.\n", " By default, atoms are space-separated but you can change that with `sep`.\"\"\"\n", - " return tuple(map(atom, text.split(sep)))\n", + " return mapt(atom, text.split(sep))\n", "\n", "def atom(text: str) -> Atom:\n", " \"\"\"Parse text into a single float or int or str.\"\"\"\n", @@ -100,7 +103,7 @@ " except ValueError:\n", " return text\n", " \n", - "def mapt(fn, *args):\n", + "def mapt(fn, *args) -> tuple:\n", " \"\"\"map(fn, *args) and return the result as a tuple.\"\"\"\n", " return tuple(map(fn, *args))" ] @@ -123,6 +126,7 @@ " 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", @@ -151,7 +155,7 @@ "\n", "cat = ''.join\n", "flatten = chain.from_iterable\n", - "cache = functools.lru_cache(None)" + "cache = lru_cache(None)" ] }, { @@ -186,12 +190,12 @@ " self.height = max(y for x, y in self) + 1\n", " self.deltas = neighbors\n", " \n", - " def neighbors(self, point) -> List[Point]:\n", - " \"\"\"Points that neighbor `point` on the grid.\"\"\"\n", - " x, y = point\n", - " return [(x+dx, y+dy) for (dx, dy) in self.deltas if (x+dx, y+dy) in self]\n", + " def copy(self) -> Grid: return Grid(self, neighbors=self.deltas)\n", " \n", - " def copy(self) -> Grid: return Grid(self, neighbors=self.deltas)" + " 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 if (x+dx, y+dy) in self]" ] }, { @@ -217,14 +221,15 @@ "name": "stdout", "output_type": "stream", "text": [ - "First 6 lines of Day 1 input:\n", - "-----------------------------\n", + "First 7 lines of Day 1 input (which is parsed into 2000 entries):\n", + "----------------------------------------------------------------\n", "148\n", "167\n", "168\n", "169\n", "182\n", - "188\n" + "188\n", + "193\n" ] } ], @@ -352,14 +357,15 @@ "name": "stdout", "output_type": "stream", "text": [ - "First 6 lines of Day 2 input:\n", - "-----------------------------\n", + "First 7 lines of Day 2 input (which is parsed into 1000 entries):\n", + "----------------------------------------------------------------\n", "forward 2\n", "down 7\n", "down 8\n", "forward 9\n", "down 8\n", - "forward 9\n" + "forward 9\n", + "forward 8\n" ] } ], @@ -409,7 +415,7 @@ "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", "\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*." + "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*." ] }, { @@ -442,13 +448,6 @@ "answer(2.2, drive2(in2), 1_954_293_920)" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The puzzle didn't say what the units are, but I'm guessing micrometers. That gives a depth of almost 2 km; a depth that only specialized research submarines can reach." - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -469,14 +468,15 @@ "name": "stdout", "output_type": "stream", "text": [ - "First 6 lines of Day 3 input:\n", - "-----------------------------\n", + "First 7 lines of Day 3 input (which is parsed into 1000 entries):\n", + "----------------------------------------------------------------\n", "101000111100\n", "000011111101\n", "011100000100\n", "100100010000\n", "011110010100\n", - "101001100000\n" + "101001100000\n", + "110001010000\n" ] } ], @@ -508,13 +508,13 @@ } ], "source": [ - "def common(strs, i) -> str: \n", - " \"\"\"The bit that is most common in position i.\"\"\"\n", + "def common(strs, i) -> Char: # '1' or '0'\n", + " \"\"\"The bit that is most common in position i among strs.\"\"\"\n", " bits = [s[i] for s in strs]\n", " return '1' if bits.count('1') >= bits.count('0') else '0'\n", "\n", - "def uncommon(strs, i) -> str: \n", - " \"\"\"The bit that is least common in position i.\"\"\"\n", + "def uncommon(strs, i) -> Char: # '1' or '0'\n", + " \"\"\"The bit that is least common in position i among strs.\"\"\"\n", " return '1' if common(strs, i) == '0' else '0'\n", "\n", "def epsilon(strs) -> str:\n", @@ -536,7 +536,9 @@ "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?" + "- **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." ] }, { @@ -558,7 +560,8 @@ "source": [ "def select_str(strs, common_fn, i=0) -> str:\n", " \"\"\"Select a str from strs according to common_fn:\n", - " Going left-to-right, repeatedly select just the strs that have the right i-th bit.\"\"\"\n", + " Going left-to-right, repeatedly select just the strs that have the right i-th bit.\n", + " When only one string is remains, return it.\"\"\"\n", " if len(strs) == 1:\n", " return strs[0]\n", " else:\n", @@ -593,21 +596,20 @@ "name": "stdout", "output_type": "stream", "text": [ - "First 8 lines of Day 4 input:\n", - "-----------------------------\n", + "First 7 lines of Day 4 input (which is parsed into 101 entries):\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,34, ...\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" + "19 22 16 13 3\n" ] } ], "source": [ - "order, *boards = in4 = parse(4, ints, sep='\\n\\n', show=8)" + "order, *boards = in4 = parse(4, ints, sep='\\n\\n')" ] }, { @@ -652,11 +654,12 @@ " [sq(square % B, y) for y in range(B)])\n", "\n", "def bingo_winners(boards, drawn, just_called) -> List[Board]:\n", - " \"\"\"Boards that win due to the number just called.\"\"\"\n", + " \"\"\"Board(s) that win due to the number just called.\"\"\"\n", " def filled(board, line) -> bool: return all(board[n] in drawn for n in line)\n", " return [board for board in boards\n", " if just_called in board\n", - " and any(filled(board, line) for line in lines(board.index(just_called)))]\n", + " and any(filled(board, line) \n", + " for line in lines(board.index(just_called)))]\n", "\n", "def bingo_score(board, drawn, just_called) -> int:\n", " \"\"\"Sum of unmarked numbers multiplied by the number just called.\"\"\"\n", @@ -740,14 +743,15 @@ "name": "stdout", "output_type": "stream", "text": [ - "First 6 lines of Day 5 input:\n", - "-----------------------------\n", + "First 7 lines of Day 5 input (which is parsed into 500 entries):\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" + "779,741 -> 779,738\n", + "949,41 -> 13,977\n" ] } ], @@ -789,15 +793,15 @@ " else: # non-orthogonal lines not allowed\n", " return []\n", " \n", - "def cover(x1, x2) -> range:\n", - " \"\"\"All the ints from x1 to x2, inclusive, with x1, x2 in either order.\"\"\"\n", - " return range(min(x1, x2), max(x1, x2) + 1)\n", - "\n", "def overlaps(lines) -> int:\n", " \"\"\"How many points overlap 2 or more lines?\"\"\"\n", " counts = Counter(flatten(map(points, lines)))\n", " return quantify(counts[p] >= 2 for p in counts)\n", "\n", + "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)" ] }, @@ -874,8 +878,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "First 1 lines of Day 6 input:\n", - "-----------------------------\n", + "First 1 lines of Day 6 input (which is parsed into 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,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,5,1, ...\n" ] } @@ -890,7 +894,9 @@ "source": [ "- **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). Instead, I'll use a `Counter` of fish, and treat all the fish of each age group together. I have a hunch that part 2 will involve a ton-o'-fish." + "Although the puzzle 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." ] }, { @@ -961,6 +967,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "That's over a trillion lanternfish. Latest [estimates](https://www.google.com/search?q=how+many+fish+are+in+the+sea) say that there are in fact trillions of fish in the sea. But not trillions of lanternfish, and not increasing from 300 to over a trillion in just 256 days.\n", + "\n", "" ] }, @@ -982,8 +990,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "First 1 lines of Day 7 input:\n", - "-----------------------------\n", + "First 1 lines of Day 7 input (which is parsed into 1000 entries):\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,101, ...\n" ] } @@ -996,6 +1004,8 @@ "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", + "\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.)" ] }, @@ -1137,7 +1147,7 @@ { "data": { "text/plain": [ - "376.0" + "[376.0, 490.543]" ] }, "execution_count": 29, @@ -1158,9 +1168,11 @@ } ], "source": [ + "stars = [median(in7), mean(in7)]\n", "plt.hist(in7, bins=33, rwidth=0.8); \n", - "plt.plot([median(in7), mean(in7)], [50, 50], 'r*')\n", - "plt.ylabel('Number of Crabs'); plt.xlabel('Position'); median(in7)" + "plt.plot(stars, [50, 50], 'r*')\n", + "plt.ylabel('Number of Crabs'); plt.xlabel('Position')\n", + "stars" ] }, { @@ -1190,14 +1202,15 @@ "name": "stdout", "output_type": "stream", "text": [ - "First 6 lines of Day 8 input:\n", - "-----------------------------\n", + "First 7 lines of Day 8 input (which is parsed into 200 entries):\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" + "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" ] } ], @@ -1326,14 +1339,15 @@ "name": "stdout", "output_type": "stream", "text": [ - "First 6 lines of Day 9 input:\n", - "-----------------------------\n", + "First 7 lines of Day 9 input (which is parsed into 100 entries):\n", + "---------------------------------------------------------------\n", "9897656789865467895698765469899988672134598894345689864101378965457932349943210987654789653198789434\n", "8789542499996878954329984398789976561012987789245678953212567892345791998899329899765678969997668912\n", "7678943978987989965998993297649875432129876567956789864487678991056899877778939769886789998766457899\n", "4578999868998996899867894976532986543299876476897899987569899989167898766567898654998898998655345678\n", "2456987657679535679756799988643498657987654345789978899789998878998919954349997543219967987543237889\n", - "1234896545568986798645678999754989767898765456998769759899987765789329863238898659301256798793156891\n" + "1234896545568986798645678999754989767898765456998769759899987765789329863238898659301256798793156891\n", + "2346789432379997987434689489899879898919876567899954346998796434678997642127789798512345989989247892\n" ] } ], @@ -1385,7 +1399,7 @@ "source": [ "- **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 be part of both 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 every basin is surrounded by either edges or height 9 locations.\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." ] @@ -1407,23 +1421,23 @@ } ], "source": [ - "def flood_fill(grid) -> Dict[Point, Point]:\n", + "def find_basins(grid) -> Dict[Point, Point]:\n", " \"\"\"Compute `basins` as a map of {point: low_point} for each point in grid.\"\"\"\n", " basins = {} # A dict mapping each non-9 location to its low point.\n", - " def flood(p, low):\n", + " def flood_fill(p, low):\n", " \"\"\"Spread from p in all directions until hitting a 9;\n", " mark each point p as being part of the basin with `low` point.\"\"\"\n", " if grid[p] < 9 and p not in basins:\n", " basins[p] = low\n", " for p2 in grid.neighbors(p):\n", - " flood(p2, low)\n", + " flood_fill(p2, low)\n", " for p in low_points(grid):\n", - " flood(p, low=p)\n", + " 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 = flood_fill(grid)\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)" @@ -1453,7 +1467,7 @@ } ], "source": [ - "assert set(low_points(in9)) == set(flood_fill(in9).values())\n", + "assert set(low_points(in9)) == set(find_basins(in9).values())\n", "\n", "len(low_points(in9))" ] @@ -1527,7 +1541,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Apropos to \"smoke in the water,\" and to the color scheme of my plot, Gary Grady's drawing for the day references Deep Purple.\n", + "Apropos to *Smoke* and *Water,* and to the color scheme of my plot, Gary Grady's drawing for the day references [Deep Purple](https://www.youtube.com/watch?v=_zO6lWfvM0g):\n", "\n", "" ] @@ -1550,14 +1564,15 @@ "name": "stdout", "output_type": "stream", "text": [ - "First 6 lines of Day 10 input:\n", - "-----------------------------\n", + "First 7 lines of Day 10 input (which is parsed into 102 entries):\n", + "----------------------------------------------------------------\n", "[(([{<{(<{{[({{}{}}{[]()})<{{}()}>]}}(([{{{}[]}[[]()]}[<{}[]]{()()}]](({{}{}}{{}()}))){[{({}())[[\n", "<(({[<([{({[{{<>()}}[{<>()}({}{})]]<{<()<>>{[]()}}(((){}>[[][]])>}([{<[]{}>(<>[])}]))<[[[[[][]\n", "(<<(<{{{{<<<[(()<>){()<>}][[()()]]>{<{[]{}}<<>()>>}>{(<{<>}([]{})><(<>())<(){}>>)<(([]{})(()()))<<() ...\n", "[[[[<[{[(<{{{({}<>)((){})}((()())[()()])}}><[([((){})]<[()[]]{{}<>}>)[[{[]<>}][([]{})[{}()]]]]>)<{(<\n", "[<(<[[((<{((<<<>[]>><<<>{}>>){<[{}<>][<>[]]><<<>()>[(){}]>})[<{[{}<>][(){}]}<[[]<>][{}[]])>{([<>[]][\n", - "(([[[[<([[{([{<>()}{()<>}][((){})]){[{[]<>}({}<>)][(<><>)[()[]]]}}<{{({}{}){[]{}}}<{<><>}({}{})>}>\n" + "(([[[[<([[{([{<>()}{()<>}][((){})]){[{[]<>}({}<>)][(<><>)[()[]]]}}<{{({}{}){[]{}}}<{<><>}({}{})>}>\n", + "{{{[<(<([<{({{[]()}[{}()]}{<()<>>(()<>)})}><<[{<()()>(()[])}<<<>[]]>][<{()}{<><>}>({{}[]})]>>](\n" ] } ], @@ -1574,7 +1589,7 @@ "- **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", "\n", - "The instructions for Part 1 say *Some of the lines aren't corrupted, just incomplete; you can ignore these lines for now.* I'm pretty sure that means we will need to deal with 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." + "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." ] }, { @@ -1601,11 +1616,11 @@ " \"\"\"A tuple of (error_score, missing_chars) for this line.\"\"\"\n", " stack = [''] # A stack of closing characters we are looking for.\n", " for c in line:\n", - " if c == stack[-1]:\n", + " if c == stack[-1]: # A correctly matched closing bracket\n", " stack.pop()\n", - " elif c in open_close:\n", + " elif c in open_close: # A new opening bracket\n", " stack.append(open_close[c])\n", - " else: # erroneous character\n", + " else: # An erroneous closing bracket\n", " return error_scores[c], cat(reversed(stack))\n", " return 0, cat(reversed(stack))\n", " \n", @@ -1618,7 +1633,7 @@ "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", "\n", - "I was right; we will use 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`." + "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`." ] }, { @@ -1671,14 +1686,15 @@ "name": "stdout", "output_type": "stream", "text": [ - "First 6 lines of Day 11 input:\n", - "-----------------------------\n", + "First 7 lines of Day 11 input (which is parsed into 10 entries):\n", + "---------------------------------------------------------------\n", "1224346384\n", "5621128587\n", "6388426546\n", "1556247756\n", "1451811573\n", - "1832388122\n" + "1832388122\n", + "2748545647\n" ] } ], @@ -1692,7 +1708,7 @@ "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", "\n", - "On each step, each octopus increases by one energy unit; then the ones that are over 9 flash, which makes their neighbors get one more energy unit (potentially causing others to flash); then the flashers reset to zero energy." + "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." ] }, { @@ -1744,7 +1760,7 @@ "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", "\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 recursive `check_flash` function." + "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." ] }, { @@ -1806,14 +1822,15 @@ "name": "stdout", "output_type": "stream", "text": [ - "First 6 lines of Day 12 input:\n", - "-----------------------------\n", + "First 7 lines of Day 12 input (which is parsed into 22 entries):\n", + "---------------------------------------------------------------\n", "xx-xh\n", "vx-qc\n", "cu-wf\n", "ny-LO\n", "cu-DR\n", - "start-xx\n" + "start-xx\n", + "LO-vx\n" ] } ], @@ -1873,7 +1890,7 @@ "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", "\n", - "At first I felt bad that I would again have to copy/paste/edit the code for Part 1. I felt a bit better when I realized that the revised function `search_paths2` would have need to call the original `search_paths`: once we add 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 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." ] }, { @@ -1914,9 +1931,9 @@ "source": [ "# [Day 13](https://adventofcode.com/2021/day/13): Transparent Origami\n", "\n", - "- **Input**: The input is in two section: first, a set of (x,y) dots, e.g. \"`6,10`\". Second, a list of fold instructions, e.g. \"`fold along y=7`\".\n", + "- **Input**: The input is in two sections: (1) a set of dots, e.g. \"`6,10`\". (2) a list of fold instructions, e.g. \"`fold along y=7`\".\n", "\n", - "My `parse` command is not set up to parse different sections differently, so I'll have `parse` do a minimal amount of work, returning two lists of lines for the two sections. Then I'll transform the first section into `dots` (a set of `(x, y)` points) and the second section into `folds` (a list of instructions like `('y', 7)`)." + "My `parse` command is not set up to parse different sections differently, so I'll only ask `parse` to do a minimal amount of work, just parsing the input file into two sections, and splitting each section into a list of lines. " ] }, { @@ -1928,19 +1945,37 @@ "name": "stdout", "output_type": "stream", "text": [ - "First 6 lines of Day 13 input:\n", - "-----------------------------\n", + "First 7 lines of Day 13 input (which is parsed into 2 entries):\n", + "--------------------------------------------------------------\n", "103,224\n", "624,491\n", "808,688\n", "1076,130\n", "700,26\n", - "55,794\n" + "55,794\n", + "119,724\n" ] } ], "source": [ - "in13 = parse(13, str.splitlines, sep='\\n\\n')\n", + "in13 = parse(13, str.splitlines, sep='\\n\\n')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then I'll further process the two sections to get two variables:\n", + "- `dots`: a set of `(x, y)` points, such as `(103, 224)`. \n", + "- `folds`: a list of fold instructions, each implemented as a tuple, such as `('y', 7)` or `('x', 15)`." + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [], + "source": [ "dots = {ints(line) for line in in13[0]}\n", "folds = [(words(line)[2], ints(line)[0]) for line in in13[1]]" ] @@ -1949,45 +1984,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The fold instructions don't show up in the first 6 lines, so let's make sure I parsed them right:" - ] - }, - { - "cell_type": "code", - "execution_count": 50, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[('x', 655),\n", - " ('y', 447),\n", - " ('x', 327),\n", - " ('y', 223),\n", - " ('x', 163),\n", - " ('y', 111),\n", - " ('x', 81),\n", - " ('y', 55),\n", - " ('x', 40),\n", - " ('y', 27),\n", - " ('y', 13),\n", - " ('y', 6)]" - ] - }, - "execution_count": 50, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "folds" - ] - }, - { - "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. 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.\n", "\n", "- **Part 1**: How many dots are visible after completing just the first fold instruction on your transparent paper?" ] @@ -2000,13 +1997,11 @@ "source": [ "def fold(dots, instruction) -> Set[Point]: \n", " \"\"\"The set of dots that result from following the fold instruction.\"\"\"\n", - " axis, line = instruction\n", - " if axis == 'x':\n", - " return {(x, y) if x <= line else (2 * line - x, y)\n", - " for (x, y) in dots}\n", + " x_or_y, line = instruction\n", + " if x_or_y == 'x':\n", + " return {(line - abs(line - x), y) for (x, y) in dots}\n", " else:\n", - " return {(x, y) if y <= line else (x, 2 * line - y)\n", - " for (x, y) in dots}" + " return {(x, line - abs(line - y)) for (x, y) in dots}" ] }, { @@ -2033,13 +2028,27 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "- **Part 2**: Finish folding the transparent paper according to the instructions. The manual says the code is always eight capital letters. " + "- **Part 2**: Finish folding the transparent paper according to the instructions. What is the code?" ] }, { "cell_type": "code", "execution_count": 53, "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": 54, + "metadata": {}, "outputs": [ { "data": { @@ -2047,7 +2056,7 @@ "True" ] }, - "execution_count": 53, + "execution_count": 54, "metadata": {}, "output_type": "execute_result" }, @@ -2065,21 +2074,265 @@ } ], "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()\n", - " \n", - "answer(13.2, origami(dots, folds), None) # actual answer: CJCKBAPB" + "answer(13.2, origami(dots, folds), None) # actual answer: \"CJCKBAPB\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "I kind of cheated here. I didn't want to write an OCR program, so I relied on my own two eyes to look at the plot and see the answer." + "I kind of cheated here. I didn't want to write an OCR program, so I relied on my own eyes to look at the dots and see the code." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 14](https://adventofcode.com/2021/day/14): Extended Polymerization" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- **Input**: The input is a a polymer template (a string of one-letter element names, such as \"`NNCB`\") followed by a list of pair insertion rules (such as \"`CH -> B`\", meaning that a `B` should be inserted into the middle of each `CH` pair).\n", + "\n", + "I'll parse each line of the input into a list of `words` (thus ignoring the \"`->`\" characters); then pick out:\n", + "- `polymer`: the sole word on the first line.\n", + "- `rules`: the third through last lines, converted into a dict, like `{'CH': 'B', ...}`." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "First 7 lines of Day 14 input (which is parsed into 102 entries):\n", + "----------------------------------------------------------------\n", + "ONSVVHNCFVBHKVPCHCPV\n", + "\n", + "VO -> C\n", + "VV -> S\n", + "HK -> H\n", + "FC -> C\n", + "VB -> V\n" + ] + } + ], + "source": [ + "in14 = parse(14, words)\n", + "polymer = in14[0][0]\n", + "rules = dict(in14[2:])" + ] + }, + { + "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", + "\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." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def pair_insertion(polymer, rules, steps) -> 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", + " \"\"\"The count of most common element minus the count of least common element.\"\"\"\n", + " [(_, most), *_, (_, least)] = Counter(polymer).most_common()\n", + " return most - least\n", + "\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)" + ] + }, + { + "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", + "\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", + "\n", + "Here's an example Counter of element pairs:" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Counter({'NN': 1, 'NC': 1, 'CB': 1})" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Counter(pairs('NNCB'))" + ] + }, + { + "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", + "\n", + "So all in all there are three representations of a polymer:" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": {}, + "outputs": [], + "source": [ + "Polymer = str # e.g. 'NNCB'\n", + "PairCounter = Counter[str] # e.g. Counter({'NN': 1, 'NC': 1, 'CB': 1})\n", + "LetterCounter = Counter[Char] # e.g. Counter({'N': 2, 'C': 1, 'B': 1})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here's how we convert a PairCounter into a LetterCounter:" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": {}, + "outputs": [], + "source": [ + "def letter_counts(pair_ctr: PairCounter, polymer: Polymer) -> LetterCounter:\n", + " \"\"\"Return a Counter of the letters in the polymer described by the `pair_ctr`.\"\"\"\n", + " letters = set(flatten(pair_ctr))\n", + " def letter_count(L) -> int:\n", + " return int(sum(pair_ctr[L+M] + pair_ctr[M+L] for M in letters) / 2\n", + " + (L == polymer[0]) / 2 + (L == polymer[-1]) / 2)\n", + " return Counter({L: letter_count(L) for L in letters})" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Counter({'C': 1, 'N': 2, 'B': 1})" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "letter_counts(Counter(pairs('NNCB')), 'NNCB')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's make sure it works when the first and last letters are the same:" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": {}, + "outputs": [], + "source": [ + "assert letter_counts(Counter(pairs('NNCB')), 'NNCB') == 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):" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def pair_insertion2(polymer, rules, steps) -> PairCounter:\n", + " \"\"\"Insert elements into polymer according to rules; repeat `steps` times.\n", + " Return a Counter of element pairs.\"\"\"\n", + " pair_ctr = Counter(pairs(polymer))\n", + " for _ in range(steps):\n", + " pair_ctr2 = Counter()\n", + " for LM in pair_ctr:\n", + " pair_ctr2[LM[0] + rules[LM]] += pair_ctr[LM]\n", + " pair_ctr2[rules[LM] + LM[1]] += pair_ctr[LM]\n", + " pair_ctr = pair_ctr2\n", + " return pair_ctr\n", + "\n", + "def pair_insertion_diff(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", + "\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", + "\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)" ] } ],