From 956352635f480bd7544879916606b8cae6088829 Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Sat, 27 Dec 2025 17:45:15 -0800 Subject: [PATCH] Add files via upload --- ipynb/Advent-2016.ipynb | 1902 ++++++++++++++++++++++----------------- 1 file changed, 1071 insertions(+), 831 deletions(-) diff --git a/ipynb/Advent-2016.ipynb b/ipynb/Advent-2016.ipynb index a7d67cf..f730a82 100644 --- a/ipynb/Advent-2016.ipynb +++ b/ipynb/Advent-2016.ipynb @@ -8,8 +8,8 @@ "\n", "## Peter Norvig\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 really competitive with the fastest solvers.\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", @@ -19,9 +19,8 @@ "On November 30th, I spent some time preparing: \n", "\n", "- I'll import my favorite modules and functions, so I don't have to do it each day.\n", - "\n", - "- From looking at [last year's](http://adventofcode.com/2015) puzzles, I knew that there would be a data file on many days, so I defined the function `Input` to open the file (and for those using this notebook on a remote machine, to fetch the file from the web). My data files are at [http://norvig.com/ipython/advent2016/](http://norvig.com/ipython/advent2016/).\n", - "\n", + "- From looking at [last year's](http://adventofcode.com/2015) puzzles, I knew that there would be a data file on many days, so I defined the function `Input` to open the file.\n", + "- I wanted a function `answer` to record the correct answer and its run time.\n", "- From working on another puzzle site, [Project Euler](https://projecteuler.net/), I had built up a collection of utility functions, shown below:" ] }, @@ -29,57 +28,93 @@ "cell_type": "code", "execution_count": 1, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ - "# Python 3.x\n", "import re\n", "import numpy as np\n", "import math\n", - "import urllib.request\n", + "import time\n", "\n", "from collections import Counter, defaultdict, namedtuple, deque\n", - "from functools import lru_cache\n", + "from functools import lru_cache, cache\n", "from itertools import permutations, combinations, chain, cycle, product, islice\n", "from heapq import heappop, heappush\n", + "from statistics import mean, median\n", + "from typing import Iterable\n", "\n", - "def Input(day):\n", - " \"Open this day's input file.\"\n", - " filename = 'advent2016/input{}.txt'.format(day)\n", - " try:\n", - " return open(filename)\n", - " except FileNotFoundError:\n", - " return urllib.request.urlopen(\"http://norvig.com/ipython/\" + filename)\n", + "def Input(day: int) -> Iterable[str]:\n", + " \"\"\"Open this day's input file.\"\"\"\n", + " return open(f'AOC/2016/input{day}.txt')\n", "\n", - "def transpose(matrix): return zip(*matrix)\n", + "answers = {} # `answers` is a dict of {puzzle_number: answer}\n", "\n", - "def first(iterable): return next(iter(iterable))\n", + "def ints(text: str) -> list[int]: \n", + " \"All the integers anywhere in text.\"\n", + " return [int(x) for x in re.findall(r'\\d+', text)]\n", "\n", - "def nth(iterable, n, default=None):\n", - " \"Returns the nth item of iterable, or a default value\"\n", + "class answer:\n", + " \"\"\"Verify that calling `code` computes the `solution` to `puzzle`. \n", + " Record results in the dict `answers`.\"\"\"\n", + " def __init__(self, puzzle: float, solution, code):\n", + " self.puzzle, self.solution, self.code = puzzle, solution, code\n", + " start = time.time()\n", + " self.got = self.code()\n", + " self.msecs = (time.time() - start) * 1000.\n", + " self.ok = (self.got == self.solution)\n", + " answers[puzzle] = self\n", + " \n", + " def __repr__(self) -> str:\n", + " \"\"\"The repr of an answer shows what happened.\"\"\"\n", + " correct = 'correct' if self.ok else 'WRONG!!'\n", + " expected = '' if self.ok else f'; EXPECTED: {self.solution}'\n", + " return f'Puzzle {self.puzzle:4.1f}: {time_str(self.msecs)}, {correct} answer: {self.got:<15}{expected}'\n", + "\n", + "def time_str(msecs: float) -> str:\n", + " \"\"\"Turn a number of milliseconds into a printable string.\"\"\"\n", + " return f'{msecs:8,.1f} msecs' if msecs < 60_000 else f'{msecs/60_000:6,.1f} MINUTES'\n", + "\n", + "def summary(answers: dict):\n", + " \"\"\"Summary report on the answers.\"\"\"\n", + " times = [answer.msecs for answer in answers.values()]\n", + " def stat(fn, times): return f'{fn.__name__} = {time_str(fn(times)).strip()}' \n", + " stats = [stat(fn, times) for fn in (sum, mean, median, max)]\n", + " print(f'Time: {\", \".join(stats)}\\n')\n", + " for day in sorted(answers):\n", + " print(answers[day])\n", + "\n", + "def transpose(matrix): \n", + " \"\"\"Transpose of a list of lists.\"\"\"\n", + " return zip(*matrix)\n", + "\n", + "def first(iterable) -> object: \n", + " \"\"\"The first element of an iterable, or None.\"\"\"\n", + " return next(iter(iterable), None)\n", + "\n", + "def nth(iterable, n, default=None) -> object:\n", + " \"The nth item of iterable, or a default value\"\n", " return next(islice(iterable, n, None), default)\n", "\n", + "flatten = chain.from_iterable\n", + "\n", "cat = ''.join\n", "\n", "Ø = frozenset() # Empty set\n", "inf = float('inf')\n", "BIG = 10 ** 999\n", "\n", - "def grep(pattern, lines):\n", - " \"Print lines that match pattern.\"\n", - " for line in lines:\n", - " if re.search(pattern, line):\n", - " print(line)\n", - "\n", - "def groupby(iterable, key=lambda it: it):\n", + "def groupby(iterable, key=lambda it: it) -> dict:\n", " \"Return a dic whose keys are key(it) and whose values are all the elements of iterable with that key.\"\n", " dic = defaultdict(list)\n", " for it in iterable:\n", " dic[key(it)].append(it)\n", " return dic\n", "\n", - "def powerset(iterable):\n", + "def powerset(iterable) -> Iterable[tuple]:\n", " \"Yield all subsets of items.\"\n", " items = list(iterable)\n", " for r in range(len(items)+1):\n", @@ -87,15 +122,15 @@ " yield c\n", "\n", "# 2-D points implemented using (x, y) tuples\n", - "def X(point): return point[0]\n", - "def Y(point): return point[1]\n", + "def X(point) -> int: return point[0]\n", + "def Y(point) -> int: return point[1]\n", "\n", - "def neighbors4(point): \n", + "def neighbors4(point) -> tuple: \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): \n", + "def neighbors8(point) -> tuple: \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", @@ -105,12 +140,8 @@ " \"City block distance between two points.\"\n", " return abs(X(p) - X(q)) + abs(Y(p) - Y(q))\n", "\n", - "def euclidean_distance(p, q=(0, 0)): \n", - " \"Euclidean (hypotenuse) distance between two points.\"\n", - " return math.hypot(X(p) - X(q), Y(p) - Y(q))\n", - "\n", "def trace1(f):\n", - " \"Print a trace of the input and output of a function on one line.\"\n", + " \"Decorator to print a trace of the input and output of a function on one line.\"\n", " def traced_f(*args):\n", " result = f(*args)\n", " print('{}({}) = {}'.format(f.__name__, ', '.join(map(str, args)), result))\n", @@ -119,7 +150,7 @@ "\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", - " frontier = [(h_func(start), start)] # A priority queue, ordered by path length, f = g + h\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", " while frontier:\n", @@ -134,7 +165,7 @@ " previous[s2] = s\n", " return dict(fail=True, front=len(frontier), prev=len(previous))\n", " \n", - "def Path(previous, s): \n", + "def Path(previous, s) -> list: \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])" ] @@ -150,7 +181,10 @@ "cell_type": "code", "execution_count": 2, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -179,13 +213,16 @@ "cell_type": "code", "execution_count": 3, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "250.0" + "Puzzle 1.1: 0.5 msecs, correct answer: 262.0 " ] }, "execution_count": 3, @@ -197,31 +234,32 @@ "Point = complex \n", "N, S, E, W = 1j, -1j, 1, -1 # Unit vectors for headings\n", "\n", - "def distance(point): \n", + "def distance(point) -> float: \n", " \"City block distance between point and the origin.\"\n", " return abs(point.real) + abs(point.imag)\n", "\n", - "def how_far(moves):\n", + "def how_far(moves) -> float:\n", " \"After following moves, how far away from the origin do we end up?\"\n", " loc, heading = 0, N # Begin at origin, heading North\n", - " for (turn, dist) in parse(moves):\n", + " for (turn, dist) in parse_moves(moves):\n", " heading *= turn\n", " loc += heading * dist\n", " return distance(loc)\n", "\n", - "def parse(text):\n", + "def parse_moves(text: str) -> list[tuple]:\n", " \"Return a list of (turn, distance) pairs from text of form 'R2, L42, ...'\"\n", " turns = dict(L=N, R=S)\n", " return [(turns[RL], int(d))\n", " for (RL, d) in re.findall(r'(R|L)(\\d+)', text)]\n", "\n", "assert distance(Point(3, 4)) == 7 # City block distance; Euclidean distance would be 5\n", - "assert parse('R2, L42') == [(S, 2), (N, 42)]\n", + "assert parse_moves('R2, L42') == [(S, 2), (N, 42)]\n", "assert how_far(\"R2, L3\") == 5\n", "assert how_far(\"R2, R2, R2\") == 2\n", "assert how_far(\"R5, L5, R5, R3\") == 12\n", "\n", - "how_far(Input(1).read())" + "answer(1.1, 262, lambda:\n", + " how_far(Input(1).read()))" ] }, { @@ -235,13 +273,16 @@ "cell_type": "code", "execution_count": 4, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "151.0" + "Puzzle 1.2: 0.6 msecs, correct answer: 131.0 " ] }, "execution_count": 4, @@ -250,11 +291,11 @@ } ], "source": [ - "def visited_twice(text):\n", + "def visited_twice(text) -> float:\n", " \"Following moves in text, find the first location we visit twice, and return the distance to it.\"\n", " loc, heading = 0, N # Begin at origin, heading North\n", " visited = {loc}\n", - " for (turn, dist) in parse(text):\n", + " for (turn, dist) in parse_moves(text):\n", " heading *= turn\n", " for i in range(dist):\n", " loc += heading\n", @@ -266,7 +307,8 @@ "assert visited_twice(\"R8, R4, R4, L8\") == None\n", "assert visited_twice(\"R8, R0, R1\") == 7\n", "\n", - "visited_twice(Input(1).read())" + "answer(1.2, 131, lambda:\n", + " visited_twice(Input(1).read()))" ] }, { @@ -285,13 +327,16 @@ "cell_type": "code", "execution_count": 5, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "'97289'" + "Puzzle 2.1: 0.6 msecs, correct answer: 14894 " ] }, "execution_count": 5, @@ -300,7 +345,8 @@ } ], "source": [ - "Keypad = str.split\n", + "Keypad = str.split # Function to make a Keypad\n", + "Key = str # Type for a key: a single character\n", "\n", "keypad = Keypad(\"\"\"\n", ".....\n", @@ -314,7 +360,7 @@ "\n", "off = '.'\n", "\n", - "def decode(instructions, x=2, y=2):\n", + "def decode(instructions, x=2, y=2) -> Iterable[str]:\n", " \"\"\"Follow instructions, keeping track of x, y position, and\n", " yielding the key at the end of each line of instructions.\"\"\"\n", " for line in instructions:\n", @@ -322,7 +368,7 @@ " x, y = move(C, x, y)\n", " yield keypad[y][x]\n", "\n", - "def move(C, x, y):\n", + "def move(C, x, y) -> tuple:\n", " \"Make the move corresponding to this character (L/R/U/D)\"\n", " if C == 'L' and keypad[y][x-1] is not off: x -= 1\n", " elif C == 'R' and keypad[y][x+1] is not off: x += 1\n", @@ -334,7 +380,8 @@ "assert move('U', 2, 1) == (2, 1)\n", "assert cat(decode(\"ULL RRDDD LURDL UUUUD\".split())) == '1985'\n", "\n", - "cat(decode(Input(2)))" + "answer(2.1, '14894', lambda:\n", + " cat(decode(Input(2))))" ] }, { @@ -348,13 +395,16 @@ "cell_type": "code", "execution_count": 6, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "'9A7DC'" + "Puzzle 2.2: 0.5 msecs, correct answer: 26B96 " ] }, "execution_count": 6, @@ -375,7 +425,8 @@ "\n", "assert keypad[3][1] == '5'\n", "\n", - "cat(decode(Input(2), x=1, y=3))" + "answer(2.2, '26B96', lambda:\n", + " cat(decode(Input(2), x=1, y=3)))" ] }, { @@ -391,13 +442,16 @@ "cell_type": "code", "execution_count": 7, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "983" + "Puzzle 3.1: 0.2 msecs, correct answer: 869 " ] }, "execution_count": 7, @@ -406,18 +460,15 @@ } ], "source": [ - "def is_triangle(sides):\n", + "def is_triangle(sides) -> bool:\n", " \"Do these side lengths form a valid triangle?\"\n", " x, y, z = sorted(sides)\n", - " return z < x + y\n", + " return x + y > z\n", "\n", - "def parse_ints(text): \n", - " \"All the integers anywhere in text.\"\n", - " return [int(x) for x in re.findall(r'\\d+', text)]\n", + "triangles = [ints(line) for line in Input(3)]\n", "\n", - "triangles = [parse_ints(line) for line in Input(3)]\n", - "\n", - "sum(map(is_triangle, triangles))" + "answer(3.1, 869, lambda:\n", + " sum(map(is_triangle, triangles)))" ] }, { @@ -449,13 +500,16 @@ "cell_type": "code", "execution_count": 8, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "1836" + "Puzzle 3.2: 0.4 msecs, correct answer: 1544 " ] }, "execution_count": 8, @@ -469,7 +523,8 @@ " for i in range(0, len(triangles), 3):\n", " yield from transpose(triangles[i:i+3])\n", "\n", - "sum(map(is_triangle, invert(triangles)))" + "answer(3.2, 1544, lambda:\n", + " sum(map(is_triangle, invert(triangles))))" ] }, { @@ -485,13 +540,16 @@ "cell_type": "code", "execution_count": 9, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "185371" + "Puzzle 4.1: 4.0 msecs, correct answer: 409147 " ] }, "execution_count": 9, @@ -500,16 +558,16 @@ } ], "source": [ - "def parse(line): \n", + "def parse(line: str) -> tuple: \n", " \"Return (name, sector, checksum).\"\n", " return re.match(r\"(.+)-(\\d+)\\[([a-z]+)\\]\", line).groups()\n", "\n", - "def sector(line):\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 int(sector) if valid(name, checksum) else 0\n", "\n", - "def valid(name, checksum):\n", + "def valid(name: str, checksum: str) -> bool:\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", @@ -520,7 +578,8 @@ "assert sector('aaaaa-bbb-z-y-x-123[abxyz]') == 123\n", "assert valid('aaaaa-bbb-z-y-x', 'abxyz')\n", "\n", - "sum(map(sector, Input(4)))" + "answer(4.1, 409147, lambda:\n", + " sum(map(sector, Input(4))))" ] }, { @@ -536,24 +595,30 @@ "cell_type": "code", "execution_count": 10, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "northpole-object-storage 984\n" - ] + "data": { + "text/plain": [ + "Puzzle 4.2: 1.6 msecs, correct answer: 991 " + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "def decrypt(line):\n", + "def decrypt(line: str) -> str:\n", " \"Decrypt the line (shift the name by sector; discard checksum).\"\n", " name, sector, _ = parse(line)\n", " return shift(name, int(sector)) + ' ' + sector\n", "\n", - "def shift(text, N, alphabet='abcdefghijklmnopqrstuvwxyz'):\n", + "def shift(text, N, alphabet='abcdefghijklmnopqrstuvwxyz') -> str:\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", @@ -562,7 +627,8 @@ "assert shift('hal', 1) == 'ibm'\n", "assert shift('qzmt-zixmtkozy-ivhz', 343) == 'very-encrypted-name'\n", "\n", - "grep(\"north\", map(decrypt, Input(4)))" + "answer(4.2, 991, lambda: \n", + " ints(first(line for line in map(decrypt, Input(4)) if 'north' in line))[0])" ] }, { @@ -578,27 +644,30 @@ "cell_type": "code", "execution_count": 11, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "515840 00000c6c3f533fe4f7b0cb6d851185a8 c\n", - "844745 000006a94bb1c9322cbb56dd8564e76e c6\n", - "2968550 000006c8c9090315b0fb38154a947c86 c66\n", - "4034943 00000970faef6424564944d5e8a59618 c669\n", - "5108969 000007b2e0e83dfeade14ebe09f9e6a7 c6697\n", - "5257971 00000bc5fdee6506b09262247ceb63f0 c6697b\n", - "5830668 0000051079ac6b44fc3a5266a1630d42 c6697b5\n", - "5833677 00000537192966c3ee924306195faede c6697b55\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", + "14821988 00000a220003ca08164ab5fbe0b7c08f 1a3099a\n", + "16734551 00000aaa1e7e216d6fb95a53fde7a594 1a3099aa\n" ] }, { "data": { "text/plain": [ - "'c6697b55'" + "Puzzle 5.1: 7,717.6 msecs, correct answer: 1a3099aa " ] }, "execution_count": 11, @@ -609,20 +678,21 @@ "source": [ "import hashlib\n", "\n", - "door = \"ffykfhsq\"\n", + "door = \"uqwqemis\"\n", "\n", - "def find_password(door):\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", " 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", - " print(i, x, password) # Just to see something happen\n", + " if verbose: print(i, x, password)\n", " if len(password) == 8: \n", " return password\n", "\n", - "find_password(door)" + "answer(5.1, '1a3099aa', lambda:\n", + " find_password(door))" ] }, { @@ -636,29 +706,30 @@ "cell_type": "code", "execution_count": 12, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "844745 000006a94bb1c9322cbb56dd8564e76e ......a.\n", - "5108969 000007b2e0e83dfeade14ebe09f9e6a7 ......ab\n", - "5830668 0000051079ac6b44fc3a5266a1630d42 .....1ab\n", - "6497076 0000008239d1bbf480ea541e9da1e494 8....1ab\n", - "8962195 00000351ce68ffb449644d4bfa4cee5d 8..5.1ab\n", - "23867827 000001c3c28bcbacf0f543a33548ef24 8c.5.1ab\n", - "24090051 000004d57fc545f376c09f27383b2c88 8c.5d1ab\n", - "26383109 0000023d12c49f028699d4679ba91780 8c35d1ab\n", - "CPU times: user 30.4 s, sys: 48.7 ms, total: 30.4 s\n", - "Wall time: 30.4 s\n" + "4515059 00000191970e97b86ecd2220e76d86b2 .9......\n", + "8038154 00000312234ca27718d52476a44c257c .9.1....\n", + "13432968 00000064ec7123bedfc9ff00cc4f55f2 69.1....\n", + "17743256 000002457920bc00c2bd4d769a3da01c 6941....\n", + "19112977 000005074f875107f82b4ffb39a1fbf0 6941.0..\n", + "20616595 0000049d19713e17d7d93e9b1f02c856 694190..\n", + "21658552 000006c0b6e2bfeabd18eb400b3aecf7 694190c.\n", + "26326685 000007d44ea65d0437b810035fec92f2 694190cd\n" ] }, { "data": { "text/plain": [ - "'8c35d1ab'" + "Puzzle 5.2: 11,693.9 msecs, correct answer: 694190cd " ] }, "execution_count": 12, @@ -667,7 +738,7 @@ } ], "source": [ - "def find_tougher_password(door):\n", + "def find_tougher_password(door, verbose=True):\n", " \"For md5 hashes that begin with '00000', the seventh digit goes in the sixth-digit slot of the password.\"\n", " password = [off] * 8\n", " for i in range(BIG):\n", @@ -676,11 +747,12 @@ " index = int(x[5], 16)\n", " if index < 8 and password[index] is off:\n", " password[index] = x[6]\n", - " print(i, x, cat(password)) # Just to see something happen\n", + " if verbose: print(i, x, cat(password)) # Just to see something happen\n", " if off not in password:\n", " return cat(password)\n", "\n", - "%time find_tougher_password(door)" + "answer(5.2, '694190cd', lambda:\n", + " find_tougher_password(door))" ] }, { @@ -689,20 +761,23 @@ "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 `Input(6).read().split()` instead of just `Input(6)` 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.)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "'tsreykjj'" + "Puzzle 6.1: 0.5 msecs, correct answer: dzqckwsd " ] }, "execution_count": 13, @@ -711,28 +786,37 @@ } ], "source": [ - "counts = [Counter(col) for col in transpose(Input(6).read().split())]\n", - "cat(c.most_common(1)[0][0] for c in counts)" + "def column_winners(stream) -> str:\n", + " \"\"\"Most common letter in each column..\"\"\"\n", + " rows = stream.read().split()\n", + " counts = [Counter(col) for col in transpose(rows)]\n", + " return cat(c.most_common(1)[0][0] for c in counts)\n", + "\n", + "answer(6.1,'dzqckwsd', lambda:\n", + " column_winners(Input(6)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Just to make it clear, here's how we ask for the most common character (and its count) in the first column:" + "In **part two**, we ask for the *least* common character in each column. Just change a \"0\" to a \"-1\":" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "[('t', 24)]" + "Puzzle 6.2: 0.4 msecs, correct answer: lragovly " ] }, "execution_count": 14, @@ -741,66 +825,14 @@ } ], "source": [ - "c = counts[0]\n", - "c.most_common(1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And here is how we pick out the `'t'` character:" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "'t'" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "c.most_common(1)[0][0]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In **part two**, we ask for the *least* common character in each column. Easy-peasy:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "'hnfbujie'" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "cat(c.most_common()[-1][0] for c in counts)" + "def column_losers(stream) -> str:\n", + " \"\"\"Least common letter in each column..\"\"\"\n", + " rows = stream.read().split()\n", + " counts = [Counter(col) for col in transpose(rows)]\n", + " return cat(c.most_common()[-1][0] for c in counts)\n", + " \n", + "answer(6.2, 'lragovly', lambda:\n", + " column_losers(Input(6)))" ] }, { @@ -809,7 +841,7 @@ "source": [ "# [Day 7](http://adventofcode.com/2016/day/7): Internet Protocol Version 7\n", "\n", - "Given input lines of the form `'abcd[1234]fghi[56789]zz[0]z'`, count the number of lines that are a *TLS*, meaning they have an *ABBA* outside of square brackets, but no *ABBA* inside brackets. An *ABBA* is a 4-character subsequence where the first two letters are the same as the last two, but not all four are the same.\n", + "Given input lines of the form `'abcd[1234]fghi[56789]zz[0]z'`, count the number of lines that are a *TLS*, meaning they have an *ABBA* outside of square brackets, but no *ABBA* inside brackets. An *ABBA* is a 4-character subsequence where the first and last letters are the same, as are the middle two, but not all four are the same.\n", "\n", "I assume brackets are in proper pairs, and are never nested. Then if I do a `re.split` on brackets, the even-indexed pieces of the split will be outside the brackets, and the odd-indexed will be inside. For example:\n", "- Given the line `'abcd[1234]fghi[56789]zz'`\n", @@ -820,18 +852,21 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 15, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "110" + "Puzzle 7.1: 12.7 msecs, correct answer: 118 " ] }, - "execution_count": 17, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -844,7 +879,8 @@ "def insides(segments): return ', '.join(segments[1::2])\n", "def tls(segments): return abba(outsides(segments)) and not abba(insides(segments))\n", "\n", - "sum(tls(segment(line)) for line in Input(7))" + "answer(7.1, 118, lambda:\n", + " sum(tls(segment(line)) for line in Input(7)))" ] }, { @@ -856,9 +892,12 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 16, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -880,18 +919,21 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 17, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "242" + "Puzzle 7.2: 118.2 msecs, correct answer: 260 " ] }, - "execution_count": 19, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -899,13 +941,14 @@ "source": [ "alphabet = 'abcdefghijklmnopqrstuvwxyz'\n", "\n", - "def ssl(segments): \n", + "def ssl(segments: list[str]) -> bool: \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", "\n", - "sum(ssl(segment(line)) for line in Input(7))" + "answer(7.2, 260, lambda:\n", + " sum(ssl(segment(line)) for line in Input(7)))" ] }, { @@ -926,24 +969,27 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 18, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "128" + "Puzzle 8.1: 0.7 msecs, correct answer: 110 " ] }, - "execution_count": 20, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def interpret(cmd, screen):\n", + "def interpret(cmd: str, screen: np.array) -> None:\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", @@ -953,57 +999,68 @@ " elif cmd.startswith('rotate col'):\n", " screen[:, A] = rotate(screen[:, A], B)\n", "\n", - "def rotate(items, n): return np.append(items[-n:], items[:-n])\n", + "def rotate(items, n) -> np.array: return np.append(items[-n:], items[:-n])\n", "\n", - "def Screen(): return np.zeros((6, 50), dtype=np.int)\n", + "def Screen(shape=(6, 50)) -> np.array: return np.zeros(shape, dtype=int)\n", "\n", - "def run(commands, screen):\n", + "def run(commands) -> np.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", " return screen\n", "\n", - "screen = run(Input(8), Screen())\n", - "np.sum(screen)" + "answer(8.1, 110, lambda:\n", + " int(np.sum(run(Input(8)))))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In **part two**, we are asked what message is on the screen. I won't try to do OCR; I'll just print the screen and look at the output:" + "In **part two**, we are asked what message is on the screen. I won't try to do OCR; I'll just print the screen and look at the output, then type that in to AoC and record it as my `answer`:" ] }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 19, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "@@@@ @@ @@ @@@ @@ @@@ @ @ @ @ @@ @@ \n", - "@ @ @ @ @ @ @ @ @ @ @ @ @ @ @@ @ @ @ \n", - "@@@ @ @ @ @ @ @ @ @ @ @@@@ @ @ @ @ @ @ \n", - "@ @ @ @@@@ @@@ @ @@ @@@ @ @ @ @@@@ @ @ \n", - "@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ \n", - "@@@@ @@ @ @ @ @ @@@ @ @ @ @ @ @ @@ \n" + "@@@@ @@ @ @ @@@ @ @ @@ @@@ @ @ @ @@ \n", + " @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ \n", + " @ @ @@@@ @ @ @@ @ @ @ @ @ @ @ \n", + " @ @ @ @ @@@ @ @ @ @@@ @ @ @ \n", + "@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ \n", + "@@@@ @@ @ @ @ @ @ @ @@ @ @@@@ @ @@ \n" ] + }, + { + "data": { + "text/plain": [ + "Puzzle 8.2: 0.5 msecs, correct answer: ZJHRKCPLYJ " + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "for row in screen:\n", - " print(cat(' @'[pixel] for pixel in row))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "My answer is `EOARGPHYAO`." + "def show_message(commands) -> None:\n", + " for row in run(commands):\n", + " print(cat(' @'[pixel] for pixel in row))\n", + "\n", + "answer(8.2, 'ZJHRKCPLYJ', lambda:\n", + " show_message(Input(8)) or 'ZJHRKCPLYJ')" ] }, { @@ -1017,18 +1074,21 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 20, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "138735" + "Puzzle 9.1: 0.4 msecs, correct answer: 115118 " ] }, - "execution_count": 22, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -1036,7 +1096,7 @@ "source": [ "matcher = re.compile(r'[(](\\d+)x(\\d+)[)]').match # e.g. matches \"(2x5)\" as ('2', '5')\n", "\n", - "def decompress(s):\n", + "def decompress(s: str) -> str:\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", @@ -1053,7 +1113,8 @@ " i += 1 # Advance past it\n", " return cat(result)\n", "\n", - "len(decompress(Input(9).read()))" + "answer(9.1, 115118, lambda:\n", + " len(decompress(Input(9).read())))" ] }, { @@ -1065,24 +1126,27 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 21, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "11125026826" + "Puzzle 9.2: 1.8 msecs, correct answer: 11107527530 " ] }, - "execution_count": 23, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def decompress_length(s):\n", + "def decompress_length(s: str) -> str:\n", " \"\"\"Decompress string s by interpreting '(2x5)' as making 5 copies of the next 2 characters.\n", " Recursively decompress these next 5 characters. Return the length of the decompressed string.\"\"\"\n", " s = re.sub(r'\\s', '', s) # \"whitespace is ignored\"\n", @@ -1100,7 +1164,8 @@ " i += 1 # Advance past it\n", " return length\n", "\n", - "decompress_length(Input(9).read())" + "answer(9.2, 11107527530, lambda:\n", + " decompress_length(Input(9).read()))" ] }, { @@ -1112,10 +1177,8 @@ }, { "cell_type": "code", - "execution_count": 24, - "metadata": { - "collapsed": true - }, + "execution_count": 22, + "metadata": {}, "outputs": [], "source": [ "assert decompress('A(2x5)BCD') == 'ABCBCBCBCBCD'\n", @@ -1156,73 +1219,89 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 23, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "bot 113 has {17, 61}\n" - ] + "data": { + "text/plain": [ + "Puzzle 10.1: 0.8 msecs, correct answer: 56 " + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "def bots(instructions, goal={17, 61}):\n", - " \"Follow the data flow instructions, and if a bot gets the goal, print it.\"\n", + "def bots(instructions, goal={17, 61}) -> tuple[dict, int]:\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", " def give(giver, chip, recip):\n", - " \"Pass the chip from giver to recipient.\"\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", - " print(recip, 'has', goal)\n", + " 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", " 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\n", + " return has, winner\n", "\n", - "has = bots(Input(10).read())" + "answer(10.1, 56, lambda:\n", + " bots(Input(10).read())[1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In **part two**, we are asked for the product of three output bins:" + "In **part two**, we are asked for the product of the three output bins numbered 0 to 2:" ] }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 24, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "12803" + "Puzzle 10.2: 0.5 msecs, correct answer: 7847 " ] }, - "execution_count": 26, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def out(i): return has['output ' + str(i)].pop()\n", + "def bin_product(instructions) -> int:\n", + " has, _ = bots(instructions)\n", + " def out(i): return first(has[f'output {i}'])\n", + " return out(0) * out(1) * out(2)\n", "\n", - "out(0) * out(1) * out(2)" + "answer(10.2, 7847, lambda:\n", + " bin_product(Input(10).read()))" ] }, { @@ -1236,7 +1315,7 @@ "* To figure out what moves can be made, consider both directions for the elevator (up or down); find all combinations of one or two items on each floor, and keep all of those moves, as long as they don't violate the constraint that we can't have a chip on the same floor as an RTG, unless the chip's own RTG is there.\n", "* To calculate the heuristic, add up the number of floors away each item is from the top floor and divide by two (since a move might carry two items).\n", "\n", - "Here is my input: \n", + "Here is my input. I'll just read this with my eyes and put it into my code.\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", @@ -1246,22 +1325,20 @@ }, { "cell_type": "code", - "execution_count": 27, - "metadata": { - "collapsed": true - }, + "execution_count": 25, + "metadata": {}, "outputs": [], "source": [ "State = namedtuple('State', 'elevator, floors')\n", "\n", - "def fs(*items): return frozenset(items)\n", + "def fset(*items): return frozenset(items)\n", "\n", "legal_floors = {0, 1, 2, 3}\n", "\n", "def combos(things):\n", " \"All subsets of 1 or 2 things.\"\n", " for s in chain(combinations(things, 1), combinations(things, 2)):\n", - " yield fs(*s)\n", + " yield fset(*s)\n", "\n", "def moves(state):\n", " \"All legal states that can be reached in one move from this state\"\n", @@ -1293,74 +1370,41 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Let's try it out on an easy sample problem:" + "The answer we need is the number of elevator moves, which is one less than the path length (because the path includes the initial state)." ] }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 26, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "[State(elevator=0, floors=(frozenset({'RG'}), frozenset(), frozenset({'RM'}), frozenset())),\n", - " State(elevator=1, floors=(frozenset(), frozenset({'RG'}), frozenset({'RM'}), frozenset())),\n", - " State(elevator=2, floors=(frozenset(), frozenset(), frozenset({'RG', 'RM'}), frozenset())),\n", - " State(elevator=3, floors=(frozenset(), frozenset(), frozenset(), frozenset({'RG', 'RM'})))]" + "Puzzle 11.1: 3,240.6 msecs, correct answer: 31 " ] }, - "execution_count": 28, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "easy = State(0, (fs('RG'), Ø, fs('RM'), Ø))\n", + "part1 = State(0, (fset('TG', 'TM', 'PG', 'SG'), fset('PM', 'SM'), fset('pM', 'pG', 'RM', 'RG'), Ø))\n", "\n", - "astar_search(easy, h_to_top, moves)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now to solve the real problem. The answer we need is the number of elevator moves, which is one less than the path length (because the path includes the initial state)." - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 11.9 s, sys: 72.3 ms, total: 12 s\n", - "Wall time: 12 s\n" - ] - }, - { - "data": { - "text/plain": [ - "31" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "part1 = State(0, (fs('TG', 'TM', 'PG', 'SG'), fs('PM', 'SM'), fs('pM', 'pG', 'RM', 'RG'), Ø))\n", + "def astar_path_length(*args) -> int:\n", + " \"\"\"The number of moves to find the optimal path to the goal\n", + " (the length of the path, not counting the starting state).\"\"\"\n", + " path = astar_search(*args)\n", + " return len(path) - 1\n", "\n", - "%time path = astar_search(part1, h_to_top, moves)\n", - "len(path) - 1" + "answer(11.1, 31, lambda:\n", + " astar_path_length(part1, h_to_top, moves))" ] }, { @@ -1372,43 +1416,38 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 27, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 14min 34s, sys: 11.4 s, total: 14min 46s\n", - "Wall time: 14min 52s\n" - ] - }, { "data": { "text/plain": [ - "55" + "Puzzle 11.2: 3.9 MINUTES, correct answer: 55 " ] }, - "execution_count": 30, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "part2 = State(0, (fs('TG', 'TM', 'PG', 'SG', 'EG', 'EM', 'DG', 'DM'), \n", - " fs('PM', 'SM'), fs('pM', 'pG', 'RM', 'RG'), Ø))\n", + "part2 = State(0, (fset('TG', 'TM', 'PG', 'SG', 'EG', 'EM', 'DG', 'DM'), \n", + " fset('PM', 'SM'), fset('pM', 'pG', 'RM', 'RG'), Ø))\n", "\n", - "%time path = astar_search(part2, h_to_top, moves)\n", - "len(path) - 1" + "answer(11.2, 55, lambda:\n", + " astar_path_length(part2, h_to_top, moves))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "It worked. And it took about 60 times longer. If I wanted to make it more efficient, I would focus on *symmetry*: when there are two symmetric moves, we only need to consider one. For example, in terms of finding the shortest path, it is the same to move, say `{'TG', 'TM'}` or `{'EG', 'EM'}` or `{'DG', 'DM'}` when they are all on the ground floor." + "It worked. And it took about 70 times longer. If I wanted to make it more efficient, I would focus on *symmetry*: when there are two symmetric moves, we only need to consider one. For example, in terms of finding the shortest path, it is the same to move, say `{'TG', 'TM'}` or `{'EG', 'EM'}` or `{'DG', 'DM'}` when they are all on the ground floor." ] }, { @@ -1422,25 +1461,28 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 28, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "{'a': 318007, 'b': 196418, 'c': 0, 'd': 0}" + "Puzzle 12.1: 109.0 msecs, correct answer: 318007 " ] }, - "execution_count": 31, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def interpret(code, regs):\n", - " \"Execute instructions until pc goes off the end.\"\n", + "def interpret(code, **regs) -> dict:\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", @@ -1453,14 +1495,13 @@ " elif op == 'jnz' and val(x): pc += y - 1\n", " return regs\n", "\n", - "def parse(line): \n", - " \"Split line into words, and convert to int where appropriate.\"\n", - " return tuple((x if x.isalpha() else int(x)) \n", - " for x in line.split())\n", + "def parse_program(lines): \n", + " \"Split lines into words, and convert to int where appropriate.\"\n", + " return [tuple((x if x.isalpha() else int(x)) for x in line.split())\n", + " for line in lines]\n", "\n", - "code = [parse(line) for line in Input(12)]\n", - "\n", - "interpret(code, dict(a=0, b=0, c=0, d=0))" + "answer(12.1, 318007, lambda:\n", + " interpret(parse_program(Input(12)), a=0, b=0, c=0, d=0)['a'])" ] }, { @@ -1474,24 +1515,28 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 29, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "{'a': 9227661, 'b': 5702887, 'c': 0, 'd': 0}" + "Puzzle 12.2: 3,059.1 msecs, correct answer: 9227661 " ] }, - "execution_count": 32, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "interpret(code, dict(a=0, b=0, c=1, d=0))" + "answer(12.2, 9227661, lambda:\n", + " interpret(parse_program(Input(12)), a=0, b=0, c=1, d=0)['a'])" ] }, { @@ -1512,18 +1557,21 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 30, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "82" + "Puzzle 13.1: 0.3 msecs, correct answer: 82 " ] }, - "execution_count": 33, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -1532,7 +1580,7 @@ "favorite = 1362\n", "goal = (31, 39)\n", "\n", - "def is_open(location):\n", + "def is_open(location) -> bool:\n", " \"Is this an open location?\"\n", " x, y = location\n", " num = x*x + 3*x + 2*x*y + y + y*y + favorite\n", @@ -1540,8 +1588,8 @@ "\n", "def open_neighbors(location): return filter(is_open, neighbors4(location))\n", "\n", - "path = astar_search((1, 1), lambda p: cityblock_distance(p, goal), open_neighbors)\n", - "len(path) - 1" + "answer(13.1, 82, lambda:\n", + " astar_path_length((1, 1), lambda p: cityblock_distance(p, goal), open_neighbors))" ] }, { @@ -1553,9 +1601,12 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 31, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { @@ -1609,24 +1660,27 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 32, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "138" + "Puzzle 13.2: 0.2 msecs, correct answer: 138 " ] }, - "execution_count": 35, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def count_locations_within(start, N, neighbors):\n", + "def count_locations_within(start, N, neighbors) -> int:\n", " \"Find how many locations are within N steps from start.\"\n", " frontier = deque([start]) # A queue of states\n", " distance = {start: 0} # distance to start; also tracks all states seen\n", @@ -1639,13 +1693,17 @@ " distance[s2] = distance[s] + 1\n", " return len(distance)\n", " \n", - "count_locations_within((1, 1), 50, open_neighbors)" + "answer(13.2, 138, lambda:\n", + " count_locations_within((1, 1), 50, open_neighbors))" ] }, { "cell_type": "markdown", "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "source": [ "# [Day 14](http://adventofcode.com/2016/day/14): One-Time Pad \n", @@ -1659,38 +1717,43 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 33, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "25427" + "Puzzle 14.1: 130.7 msecs, correct answer: 16106 " ] }, - "execution_count": 36, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "salt = 'yjdafjpo'\n", + "salt = 'zpqevtbw' # My input\n", "\n", "@lru_cache(1001)\n", "def hashval(i): return hashlib.md5(bytes(salt + str(i), 'utf-8')).hexdigest()\n", "\n", - "def is_key(i):\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", " three = re.search(r'(.)\\1\\1', hashval(i))\n", " if three:\n", " five = three.group(1) * 5\n", " return any(five in hashval(i+delta) for delta in range(1, 1001))\n", + " return False\n", " \n", - "def nth_key(N): return nth(filter(is_key, range(BIG)), N)\n", + "def nth_key(N) -> int: return nth(filter(is_key, range(BIG)), N)\n", "\n", - "nth_key(63)" + "answer(14.1, 16106, lambda:\n", + " nth_key(63))" ] }, { @@ -1702,46 +1765,42 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 34, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 36.4 s, sys: 314 ms, total: 36.7 s\n", - "Wall time: 37 s\n" - ] - }, { "data": { "text/plain": [ - "22045" + "Puzzle 14.2: 14,479.4 msecs, correct answer: 22423 " ] }, - "execution_count": 37, + "execution_count": 34, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "@lru_cache(1001)\n", + "@cache\n", "def hashval(i, stretch=2016): \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", " return h\n", "\n", - "%time nth_key(63)" + "answer(14.2, 22423, lambda:\n", + " nth_key(63))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "This was my highest-scoring day, finishing #20 on part two." + "This was my highest-scoring day on the leaderboard, finishing #20 on part two." ] }, { @@ -1755,86 +1814,69 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 35, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "376777" + "Puzzle 15.1: 53.4 msecs, correct answer: 203660 " ] }, - "execution_count": 38, + "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def parse(inputs): \n", - " \"Parse an input string into (disc#, positions, pos) triples.\"\n", - " return [tuple(map(int, triple))\n", - " for triple in re.findall(r'#(\\d+).* (\\d+) positions.* (\\d+)[.]', inputs)]\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", " \n", - "discs = parse('''\n", - "Disc #1 has 13 positions; at time=0, it is at position 1.\n", - "Disc #2 has 19 positions; at time=0, it is at position 10.\n", - "Disc #3 has 3 positions; at time=0, it is at position 2.\n", - "Disc #4 has 7 positions; at time=0, it is at position 1.\n", - "Disc #5 has 5 positions; at time=0, it is at position 3.\n", - "Disc #6 has 17 positions; at time=0, it is at position 5.\n", - "''')\n", - "\n", - "def falls(t, discs):\n", + "def falls(t, discs) -> bool:\n", " \"If we drop the capsule at time t, does it fall through all slots?\"\n", - " return all((pos + t + d) % positions == 0 \n", - " for (d, positions, pos) in discs)\n", + " return all((start + t + d) % positions == 0 \n", + " for (d, positions, _, start) in discs)\n", "\n", - "first(t for t in range(BIG) if falls(t, discs))" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "assert discs == [(1, 13, 1), (2, 19, 10), (3, 3, 2), (4, 7, 1), (5, 5, 3), (6, 17, 5)]\n", - "assert falls(5, [(1, 5, 4), (2, 2, 1)])" + "def all_falling_times(discs) -> Iterable[int]:\n", + " \"\"\"Times t that a capsule could fall through all the discs.\"\"\"\n", + " return (t for t in range(BIG) if falls(t, discs))\n", + "\n", + "answer(15.1, 203660,\n", + " lambda: first(all_falling_times(parse_discs(Input(15)))))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "For **part two**, we add a 7th disc, with 11 positions, at position 0 at time 0. I coud go through all the possible times, just as in part one, but I see a way to get a 19-fold speedup: Disc #2 is the largest, with 19 positions, so we only need consider every 19 values of `t`. But what is the first one to consider? Disc @2 starts at position 10, so to get back to position 0, we have to release at `t=7`, because 10 + 2 + 7 = 19 = 0 mod 19. So we will iterate `t` over `range(7, BIG, 19)`:" + "For **part two**, we add a 7th disc, with 11 positions, at position 0 at time 0. " ] }, { "cell_type": "code", - "execution_count": 40, - "metadata": { - "collapsed": false - }, + "execution_count": 36, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "3903937" + "Puzzle 15.2: 571.1 msecs, correct answer: 2408135 " ] }, - "execution_count": 40, + "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "discs.append((7, 11, 0))\n", - "\n", - "first(t for t in range(7, BIG, 19) if falls(t, discs))" + "answer(15.2, 2408135,\n", + " lambda: first(all_falling_times([*parse_discs(Input(15)), (7,11, 0, 0)])))" ] }, { @@ -1864,24 +1906,27 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 37, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "'10010110010011110'" + "Puzzle 16.1: 0.0 msecs, correct answer: 01110011101111011" ] }, - "execution_count": 41, + "execution_count": 37, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def expand(a, N):\n", + "def expand(a: str, N: int) -> str:\n", " \"Expand seed `a` until it has length N.\"\n", " while len(a) < N:\n", " b = flip(a[::-1])\n", @@ -1890,16 +1935,17 @@ "\n", "def flip(text, table=str.maketrans('10', '01')): return text.translate(table)\n", "\n", - "def checksum(a):\n", + "def checksum(a: str) -> str:\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", " return a\n", " \n", - "seed = '10010000000110000'\n", + "seed = '11110010111001001'\n", "\n", - "checksum(expand(seed, 272))" + "answer(16.1, '01110011101111011', lambda:\n", + " checksum(expand(seed, 272)))" ] }, { @@ -1911,32 +1957,28 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 38, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 6.17 s, sys: 326 ms, total: 6.49 s\n", - "Wall time: 6.54 s\n" - ] - }, { "data": { "text/plain": [ - "'01101011101100011'" + "Puzzle 16.2: 1,532.5 msecs, correct answer: 11001111011000111" ] }, - "execution_count": 42, + "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "%time checksum(expand(seed, 35651584)) " + "answer(16.2, '11001111011000111', lambda:\n", + " checksum(expand(seed, 35651584)))" ] }, { @@ -1950,34 +1992,27 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 39, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "[((0, 0), ''),\n", - " ((1, 0), 'R'),\n", - " ((1, 1), 'RD'),\n", - " ((1, 0), 'RDU'),\n", - " ((2, 0), 'RDUR'),\n", - " ((3, 0), 'RDURR'),\n", - " ((3, 1), 'RDURRD'),\n", - " ((3, 2), 'RDURRDD'),\n", - " ((2, 2), 'RDURRDDL'),\n", - " ((3, 2), 'RDURRDDLR'),\n", - " ((3, 3), 'RDURRDDLRD')]" + "Puzzle 17.1: 0.0 msecs, correct answer: RRRLDRDUDD " ] }, - "execution_count": 43, + "execution_count": 39, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "passcode = 'awrkjxxr'\n", + "passcode = 'qtetzkpl'\n", "\n", "openchars = 'bcdef'\n", "\n", @@ -1985,14 +2020,14 @@ "\n", "start, goal = (0, 0), (3, 3)\n", "\n", - "def to_goal(state): \n", + "def to_goal(state: tuple) -> int: \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):\n", + "def moves(state: tuple) -> Iterable[tuple]:\n", " \"All states reachable from this state.\"\n", " (x, y), path = state\n", " hashx = hashlib.md5(bytes(passcode + path, 'utf-8')).hexdigest()\n", @@ -2001,7 +2036,8 @@ " if hashx[i] in openchars and pos2 in grid:\n", " yield (pos2, path+p)\n", " \n", - "astar_search((start, ''), to_goal, moves)" + "answer(17.1, 'RRRLDRDUDD', lambda:\n", + " astar_search((start, ''), to_goal, moves)[-1][1])" ] }, { @@ -2013,25 +2049,28 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 40, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "526" + "Puzzle 17.2: 43.6 msecs, correct answer: 706 " ] }, - "execution_count": 44, + "execution_count": 40, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def longest_search(state, goal, moves):\n", - " \"Find the longest path to goal by depth-first search.\"\n", + "def longest_search(state, goal, moves) -> int:\n", + " \"Find the length of the longest path to goal by depth-first search.\"\n", " longest = 0\n", " frontier = [state]\n", " while frontier:\n", @@ -2042,7 +2081,8 @@ " frontier.extend(moves(state))\n", " return longest\n", " \n", - "longest_search((start, ''), goal, moves)" + "answer(17.2, 706, lambda:\n", + " longest_search((start, ''), goal, moves))" ] }, { @@ -2056,36 +2096,39 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 41, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "1951" + "Puzzle 18.1: 0.6 msecs, correct answer: 1987 " ] }, - "execution_count": 45, + "execution_count": 41, "metadata": {}, "output_type": "execute_result" } ], "source": [ "safe, trap = '.', '^' \n", - "initial = '.^^.^^^..^.^..^.^^.^^^^.^^.^^...^..^...^^^..^^...^..^^^^^^..^.^^^..^.^^^^.^^^.^...^^^.^^.^^^.^.^^.^.'\n", "\n", - "def rows(n, row=initial):\n", + "def rows(n: int, initial_row: str) -> list[str]:\n", " \"The first n rows of tiles (given the initial row).\"\n", - " result = [row]\n", + " result = [initial_row]\n", " for i in range(n-1):\n", " previous = safe + result[-1] + safe\n", " result.append(cat((trap if previous[i-1] != previous[i+1] else safe)\n", " for i in range(1, len(previous) - 1)))\n", " return result\n", "\n", - "cat(rows(40)).count(safe)" + "answer(18.1, 1987, lambda:\n", + " cat(rows(40, Input(18).read().strip())).count(safe))" ] }, { @@ -2097,9 +2140,12 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 42, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { @@ -2117,7 +2163,7 @@ " '^^.^^^..^^']" ] }, - "execution_count": 46, + "execution_count": 42, "metadata": {}, "output_type": "execute_result" } @@ -2135,32 +2181,28 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 43, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 7.92 s, sys: 90.6 ms, total: 8.01 s\n", - "Wall time: 8.08 s\n" - ] - }, { "data": { "text/plain": [ - "20002936" + "Puzzle 18.2: 2,019.0 msecs, correct answer: 19984714 " ] }, - "execution_count": 47, + "execution_count": 43, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "%time cat(rows(400000)).count(safe)" + "answer(18.2, 19984714, lambda:\n", + " cat(rows(400000, Input(18).read().strip())).count(safe))" ] }, { @@ -2189,32 +2231,42 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 44, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "1842613" + "Puzzle 19.1: 0.0 msecs, correct answer: 1815603 " ] }, - "execution_count": 48, + "execution_count": 44, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def Elves(N=3018458): return range(1, N+1) \n", + "N_elves = 3004953\n", "\n", - "def winner(elves): return (elves[0] if (len(elves) == 1) else winner(one_round(elves)))\n", + "def Elves(N: int) -> range: return range(1, N+1) \n", "\n", - "def one_round(elves): return (elves[0::2] if (len(elves) % 2 == 0) else elves[2::2])\n", + "def winner(elves: range) -> int: \n", + " \"\"\"The elf who ends up with all the presents.\"\"\"\n", + " return (elves[0] if (len(elves) == 1) else winner(one_round(elves)))\n", + "\n", + "def one_round(elves: range) -> range: \n", + " \"\"\"Do one round of present-taking.\"\"\"\n", + " return (elves[0::2] if (len(elves) % 2 == 0) else elves[2::2])\n", "\n", "assert winner(Elves(5)) == 3\n", "\n", - "winner(Elves())" + "answer(19.1, 1815603, lambda:\n", + " winner(Elves(N_elves)))" ] }, { @@ -2228,52 +2280,55 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 45, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "one_round(range(1, 3018459)) = range(1, 3018459, 2)\n", - "one_round(range(1, 3018459, 2)) = range(5, 3018459, 4)\n", - "one_round(range(5, 3018459, 4)) = range(5, 3018461, 8)\n", - "one_round(range(5, 3018461, 8)) = range(21, 3018461, 16)\n", - "one_round(range(21, 3018461, 16)) = range(53, 3018469, 32)\n", - "one_round(range(53, 3018469, 32)) = range(53, 3018485, 64)\n", - "one_round(range(53, 3018485, 64)) = range(181, 3018485, 128)\n", - "one_round(range(181, 3018485, 128)) = range(437, 3018549, 256)\n", - "one_round(range(437, 3018549, 256)) = range(437, 3018677, 512)\n", - "one_round(range(437, 3018677, 512)) = range(1461, 3018677, 1024)\n", - "one_round(range(1461, 3018677, 1024)) = range(3509, 3019189, 2048)\n", - "one_round(range(3509, 3019189, 2048)) = range(7605, 3020213, 4096)\n", - "one_round(range(7605, 3020213, 4096)) = range(7605, 3022261, 8192)\n", - "one_round(range(7605, 3022261, 8192)) = range(7605, 3022261, 16384)\n", - "one_round(range(7605, 3022261, 16384)) = range(7605, 3022261, 32768)\n", - "one_round(range(7605, 3022261, 32768)) = range(7605, 3022261, 65536)\n", - "one_round(range(7605, 3022261, 65536)) = range(7605, 3022261, 131072)\n", - "one_round(range(7605, 3022261, 131072)) = range(269749, 3022261, 262144)\n", - "one_round(range(269749, 3022261, 262144)) = range(794037, 3153333, 524288)\n", - "one_round(range(794037, 3153333, 524288)) = range(1842613, 3415477, 1048576)\n", - "one_round(range(1842613, 3415477, 1048576)) = range(1842613, 3939765, 2097152)\n" + "one_round(range(1, 3004954)) = range(3, 3004954, 2)\n", + "one_round(range(3, 3004954, 2)) = range(3, 3004955, 4)\n", + "one_round(range(3, 3004955, 4)) = range(3, 3004955, 8)\n", + "one_round(range(3, 3004955, 8)) = range(19, 3004955, 16)\n", + "one_round(range(19, 3004955, 16)) = range(51, 3004963, 32)\n", + "one_round(range(51, 3004963, 32)) = range(51, 3004979, 64)\n", + "one_round(range(51, 3004979, 64)) = range(51, 3004979, 128)\n", + "one_round(range(51, 3004979, 128)) = range(51, 3004979, 256)\n", + "one_round(range(51, 3004979, 256)) = range(51, 3004979, 512)\n", + "one_round(range(51, 3004979, 512)) = range(1075, 3004979, 1024)\n", + "one_round(range(1075, 3004979, 1024)) = range(1075, 3005491, 2048)\n", + "one_round(range(1075, 3005491, 2048)) = range(5171, 3005491, 4096)\n", + "one_round(range(5171, 3005491, 4096)) = range(13363, 3007539, 8192)\n", + "one_round(range(13363, 3007539, 8192)) = range(13363, 3011635, 16384)\n", + "one_round(range(13363, 3011635, 16384)) = range(46131, 3011635, 32768)\n", + "one_round(range(46131, 3011635, 32768)) = range(111667, 3028019, 65536)\n", + "one_round(range(111667, 3028019, 65536)) = range(242739, 3060787, 131072)\n", + "one_round(range(242739, 3060787, 131072)) = range(242739, 3126323, 262144)\n", + "one_round(range(242739, 3126323, 262144)) = range(767027, 3126323, 524288)\n", + "one_round(range(767027, 3126323, 524288)) = range(1815603, 3388467, 1048576)\n", + "one_round(range(1815603, 3388467, 1048576)) = range(1815603, 3912755, 2097152)\n" ] }, { "data": { "text/plain": [ - "1842613" + "1815603" ] }, - "execution_count": 49, + "execution_count": 45, "metadata": {}, "output_type": "execute_result" } ], "source": [ "one_round = trace1(one_round)\n", - "winner(Elves())" + "winner(Elves(N_elves))" ] }, { @@ -2282,7 +2337,7 @@ "source": [ "In **part two** the rules have changed, and each elf now takes from the elf *across* the circle. If there is an even number of elves, take from the elf directly across. Fo example, with 12 elves in a circle (like a clock face), Elf 1 takes from Elf 7. With an odd number of elves, directly across the circle falls between two elves, so choose the one that is earlier in the circle. For example, with 11 elves, Elf 2 takes from the Elf at position 7. Now who ends up with the presents?\n", "\n", - "This is tougher. I can't think of a simple `range` expression to describe who gets eliminated in a round. But I can represent the circle as a list and write a loop to eliminate elves one at a time. Again, if I did that with a `del` statement for each elf, it would be O(*N*2). But if instead I do one round at a time, replacing each eliminated elf with `None` in the list, and then filtering out the `None` values, then each round is only O(*N*), and sine there will be log(*N*) rounds, the whole thing is only O(*N* log(*N*)). That should be reasonably fast.\n", + "This is tougher. I can't think of a simple `range` expression to describe who gets eliminated in a round. But I can represent the circle as a list and write a loop to eliminate elves one at a time. Again, if I did that with a `del` statement for each elf, it would be O(*N*2). But if instead I do one round at a time, replacing each eliminated elf with `None` in the list, and then filtering out the `None` values, then each round is only O(*N*), and since there will be log(*N*) rounds, the whole thing is only O(*N* log(*N*)). That should be reasonably fast.\n", "\n", "It is still tricky to know which elf to eliminate. If there are *N* elves, then the elf at position *i* should elminate the one at position *i* + *N* // 2. But we have to skip over the already-eliminated spaces; we can do that by keeping track of the number of eliminated elves in the variable `eliminated`. We also need to keep track of the current value of `N`, since it will change. And, since I don't want to deal with the headaches of wrapping around the circletconn, I will only deal with the first third of the elves: the first third all eliminate elves in the other two-thirds; if we went more than 1/3 of the way through, we would have to worry about wrapping around. (I had a bug here: at first I just iterated through `N // 3`. But when `N` is 2, that does no iteration at all, which is wrong; with two elves, the first should eliminate the other. It turns out it is safe to iterate through `ceil(N /3)` on each round.)\n", "\n", @@ -2291,34 +2346,29 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 46, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 1.26 s, sys: 74.7 ms, total: 1.33 s\n", - "Wall time: 1.34 s\n" - ] - }, { "data": { "text/plain": [ - "1424135" + "Puzzle 19.2: 320.8 msecs, correct answer: 1410630 " ] }, - "execution_count": 50, + "execution_count": 46, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def Elves(N=3018458): return list(range(1, N+1))\n", + "def Elves(N: int) -> list: return list(range(1, N + 1))\n", "\n", - "def one_round(elves):\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", " N = len(elves)\n", " eliminated = 0\n", @@ -2333,14 +2383,15 @@ "\n", "assert one_round(Elves(5)) == [4, 1, 2]\n", "\n", - "%time winner(Elves())" + "answer(19.2, 1410630, lambda:\n", + " winner(Elves(N_elves)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "I was worried that this solution might take over a minute to run, but it turns out to only take about a second." + "I was worried that this solution might take over a minute to run, but it turns out to take under a second." ] }, { @@ -2354,28 +2405,31 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 47, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "[[0, 97802],\n", - " [5682, 591077],\n", - " [591078, 868213],\n", - " [868214, 1216244],\n", - " [1216245, 1730562]]" + "[[0, 570348],\n", + " [263908, 753114],\n", + " [753115, 1178173],\n", + " [840301, 1235975],\n", + " [1235976, 1247936]]" ] }, - "execution_count": 51, + "execution_count": 47, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "pairs = sorted(map(parse_ints, Input(20)))\n", + "pairs = sorted(map(ints, Input(20)))\n", "\n", "pairs[:5]" ] @@ -2389,31 +2443,35 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 48, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "4793564" + "Puzzle 20.1: 0.0 msecs, correct answer: 32259706 " ] }, - "execution_count": 52, + "execution_count": 48, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def unblocked(pairs):\n", + "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", - "first(unblocked(pairs)) " + "answer(20.1, 32259706, lambda:\n", + " first(unblocked(pairs)))" ] }, { @@ -2425,30 +2483,37 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 49, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "146" + "Puzzle 20.2: 0.1 msecs, correct answer: 113 " ] }, - "execution_count": 53, + "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "len(list(unblocked(pairs)))" + "answer(20.2, 113, lambda:\n", + " len(list(unblocked(pairs))))" ] }, { "cell_type": "markdown", "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "source": [ "# [Day 21](http://adventofcode.com/2016/day/21) Scrambled Letters and Hash \n", @@ -2461,31 +2526,34 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 50, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "'gcedfahb'" + "Puzzle 21.1: 0.4 msecs, correct answer: gfdhebac " ] }, - "execution_count": 54, + "execution_count": 50, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def scramble(password, instructions=list(Input(21)), verbose=False):\n", + "def scramble(password: str, instructions: Iterable[str], verbose=False) -> str:\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", " for line in instructions:\n", " words = line.split()\n", - " A, B, = parse_ints(line + ' 0 0')[:2]\n", + " A, B, = ints(line + ' 0 0')[:2]\n", " cmd = line.startswith\n", " if cmd('swap position'): swap(A, B)\n", " elif cmd('swap letter'): swap(pw.index(words[2]), pw.index(words[5]))\n", @@ -2500,7 +2568,8 @@ " print(line + ': ' + cat(pw))\n", " return cat(pw)\n", "\n", - "scramble('abcdefgh')" + "answer(21.1, 'gfdhebac', lambda:\n", + " scramble('abcdefgh', Input(21)))" ] }, { @@ -2512,9 +2581,12 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 51, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { @@ -2537,7 +2609,7 @@ "'decab'" ] }, - "execution_count": 55, + "execution_count": 51, "metadata": {}, "output_type": "execute_result" } @@ -2568,31 +2640,38 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 52, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "{'hegbdcfa'}" + "Puzzle 21.2: 2,336.8 msecs, correct answer: dhaegfbc " ] }, - "execution_count": 56, + "execution_count": 52, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "{cat(p) for p in permutations('fbgdceah') \n", - " if scramble(p) == 'fbgdceah'}" + "answer(21.2, 'dhaegfbc', lambda:\n", + " first(cat(p) for p in permutations('fbgdceah') \n", + " if scramble(p, Input(21)) == 'fbgdceah'))" ] }, { "cell_type": "markdown", "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "source": [ "# [Day 22](http://adventofcode.com/2016/day/22) Grid Computing\n", @@ -2614,18 +2693,21 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": 53, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "903" + "Puzzle 22.1: 130.1 msecs, correct answer: 1045 " ] }, - "execution_count": 57, + "execution_count": 53, "metadata": {}, "output_type": "execute_result" } @@ -2633,44 +2715,12 @@ "source": [ "Node = namedtuple('Node', 'x, y, size, used, avail, pct')\n", "\n", - "nodes = [Node(*parse_ints(line)) for line in Input(22) if line.startswith('/dev')]\n", + "nodes = [Node(*ints(line)) for line in Input(22) if line.startswith('/dev')]\n", "\n", - "def viable(A, B): return A != B and 0 < A.used <= B.avail\n", + "def viable(A, B) -> bool: return A != B and 0 < A.used <= B.avail\n", "\n", - "sum(viable(A, B) for A in nodes for B in nodes)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "That worked, but let's make sure the nodes look reasonable:" - ] - }, - { - "cell_type": "code", - "execution_count": 60, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[Node(x=0, y=0, size=92, used=70, avail=22, pct=76),\n", - " Node(x=0, y=1, size=86, used=65, avail=21, pct=75),\n", - " Node(x=0, y=2, size=88, used=73, avail=15, pct=82),\n", - " Node(x=0, y=3, size=91, used=67, avail=24, pct=73),\n", - " Node(x=0, y=4, size=87, used=70, avail=17, pct=80)]" - ] - }, - "execution_count": 60, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "nodes[:5]" + "answer(22.1, 1045, lambda:\n", + " sum(viable(A, B) for A in nodes for B in nodes))" ] }, { @@ -2682,18 +2732,21 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": 54, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "(Node(x=35, y=21, size=91, used=0, avail=91, pct=0), 37)" + "(Node(x=35, y=27, size=89, used=0, avail=89, pct=0), 35)" ] }, - "execution_count": 58, + "execution_count": 54, "metadata": {}, "output_type": "execute_result" } @@ -2714,10 +2767,8 @@ }, { "cell_type": "code", - "execution_count": 59, - "metadata": { - "collapsed": true - }, + "execution_count": 55, + "metadata": {}, "outputs": [], "source": [ "grid = {(node.x, node.y): node \n", @@ -2733,18 +2784,21 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 56, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "215" + "Puzzle 22.2: 1,271.5 msecs, correct answer: 265 " ] }, - "execution_count": 61, + "execution_count": 56, "metadata": {}, "output_type": "execute_result" } @@ -2752,9 +2806,9 @@ "source": [ "State = namedtuple('State', 'datapos, emptypos')\n", "\n", - "def distance(state): return cityblock_distance(state.datapos)\n", + "def distance(state: State) -> int: return cityblock_distance(state.datapos)\n", "\n", - "def moves(state):\n", + "def moves(state: State) -> Iterable[State]:\n", " \"Try moving any neighbor we can into the empty position.\"\n", " for pos in neighbors4(state.emptypos):\n", " if pos in grid:\n", @@ -2764,14 +2818,17 @@ " newdatapos = (state.emptypos if pos == state.datapos else state.datapos)\n", " yield State(newdatapos, pos)\n", " \n", - "path = astar_search(State((maxx, 0), (empty.x, empty.y)), distance, moves)\n", - "len(path) - 1" + "answer(22.2, 265, lambda:\n", + " len(astar_search(State((maxx, 0), (empty.x, empty.y)), distance, moves)) - 1)" ] }, { "cell_type": "markdown", "metadata": { - "collapsed": true + "collapsed": true, + "jupyter": { + "outputs_hidden": true + } }, "source": [ "# [Day 23](http://adventofcode.com/2016/day/23) Safe Cracking\n", @@ -2786,13 +2843,27 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 57, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 23.1: 7.3 msecs, correct answer: 10661 " + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "def interpret(code, regs):\n", + "def interpret(code, **regs) -> dict:\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", @@ -2816,113 +2887,231 @@ " 'cpy' if inst[0] == 'jnz' else \n", " 'jnz')\n", "\n", - "def parse(line): \n", - " \"Split line into words, and convert to int where appropriate.\"\n", - " return [(x if x.isalpha() else int(x)) \n", - " for x in line.split()]" + "def parse_program(lines): \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", + "def crack_safe(lines, **regs):\n", + " code = parse_program(lines)\n", + " return interpret(code, **regs)['a']\n", + "\n", + "answer(23.1, 10661, lambda:\n", + " crack_safe(Input(23), a=7, b=0, c=0, d=0))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In **part two**, we are told to run the same computation, but with register `a` set to 12. We are also warned that this will take a long time, and we might consider implementing a multiply instruction.\n", + "\n", + "At first I just ran my code as is, and it took about 8 minutes to run. That was by far my longest run time, so after the end of AoC I decided to optimize it, by figuring out how the multiply works. First, here's my program, with line numbers:" ] }, { "cell_type": "code", - "execution_count": 63, - "metadata": { - "collapsed": false - }, + "execution_count": 58, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'a': 11340, 'b': 1, 'c': 0, 'd': 0}" + "{0: 'cpy a b\\n',\n", + " 1: 'dec b\\n',\n", + " 2: 'cpy a d\\n',\n", + " 3: 'cpy 0 a\\n',\n", + " 4: 'cpy b c\\n',\n", + " 5: 'inc a\\n',\n", + " 6: 'dec c\\n',\n", + " 7: 'jnz c -2\\n',\n", + " 8: 'dec d\\n',\n", + " 9: 'jnz d -5\\n',\n", + " 10: 'dec b\\n',\n", + " 11: 'cpy b c\\n',\n", + " 12: 'cpy c d\\n',\n", + " 13: 'dec d\\n',\n", + " 14: 'inc c\\n',\n", + " 15: 'jnz d -2\\n',\n", + " 16: 'tgl c\\n',\n", + " 17: 'cpy -16 c\\n',\n", + " 18: 'jnz 1 c\\n',\n", + " 19: 'cpy 77 c\\n',\n", + " 20: 'jnz 73 d\\n',\n", + " 21: 'inc a\\n',\n", + " 22: 'inc d\\n',\n", + " 23: 'jnz d -2\\n',\n", + " 24: 'inc c\\n',\n", + " 25: 'jnz c -5\\n'}" ] }, - "execution_count": 63, + "execution_count": 58, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "text = '''\n", - "cpy a b\n", - "dec b\n", - "cpy a d\n", - "cpy 0 a\n", + "dict(enumerate(Input(23)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "I decided to profile the code to figure out where the time goes:" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[(0, 1),\n", + " (1, 1),\n", + " (2, 6),\n", + " (3, 6),\n", + " (4, 28960),\n", + " (5, 69272),\n", + " (6, 69272),\n", + " (7, 69272),\n", + " (8, 28960),\n", + " (9, 28960),\n", + " (10, 6),\n", + " (11, 6),\n", + " (12, 6),\n", + " (13, 21),\n", + " (14, 21),\n", + " (15, 21),\n", + " (16, 6),\n", + " (17, 6),\n", + " (18, 6),\n", + " (19, 1),\n", + " (20, 77),\n", + " (21, 5621),\n", + " (22, 5621),\n", + " (23, 5621),\n", + " (24, 77),\n", + " (25, 77)]" + ] + }, + "execution_count": 59, + "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", + " def val(x): return (regs[x] if x in regs else x)\n", + " pc = 0\n", + " while 0 <= pc < len(code):\n", + " yield pc ## Added this to profile where the pc is\n", + " inst = code[pc]\n", + " op, x, y = inst[0], inst[1], inst[-1]\n", + " pc += 1\n", + " if op == 'cpy' and y in regs: regs[y] = val(x)\n", + " elif op == 'inc': regs[x] += 1 \n", + " elif op == 'dec': regs[x] -= 1 \n", + " elif op == 'jnz' and val(x): pc += val(y) - 1 \n", + " elif op == 'tgl': toggle(code, pc - 1 + val(x))\n", + "\n", + "code = parse_program(Input(23))\n", + "sorted(Counter(profile(code, a=8, b=0, c=0, d=0)).items())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The bulk of the time is in instructions 4–9:\n", + "\n", + " 4: 'cpy b c\\n',\n", + " 5: 'inc a\\n',\n", + " 6: 'dec c\\n',\n", + " 7: 'jnz c -2\\n',\n", + " 8: 'dec d\\n',\n", + " 9: 'jnz d -5\\n',\n", + "\n", + "After tracing through by hand, it becomes clear that this increments `a` by `b * d`. So I'll introduce a `mul` instruction to do that:" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": {}, + "outputs": [], + "source": [ + "def interpret(code, **regs) -> dict:\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", + " inst = code[pc]\n", + " op, x, y = inst[0], inst[1], inst[-1]\n", + " pc += 1\n", + " if op == 'cpy' and y in regs: regs[y] = val(x)\n", + " elif op == 'inc': regs[x] += 1 \n", + " elif op == 'dec': regs[x] -= 1 \n", + " elif op == 'jnz' and val(x): pc += val(y) - 1 \n", + " elif op == 'tgl': toggle(code, pc - 1 + val(x))\n", + " elif op == 'mul': regs[x] += regs[y] * regs[inst[2]]\n", + " return regs" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "Now I just need to replace those instructions 4–9 with a `mul a b d` instruction. But I don't want to change the number of instructions in the program (because of toggles and jumps), so I'll include 5 no-op instruction. I can use `cpy 0 0` for that:" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 23.2: 2.6 msecs, correct answer: 479007221 " + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def crack_safe_with_optimizer(lines, **regs):\n", + " old = \"\"\"\\\n", "cpy b c\n", "inc a\n", "dec c\n", "jnz c -2\n", "dec d\n", - "jnz d -5\n", - "dec b\n", - "cpy b c\n", - "cpy c d\n", - "dec d\n", - "inc c\n", - "jnz d -2\n", - "tgl c\n", - "cpy -16 c\n", - "jnz 1 c\n", - "cpy 84 c\n", - "jnz 75 d\n", - "inc a\n", - "inc d\n", - "jnz d -2\n", - "inc c\n", - "jnz c -5\n", - "'''.strip()\n", + "jnz d -5\"\"\"\n", + " new = \"mul a d b\" + 5 * \"\\ncpy 0 0\"\n", + " lines = cat(lines).replace(old, new).splitlines() \n", + " code = parse_program(lines)\n", + " return interpret(code, **regs)['a']\n", "\n", - "code = [parse(line) for line in text.splitlines()]\n", - "\n", - "regs = dict(a=7, b=0, c=0, d=0)\n", - "\n", - "interpret(code, regs)" + "answer(23.2, 479007221, lambda:\n", + " crack_safe_with_optimizer(Input(23), a=12, b=0, c=0, d=0))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In **part two**, we are told to run the same computation, but with register `a` set to 12. We are also warned that this will take a long time, and we might consider implementing a multiply instruction, but I was too lazy to make sense of the assembly code, and just let my interpreter run to completion, even if it takes a while." - ] - }, - { - "cell_type": "code", - "execution_count": 64, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 25min 21s, sys: 5.37 s, total: 25min 27s\n", - "Wall time: 25min 31s\n" - ] - }, - { - "data": { - "text/plain": [ - "{'a': 479007900, 'b': 1, 'c': 0, 'd': 0}" - ] - }, - "execution_count": 64, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "code = [parse(line) for line in text.splitlines()]\n", - "\n", - "regs = dict(a=12, b=0, c=0, d=0)\n", - "\n", - "%time interpret(code, regs)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Well, it completed, and gave me the right answer. But I feel like the intent of *Advent of Code* is like *Project Euler*: all code should run in about a minute or less. So I don't think this counts as a \"real\" solution." + "That's 40,000 times faster!" ] }, { @@ -2931,70 +3120,68 @@ "source": [ "# [Day 24](http://adventofcode.com/2016/day/24) Air Duct Spelunking \n", "\n", - "This is another maze-solving problem; it should be easy for my `astar_search`. First the maze:" + "This is another maze-solving problem; it should be easy for my `astar_search`. The tricky part is that we have to visit all the digits in the maze, starting at `0`, and not necessarily going in order. How many digits are there?" ] }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 62, "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "maze = tuple(Input(24))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The tricky part is that we have to visit all the digits in the maze, starting at `0`, and not necessarily going in order. How many digits are there?" - ] - }, - { - "cell_type": "code", - "execution_count": 66, - "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "{'\\n', '#', '.', '0', '1', '2', '3', '4', '5', '6', '7'}" + "Counter({'.': 3804,\n", + " '#': 3247,\n", + " '3': 1,\n", + " '1': 1,\n", + " '2': 1,\n", + " '0': 1,\n", + " '4': 1,\n", + " '5': 1,\n", + " '7': 1,\n", + " '6': 1})" ] }, - "execution_count": 66, + "execution_count": 62, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "set(cat(maze))" + "maze = Input(24).read().splitlines()\n", + "Counter(cat(maze))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "OK, there are 8 digits. What is the start square (the square that currently holds a `'0'`)?" + "OK, there are 8 digits, which each appear once. What is the start square (the square that currently holds a `'0'`)?" ] }, { "cell_type": "code", - "execution_count": 67, + "execution_count": 63, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "(33, 11)" + "(1, 19)" ] }, - "execution_count": 67, + "execution_count": 63, "metadata": {}, "output_type": "execute_result" } @@ -3013,18 +3200,21 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": 64, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "448" + "Puzzle 24.1: 297.2 msecs, correct answer: 430 " ] }, - "execution_count": 68, + "execution_count": 64, "metadata": {}, "output_type": "execute_result" } @@ -3044,8 +3234,8 @@ " visited1 = (visited if c in visited or c == '.' else cat(sorted(visited + c)))\n", " yield (x1, y1), visited1\n", "\n", - "path = astar_search((zero, '0'), h, moves)\n", - "len(path) - 1" + "answer(24.1, 430, lambda:\n", + " len(astar_search((zero, '0'), h, moves)) - 1)" ] }, { @@ -3057,18 +3247,21 @@ }, { "cell_type": "code", - "execution_count": 69, + "execution_count": 65, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "672" + "Puzzle 24.2: 975.8 msecs, correct answer: 700 " ] }, - "execution_count": 69, + "execution_count": 65, "metadata": {}, "output_type": "execute_result" } @@ -3079,8 +3272,8 @@ " pos, visited = state\n", " return 8 - len(visited) + cityblock_distance(pos, zero)\n", "\n", - "path2 = astar_search((zero, '0'), h2, moves)\n", - "len(path2) - 1" + "answer(24.2, 700, lambda:\n", + " len(astar_search((zero, '0'), h2, moves)) - 1)" ] }, { @@ -3096,20 +3289,18 @@ }, { "cell_type": "code", - "execution_count": 70, - "metadata": { - "collapsed": true - }, + "execution_count": 66, + "metadata": {}, "outputs": [], "source": [ - "def interpret(code, regs, steps=BIG):\n", + "def interpret(code, steps, **regs):\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", " if not (0 <= pc < len(code)):\n", " return\n", - " inst = code[pc]\n", + " inst = code[pc] \n", " op, x, y = inst[0], inst[1], inst[-1]\n", " pc += 1\n", " if op == 'cpy' and y in regs: regs[y] = val(x)\n", @@ -3129,61 +3320,33 @@ }, { "cell_type": "code", - "execution_count": 71, + "execution_count": 67, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [ { "data": { "text/plain": [ - "198" + "Puzzle 25.1: 910.8 msecs, correct answer: 189 " ] }, - "execution_count": 71, + "execution_count": 67, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "text = '''\n", - "cpy a d\n", - "cpy 4 c\n", - "cpy 633 b\n", - "inc d\n", - "dec b\n", - "jnz b -2\n", - "dec c\n", - "jnz c -5\n", - "cpy d a\n", - "jnz 0 0\n", - "cpy a b\n", - "cpy 0 a\n", - "cpy 2 c\n", - "jnz b 2\n", - "jnz 1 6\n", - "dec b\n", - "dec c\n", - "jnz c -4\n", - "inc a\n", - "jnz 1 -7\n", - "cpy 2 b\n", - "jnz c 2\n", - "jnz 1 4\n", - "dec b\n", - "dec c\n", - "jnz 1 -4\n", - "jnz 0 0\n", - "out b\n", - "jnz a -19\n", - "jnz 1 -21\n", - "'''.strip()\n", + "def clock_signal(lines) -> int:\n", + " code = parse_program(lines)\n", + " return first(a for a in range(1, BIG) if repeats(a, code))\n", "\n", - "code = [parse(line) for line in text.splitlines()]\n", - "\n", - "def repeats(a, code, steps=10**6, minsignals=100):\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", - " signals = interpret(code, dict(a=a, b=0, c=0, d=0), steps)\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", " if signal != expected:\n", @@ -3191,7 +3354,84 @@ " # We'll say \"yes\" if the code outputs at least a minimum number of 0, 1, ... signals, and nothing else.\n", " return i >= minsignals\n", " \n", - "first(a for a in range(1, BIG) if repeats(a, code))" + "answer(25.1, 189, lambda:\n", + " clock_signal(Input(25)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Summary\n", + "\n", + "I got all the puzzles solved, but some of my code ran slowly: 11.2 took 4 minutes, and here were 9 other puzzles that took over a second of run time. But the median was just 2.6 milliseconds." + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time: sum = 4.7 MINUTES, mean = 5,798.9 msecs, median = 2.6 msecs, max = 3.9 MINUTES\n", + "\n", + "Puzzle 1.1: 0.5 msecs, correct answer: 262.0 \n", + "Puzzle 1.2: 0.6 msecs, correct answer: 131.0 \n", + "Puzzle 2.1: 0.6 msecs, correct answer: 14894 \n", + "Puzzle 2.2: 0.5 msecs, correct answer: 26B96 \n", + "Puzzle 3.1: 0.2 msecs, correct answer: 869 \n", + "Puzzle 3.2: 0.4 msecs, correct answer: 1544 \n", + "Puzzle 4.1: 4.0 msecs, correct answer: 409147 \n", + "Puzzle 4.2: 1.6 msecs, correct answer: 991 \n", + "Puzzle 5.1: 7,717.6 msecs, correct answer: 1a3099aa \n", + "Puzzle 5.2: 11,693.9 msecs, correct answer: 694190cd \n", + "Puzzle 6.1: 0.5 msecs, correct answer: dzqckwsd \n", + "Puzzle 6.2: 0.4 msecs, correct answer: lragovly \n", + "Puzzle 7.1: 12.7 msecs, correct answer: 118 \n", + "Puzzle 7.2: 118.2 msecs, correct answer: 260 \n", + "Puzzle 8.1: 0.7 msecs, correct answer: 110 \n", + "Puzzle 8.2: 0.5 msecs, correct answer: ZJHRKCPLYJ \n", + "Puzzle 9.1: 0.4 msecs, correct answer: 115118 \n", + "Puzzle 9.2: 1.8 msecs, correct answer: 11107527530 \n", + "Puzzle 10.1: 0.8 msecs, correct answer: 56 \n", + "Puzzle 10.2: 0.5 msecs, correct answer: 7847 \n", + "Puzzle 11.1: 3,240.6 msecs, correct answer: 31 \n", + "Puzzle 11.2: 3.9 MINUTES, correct answer: 55 \n", + "Puzzle 12.1: 109.0 msecs, correct answer: 318007 \n", + "Puzzle 12.2: 3,059.1 msecs, correct answer: 9227661 \n", + "Puzzle 13.1: 0.3 msecs, correct answer: 82 \n", + "Puzzle 13.2: 0.2 msecs, correct answer: 138 \n", + "Puzzle 14.1: 130.7 msecs, correct answer: 16106 \n", + "Puzzle 14.2: 14,479.4 msecs, correct answer: 22423 \n", + "Puzzle 15.1: 53.4 msecs, correct answer: 203660 \n", + "Puzzle 15.2: 571.1 msecs, correct answer: 2408135 \n", + "Puzzle 16.1: 0.0 msecs, correct answer: 01110011101111011\n", + "Puzzle 16.2: 1,532.5 msecs, correct answer: 11001111011000111\n", + "Puzzle 17.1: 0.0 msecs, correct answer: RRRLDRDUDD \n", + "Puzzle 17.2: 43.6 msecs, correct answer: 706 \n", + "Puzzle 18.1: 0.6 msecs, correct answer: 1987 \n", + "Puzzle 18.2: 2,019.0 msecs, correct answer: 19984714 \n", + "Puzzle 19.1: 0.0 msecs, correct answer: 1815603 \n", + "Puzzle 19.2: 320.8 msecs, correct answer: 1410630 \n", + "Puzzle 20.1: 0.0 msecs, correct answer: 32259706 \n", + "Puzzle 20.2: 0.1 msecs, correct answer: 113 \n", + "Puzzle 21.1: 0.4 msecs, correct answer: gfdhebac \n", + "Puzzle 21.2: 2,336.8 msecs, correct answer: dhaegfbc \n", + "Puzzle 22.1: 130.1 msecs, correct answer: 1045 \n", + "Puzzle 22.2: 1,271.5 msecs, correct answer: 265 \n", + "Puzzle 23.1: 7.3 msecs, correct answer: 10661 \n", + "Puzzle 23.2: 2.6 msecs, correct answer: 479007221 \n", + "Puzzle 24.1: 297.2 msecs, correct answer: 430 \n", + "Puzzle 24.2: 975.8 msecs, correct answer: 700 \n", + "Puzzle 25.1: 910.8 msecs, correct answer: 189 \n" + ] + } + ], + "source": [ + "summary(answers)" ] }, { @@ -3204,7 +3444,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -3218,9 +3458,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.1" + "version": "3.13.3" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 4 }