From df535bdb9ad10da58c9fff4194b3ecc24bc312ca Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Mon, 29 Dec 2025 00:23:37 -0800 Subject: [PATCH] Add files via upload --- ipynb/Advent-2016.ipynb | 599 +++++++++++++++++++--------------------- 1 file changed, 279 insertions(+), 320 deletions(-) diff --git a/ipynb/Advent-2016.ipynb b/ipynb/Advent-2016.ipynb index 37ef084..728fabc 100644 --- a/ipynb/Advent-2016.ipynb +++ b/ipynb/Advent-2016.ipynb @@ -4,15 +4,15 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# December 2016: Advent of Code Solutions\n", + "
Peter Norvig, December 2016
\n", "\n", - "## Peter Norvig\n", + "# Advent of Code 2016 Solutions\n", "\n", - "From Dec. 1 to Dec. 25, [I](http://norvig.com) will be solving the puzzles that appear each day at **[Advent of Code](http://adventofcode.com/)**. The two-part puzzles are released at midnight EST (9:00PM PST); points are awarded to the first 100 people to solve the day's puzzles. The code shown here basically represents what I did to solve the problem, but slightly cleaned up:\n", - "- On days when I start at 9:00PM and am competing against the clock, I take shortcuts. I use shorter names, because I'm not a fast typist. I run test cases in the Jupyter Notebook, but don't make them into `assert` statements. Even then, I'm not at all competitive with the fastest solvers.\n", - "- On days when I didn't get a chance to start until after all the points are gone, what you see here is pretty much exactly what I did, or at least what I ended up with after correcting typos and other errors. \n", "\n", - "To understand the problems completely, you will have to read the full description in the **\"[Day 1](http://adventofcode.com/2016/day/1):\"** link in each day's section header.\n", + "From Dec. 1 to Dec. 25, [I](http://norvig.com) will be solving the puzzles that appear each day at **[Advent of Code](http://adventofcode.com/)**. The two-part puzzles are released at midnight EST (9:00PM PST); points are awarded to the first 100 people to solve the day's puzzles. The code shown here basically represents what I did to solve the problem, but slightly cleaned up. I'm not a threat to dominate the leaderboards, so I concentrate on clean code rather than solving speed.\n", + "\n", + "\n", + "To understand the problems, you will have to read the full description in the **\"[Day 1](http://adventofcode.com/2016/day/1):\"** link in each day's section header.\n", "\n", "# Day 0: Getting Ready\n", "\n", @@ -35,7 +35,7 @@ }, "outputs": [], "source": [ - "#### Imports\n", + "################ Imports\n", "\n", "import re\n", "import numpy as np\n", @@ -49,7 +49,7 @@ "from statistics import mean, median\n", "from typing import Iterable\n", "\n", - "#### Daily `Input` and `answer`\n", + "################ Daily `Input` and `answer`\n", "\n", "def Input(day: int) -> Iterable[str]:\n", " \"\"\"Open this day's input file.\"\"\"\n", @@ -83,10 +83,10 @@ " for day in sorted(answers):\n", " print(answers[day])\n", "\n", - "#### Utility Functions\n", + "################ Utility Functions\n", "\n", "def ints(text: str) -> list[int]: \n", - " \"All the integers anywhere in text.\"\n", + " \"\"\"All the integers anywhere in text.\"\"\"\n", " return [int(x) for x in re.findall(r'\\d+', text)]\n", " \n", "def transpose(matrix): \n", @@ -131,31 +131,31 @@ " return result\n", " return traced_f\n", "\n", - "#### 2D (x, y) Points\n", + "################ 2D (x, y) Points\n", "\n", - "# 2-D points implemented using (x, y) tuples\n", + "Point = tuple[int, int] # Type for a 2D point\n", "def X(point) -> int: return point[0]\n", "def Y(point) -> int: return point[1]\n", "\n", - "def neighbors4(point) -> tuple: \n", - " \"The four neighbors (without diagonals).\"\n", + "def neighbors4(point: Point) -> tuple[Point]: \n", + " \"\"\"The four neighbors (without diagonals).\"\"\"\n", " x, y = point\n", " return ((x+1, y), (x-1, y), (x, y+1), (x, y-1))\n", "\n", - "def neighbors8(point) -> tuple: \n", - " \"The eight neighbors (with diagonals).\"\n", + "def neighbors8(point: Point) -> tuple[Point]: \n", + " \"\"\"The eight neighbors (with diagonals).\"\"\"\n", " x, y = point \n", " return ((x+1, y), (x-1, y), (x, y+1), (x, y-1),\n", " (x+1, y+1), (x-1, y-1), (x+1, y-1), (x-1, y+1))\n", "\n", "def cityblock_distance(p, q=(0, 0)) -> int: \n", - " \"City block distance between two points.\"\n", + " \"\"\"City block distance between two points.\"\"\"\n", " return abs(X(p) - X(q)) + abs(Y(p) - Y(q))\n", "\n", - "#### A* Search\n", + "################ A* Search\n", "\n", "def astar_search(start, h_func, moves_func):\n", - " \"Find a shortest sequence of states from start to a goal state (a state s with h_func(s) == 0).\"\n", + " \"\"\"Find a shortest sequence of states from start to a goal state (a state s with h_func(s) == 0).\"\"\"\n", " frontier = [(h_func(start), start)] # A priority queue, ordered by f = path_length + h\n", " previous = {start: None} # start state has no previous state; other states will\n", " path_cost = {start: 0} # The cost of the best path to a state.\n", @@ -172,7 +172,7 @@ " return None\n", " \n", "def Path(previous, s) -> list: \n", - " \"Return a list of states that lead to state s, according to the previous dict.\"\n", + " \"\"\"Return a list of states that lead to state s, according to the previous dict.\"\"\"\n", " return ([] if (s is None) else Path(previous, previous[s]) + [s])\n", "\n", "def astar_step_count(*args) -> int:\n", @@ -215,11 +215,13 @@ "source": [ "def how_far(text: str) -> int:\n", " \"\"\"After following moves, how far away from the origin do we end up?\"\"\"\n", - " h, x, y, = 0, 0, 0 # Begin at origin, heading North\n", + " dxs = [0, 1, 0, -1] # N, E, S, W headings\n", + " dys = [1, 0, -1, 0] # N, E, S, W headings\n", + " x, y, h = 0, 0, 0 # Begin at origin, heading North\n", " for (turn, dist) in parse_moves(text):\n", " h = (h + turn) % 4\n", - " x += X(headings[h]) * dist\n", - " y += Y(headings[h]) * dist\n", + " x += dxs[h] * dist\n", + " y += dys[h] * dist\n", " return cityblock_distance((x, y))\n", "\n", "def parse_moves(text: str) -> list[tuple]:\n", @@ -228,8 +230,6 @@ " return [(turns[RL], int(d))\n", " for (RL, d) in re.findall(r'(R|L)(\\d+)', text)]\n", "\n", - "headings = [(0, 1), (1, 0), (0, -1), (-1, 0)] # North, East, South, West\n", - "\n", "answer(1.1, 262, lambda:\n", " how_far(Input(1).read()))" ] @@ -260,14 +260,15 @@ "source": [ "def visited_twice(text: str) -> int:\n", " \"\"\"Following moves, find the first location we visit twice, and return the distance to it.\"\"\"\n", - " headings = [(0, 1), (1, 0), (0, -1), (-1, 0)] # N, E, S, W\n", + " dxs = [0, 1, 0, -1] # N, E, S, W headings\n", + " dys = [1, 0, -1, 0] # N, E, S, W headings\n", " x, y, h = 0, 0, 0 # Begin at origin, heading North\n", " visited = {(x, y)}\n", " for (turn, dist) in parse_moves(text):\n", " h = (h + turn) % 4\n", " for _ in range(dist):\n", - " x += X(headings[h])\n", - " y += Y(headings[h])\n", + " x += dxs[h]\n", + " y += dys[h]\n", " if (x, y) in visited:\n", " return cityblock_distance((x, y))\n", " else:\n", @@ -370,7 +371,7 @@ { "data": { "text/plain": [ - "Puzzle 2.2: 0.4 milliseconds, correct answer: 26B96 " + "Puzzle 2.2: 0.6 milliseconds, correct answer: 26B96 " ] }, "execution_count": 5, @@ -426,7 +427,7 @@ } ], "source": [ - "def is_triangle(sides) -> bool:\n", + "def is_triangle(sides: tuple[int, int, int]) -> bool:\n", " \"Do these side lengths form a valid triangle?\"\n", " x, y, z = sorted(sides)\n", " return x + y > z\n", @@ -475,7 +476,7 @@ { "data": { "text/plain": [ - "Puzzle 3.2: 0.4 milliseconds, correct answer: 1544 " + "Puzzle 3.2: 0.3 milliseconds, correct answer: 1544 " ] }, "execution_count": 7, @@ -515,7 +516,7 @@ { "data": { "text/plain": [ - "Puzzle 4.1: 4.1 milliseconds, correct answer: 409147 " + "Puzzle 4.1: 4.0 milliseconds, correct answer: 409147 " ] }, "execution_count": 8, @@ -524,23 +525,23 @@ } ], "source": [ - "def parse(line: str) -> tuple: \n", - " \"Return (name, sector, checksum).\"\n", + "def parse_room_name(line: str) -> tuple[str, str, str]: \n", + " \"\"\"Return (name, sector, checksum).\"\"\"\n", " return re.match(r\"(.+)-(\\d+)\\[([a-z]+)\\]\", line).groups()\n", "\n", "def sector(line: str) -> int:\n", - " \"Return the sector number if valid, or 0 if not.\"\n", - " name, sector, checksum = parse(line)\n", + " \"\"\"Return the sector number if valid, or 0 if not.\"\"\"\n", + " name, sector, checksum = parse_room_name(line)\n", " return int(sector) if valid(name, checksum) else 0\n", "\n", "def valid(name: str, checksum: str) -> bool:\n", - " \"Determine if name is valid according to checksum.\"\n", + " \"\"\"Determine if name is valid according to checksum.\"\"\"\n", " counts = Counter(name.replace('-', '')) \n", " # Note: counts.most_common(5) doesn't work because it breaks ties arbitrarily.\n", " letters = sorted(counts, key=lambda L: (-counts[L], L))\n", " return checksum == cat(letters[:5])\n", "\n", - "assert parse('aaaaa-bbb-z-y-x-123[abxyz]') == ('aaaaa-bbb-z-y-x', '123', 'abxyz')\n", + "assert parse_room_name('aaaaa-bbb-z-y-x-123[abxyz]') == ('aaaaa-bbb-z-y-x', '123', 'abxyz')\n", "assert sector('aaaaa-bbb-z-y-x-123[abxyz]') == 123\n", "assert valid('aaaaa-bbb-z-y-x', 'abxyz')\n", "\n", @@ -570,7 +571,7 @@ { "data": { "text/plain": [ - "Puzzle 4.2: 1.7 milliseconds, correct answer: 991 " + "Puzzle 4.2: 1.6 milliseconds, correct answer: 991 " ] }, "execution_count": 9, @@ -580,12 +581,12 @@ ], "source": [ "def decrypt(line: str) -> str:\n", - " \"Decrypt the line (shift the name by sector; discard checksum).\"\n", - " name, sector, _ = parse(line)\n", + " \"\"\"Decrypt the line (shift the name by sector; discard checksum).\"\"\"\n", + " name, sector, _ = parse_room_name(line)\n", " return shift(name, int(sector)) + ' ' + sector\n", "\n", "def shift(text, N, alphabet='abcdefghijklmnopqrstuvwxyz') -> str:\n", - " \"Shift cipher: letters in text rotate forward in alphabet by N places.\"\n", + " \"\"\"Shift cipher: letters in text rotate forward in alphabet by N places.\"\"\"\n", " N = N % len(alphabet)\n", " tr = str.maketrans(alphabet, alphabet[N:] + alphabet[:N])\n", " return text.translate(tr)\n", @@ -620,9 +621,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "4515059 00000191970e97b86ecd2220e76d86b2 1\n", - "6924074 00000a1568b97dfc4736c4248df549b3 1a\n", - "8038154 00000312234ca27718d52476a44c257c 1a3\n", + " 4515059 00000191970e97b86ecd2220e76d86b2 1\n", + " 6924074 00000a1568b97dfc4736c4248df549b3 1a\n", + " 8038154 00000312234ca27718d52476a44c257c 1a3\n", "13432968 00000064ec7123bedfc9ff00cc4f55f2 1a30\n", "13540621 0000091c9c2cd243304328869af7bab2 1a309\n", "14095580 0000096753dd21d352853f1d97e19d01 1a3099\n", @@ -633,7 +634,7 @@ { "data": { "text/plain": [ - "Puzzle 5.1: 7,316.0 milliseconds, correct answer: 1a3099aa " + "Puzzle 5.1: 7,596.7 milliseconds, correct answer: 1a3099aa " ] }, "execution_count": 10, @@ -647,13 +648,13 @@ "door = \"uqwqemis\"\n", "\n", "def find_password(door: str, verbose=True) -> str:\n", - " \"First 8 sixth digits of md5 hashes of door+i that begin with '00000'.\"\n", + " \"\"\"First 8 sixth digits of md5 hashes of door+i that begin with '00000'.\"\"\"\n", " password = ''\n", " for i in range(BIG):\n", - " x = hashlib.md5( bytes(door + str(i), 'utf-8')).hexdigest()\n", + " x = hashlib.md5(bytes(door + str(i), 'utf-8')).hexdigest()\n", " if x.startswith('00000'):\n", " password += x[5]\n", - " if verbose: print(i, x, password)\n", + " if verbose: print(f'{i:8} {x} {password}') # Just to see something happen\n", " if len(password) == 8: \n", " return password\n", "\n", @@ -682,8 +683,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "4515059 00000191970e97b86ecd2220e76d86b2 .9......\n", - "8038154 00000312234ca27718d52476a44c257c .9.1....\n", + " 4515059 00000191970e97b86ecd2220e76d86b2 .9......\n", + " 8038154 00000312234ca27718d52476a44c257c .9.1....\n", "13432968 00000064ec7123bedfc9ff00cc4f55f2 69.1....\n", "17743256 000002457920bc00c2bd4d769a3da01c 6941....\n", "19112977 000005074f875107f82b4ffb39a1fbf0 6941.0..\n", @@ -695,7 +696,7 @@ { "data": { "text/plain": [ - "Puzzle 5.2: 11,520.3 milliseconds, correct answer: 694190cd " + "Puzzle 5.2: 11,989.2 milliseconds, correct answer: 694190cd " ] }, "execution_count": 11, @@ -713,7 +714,7 @@ " index = int(x[5], 16)\n", " if index < 8 and password[index] is off:\n", " password[index] = x[6]\n", - " if verbose: print(i, x, cat(password)) # Just to see something happen\n", + " if verbose: print(f'{i:8} {x} {cat(password)}') # Just to see something happen\n", " if off not in password:\n", " return cat(password)\n", "\n", @@ -727,7 +728,7 @@ "source": [ "# [Day 6](http://adventofcode.com/2016/day/6): Signals and Noise\n", "\n", - "Given a file, where each line is a string of letters and every line has the same length, find the most common letter in each column. We can easily do this with the help of `Counter.most_common`. (Note I use `stream).read().split()` so that I don't get the `'\\n'` at the end of each line.)" + "Given a file, where each line is a string of letters and every line has the same length, find the most common letter in each column. We can easily do this with the help of `Counter.most_common`. (Note I use `stream.read().split()` so that I don't get the `'\\n'` at the end of each line.)" ] }, { @@ -743,7 +744,7 @@ { "data": { "text/plain": [ - "Puzzle 6.1: 0.5 milliseconds, correct answer: dzqckwsd " + "Puzzle 6.1: 0.6 milliseconds, correct answer: dzqckwsd " ] }, "execution_count": 12, @@ -829,7 +830,7 @@ { "data": { "text/plain": [ - "Puzzle 7.1: 12.4 milliseconds, correct answer: 118 " + "Puzzle 7.1: 12.3 milliseconds, correct answer: 118 " ] }, "execution_count": 14, @@ -896,7 +897,7 @@ { "data": { "text/plain": [ - "Puzzle 7.2: 119.6 milliseconds, correct answer: 260 " + "Puzzle 7.2: 119.5 milliseconds, correct answer: 260 " ] }, "execution_count": 16, @@ -908,7 +909,7 @@ "alphabet = 'abcdefghijklmnopqrstuvwxyz'\n", "\n", "def ssl(segments: list[str]) -> bool: \n", - " \"Is there an ABA outside brackets, and the corresponding BAB inside?\"\n", + " \"\"\"Is there an ABA outside brackets, and the corresponding BAB inside?\"\"\"\n", " outs, ins = outsides(segments), insides(segments)\n", " return any(a+b+a in outs and b+a+b in ins\n", " for a in alphabet for b in alphabet if a != b)\n", @@ -946,7 +947,7 @@ { "data": { "text/plain": [ - "Puzzle 8.1: 0.7 milliseconds, correct answer: 110 " + "Puzzle 8.1: 0.6 milliseconds, correct answer: 110 " ] }, "execution_count": 17, @@ -956,7 +957,7 @@ ], "source": [ "def interpret(cmd: str, screen: np.array) -> None:\n", - " \"Interpret this command to mutate screen.\"\n", + " \"\"\"Interpret this command to mutate screen.\"\"\"\n", " A, B = map(int, re.findall(r'(\\d+)', cmd)) # There should be 2 numbers on every command line\n", " if cmd.startswith('rect'):\n", " screen[:B, :A] = 1\n", @@ -970,7 +971,7 @@ "def Screen(shape=(6, 50)) -> np.array: return np.zeros(shape, dtype=int)\n", "\n", "def run(commands) -> np.array:\n", - " \"Do all the commands and return the final pixel array.\"\n", + " \"\"\"Do all the commands and return the final pixel array.\"\"\"\n", " screen = Screen()\n", " for cmd in commands:\n", " interpret(cmd, screen) \n", @@ -1012,7 +1013,7 @@ { "data": { "text/plain": [ - "Puzzle 8.2: 0.6 milliseconds, correct answer: ZJHRKCPLYJ " + "Puzzle 8.2: 0.7 milliseconds, correct answer: ZJHRKCPLYJ " ] }, "execution_count": 18, @@ -1022,6 +1023,7 @@ ], "source": [ "def show_message(commands) -> None:\n", + " \"\"\"Run the commands and print the resulting pixel array.\"\"\"\n", " for row in run(commands):\n", " print(cat(' @'[pixel] for pixel in row))\n", "\n", @@ -1051,7 +1053,7 @@ { "data": { "text/plain": [ - "Puzzle 9.1: 0.3 milliseconds, correct answer: 115118 " + "Puzzle 9.1: 0.6 milliseconds, correct answer: 115118 " ] }, "execution_count": 19, @@ -1063,7 +1065,7 @@ "matcher = re.compile(r'[(](\\d+)x(\\d+)[)]').match # e.g. matches \"(2x5)\" as ('2', '5')\n", "\n", "def decompress(s: str) -> str:\n", - " \"Decompress string s by interpreting '(2x5)' as making 5 copies of the next 2 characters.\"\n", + " \"\"\"Decompress string s by interpreting '(2x5)' as making 5 copies of the next 2 characters.\"\"\"\n", " s = re.sub(r'\\s', '', s) # \"whitespace is ignored\"\n", " result = []\n", " i = 0\n", @@ -1103,7 +1105,7 @@ { "data": { "text/plain": [ - "Puzzle 9.2: 1.7 milliseconds, correct answer: 11107527530 " + "Puzzle 9.2: 2.0 milliseconds, correct answer: 11107527530 " ] }, "execution_count": 20, @@ -1186,12 +1188,7 @@ { "cell_type": "code", "execution_count": 22, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, + "metadata": {}, "outputs": [ { "data": { @@ -1205,31 +1202,31 @@ } ], "source": [ - "def bots(instructions, goal={17, 61}) -> tuple[dict, int]:\n", + "def bots(instructions, goal={17, 61}) -> dict:\n", " \"\"\"Follow the data flow instructions, returning a dict of who has what,\n", - " and the number of the bot who handles the goal set of chips.\"\"\"\n", - " winner = None\n", + " and the number of the winner: the bot who gets the goal set of chips.\"\"\"\n", + " \n", " def give(giver, chip, recip):\n", " \"\"\"Pass the chip from giver to recipient.\"\"\"\n", - " nonlocal winner\n", " has[giver].discard(chip)\n", " has[recip].add(chip)\n", " chips = has[recip]\n", " if chips == goal:\n", - " winner = first(ints(recip))\n", + " has['winner'] = first(ints(recip))\n", " if len(chips) == 2:\n", " give(recip, min(chips), gives[recip][0])\n", " give(recip, max(chips), gives[recip][1])\n", + " \n", " has = defaultdict(set) # who has what\n", - " gives = {giver: (dest1, dest2) # who will give what\n", + " gives = {giver: (dest1, dest2) # who will give what; used by `give` function\n", " for (giver, dest1, dest2) \n", " in re.findall(r'(bot \\d+) gives low to (\\w+ \\d+) and high to (\\w+ \\d+)', instructions)}\n", " for (chip, recip) in re.findall(r'value (\\d+) goes to (\\w+ \\d+)', instructions):\n", " give('input bin', int(chip), recip)\n", - " return has, winner\n", + " return has\n", "\n", "answer(10.1, 56, lambda:\n", - " bots(Input(10).read())[1])" + " bots(Input(10).read())['winner'])" ] }, { @@ -1252,7 +1249,7 @@ { "data": { "text/plain": [ - "Puzzle 10.2: 0.8 milliseconds, correct answer: 7847 " + "Puzzle 10.2: 0.6 milliseconds, correct answer: 7847 " ] }, "execution_count": 23, @@ -1262,7 +1259,7 @@ ], "source": [ "def bin_product(instructions) -> int:\n", - " has, _ = bots(instructions)\n", + " has = bots(instructions)\n", " def out(i): return first(has[f'output {i}'])\n", " return out(0) * out(1) * out(2)\n", "\n", @@ -1276,21 +1273,21 @@ "source": [ "# [Day 11](http://adventofcode.com/2016/day/11): Radioisotope Thermoelectric Generators \n", "\n", - "I *knew* my `astar_search` function would come in handy! To search for the shortest path to the goal, I need to provide:\n", - "- an initial state of the world,\n", - "- a heuristic function (which estimates how many moves away from the goal a state is),\n", - "- a function that says what states can be reached by moving the elevator up or down, and carrying some stuff.\n", - "\n", "Here is my input. I'll just read this with my eyes and hand-code it. I won't write a parser for it.\n", "\n", - "* The first floor contains a **thulium generator**, a **thulium-compatible microchip**, a **plutonium generator**, and a **strontium generator**.\n", - "* The second floor contains a **plutonium-compatible microchip** and a **strontium-compatible microchip**.\n", - "* The third floor contains a **promethium generator**, a **promethium-compatible microchip**, a **ruthenium generator**, and a **ruthenium-compatible microchip**.\n", - "* The fourth floor contains nothing relevant.\n", + " The first floor contains a hydrogen-compatible microchip and a lithium-compatible microchip.\n", + " The second floor contains a hydrogen generator.\n", + " The third floor contains a lithium generator.\n", + " The fourth floor contains nothing relevant.\n", + " \n", + "The goal is to get everything to the fourth floor, moving at most two things at a time up or down a floor, and never leaving a microchip on a floor with a generator, unless it is plugged in to its own generator. I *knew* my `astar_search` function would come in handy! To search for the shortest path to the goal, I need to provide:\n", + "- an initial state of the world: `initial`.\n", + "- a heuristic function which estimates how many moves away from the goal a state is: `h_to_top`.\n", + "- a function `moves` that says what states can be reached by carrying some stuff up or down a floor in the elevator.\n", "\n", - "I initially represented the initial state (and all states) with a set of objects for each floor. That worked fine, and solved Part 1 in 3 seconds. But Part 2 took 4 minutes. So I decided I needed a representation that made the search space smaller. I can do that by taking advantage of the *symmetry* in the problem: what really matters is which microchips are paired with which generators; their names don't matter. For example, my third floor has a promethium generator/microchip pair and a ruthenium microchip/generator pair. If I move either pair, the effect on the puzzle is the same. I shouldn't count moving the promethium pair as a different state than moving the ruthenium pair; they should be the same state.\n", + "I initially represented states with a set of objects for each floor. That worked fine, and solved Part 1 in 3 seconds. But Part 2 took 4 minutes. After AoC was over I decided that's too slow; I should go back and find a representation that made the search space smaller. I can do that by taking advantage of the *symmetry* in the problem: what really matters is which microchips are paired with which generators; their names don't matter. For example, my third floor has a promethium generator/microchip pair and a ruthenium microchip/generator pair. If I move either pair, the effect on the puzzle is the same. I shouldn't count moving the promethium pair as a different state than moving the ruthenium pair; they should be the same state.\n", "\n", - "To figure out the representation, consider the simpler example from AoC:\n", + "To figure out the representation, consider the simple example from AoC:\n", "\n", " #3 . . . . . \n", " #2 . . . LG . \n", @@ -1326,7 +1323,7 @@ { "data": { "text/plain": [ - "Puzzle 11.1: 52.7 milliseconds, correct answer: 31 " + "Puzzle 11.1: 58.0 milliseconds, correct answer: 31 " ] }, "execution_count": 24, @@ -1338,6 +1335,7 @@ "State = namedtuple('State', 'elevator, pairs')\n", "FLOORS = {0, 1, 2, 3}\n", "\n", + "# The initial state, corresponding to my problem description.\n", "initial = State(0, tuple(sorted(dict(Th=(0, 0), Pl=(1, 0), St=(1, 0), Pr=(2, 2), Ru=(2, 2)).values())))\n", "assert initial == State(elevator=0, pairs=((0, 0), (1, 0), (1, 0), (2, 2), (2, 2)))\n", "\n", @@ -1349,7 +1347,6 @@ "\n", "def h_to_top(state) -> int:\n", " \"\"\"An estimate of the number of moves needed to move everything to top.\"\"\"\n", - " return 0 if set(state.pairs) == {(3, 3)} else 1000\n", " top = max(FLOORS)\n", " total = sum((top - f) for f in flatten(state.pairs))\n", " return math.ceil(total / 2) # Can move two items in one move.\n", @@ -1393,7 +1390,7 @@ { "data": { "text/plain": [ - "Puzzle 11.2: 311.7 milliseconds, correct answer: 55 " + "Puzzle 11.2: 325.9 milliseconds, correct answer: 55 " ] }, "execution_count": 25, @@ -1402,7 +1399,7 @@ } ], "source": [ - "part2 = State(elevator=0, pairs=((0, 0), (0, 0), (0, 0), (1, 0), (1, 0), (2, 2), (2, 2)))\n", + "part2 = State(elevator=0, pairs=initial.pairs + ((0, 0), (0, 0)))\n", "\n", "answer(11.2, 55, lambda:\n", " astar_step_count(part2, h_to_top, moves))" @@ -1412,7 +1409,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "That's 12 times faster than the floor-based representation that does not account for symmetry!" + "That's down from 4 minutes to 1/3 of a second by taking advantage of symmetry!" ] }, { @@ -1437,7 +1434,7 @@ { "data": { "text/plain": [ - "Puzzle 12.1: 109.3 milliseconds, correct answer: 318007 " + "Puzzle 12.1: 108.1 milliseconds, correct answer: 318007 " ] }, "execution_count": 26, @@ -1447,7 +1444,7 @@ ], "source": [ "def interpret(code, **regs) -> dict:\n", - " \"Execute instructions until pc goes off the end; return registers.\"\n", + " \"\"\"Execute instructions until pc goes off the end; return registers.\"\"\"\n", " def val(x): return (regs[x] if x in regs else x)\n", " pc = 0\n", " while pc < len(code):\n", @@ -1491,7 +1488,7 @@ { "data": { "text/plain": [ - "Puzzle 12.2: 3,142.9 milliseconds, correct answer: 9227661 " + "Puzzle 12.2: 3,036.3 milliseconds, correct answer: 9227661 " ] }, "execution_count": 27, @@ -1545,8 +1542,8 @@ "favorite = 1362\n", "goal = (31, 39)\n", "\n", - "def is_open(location) -> bool:\n", - " \"Is this an open location?\"\n", + "def is_open(location: Point) -> bool:\n", + " \"\"\"Is this an open location according to the formula?\"\"\"\n", " x, y = location\n", " num = x*x + 3*x + 2*x*y + y + y*y + favorite\n", " return x >= 0 and y >= 0 and bin(num).count('1') % 2 == 0\n", @@ -1578,42 +1575,42 @@ "name": "stdout", "output_type": "stream", "text": [ - "#..###.#...#..#....##.#...#....#....#..#.#.##...##.##.##.#####.#####.####.#.##..#..#.#.#.#\n", - "#.#..#.##.#######.#.#.####.##.###...##.#..#.###...#.#..#.##...#..###....##...##.#.##.#.#..\n", - "#..#.#.##.#....##...#..#.####.####...#..#..#..#.#...##......#.....######.#.#...##.##.#..##\n", - "##..##.##.#..#...###.#.....#.....##.######....#..###########.####..##....#...#......###..#\n", - "..#.##..########.#.#####...#..##.##.#.....##.###.#..#...##.###..#....####.#######.#.#.####\n", - "#............###.....#####.#####....#..##..#.###....#....#......#..#.##.#....##.###.###..#\n", - "###.##.####...#.##....#..#.#..#.##.####.##....####.###.#.##.##.###......###........#...#..\n", - "#.#..#.##.#.#.######..#.##..#.####..#.#######..###..##..#.##.#.##########.#####..#.#.#....\n", - ".###......#..#..#..####.###..#..#.#.##..#.......###...#....#....##...#......#####..#..##.#\n", - ".####..###.#.##.#.#...#..#.#.##.##......#..####..#.######.#####....#...##.......#.###..#.#\n", - "..######.###..###..##.#..###..##.#..##.####...#..##....##.#...#.###.######.######.#.##....\n", - "#..##..#.......###.#..###......#.#####..#.#...#...#..#....#...#...###....#.##..##.######..\n", - ".......##.##.#.....#....#.##.#.###..#.#.####..##.###########..##.#....##.....#..##..#..##.\n", - "#####.#.#.##...###.##...##.#..#..##.##...#######.....##..#######.#########.#.....##.#.#.#.\n", - "...##..##.#.###..#.###.#.##.#..#..##.#.....#..#####....#..##..##...#..##.#..##.#..###..##.\n", - ".#.###.#..#...#.##...#..#.#..#.......##..#.##..##.#.##.....#.....#..#...###..#......##.#..\n", - "##..#..#.######.#####.#..###..##.###.####...#.....#.#.##.#.##.#####...#.####..##.##.#..#.#\n", - "....##.#.##..#...##.#.##.####..#.###....#...#..###..#..#..#.#....#.##.#..#######.#..##.#..\n", - "##...#.......#......#.....#.###...########.#####.#..##..#..####..##.#.#...##..#..#...#..#.\n", - "###.#######.########.##.#.###.##...##..#.#.##..#.#########.##.###.##..###.....##.##.######\n", - ".##.#...###.##...#.##.#..#.....##....#.##......#.#..#........#..##.#......##.#.#.##.#...#.\n", - ".##.#.....#....#..#.##.#.####.#.#.##..#.#####.##.#..#..#####......###..##..#..##.#..#...##\n", - "...###.##.#.###.#.##.###...##..##.#.#......##.#..#.####.....##.##.#####.##..#.#..#####...#\n", - ".#..##.#..##..##...#.....#..##.#..#..##..#.##.#.##..#.#..##..#.#....#.#######.#.#....##.##\n", - "###....#...###.#...##.#####.#..#..##.#####..###.#.#.#####.###..#.##.##..#...###...##.##.##\n", - ".#.##.###.#..#.##.#.###..#..##.#####..........#.##...##.###.##.#.#....#.#.....######.#....\n", - ".####..##..#.#.##..#.....#...#.#..#####.##.##.##.#.....#.....#...#.##..###.##..#..#..#..##\n", - "...#.#...#..##..##...###.##.##..#..##.#.##.#...#.##..#.#.##.###..#.#.#..##.#.#..#.#.###...\n", - "#..##.##..#.###.#.####.#.##.###.#.....#....#.#.######..#.##.#.###..##.#....####..##.#####.\n", - "##..#######..#..#.....##.....#..######.##..#..#.....#.##.#..###.##..##.##.#..#.#..#..##.#.\n" + "#..###.#...#..#....##.#...#....#....#..#.#.##...##.##.##.#####.#####.####.#.##..#..#.#.#.#..##..#...\n", + "#.#..#.##.#######.#.#.####.##.###...##.#..#.###...#.#..#.##...#..###....##...##.#.##.#.#........#..#\n", + "#..#.#.##.#....##...#..#.####.####...#..#..#..#.#...##......#.....######.#.#...##.##.#..##..##.####.\n", + "##..##.##.#..#...###.#.....#.....##.######....#..###########.####..##....#...#......###..#####..#.##\n", + "..#.##..########.#.#####...#..##.##.#.....##.###.#..#...##.###..#....####.#######.#.#.####..#.#.##.#\n", + "#............###.....#####.#####....#..##..#.###....#....#......#..#.##.#....##.###.###..##.##...##.\n", + "###.##.####...#.##....#..#.#..#.##.####.##....####.###.#.##.##.###......###........#...#..##.#......\n", + "#.#..#.##.#.#.######..#.##..#.####..#.#######..###..##..#.##.#.##########.#####..#.#.#.......##..###\n", + ".###......#..#..#..####.###..#..#.#.##..#.......###...#....#....##...#......#####..#..##.###.#####..\n", + ".####..###.#.##.#.#...#..#.#.##.##......#..####..#.######.#####....#...##.......#.###..#.###.......#\n", + "..######.###..###..##.#..###..##.#..##.####...#..##....##.#...#.###.######.######.#.##....#####.####\n", + "#..##..#.......###.#..###......#.#####..#.#...#...#..#....#...#...###....#.##..##.######...##.#.....\n", + ".......##.##.#.....#....#.##.#.###..#.#.####..##.###########..##.#....##.....#..##..#..##.....##..##\n", + "#####.#.#.##...###.##...##.#..#..##.##...#######.....##..#######.#########.#.....##.#.#.#.#######..#\n", + "...##..##.#.###..#.###.#.##.#..#..##.#.....#..#####....#..##..##...#..##.#..##.#..###..##.##..#.##.#\n", + ".#.###.#..#...#.##...#..#.#..#.......##..#.##..##.#.##.....#.....#..#...###..#......##.#...#..####..\n", + "##..#..#.######.#####.#..###..##.###.####...#.....#.#.##.#.##.#####...#.####..##.##.#..#.#.###..####\n", + "....##.#.##..#...##.#.##.####..#.###....#...#..###..#..#..#.#....#.##.#..#######.#..##.#..#...#..##.\n", + "##...#.......#......#.....#.###...########.#####.#..##..#..####..##.#.#...##..#..#...#..#..##.#.....\n", + "###.#######.########.##.#.###.##...##..#.#.##..#.#########.##.###.##..###.....##.##.######.#..##.###\n", + ".##.#...###.##...#.##.#..#.....##....#.##......#.#..#........#..##.#......##.#.#.##.#...#..#...#.#..\n", + ".##.#.....#....#..#.##.#.####.#.#.##..#.#####.##.#..#..#####......###..##..#..##.#..#...##.##.##..#.\n", + "...###.##.#.###.#.##.###...##..##.#.#......##.#..#.####.....##.##.#####.##..#.#..#####...#.##.#.#..#\n", + ".#..##.#..##..##...#.....#..##.#..#..##..#.##.#.##..#.#..##..#.#....#.#######.#.#....##.##.##.##.#..\n", + "###....#...###.#...##.#####.#..#..##.#####..###.#.#.#####.###..#.##.##..#...###...##.##.##...#.##.#.\n", + ".#.##.###.#..#.##.#.###..#..##.#####..........#.##...##.###.##.#.#....#.#.....######.#.....#.#..##.#\n", + ".####..##..#.#.##..#.....#...#.#..#####.##.##.##.#.....#.....#...#.##..###.##..#..#..#..####.###.##.\n", + "...#.#...#..##..##...###.##.##..#..##.#.##.#...#.##..#.#.##.###..#.#.#..##.#.#..#.#.###.....#.....#.\n", + "#..##.##..#.###.#.####.#.##.###.#.....#....#.#.######..#.##.#.###..##.#....####..##.#####.#..######.\n", + "##..#######..#..#.....##.....#..######.##..#..#.....#.##.#..###.##..##.##.#..#.#..#..##.#..#.##...#.\n" ] } ], "source": [ "for y in range(30):\n", - " print(cat(('.' if is_open((x, y)) else '#') for x in range(90)))" + " print(cat('#.'[is_open((x, y))] for x in range(100)))" ] }, { @@ -1693,7 +1690,7 @@ { "data": { "text/plain": [ - "Puzzle 14.1: 125.8 milliseconds, correct answer: 16106 " + "Puzzle 14.1: 127.3 milliseconds, correct answer: 16106 " ] }, "execution_count": 31, @@ -1708,7 +1705,7 @@ "def hashval(i): return hashlib.md5(bytes(salt + str(i), 'utf-8')).hexdigest()\n", "\n", "def is_key(i: int) -> bool:\n", - " \"A key has a triple like '777', and then '77777' in one of the next thousand hashval(i).\"\n", + " \"\"\"A key has a triple like '777', and then '77777' in one of the next thousand hashval(i).\"\"\"\n", " three = re.search(r'(.)\\1\\1', hashval(i))\n", " if three:\n", " five = three.group(1) * 5\n", @@ -1725,7 +1722,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In **part two**, we do *key stretching*, hashing an additional 2016 times. Eeverything else is the same:" + "In **part two**, we do *key stretching*, hashing an additional 2016 times. Everything else is the same:" ] }, { @@ -1741,7 +1738,7 @@ { "data": { "text/plain": [ - "Puzzle 14.2: 14,487.3 milliseconds, correct answer: 22423 " + "Puzzle 14.2: 14,443.9 milliseconds, correct answer: 22423 " ] }, "execution_count": 32, @@ -1752,6 +1749,7 @@ "source": [ "@cache\n", "def hashval(i, stretch=2016): \n", + " \"\"\"The hash value with key stretching.\"\"\"\n", " h = hashlib.md5(bytes(salt + str(i), 'utf-8')).hexdigest()\n", " for i in range(stretch):\n", " h = hashlib.md5(bytes(h, 'utf-8')).hexdigest()\n", @@ -1765,7 +1763,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This was my highest-scoring day on the leaderboard, finishing #20 on part two." + "This was my highest-scoring day on the leaderboard, finishing #20 on part two. It was also my slowest-running answer. Maybe other people were trying to be clever, while I was just letting my program run." ] }, { @@ -1780,17 +1778,12 @@ { "cell_type": "code", "execution_count": 33, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Puzzle 15.1: 49.0 milliseconds, correct answer: 203660 " + "Puzzle 15.1: 53.2 milliseconds, correct answer: 203660 " ] }, "execution_count": 33, @@ -1799,14 +1792,16 @@ } ], "source": [ + "Disc = namedtuple('Disc', 'number, positions, start_time, start_pos')\n", + "\n", "def parse_discs(lines) -> list[tuple]:\n", - " \"\"\"Parse discs into (disc_number, positions, start_time, start_position) tuples.\"\"\"\n", - " return [ints(line) for line in lines]\n", + " \"\"\"Parse discs.\"\"\"\n", + " return [Disc(*ints(line)) for line in lines]\n", " \n", "def falls(t, discs) -> bool:\n", - " \"If we drop the capsule at time t, does it fall through all slots?\"\n", - " return all((start + t + d) % positions == 0 \n", - " for (d, positions, _, start) in discs)\n", + " \"\"\"If we drop the capsule at time t, does it fall through all slots?\"\"\"\n", + " return all((d.start_pos + t + d.number) % d.positions == 0 \n", + " for d in discs)\n", "\n", "def all_falling_times(discs) -> Iterable[int]:\n", " \"\"\"Times t that a capsule could fall through all the discs.\"\"\"\n", @@ -1831,7 +1826,7 @@ { "data": { "text/plain": [ - "Puzzle 15.2: 583.3 milliseconds, correct answer: 2408135 " + "Puzzle 15.2: 634.8 milliseconds, correct answer: 2408135 " ] }, "execution_count": 34, @@ -1841,7 +1836,7 @@ ], "source": [ "answer(15.2, 2408135,\n", - " lambda: first(all_falling_times([*parse_discs(Input(15)), (7,11, 0, 0)])))" + " lambda: first(all_falling_times([*parse_discs(Input(15)), Disc(7, 11, 0, 0)])))" ] }, { @@ -1892,7 +1887,7 @@ ], "source": [ "def expand(a: str, N: int) -> str:\n", - " \"Expand seed `a` until it has length N.\"\n", + " \"\"\"Expand seed `a` until it has length N.\"\"\"\n", " while len(a) < N:\n", " b = flip(a[::-1])\n", " a = a + '0' + b\n", @@ -1901,7 +1896,7 @@ "def flip(text, table=str.maketrans('10', '01')): return text.translate(table)\n", "\n", "def checksum(a: str) -> str:\n", - " \"Compute the checksum of `a` by comparing pairs until len is odd.\"\n", + " \"\"\"Compute the checksum of `a` by comparing pairs until len is odd.\"\"\"\n", " while len(a) % 2 == 0:\n", " a = cat(('1' if a[i] == a[i+1] else '0') \n", " for i in range(0, len(a), 2))\n", @@ -1933,7 +1928,7 @@ { "data": { "text/plain": [ - "Puzzle 16.2: 1,549.0 milliseconds, correct answer: 11001111011000111" + "Puzzle 16.2: 1,560.4 milliseconds, correct answer: 11001111011000111" ] }, "execution_count": 36, @@ -1986,14 +1981,14 @@ "start, goal = (0, 0), (3, 3)\n", "\n", "def to_goal(state: tuple) -> int: \n", - " \"City block distance between state's position and goal.\"\n", + " \"\"\"City block distance between state's position and goal.\"\"\"\n", " pos, path = state\n", " return cityblock_distance(pos, goal)\n", "\n", "directions = [(0, 'U', (0, -1)), (1, 'D', (0, 1)), (2, 'L', (-1, 0)), (3, 'R', (1, 0))]\n", "\n", "def moves(state: tuple) -> Iterable[tuple]:\n", - " \"All states reachable from this state.\"\n", + " \"\"\"All states reachable from this state.\"\"\"\n", " (x, y), path = state\n", " hashx = hashlib.md5(bytes(passcode + path, 'utf-8')).hexdigest()\n", " for (i, p, (dx, dy)) in directions:\n", @@ -2025,7 +2020,7 @@ { "data": { "text/plain": [ - "Puzzle 17.2: 44.1 milliseconds, correct answer: 706 " + "Puzzle 17.2: 44.3 milliseconds, correct answer: 706 " ] }, "execution_count": 38, @@ -2035,7 +2030,7 @@ ], "source": [ "def longest_search(state, goal, moves) -> int:\n", - " \"Find the length of the longest path to goal by depth-first search.\"\n", + " \"\"\"Find the length of the longest path to goal by depth-first search.\"\"\"\n", " longest = 0\n", " frontier = [state]\n", " while frontier:\n", @@ -2072,7 +2067,7 @@ { "data": { "text/plain": [ - "Puzzle 18.1: 0.6 milliseconds, correct answer: 1987 " + "Puzzle 18.1: 0.5 milliseconds, correct answer: 1987 " ] }, "execution_count": 39, @@ -2084,7 +2079,7 @@ "safe, trap = '.', '^' \n", "\n", "def rows(n: int, initial_row: str) -> list[str]:\n", - " \"The first n rows of tiles (given the initial row).\"\n", + " \"\"\"The first n rows of tiles (given the initial row).\"\"\"\n", " result = [initial_row]\n", " for i in range(n-1):\n", " previous = safe + result[-1] + safe\n", @@ -2157,7 +2152,7 @@ { "data": { "text/plain": [ - "Puzzle 18.2: 2,000.0 milliseconds, correct answer: 19984714 " + "Puzzle 18.2: 2,027.1 milliseconds, correct answer: 19984714 " ] }, "execution_count": 41, @@ -2322,7 +2317,7 @@ { "data": { "text/plain": [ - "Puzzle 19.2: 313.6 milliseconds, correct answer: 1410630 " + "Puzzle 19.2: 318.2 milliseconds, correct answer: 1410630 " ] }, "execution_count": 44, @@ -2334,7 +2329,7 @@ "def Elves(N: int) -> list: return list(range(1, N + 1))\n", "\n", "def one_round(elves: list[int]) -> list[int]:\n", - " \"The first third of elves eliminate ones across the circle from them; who is left?\"\n", + " \"\"\"The first third of elves eliminate ones across the circle from them; who is left?\"\"\"\n", " N = len(elves)\n", " eliminated = 0\n", " for i in range(int(math.ceil(N / 3))):\n", @@ -2365,7 +2360,9 @@ "source": [ "# [Day 20](http://adventofcode.com/2016/day/20) Firewall Rules\n", "\n", - "We are given a list of blocked IP addresses, in the form `\"2365712272-2390766206\"`, indicating the low and high numbers that are blocked by the firewall. I will parse the numbers into `(low, high)` pairs, and sort them by the low number first (and peek at the first 5 to see if I got it right): " + "We are given a list of blocked IP addresses, in the form `\"2365712272-2390766206\"`, indicating the low and high numbers that are blocked by the firewall. So I'll parse each line into a pair of ints.\n", + "\n", + "We are asked what is the lowest non-negative integer that is not blocked. I will generate all the unblocked numbers, and just ask for the first one. (Why do it that way? Because it feels like `unblocked` is the fundamental issue of the problem, and we already have a function to compute `first`; there's no need for a `first_unblocked` function that conflates two ideas.) To find unblocked numbers, start a counter, `i` at zero, and increment it past the high value of each range, after yielding any numbers from `i` to the low value of the range:" ] }, { @@ -2381,11 +2378,7 @@ { "data": { "text/plain": [ - "[[0, 570348],\n", - " [263908, 753114],\n", - " [753115, 1178173],\n", - " [840301, 1235975],\n", - " [1235976, 1247936]]" + "Puzzle 20.1: 1.2 milliseconds, correct answer: 32259706 " ] }, "execution_count": 45, @@ -2394,16 +2387,22 @@ } ], "source": [ - "pairs = sorted(map(ints, Input(20)))\n", - "\n", - "pairs[:5]" + "def unblocked(pairs: list[tuple[int, int]]) -> Iterable[int]:\n", + " \"\"\"Yield the unblocked integers, given the sorted low-high pairs of blocked number ranges.\"\"\"\n", + " i = 0\n", + " for (low, high) in pairs:\n", + " yield from range(i, low)\n", + " i = max(i, high + 1)\n", + " \n", + "answer(20.1, 32259706, lambda:\n", + " first(unblocked(sorted(map(ints, Input(20))))))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We are asked what is the lowest non-negative integer that is not blocked. I will generate all the unblocked numbers, and just ask for the first one. (Why do it that way? Because it feels like `unblocked` is the fundamental issue of the problem, and we already have a function to compute `first`; there's no need for a `first_unblocked` function that conflates two ideas.) To find unblocked numbers, start a counter, `i` at zero, and increment it past the high value of each range, after yielding any numbers from `i` to the low value of the range:" + "In **part two** we are asked how many numbers are unblocked:" ] }, { @@ -2419,7 +2418,7 @@ { "data": { "text/plain": [ - "Puzzle 20.1: 0.0 milliseconds, correct answer: 32259706 " + "Puzzle 20.2: 0.9 milliseconds, correct answer: 113 " ] }, "execution_count": 46, @@ -2427,49 +2426,9 @@ "output_type": "execute_result" } ], - "source": [ - "def unblocked(pairs: list[tuple[int, int]]) -> Iterable[int]:\n", - " \"Find the lowest unblocked integer, given the sorted pairs of blocked numbers.\"\n", - " i = 0\n", - " for (low, high) in pairs:\n", - " yield from range(i, low)\n", - " i = max(i, high + 1)\n", - " \n", - "answer(20.1, 32259706, lambda:\n", - " first(unblocked(pairs)))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In **part two** we are asked how many numbers are unblocked:" - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "Puzzle 20.2: 0.1 milliseconds, correct answer: 113 " - ] - }, - "execution_count": 47, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "answer(20.2, 113, lambda:\n", - " len(list(unblocked(pairs))))" + " len(list(unblocked(sorted(map(ints, Input(20)))))))" ] }, { @@ -2491,7 +2450,7 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 47, "metadata": { "collapsed": false, "jupyter": { @@ -2505,14 +2464,14 @@ "Puzzle 21.1: 0.3 milliseconds, correct answer: gfdhebac " ] }, - "execution_count": 48, + "execution_count": 47, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def scramble(password: str, instructions: Iterable[str], verbose=False) -> str:\n", - " \"Scramble the password according to the instructions.\"\n", + " \"\"\"Scramble the password according to the instructions.\"\"\"\n", " pw = list(password) \n", " def rot(N): pw[:] = pw[-N:] + pw[:-N]\n", " def swap(A, B): pw[A], pw[B] = pw[B], pw[A] \n", @@ -2530,7 +2489,7 @@ " i = pw.index(words[6])\n", " rot((i + 1 + (i >= 4)) % len(pw))\n", " if verbose: \n", - " print(line + ': ' + cat(pw))\n", + " print(f'{line:37} \"{cat(pw)}\"')\n", " return cat(pw)\n", "\n", "answer(21.1, 'gfdhebac', lambda:\n", @@ -2546,7 +2505,7 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 48, "metadata": { "collapsed": false, "jupyter": { @@ -2558,14 +2517,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "swap position 4 with position 0: ebcda\n", - "swap letter d with letter b: edcba\n", - "reverse positions 0 through 4: abcde\n", - "rotate left 1 step: bcdea\n", - "move position 1 to position 4: bdeac\n", - "move position 3 to position 0: abdec\n", - "rotate based on position of letter b: ecabd\n", - "rotate based on position of letter d: decab\n" + "swap position 4 with position 0 \"ebcda\"\n", + "swap letter d with letter b \"edcba\"\n", + "reverse positions 0 through 4 \"abcde\"\n", + "rotate left 1 step \"bcdea\"\n", + "move position 1 to position 4 \"bdeac\"\n", + "move position 3 to position 0 \"abdec\"\n", + "rotate based on position of letter b \"ecabd\"\n", + "rotate based on position of letter d \"decab\"\n" ] }, { @@ -2574,7 +2533,7 @@ "'decab'" ] }, - "execution_count": 49, + "execution_count": 48, "metadata": {}, "output_type": "execute_result" } @@ -2605,7 +2564,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 49, "metadata": { "collapsed": false, "jupyter": { @@ -2616,10 +2575,10 @@ { "data": { "text/plain": [ - "Puzzle 21.2: 2,381.0 milliseconds, correct answer: dhaegfbc " + "Puzzle 21.2: 2,384.4 milliseconds, correct answer: dhaegfbc " ] }, - "execution_count": 50, + "execution_count": 49, "metadata": {}, "output_type": "execute_result" } @@ -2658,7 +2617,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 50, "metadata": { "collapsed": false, "jupyter": { @@ -2669,10 +2628,10 @@ { "data": { "text/plain": [ - "Puzzle 22.1: 128.9 milliseconds, correct answer: 1045 " + "Puzzle 22.1: 129.8 milliseconds, correct answer: 1045 " ] }, - "execution_count": 51, + "execution_count": 50, "metadata": {}, "output_type": "execute_result" } @@ -2697,7 +2656,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 51, "metadata": { "collapsed": false, "jupyter": { @@ -2711,7 +2670,7 @@ "(Node(x=35, y=27, size=89, used=0, avail=89, pct=0), 35)" ] }, - "execution_count": 52, + "execution_count": 51, "metadata": {}, "output_type": "execute_result" } @@ -2732,7 +2691,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 52, "metadata": {}, "outputs": [], "source": [ @@ -2749,7 +2708,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 53, "metadata": { "collapsed": false, "jupyter": { @@ -2760,10 +2719,10 @@ { "data": { "text/plain": [ - "Puzzle 22.2: 1,208.4 milliseconds, correct answer: 265 " + "Puzzle 22.2: 1,212.4 milliseconds, correct answer: 265 " ] }, - "execution_count": 54, + "execution_count": 53, "metadata": {}, "output_type": "execute_result" } @@ -2808,7 +2767,7 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 54, "metadata": { "collapsed": false, "jupyter": { @@ -2819,17 +2778,17 @@ { "data": { "text/plain": [ - "Puzzle 23.1: 7.2 milliseconds, correct answer: 10661 " + "Puzzle 23.1: 7.3 milliseconds, correct answer: 10661 " ] }, - "execution_count": 55, + "execution_count": 54, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def interpret(code, **regs) -> dict:\n", - " \"Execute instructions until pc goes off the end.\"\n", + " \"\"\"Execute instructions until pc goes off the end.\"\"\"\n", " def val(x): return (regs[x] if x in regs else x)\n", " pc = 0\n", " while 0 <= pc < len(code):\n", @@ -2844,7 +2803,7 @@ " return regs\n", "\n", "def toggle(code, i):\n", - " \"Toggle the instruction at location i.\"\n", + " \"\"\"Toggle the instruction at location i.\"\"\"\n", " if 0 <= i < len(code): \n", " inst = code[i]\n", " inst[0] = ('dec' if inst[0] == 'inc' else \n", @@ -2853,7 +2812,7 @@ " 'jnz')\n", "\n", "def parse_program(lines): \n", - " \"Split lines into words, and convert to int where appropriate.\"\n", + " \"\"\"Split lines into words, and convert to int where appropriate.\"\"\"\n", " return [[(x if x.isalpha() else int(x)) for x in line.split()]\n", " for line in lines]\n", "\n", @@ -2876,7 +2835,7 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 55, "metadata": {}, "outputs": [ { @@ -2910,7 +2869,7 @@ " 25: 'jnz c -5\\n'}" ] }, - "execution_count": 56, + "execution_count": 55, "metadata": {}, "output_type": "execute_result" } @@ -2928,7 +2887,7 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": 56, "metadata": {}, "outputs": [ { @@ -2962,14 +2921,14 @@ " (25, 77)]" ] }, - "execution_count": 57, + "execution_count": 56, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def profile(code, **regs) -> dict:\n", - " \"Execute instructions until pc goes off the end, yielding the pc each step.\"\n", + " \"\"\"Execute instructions until pc goes off the end, yielding the pc each step.\"\"\"\n", " def val(x): return (regs[x] if x in regs else x)\n", " pc = 0\n", " while 0 <= pc < len(code):\n", @@ -3005,12 +2964,12 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": 57, "metadata": {}, "outputs": [], "source": [ "def interpret(code, **regs) -> dict:\n", - " \"Execute instructions until pc goes off the end.\"\n", + " \"\"\"Execute instructions until pc goes off the end.\"\"\"\n", " def val(x): return (regs[x] if x in regs else x)\n", " pc = 0\n", " while 0 <= pc < len(code):\n", @@ -3040,16 +2999,16 @@ }, { "cell_type": "code", - "execution_count": 59, + "execution_count": 58, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Puzzle 23.2: 2.6 milliseconds, correct answer: 479007221 " + "Puzzle 23.2: 2.8 milliseconds, correct answer: 479007221 " ] }, - "execution_count": 59, + "execution_count": 58, "metadata": {}, "output_type": "execute_result" } @@ -3090,7 +3049,7 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 59, "metadata": { "collapsed": false, "jupyter": { @@ -3113,7 +3072,7 @@ " '6': 1})" ] }, - "execution_count": 60, + "execution_count": 59, "metadata": {}, "output_type": "execute_result" } @@ -3132,7 +3091,7 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 60, "metadata": { "collapsed": false, "jupyter": { @@ -3146,7 +3105,7 @@ "(1, 19)" ] }, - "execution_count": 61, + "execution_count": 60, "metadata": {}, "output_type": "execute_result" } @@ -3165,7 +3124,7 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 61, "metadata": { "collapsed": false, "jupyter": { @@ -3176,27 +3135,27 @@ { "data": { "text/plain": [ - "Puzzle 24.1: 272.9 milliseconds, correct answer: 430 " + "Puzzle 24.1: 268.3 milliseconds, correct answer: 430 " ] }, - "execution_count": 62, + "execution_count": 61, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def h(state):\n", - " \"Heuristic: the number of digits not yet visited.\"\n", + " \"\"\"Heuristic: the number of digits not yet visited.\"\"\"\n", " _, visited = state\n", " return 8 - len(visited) # Note: 8 == len('01234567')\n", " \n", "def moves(state):\n", - " \"Move to any neighboring square that is not a wall. Track the digits visited.\"\n", + " \"\"\"Move to any neighboring square that is not a wall. Track the digits visited.\"\"\"\n", " pos, visited = state\n", " for x1, y1 in neighbors4(pos):\n", " c = maze[y1][x1]\n", " if c != '#':\n", - " visited1 = (visited if c in visited or c == '.' else cat(sorted(visited + c)))\n", + " visited1 = (visited if (c == '.' or c in visited) else cat(sorted(visited + c)))\n", " yield (x1, y1), visited1\n", "\n", "answer(24.1, 430, lambda:\n", @@ -3212,7 +3171,7 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 62, "metadata": { "collapsed": false, "jupyter": { @@ -3223,17 +3182,17 @@ { "data": { "text/plain": [ - "Puzzle 24.2: 987.9 milliseconds, correct answer: 700 " + "Puzzle 24.2: 984.7 milliseconds, correct answer: 700 " ] }, - "execution_count": 63, + "execution_count": 62, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def h2(state):\n", - " \"Heuristic: the number of digits not yet visited, plus the distance back to start.\"\n", + " \"\"\"Heuristic: the number of digits not yet visited, plus the distance back to start.\"\"\"\n", " pos, visited = state\n", " return 8 - len(visited) + cityblock_distance(pos, zero)\n", "\n", @@ -3254,12 +3213,12 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 63, "metadata": {}, "outputs": [], "source": [ "def interpret(code, steps, **regs):\n", - " \"Execute instructions until pc goes off the end, or until we execute the given number of steps.\"\n", + " \"\"\"Execute instructions until pc goes off the end, or until we execute the given number of steps.\"\"\"\n", " def val(x): return (regs[x] if x in regs else x)\n", " pc = 0\n", " for _ in range(steps):\n", @@ -3285,7 +3244,7 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 64, "metadata": { "collapsed": false, "jupyter": { @@ -3296,24 +3255,24 @@ { "data": { "text/plain": [ - "Puzzle 25.1: 910.4 milliseconds, correct answer: 189 " + "Puzzle 25.1: 904.6 milliseconds, correct answer: 189 " ] }, - "execution_count": 65, + "execution_count": 64, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def clock_signal(lines) -> int:\n", + " \"\"\"Find the first value of register a that causes program to output 0, 1, 0, 1, ...\"\"\"\n", " code = parse_program(lines)\n", " return first(a for a in range(1, BIG) if repeats(a, code))\n", "\n", "def repeats(a, code, steps=10**6, minsignals=100) -> bool:\n", - " \"Does this value for register a cause code to repeat `out` signals of 0, 1, 0, 1, ...?\"\n", + " \"\"\"Does this value for register a cause code to repeat `out` signals of 0, 1, 0, 1, ...?\"\"\"\n", " signals = interpret(code, steps, a=a, b=0, c=0, d=0)\n", - " expecteds = cycle((0, 1))\n", - " for (i, (signal, expected)) in enumerate(zip(signals, expecteds)):\n", + " for (i, (signal, expected)) in enumerate(zip(signals, cycle((0, 1)))):\n", " if signal != expected:\n", " return False\n", " # We'll say \"yes\" if the code outputs at least a minimum number of 0, 1, ... signals, and nothing else.\n", @@ -3329,69 +3288,69 @@ "source": [ "# Summary\n", "\n", - "I got all the puzzles solved, but some of my code ran slowly. Originally 11.2 and 23.2 took over 10 minutes between them, but I went back and optimized them and now they're under 1/10 of a second. There are 8 puzzles that still take over a second of run time. But the median is just 2.6 milliseconds." + "I got all the puzzles solved, but some of my code ran slowly. Originally 11.2 and 23.2 took over 10 minutes, but I went back and optimized them and now they're under 1/10 of a second. There are 8 puzzles that still take over a second of run time. But the mean is under a second and the median is just 2.8 milliseconds." ] }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 65, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Time in milliseconds: sum = 47,651.4, mean = 972.5, median = 2.6, max = 14,487.3\n", + "Time in milliseconds: sum = 48,367.1, mean = 987.1, median = 2.8, max = 14,443.9\n", "\n", "Puzzle 1.1: 0.4 milliseconds, correct answer: 262 \n", "Puzzle 1.2: 0.3 milliseconds, correct answer: 131 \n", "Puzzle 2.1: 0.5 milliseconds, correct answer: 14894 \n", - "Puzzle 2.2: 0.4 milliseconds, correct answer: 26B96 \n", + "Puzzle 2.2: 0.6 milliseconds, correct answer: 26B96 \n", "Puzzle 3.1: 0.2 milliseconds, correct answer: 869 \n", - "Puzzle 3.2: 0.4 milliseconds, correct answer: 1544 \n", - "Puzzle 4.1: 4.1 milliseconds, correct answer: 409147 \n", - "Puzzle 4.2: 1.7 milliseconds, correct answer: 991 \n", - "Puzzle 5.1: 7,316.0 milliseconds, correct answer: 1a3099aa \n", - "Puzzle 5.2: 11,520.3 milliseconds, correct answer: 694190cd \n", - "Puzzle 6.1: 0.5 milliseconds, correct answer: dzqckwsd \n", + "Puzzle 3.2: 0.3 milliseconds, correct answer: 1544 \n", + "Puzzle 4.1: 4.0 milliseconds, correct answer: 409147 \n", + "Puzzle 4.2: 1.6 milliseconds, correct answer: 991 \n", + "Puzzle 5.1: 7,596.7 milliseconds, correct answer: 1a3099aa \n", + "Puzzle 5.2: 11,989.2 milliseconds, correct answer: 694190cd \n", + "Puzzle 6.1: 0.6 milliseconds, correct answer: dzqckwsd \n", "Puzzle 6.2: 0.4 milliseconds, correct answer: lragovly \n", - "Puzzle 7.1: 12.4 milliseconds, correct answer: 118 \n", - "Puzzle 7.2: 119.6 milliseconds, correct answer: 260 \n", - "Puzzle 8.1: 0.7 milliseconds, correct answer: 110 \n", - "Puzzle 8.2: 0.6 milliseconds, correct answer: ZJHRKCPLYJ \n", - "Puzzle 9.1: 0.3 milliseconds, correct answer: 115118 \n", - "Puzzle 9.2: 1.7 milliseconds, correct answer: 11107527530 \n", + "Puzzle 7.1: 12.3 milliseconds, correct answer: 118 \n", + "Puzzle 7.2: 119.5 milliseconds, correct answer: 260 \n", + "Puzzle 8.1: 0.6 milliseconds, correct answer: 110 \n", + "Puzzle 8.2: 0.7 milliseconds, correct answer: ZJHRKCPLYJ \n", + "Puzzle 9.1: 0.6 milliseconds, correct answer: 115118 \n", + "Puzzle 9.2: 2.0 milliseconds, correct answer: 11107527530 \n", "Puzzle 10.1: 0.6 milliseconds, correct answer: 56 \n", - "Puzzle 10.2: 0.8 milliseconds, correct answer: 7847 \n", - "Puzzle 11.1: 52.7 milliseconds, correct answer: 31 \n", - "Puzzle 11.2: 311.7 milliseconds, correct answer: 55 \n", - "Puzzle 12.1: 109.3 milliseconds, correct answer: 318007 \n", - "Puzzle 12.2: 3,142.9 milliseconds, correct answer: 9227661 \n", + "Puzzle 10.2: 0.6 milliseconds, correct answer: 7847 \n", + "Puzzle 11.1: 58.0 milliseconds, correct answer: 31 \n", + "Puzzle 11.2: 325.9 milliseconds, correct answer: 55 \n", + "Puzzle 12.1: 108.1 milliseconds, correct answer: 318007 \n", + "Puzzle 12.2: 3,036.3 milliseconds, correct answer: 9227661 \n", "Puzzle 13.1: 0.3 milliseconds, correct answer: 82 \n", "Puzzle 13.2: 0.2 milliseconds, correct answer: 138 \n", - "Puzzle 14.1: 125.8 milliseconds, correct answer: 16106 \n", - "Puzzle 14.2: 14,487.3 milliseconds, correct answer: 22423 \n", - "Puzzle 15.1: 49.0 milliseconds, correct answer: 203660 \n", - "Puzzle 15.2: 583.3 milliseconds, correct answer: 2408135 \n", + "Puzzle 14.1: 127.3 milliseconds, correct answer: 16106 \n", + "Puzzle 14.2: 14,443.9 milliseconds, correct answer: 22423 \n", + "Puzzle 15.1: 53.2 milliseconds, correct answer: 203660 \n", + "Puzzle 15.2: 634.8 milliseconds, correct answer: 2408135 \n", "Puzzle 16.1: 0.0 milliseconds, correct answer: 01110011101111011\n", - "Puzzle 16.2: 1,549.0 milliseconds, correct answer: 11001111011000111\n", + "Puzzle 16.2: 1,560.4 milliseconds, correct answer: 11001111011000111\n", "Puzzle 17.1: 0.0 milliseconds, correct answer: RRRLDRDUDD \n", - "Puzzle 17.2: 44.1 milliseconds, correct answer: 706 \n", - "Puzzle 18.1: 0.6 milliseconds, correct answer: 1987 \n", - "Puzzle 18.2: 2,000.0 milliseconds, correct answer: 19984714 \n", + "Puzzle 17.2: 44.3 milliseconds, correct answer: 706 \n", + "Puzzle 18.1: 0.5 milliseconds, correct answer: 1987 \n", + "Puzzle 18.2: 2,027.1 milliseconds, correct answer: 19984714 \n", "Puzzle 19.1: 0.0 milliseconds, correct answer: 1815603 \n", - "Puzzle 19.2: 313.6 milliseconds, correct answer: 1410630 \n", - "Puzzle 20.1: 0.0 milliseconds, correct answer: 32259706 \n", - "Puzzle 20.2: 0.1 milliseconds, correct answer: 113 \n", + "Puzzle 19.2: 318.2 milliseconds, correct answer: 1410630 \n", + "Puzzle 20.1: 1.2 milliseconds, correct answer: 32259706 \n", + "Puzzle 20.2: 0.9 milliseconds, correct answer: 113 \n", "Puzzle 21.1: 0.3 milliseconds, correct answer: gfdhebac \n", - "Puzzle 21.2: 2,381.0 milliseconds, correct answer: dhaegfbc \n", - "Puzzle 22.1: 128.9 milliseconds, correct answer: 1045 \n", - "Puzzle 22.2: 1,208.4 milliseconds, correct answer: 265 \n", - "Puzzle 23.1: 7.2 milliseconds, correct answer: 10661 \n", - "Puzzle 23.2: 2.6 milliseconds, correct answer: 479007221 \n", - "Puzzle 24.1: 272.9 milliseconds, correct answer: 430 \n", - "Puzzle 24.2: 987.9 milliseconds, correct answer: 700 \n", - "Puzzle 25.1: 910.4 milliseconds, correct answer: 189 \n" + "Puzzle 21.2: 2,384.4 milliseconds, correct answer: dhaegfbc \n", + "Puzzle 22.1: 129.8 milliseconds, correct answer: 1045 \n", + "Puzzle 22.2: 1,212.4 milliseconds, correct answer: 265 \n", + "Puzzle 23.1: 7.3 milliseconds, correct answer: 10661 \n", + "Puzzle 23.2: 2.8 milliseconds, correct answer: 479007221 \n", + "Puzzle 24.1: 268.3 milliseconds, correct answer: 430 \n", + "Puzzle 24.2: 984.7 milliseconds, correct answer: 700 \n", + "Puzzle 25.1: 904.6 milliseconds, correct answer: 189 \n" ] } ],