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"
]
}
],