{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
Peter Norvig, December 2016
\n", "\n", "# Advent of Code 2016 Solutions\n", "\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. I'm not a threat to dominate the leaderboards, so I concentrate on clean code rather than solving speed.\n", "\n", "\n", "To understand the problems, you will have to read the full description in the **\"[Day 1](http://adventofcode.com/2016/day/1):\"** link in each day's section header.\n", "\n", "# Day 0: Getting Ready\n", "\n", "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", "- 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 functionto 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:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [], "source": [ "################ Imports\n", "\n", "import re\n", "import numpy as np\n", "import math\n", "import time\n", "\n", "from collections import Counter, defaultdict, namedtuple, deque\n", "from functools import lru_cache, cache\n", "from itertools import permutations, combinations, combinations_with_replacement, chain, cycle, product, islice\n", "from heapq import heappop, heappush\n", "from statistics import mean, median\n", "from typing import Iterable\n", "\n", "################ Daily `Input` and `answer`\n", "\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", "answers = {} # `answers` is a dict of {puzzle_number: answer}\n", "\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}: {self.msecs:8,.1f} milliseconds, {correct} answer: {self.got:<15}{expected}'\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__} = {fn(times):,.1f}' \n", " stats = [stat(fn, times) for fn in (sum, mean, median, max)]\n", " print(f'Time in milliseconds: {\", \".join(stats)}\\n')\n", " for day in sorted(answers):\n", " print(answers[day])\n", "\n", "################ Utility Functions\n", "\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 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 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) -> Iterable[tuple]:\n", " \"Yield all subsets of items.\"\n", " items = list(iterable)\n", " for r in range(len(items)+1):\n", " for c in combinations(items, r):\n", " yield c\n", "\n", "def trace1(f):\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", " return result\n", " return traced_f\n", "\n", "################ 2D (x, y) Points\n", "\n", "Point = tuple[int, int] # Type for a 2D point\n", "def X(point) -> int: return point[0]\n", "def Y(point) -> int: return point[1]\n", "\n", "def neighbors4(point: Point) -> tuple[Point]: \n", " \"\"\"The four neighbors (without diagonals).\"\"\"\n", " x, y = point\n", " return ((x+1, y), (x-1, y), (x, y+1), (x, y-1))\n", "\n", "def neighbors8(point: Point) -> tuple[Point]: \n", " \"\"\"The eight neighbors (with diagonals).\"\"\"\n", " x, y = point \n", " return ((x+1, y), (x-1, y), (x, y+1), (x, y-1),\n", " (x+1, y+1), (x-1, y-1), (x+1, y-1), (x-1, y+1))\n", "\n", "def cityblock_distance(p, q=(0, 0)) -> int: \n", " \"\"\"City block distance between two points.\"\"\"\n", " return abs(X(p) - X(q)) + abs(Y(p) - Y(q))\n", "\n", "################ A* Search\n", "\n", "def astar_search(start, h_func, moves_func):\n", " \"\"\"Find a shortest sequence of states from start to a goal state (a state s with h_func(s) == 0).\"\"\"\n", " 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", " (f, s) = heappop(frontier)\n", " if h_func(s) == 0:\n", " return Path(previous, s)\n", " for s2 in moves_func(s):\n", " new_cost = path_cost[s] + 1\n", " if s2 not in path_cost or new_cost < path_cost[s2]:\n", " heappush(frontier, (new_cost + h_func(s2), s2))\n", " path_cost[s2] = new_cost\n", " previous[s2] = s\n", " return None\n", " \n", "def Path(previous, s) -> list: \n", " \"\"\"Return a list of states that lead to state s, according to the previous dict.\"\"\"\n", " return ([] if (s is None) else Path(previous, previous[s]) + [s])\n", "\n", "def astar_step_count(*args) -> int:\n", " \"\"\"The number of steps to get to the goal.\n", " (the length of the path, not counting the starting state).\"\"\"\n", " path = astar_search(*args)\n", " return len(path) - 1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 1](http://adventofcode.com/2016/day/1): No Time for a Taxicab\n", "\n", "Given a sequence of moves, such as `\"R2, L3\"`, which means turn 90° to the right and go forward 2 blocks, then turn 90° left and go 3 blocks, how many blocks do we end up away from the start? I make the following choices:\n", "* **Intersection Points** in the city grid will be represented as `(x, y)` pairs.\n", "* **Headings and turns** are represented by unit vectors in a `headings` list, and an index into the list that gets updated. \n", "* **Moves** of the form `\"R20\"` will be parsed as `(+1, 20)` and `\"L30\"` as `(-1, 30)`.\n", "\n", "To solve the puzzle with the function `how_far(moves)`, I initialize the starting location as the origin and the starting heading as North, and follow the list of moves, updating the heading and location on each step, before returning the distance from the final location to the origin." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 1.1: 0.4 milliseconds, correct answer: 262 " ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def how_far(text: str) -> int:\n", " \"\"\"After following moves, how far away from the origin do we end up?\"\"\"\n", " dxs = [0, 1, 0, -1] # N, E, S, W headings\n", " dys = [1, 0, -1, 0] # N, E, S, W headings\n", " x, y, h = 0, 0, 0 # Begin at origin, heading North\n", " for (turn, dist) in parse_moves(text):\n", " h = (h + turn) % 4\n", " x += dxs[h] * dist\n", " y += dys[h] * dist\n", " return cityblock_distance((x, y))\n", "\n", "def parse_moves(text: str) -> list[tuple]:\n", " \"\"\"Return a list of pairs: from text of form \"R2, L42, ...\", return [(+1, 2), (-1, 42), ...].\"\"\"\n", " turns = dict(R=+1, L=-1)\n", " return [(turns[RL], int(d))\n", " for (RL, d) in re.findall(r'(R|L)(\\d+)', text)]\n", "\n", "answer(1.1, 262, lambda:\n", " how_far(Input(1).read()))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In **part two** of this puzzle, I have to find the first point that is visited twice. To support that, I keep track of the set of visited points. My first submission was wrong, because I didn't consider that the first point visited twice might be in the middle of a move, not the end, so I added the \"`for`\" loop to iterate over the path of a move, one point at a time." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 1.2: 0.3 milliseconds, correct answer: 131 " ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def visited_twice(text: str) -> int:\n", " \"\"\"Following moves, find the first location we visit twice, and return the distance to it.\"\"\"\n", " dxs = [0, 1, 0, -1] # N, E, S, W headings\n", " dys = [1, 0, -1, 0] # N, E, S, W headings\n", " x, y, h = 0, 0, 0 # Begin at origin, heading North\n", " visited = {(x, y)}\n", " for (turn, dist) in parse_moves(text):\n", " h = (h + turn) % 4\n", " for _ in range(dist):\n", " x += dxs[h]\n", " y += dys[h]\n", " if (x, y) in visited:\n", " return cityblock_distance((x, y))\n", " else:\n", " visited.add((x, y))\n", "\n", "answer(1.2, 131, lambda:\n", " visited_twice(Input(1).read()))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 2](http://adventofcode.com/2016/day/2): Bathroom Security\n", "\n", "Given instructions in the form of a sequence of Up/Down/Right/Left moves, such as `'ULL'`, output the keys on the bathroom lock keypad that the instructions correspond to. Start at the 5 key. Representation choices:\n", "* **Keypad**: a keypad is an array of strings: `keypad[y][x]` is a key. The character `'.'` indicates a location that is `off` the keypad; by surrounding the keys with a border of `off` characters, I avoid having to write code that checks to see if we hit the edge.\n", "* **Key**: A key is a character other than `'.'`.\n", "* **Instructions**: A sequence of lines of `\"UDRL\"` characters, where each line leads to the output of one key." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 2.1: 0.5 milliseconds, correct answer: 14894 " ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Keypad = str.split # Function to make a Keypad\n", "Key = str # Type for a key: a single character\n", "\n", "keypad = Keypad(\"\"\"\n", ".....\n", ".123.\n", ".456.\n", ".789.\n", ".....\n", "\"\"\")\n", "\n", "assert keypad[2][2] == '5'\n", "\n", "off = '.'\n", "\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", " for C in line:\n", " x, y = move(C, x, y)\n", " yield keypad[y][x]\n", "\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", " elif C == 'U' and keypad[y-1][x] is not off: y -= 1\n", " elif C == 'D' and keypad[y+1][x] is not off: y += 1\n", " return x, y\n", "\n", "assert move('U', 2, 2) == (2, 1)\n", "assert move('U', 2, 1) == (2, 1)\n", "assert cat(decode(\"ULL RRDDD LURDL UUUUD\".split())) == '1985'\n", "\n", "answer(2.1, '14894', lambda:\n", " cat(decode(Input(2))))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In **part two**, we have to deal with a different keypad. I won't need any new functions, but I will need to redefine the global variable `keypad`, and provide `decode` with the new `x` and `y` coordinates of the `5` key:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 2.2: 0.6 milliseconds, correct answer: 26B96 " ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "keypad = Keypad(\"\"\"\n", ".......\n", "...1...\n", "..234..\n", ".56789.\n", "..ABC..\n", "...D...\n", ".......\n", "\"\"\")\n", "\n", "assert keypad[3][1] == '5'\n", "\n", "answer(2.2, '26B96', lambda:\n", " cat(decode(Input(2), x=1, y=3)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 3](http://adventofcode.com/2016/day/3): Squares With Three Sides\n", "\n", "From a file of numbers, three to a line, count the number that represent valid triangles; that is, numbers that satisfy the [triangle inequality](https://en.wikipedia.org/wiki/Triangle_inequality)." ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 3.1: 0.2 milliseconds, correct answer: 869 " ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def is_triangle(sides: tuple[int, int, int]) -> bool:\n", " \"Do these side lengths form a valid triangle?\"\n", " x, y, z = sorted(sides)\n", " return x + y > z\n", "\n", "triangles = [ints(line) for line in Input(3)]\n", "\n", "answer(3.1, 869, lambda:\n", " sum(map(is_triangle, triangles)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In **part two**, the triangles are denoted not by three sides in the same line, but by three sides in the same column. For example, given the text:\n", "\n", " 101 301 501\n", " 102 302 502\n", " 103 303 503\n", " 201 401 601\n", " 202 402 602\n", " 203 403 603\n", " \n", "The triangles are:\n", "\n", " [101, 102, 103]\n", " [301, 302, 303]\n", " [501, 502, 503]\n", " [201, 202, 203]\n", " [401, 402, 403]\n", " [601, 602, 603]\n", " \n", "The task is still to count the number of valid triangles." ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 3.2: 0.3 milliseconds, correct answer: 1544 " ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def invert(triangles):\n", " \"Take each 3 lines and transpose them.\"\n", " for i in range(0, len(triangles), 3):\n", " yield from transpose(triangles[i:i+3])\n", "\n", "answer(3.2, 1544, lambda:\n", " sum(map(is_triangle, invert(triangles))))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 4](http://adventofcode.com/2016/day/4): Security Through Obscurity\n", "\n", "Given a list of room names like `\"aaaaa-bbb-z-y-x-123[abxyz]\"`, consisting of an encrypted name followed by a dash, a sector ID, and a checksum in square brackets, compute the sum of the sectors of the valid rooms. A room is valid if the checksum is the five most common characters, in order (ties listed in alphabetical order)." ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 4.1: 4.0 milliseconds, correct answer: 409147 " ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def parse_room_name(line: str) -> tuple[str, str, str]: \n", " \"\"\"Return (name, sector, checksum).\"\"\"\n", " return re.match(r\"(.+)-(\\d+)\\[([a-z]+)\\]\", line).groups()\n", "\n", "def sector(line: str) -> int:\n", " \"\"\"Return the sector number if valid, or 0 if not.\"\"\"\n", " name, sector, checksum = parse_room_name(line)\n", " return int(sector) if valid(name, checksum) else 0\n", "\n", "def valid(name: str, checksum: str) -> bool:\n", " \"\"\"Determine if name is valid according to checksum.\"\"\"\n", " counts = Counter(name.replace('-', '')) \n", " # Note: counts.most_common(5) doesn't work because it breaks ties arbitrarily.\n", " letters = sorted(counts, key=lambda L: (-counts[L], L))\n", " return checksum == cat(letters[:5])\n", "\n", "assert parse_room_name('aaaaa-bbb-z-y-x-123[abxyz]') == ('aaaaa-bbb-z-y-x', '123', 'abxyz')\n", "assert sector('aaaaa-bbb-z-y-x-123[abxyz]') == 123\n", "assert valid('aaaaa-bbb-z-y-x', 'abxyz')\n", "\n", "answer(4.1, 409147, lambda:\n", " sum(map(sector, Input(4))))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Initially I had a bug: I forgot the `name.replace('-', '')` to make sure that we don't count hyphens. \n", "\n", "In **part two**, we are asked *\"What is the sector ID of the room where North Pole objects are stored?\"* We are told that names are to be decrypted by a shift cipher, shifting each letter forward in the alphabet by the sector number." ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 4.2: 1.6 milliseconds, correct answer: 991 " ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def decrypt(line: str) -> str:\n", " \"\"\"Decrypt the line (shift the name by sector; discard checksum).\"\"\"\n", " name, sector, _ = parse_room_name(line)\n", " return shift(name, int(sector)) + ' ' + sector\n", "\n", "def shift(text, N, alphabet='abcdefghijklmnopqrstuvwxyz') -> str:\n", " \"\"\"Shift cipher: letters in text rotate forward in alphabet by N places.\"\"\"\n", " N = N % len(alphabet)\n", " tr = str.maketrans(alphabet, alphabet[N:] + alphabet[:N])\n", " return text.translate(tr)\n", "\n", "assert shift('hal', 1) == 'ibm'\n", "assert shift('qzmt-zixmtkozy-ivhz', 343) == 'very-encrypted-name'\n", "\n", "answer(4.2, 991, lambda: \n", " ints(first(line for line in map(decrypt, Input(4)) if 'north' in line))[0])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 5](http://adventofcode.com/2016/day/5): How About a Nice Game of Chess?\n", "\n", "This puzzle involves md5 hashes and byte encodings; it took me a while to look up how to do that. What I have to do, for integers starting at 0, is concatenate my door ID string with the integer, get the md5 hex hash, and if the first five digits of the hash are 0, collect the sixth digit; repeat until I have collected eight digits:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " 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": [ "Puzzle 5.1: 7,596.7 milliseconds, correct answer: 1a3099aa " ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import hashlib\n", "\n", "door = \"uqwqemis\"\n", "\n", "def find_password(door: str, verbose=True) -> str:\n", " \"\"\"First 8 sixth digits of md5 hashes of door+i that begin with '00000'.\"\"\"\n", " password = ''\n", " for i in range(BIG):\n", " x = hashlib.md5(bytes(door + str(i), 'utf-8')).hexdigest()\n", " if x.startswith('00000'):\n", " password += x[5]\n", " if verbose: print(f'{i:8} {x} {password}') # Just to see something happen\n", " if len(password) == 8: \n", " return password\n", "\n", "answer(5.1, '1a3099aa', lambda:\n", " find_password(door))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In **part two**, the sixth digit of the hash that starts with `'00000'` is to be treated as an index that tells where in the password to place the *seventh* digit of the hash. For example, if the sixth digit is `2` and the seventh digit is `a`, then place `a` as the second digit of the final password. Do nothing if the sixth digit is not less than 8, or if a digit has already been placed at that index location." ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " 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": [ "Puzzle 5.2: 11,989.2 milliseconds, correct answer: 694190cd " ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "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", " x = hashlib.md5(bytes(door + str(i), 'utf-8')).hexdigest()\n", " if x.startswith('00000'):\n", " index = int(x[5], 16)\n", " if index < 8 and password[index] is off:\n", " password[index] = x[6]\n", " if verbose: print(f'{i:8} {x} {cat(password)}') # Just to see something happen\n", " if off not in password:\n", " return cat(password)\n", "\n", "answer(5.2, '694190cd', lambda:\n", " find_tougher_password(door))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 6](http://adventofcode.com/2016/day/6): Signals and Noise\n", "\n", "Given a file, where each line is a string of letters and every line has the same length, find the most common letter in each column. We can easily do this with the help of `Counter.most_common`. (Note I use `stream.read().split()` so that I don't get the `'\\n'` at the end of each line.)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 6.1: 0.6 milliseconds, correct answer: dzqckwsd " ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "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": [ "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": 13, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 6.2: 0.4 milliseconds, correct answer: lragovly " ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "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)))" ] }, { "cell_type": "markdown", "metadata": {}, "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 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", "- Split on brackets to get `['abcd', '1234', 'fghi', '56789', 'zz']`\n", "- Outsides of brackets are `'abcd, fghi, zz'` at indexes 0, 2, 4.\n", "- Insides of brackets are `'1234, 56789'` at indexes 1, 3." ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 7.1: 12.3 milliseconds, correct answer: 118 " ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def abba(text): return any(a == d != b == c for (a, b, c, d) in subsequences(text, 4))\n", "def subsequences(seq, n): return [seq[i:i+n] for i in range(len(seq) + 1 - n)]\n", "def segment(line): return re.split(r'\\[|\\]', line)\n", "def outsides(segments): return ', '.join(segments[0::2])\n", "def insides(segments): return ', '.join(segments[1::2])\n", "def tls(segments): return abba(outsides(segments)) and not abba(insides(segments))\n", "\n", "answer(7.1, 118, lambda:\n", " sum(tls(segment(line)) for line in Input(7)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here are some tests:" ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [], "source": [ "assert abba('abba') and not abba('aaaa') and not abba('abbc')\n", "assert subsequences('abcdefg', 4) == ['abcd', 'bcde', 'cdef', 'defg']\n", "assert segment('abcd[1234]fghi[56789]zz') == ['abcd', '1234', 'fghi', '56789', 'zz']\n", "assert outsides(['abcd', '1234', 'fghi', '56789', 'zz']) == 'abcd, fghi, zz'\n", "assert insides(['abcd', '1234', 'fghi', '56789', 'zz']) == '1234, 56789'\n", "assert tls(['abba', '123']) \n", "assert not tls(['bookkeeper', '123']) and not tls(['abba', 'xxyyx'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In **part two**, we are asked to count the number of *SSL* lines: an *SSL* is when there is an *ABA* outside brackets, and the corresponding *BAB* inside brackets. An *ABA* is a three-character sequence with first and third (but not all three) the same. The corresponding *BAB* has the first character of the *ABA* surrounded by two copies of the second character." ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 7.2: 119.5 milliseconds, correct answer: 260 " ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "alphabet = 'abcdefghijklmnopqrstuvwxyz'\n", "\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", "answer(7.2, 260, lambda:\n", " sum(ssl(segment(line)) for line in Input(7)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 8](http://adventofcode.com/2016/day/8): Two-Factor Authentication\n", "\n", "Given an array of pixels on a screen, follow commands that can:\n", "- Turn on a sub-rectangle of pixels in the upper left corner: `rect 3x2`\n", "- Rotate a row of pixels: `rotate row y=0 by 4`\n", "- Rotate a column of pixels: `rotate column x=1 by 1`\n", "\n", "Then count the total number of `1` pixels in the screen.\n", "\n", "I will use `numpy` two-dimensional arrays, mostly because of the `screen[:, A]` notation for getting at a column." ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 8.1: 0.6 milliseconds, correct answer: 110 " ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "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", " screen[:B, :A] = 1\n", " elif cmd.startswith('rotate row'):\n", " screen[A, :] = rotate(screen[A, :], B)\n", " elif cmd.startswith('rotate col'):\n", " screen[:, A] = rotate(screen[:, A], B)\n", "\n", "def rotate(items, n) -> np.array: return np.append(items[-n:], items[:-n])\n", "\n", "def Screen(shape=(6, 50)) -> np.array: return np.zeros(shape, dtype=int)\n", "\n", "def run(commands) -> np.array:\n", " \"\"\"Do all the commands and return the final pixel array.\"\"\"\n", " screen = Screen()\n", " for cmd in commands:\n", " interpret(cmd, screen) \n", " return screen\n", "\n", "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, then type that in to AoC and record it as my `answer`:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "@@@@ @@ @ @ @@@ @ @ @@ @@@ @ @ @ @@ \n", " @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ \n", " @ @ @@@@ @ @ @@ @ @ @ @ @ @ @ \n", " @ @ @ @ @@@ @ @ @ @@@ @ @ @ \n", "@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ \n", "@@@@ @@ @ @ @ @ @ @ @@ @ @@@@ @ @@ \n" ] }, { "data": { "text/plain": [ "Puzzle 8.2: 0.7 milliseconds, correct answer: ZJHRKCPLYJ " ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def show_message(commands) -> None:\n", " \"\"\"Run the commands and print the resulting pixel array.\"\"\"\n", " for row in run(commands):\n", " print(cat(' @'[pixel] for pixel in row))\n", "\n", "answer(8.2, 'ZJHRKCPLYJ', lambda:\n", " show_message(Input(8)) or 'ZJHRKCPLYJ')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 9](http://adventofcode.com/2016/day/9): Explosives in Cyberspace\n", "\n", "In this puzzle we are asked to decompress text of the form `'A(2x5)BCD'`, where the `'(2x5)'` means to make 5 copies of the next 2 characters, yielding `'ABCBCBCBCBCD'`. We'll go through the input text, a character at a time, and if a `re` matcher detects a `'(CxR)'` pattern, process it; otherwise just collect the character. Note that the `C` characters that are to be repeated `R` times are taken literally; even if they contain an embedded `'(1x5)'`." ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 9.1: 0.6 milliseconds, correct answer: 115118 " ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "matcher = re.compile(r'[(](\\d+)x(\\d+)[)]').match # e.g. matches \"(2x5)\" as ('2', '5')\n", "\n", "def decompress(s: str) -> str:\n", " \"\"\"Decompress string s by interpreting '(2x5)' as making 5 copies of the next 2 characters.\"\"\"\n", " s = re.sub(r'\\s', '', s) # \"whitespace is ignored\"\n", " result = []\n", " i = 0\n", " while i < len(s):\n", " m = matcher(s, i)\n", " if m:\n", " i = m.end() # Advance to end of '(CxR)' match\n", " C, R = map(int, m.groups())\n", " result.append(s[i:i+C] * R) # Collect the C characters, repeated R times\n", " i += C # Advance past the C characters \n", " else:\n", " result.append(s[i]) # Collect 1 regular character\n", " i += 1 # Advance past it\n", " return cat(result)\n", "\n", "answer(9.1, 115118, lambda:\n", " len(decompress(Input(9).read())))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In **part two**, the copied characters *are* recursively decompressed. So, given `'(8x2)(3x3)ABC'`, the `(8x2)` directive picks out the 8 characters `'(3x3)ABC'`, which would then be decompressed to get `'ABCABCABC'` and then the `'x2'` is applied to get `'ABCABCABCABCABCABC'`. However, for this part, we are not asked to actually build up the decompressed string, just to compute its length:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 9.2: 2.0 milliseconds, correct answer: 11107527530 " ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "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", " length = 0\n", " i = 0\n", " while i < len(s):\n", " m = matcher(s, i)\n", " if m:\n", " C, R = map(int, m.groups())\n", " i = m.end(0) # Advance to end of '(CxR)'\n", " length += R * decompress_length(s[i:i+C]) # Decompress C chars and add to length\n", " i += C # Advance past the C characters \n", " else:\n", " length += 1 # Add 1 regular character to length\n", " i += 1 # Advance past it\n", " return length\n", "\n", "answer(9.2, 11107527530, lambda:\n", " decompress_length(Input(9).read()))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here are some tests:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "assert decompress('A(2x5)BCD') == 'ABCBCBCBCBCD'\n", "assert decompress('ADVENT') == 'ADVENT'\n", "assert decompress('(3x3)XYZ') == 'XYZXYZXYZ'\n", "assert decompress('(5x4)(3x2)') == '(3x2)(3x2)(3x2)(3x2)'\n", "assert decompress('X(8x2)(3x3)ABCY') == 'X(3x3)ABC(3x3)ABCY'\n", " \n", "assert decompress_length('(8x2)(3x3)ABC') == 18\n", "assert decompress_length('(25x3)(3x3)ABC(2x3)XY(5x2)PQRSTX(18x9)(3x2)TWO(5x7)SEVEN') == 445\n", "assert decompress_length('(9x999)(2x999)xx') == 999 * 999 * 2 == 1996002" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 10](http://adventofcode.com/2016/day/10): Balance Bots\n", "\n", "In this puzzle, a fleet of robots exchange some chips from input bins, \n", "passing them among themselves, and eventually putting them in output bins. We are given instructions like this:\n", "\n", " value 5 goes to bot 2\n", " bot 2 gives low to bot 1 and high to bot 0\n", " value 3 goes to bot 1\n", " bot 1 gives low to output 1 and high to bot 0\n", " bot 0 gives low to output 2 and high to output 0\n", " value 2 goes to bot 2\n", " \n", "At first I thought I just had to interpret these instructions sequentially, but then I realized this is actually a *data flow* problem: *whenever* a bot acquires two chips, it passes the low number chip to one destination and the high number to another. So my representation choices are:\n", "- Bots and bins are represented as strings: `'bot 1'` and `'output 2'`.\n", "- Chips are represented by ints. (Not strings, because we want 9 to be less than 10).\n", "- Keep track of which bot currently has which chip(s) with a dict: `has['bot 2'] = {5}`\n", "- Keep track of what a bot does when it gets 2 chips with a dict: `gives['bot 1'] = ('output 1', 'bot 0')`\n", "- Pull this information from instructions with `re.findall`. The order of instructions is not important.\n", "- A function, `give`, moves a chip to a recipient, and if the recipient now has two chips, that triggers two more `give` calls." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 10.1: 0.6 milliseconds, correct answer: 56 " ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def bots(instructions, goal={17, 61}) -> dict:\n", " \"\"\"Follow the data flow instructions, returning a dict of who has what,\n", " and the number of the winner: the bot who gets the goal set of chips.\"\"\"\n", " \n", " def give(giver, chip, recip):\n", " \"\"\"Pass the chip from giver to recipient.\"\"\"\n", " has[giver].discard(chip)\n", " has[recip].add(chip)\n", " chips = has[recip]\n", " if chips == goal:\n", " has['winner'] = first(ints(recip))\n", " if len(chips) == 2:\n", " give(recip, min(chips), gives[recip][0])\n", " give(recip, max(chips), gives[recip][1])\n", " \n", " has = defaultdict(set) # who has what\n", " gives = {giver: (dest1, dest2) # who will give what; used by `give` function\n", " for (giver, dest1, dest2) \n", " in re.findall(r'(bot \\d+) gives low to (\\w+ \\d+) and high to (\\w+ \\d+)', instructions)}\n", " for (chip, recip) in re.findall(r'value (\\d+) goes to (\\w+ \\d+)', instructions):\n", " give('input bin', int(chip), recip)\n", " return has\n", "\n", "answer(10.1, 56, lambda:\n", " bots(Input(10).read())['winner'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In **part two**, we are asked for the product of the three output bins numbered 0 to 2:" ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 10.2: 0.6 milliseconds, correct answer: 7847 " ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "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", "answer(10.2, 7847, lambda:\n", " bin_product(Input(10).read()))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 11](http://adventofcode.com/2016/day/11): Radioisotope Thermoelectric Generators \n", "\n", "Here is my input. I'll just read this with my eyes and hand-code it. I won't write a parser for it.\n", "\n", " The first floor contains a hydrogen-compatible microchip and a lithium-compatible microchip.\n", " The second floor contains a hydrogen generator.\n", " The third floor contains a lithium generator.\n", " The fourth floor contains nothing relevant.\n", " \n", "The goal is to get everything to the fourth floor, moving at most two things at a time up or down a floor, and never leaving a microchip on a floor with a generator, unless it is plugged in to its own generator. I *knew* my `astar_search` function would come in handy! To search for the shortest path to the goal, I need to provide:\n", "- an initial state of the world: `initial`.\n", "- a heuristic function which estimates how many moves away from the goal a state is: `h_to_top`.\n", "- a function `moves` that says what states can be reached by carrying some stuff up or down a floor in the elevator.\n", "\n", "I initially represented states with a set of objects for each floor. That worked fine, and solved Part 1 in 3 seconds. But Part 2 took 4 minutes. After AoC was over I decided that's too slow; I should go back and find a representation that made the search space smaller. I can do that by taking advantage of the *symmetry* in the problem: what really matters is which microchips are paired with which generators; their names don't matter. For example, my third floor has a promethium generator/microchip pair and a ruthenium microchip/generator pair. If I move either pair, the effect on the puzzle is the same. I shouldn't count moving the promethium pair as a different state than moving the ruthenium pair; they should be the same state.\n", "\n", "To figure out the representation, consider the simple example from AoC:\n", "\n", " #3 . . . . . \n", " #2 . . . LG . \n", " #1 . HG . . . \n", " #0 . . HM . LM \n", "\n", "I'll list the floors for each object name with a dict of `{name: (microchip_floor, generator_floor)}`:\n", "\n", " {'H': (0, 1), 'L': (0, 2)}\n", "\n", "We can then drop the names and sort the tuples:\n", "\n", " ((0, 1), (0, 2))\n", "\n", "we can make that a state:\n", "\n", " State(elevator=0, pairs=((0, 1), (0, 2)))\n", "\n", "From that initial state we get three possible next states, of which only one is legal:\n", "\n", " State(elevator=1, pairs=((0, 2), (1, 1))) # Move the HM up\n", " State(elevator=1, pairs=((1, 1), (1, 2))) # Move the HM and the LM up; illegal because the HG is on 1\n", " State(elevator=1, pairs=((0, 1), (1, 2))) # Move the LM up; illegal because the HG is on 1\n", "\n", "I'm ready to implement a search with this representation:" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 11.1: 58.0 milliseconds, correct answer: 31 " ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "State = namedtuple('State', 'elevator, pairs')\n", "FLOORS = {0, 1, 2, 3}\n", "\n", "# The initial state, corresponding to my problem description.\n", "initial = State(0, tuple(sorted(dict(Th=(0, 0), Pl=(1, 0), St=(1, 0), Pr=(2, 2), Ru=(2, 2)).values())))\n", "assert initial == State(elevator=0, pairs=((0, 0), (1, 0), (1, 0), (2, 2), (2, 2)))\n", "\n", "def is_legal(state) -> bool:\n", " \"\"\"State is legal unless a floor has an RTG and also a microchip with no corresponding RTG.\"\"\"\n", " rtg_floors = {g for (m, g) in state.pairs}\n", " unconnected_chip_floors = {m for (m, g) in state.pairs if m != g}\n", " return not (unconnected_chip_floors & rtg_floors)\n", "\n", "def h_to_top(state) -> int:\n", " \"\"\"An estimate of the number of moves needed to move everything to top.\"\"\"\n", " top = max(FLOORS)\n", " total = sum((top - f) for f in flatten(state.pairs))\n", " return math.ceil(total / 2) # Can move two items in one move.\n", "\n", "def moves(state) -> Iterable[State]:\n", " \"\"\"The legal moves from this state.\"\"\"\n", " L, pairs = state\n", " copy = [list(pair) for pair in pairs] # A mutable copy of the pairs\n", " positions = [(i, j) for i, pair in enumerate(pairs) for j in (0, 1) \n", " if pairs[i][j] == L] # All pairs[i][j] positions that are on this floor.\n", " legal_moves = set()\n", " for L2 in {L + 1, L - 1} & FLOORS: # Possible floors to move to\n", " for (i, j), (k, l) in set(combinations_with_replacement(positions, 2)):\n", " # For all possible moves of 2 things, make the change (on the copy),\n", " # check if it is legal, and then restore the copy.\n", " # `combinations_with_replacement` means we also get moves of 1 thing\n", " copy[i][j] = L2\n", " copy[k][l] = L2\n", " if is_legal(State(L2, copy)):\n", " legal_moves.add(State(L2, tuple(sorted(tuple(pair) for pair in copy))))\n", " copy[i][j] = pairs[i][j]\n", " copy[k][l] = pairs[k][l]\n", " return legal_moves\n", "\n", "answer(11.1, 31, lambda:\n", " astar_step_count(initial, h_to_top, moves))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In **Part 2**, we add two more pairs of objects to the ground floor. That is, we add two more `(0, 0)` pairs to the initial state." ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 11.2: 325.9 milliseconds, correct answer: 55 " ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "part2 = State(elevator=0, pairs=initial.pairs + ((0, 0), (0, 0)))\n", "\n", "answer(11.2, 55, lambda:\n", " astar_step_count(part2, h_to_top, moves))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That's down from 4 minutes to 1/3 of a second by taking advantage of symmetry!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 12](http://adventofcode.com/2016/day/12): Leonardo's Monorail\n", "\n", "This one looks pretty easy: an interpreter for an assembly language with 4 op codes and 4 registers. We start by parsing a line like `\"cpy 1 a\"` into a tuple, `('cpy', 1, 'a')`. Then to `interpret` the code, we set the program counter, `pc`, to 0, and interpret the instruction at `code[0]`, and continue until the `pc` is past the end of the code:" ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 12.1: 108.1 milliseconds, correct answer: 318007 " ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "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", " inst = code[pc]\n", " op, x, y = inst[0], inst[1], inst[-1]\n", " pc += 1\n", " if op == 'cpy': 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 += y - 1\n", " return regs\n", "\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", "answer(12.1, 318007, lambda:\n", " interpret(parse_program(Input(12)), a=0, b=0, c=0, d=0)['a'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "I had a bug initially: in the `jnz` instruction, I had `pc += y`, to do the relative jump, but I forgot the `-1` to offset the previous `pc += 1`.\n", "\n", "In **part two** all we have to do is initialize register `c` to 1, not 0:" ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 12.2: 3,036.3 milliseconds, correct answer: 9227661 " ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(12.2, 9227661, lambda:\n", " interpret(parse_program(Input(12)), a=0, b=0, c=1, d=0)['a'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 13](http://adventofcode.com/2016/day/13): A Maze of Twisty Little Cubicles \n", "\n", "This is a maze-solving puzzle, where the maze is infinite in the non-negative (x, y) quarter-plane. Each space in that infinite grid is open or closed according to this computation:\n", "> Find `x*x + 3*x + 2*x*y + y + y*y`.\n", "Add the office designer's favorite number (your puzzle input).\n", "Find the binary representation of that sum; count the number of bits that are 1.\n", "If the number of bits that are 1 is even, it's an open space (denoted `'.'`).\n", "If the number of bits that are 1 is odd, it's a wall (denoted `'#'`).\n", "\n", "The problem is to find the length of the shortest path to the goal location, (31, 39). So I'll be using `astar_search` again." ] }, { "cell_type": "code", "execution_count": 28, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 13.1: 0.3 milliseconds, correct answer: 82 " ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "favorite = 1362\n", "goal = (31, 39)\n", "\n", "def is_open(location: Point) -> bool:\n", " \"\"\"Is this an open location according to the formula?\"\"\"\n", " x, y = location\n", " num = x*x + 3*x + 2*x*y + y + y*y + favorite\n", " return x >= 0 and y >= 0 and bin(num).count('1') % 2 == 0\n", "\n", "def open_neighbors(location): return filter(is_open, neighbors4(location))\n", "\n", "answer(13.1, 82, lambda:\n", " astar_step_count((1, 1), lambda p: cityblock_distance(p, goal), open_neighbors))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here we see a portion of the maze:" ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "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", ".......##.##.#.....#....#.##.#.###..#.#.####..##.###########..##.#....##.....#..##..#..##.....##..##\n", "#####.#.#.##...###.##...##.#..#..##.##...#######.....##..#######.#########.#.....##.#.#.#.#######..#\n", "...##..##.#.###..#.###.#.##.#..#..##.#.....#..#####....#..##..##...#..##.#..##.#..###..##.##..#.##.#\n", ".#.###.#..#...#.##...#..#.#..#.......##..#.##..##.#.##.....#.....#..#...###..#......##.#...#..####..\n", "##..#..#.######.#####.#..###..##.###.####...#.....#.#.##.#.##.#####...#.####..##.##.#..#.#.###..####\n", "....##.#.##..#...##.#.##.####..#.###....#...#..###..#..#..#.#....#.##.#..#######.#..##.#..#...#..##.\n", "##...#.......#......#.....#.###...########.#####.#..##..#..####..##.#.#...##..#..#...#..#..##.#.....\n", "###.#######.########.##.#.###.##...##..#.#.##..#.#########.##.###.##..###.....##.##.######.#..##.###\n", ".##.#...###.##...#.##.#..#.....##....#.##......#.#..#........#..##.#......##.#.#.##.#...#..#...#.#..\n", ".##.#.....#....#..#.##.#.####.#.#.##..#.#####.##.#..#..#####......###..##..#..##.#..#...##.##.##..#.\n", "...###.##.#.###.#.##.###...##..##.#.#......##.#..#.####.....##.##.#####.##..#.#..#####...#.##.#.#..#\n", ".#..##.#..##..##...#.....#..##.#..#..##..#.##.#.##..#.#..##..#.#....#.#######.#.#....##.##.##.##.#..\n", "###....#...###.#...##.#####.#..#..##.#####..###.#.#.#####.###..#.##.##..#...###...##.##.##...#.##.#.\n", ".#.##.###.#..#.##.#.###..#..##.#####..........#.##...##.###.##.#.#....#.#.....######.#.....#.#..##.#\n", ".####..##..#.#.##..#.....#...#.#..#####.##.##.##.#.....#.....#...#.##..###.##..#..#..#..####.###.##.\n", "...#.#...#..##..##...###.##.##..#..##.#.##.#...#.##..#.#.##.###..#.#.#..##.#.#..#.#.###.....#.....#.\n", "#..##.##..#.###.#.####.#.##.###.#.....#....#.#.######..#.##.#.###..##.#....####..##.#####.#..######.\n", "##..#######..#..#.....##.....#..######.##..#..#.....#.##.#..###.##..##.##.#..#.#..#..##.#..#.##...#.\n" ] } ], "source": [ "for y in range(30):\n", " print(cat('#.'[is_open((x, y))] for x in range(100)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In **part two**, we're asked how many locations we can reach in 50 moves or less. I'll grab the `breadth_first` search function from [aima-python](https://github.com/aimacode/aima-python/blob/master/search-4e.ipynb) and modify it to find all the states within N steps:" ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 13.2: 0.2 milliseconds, correct answer: 138 " ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "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", " while frontier:\n", " s = frontier.popleft()\n", " if distance[s] < N:\n", " for s2 in neighbors(s):\n", " if s2 not in distance:\n", " frontier.append(s2)\n", " distance[s2] = distance[s] + 1\n", " return len(distance)\n", " \n", "answer(13.2, 138, lambda:\n", " count_locations_within((1, 1), 50, open_neighbors))" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "jupyter": { "outputs_hidden": true } }, "source": [ "# [Day 14](http://adventofcode.com/2016/day/14): One-Time Pad \n", "\n", "For this problem I again have to take the md5 hash of a string with increasing integers appended. The puzzle is to find the integer that yields the 64th key, where a hash is a key if:\n", "- It contains three of the same character in a row, like 777. Only consider the first such triplet in a hash.\n", "- One of the next 1000 hashes in the stream contains that same character five times in a row, like 77777.\n", "\n", "I'll use `lru_cache` to avoid repeating the hashing of the next 1000." ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 14.1: 127.3 milliseconds, correct answer: 16106 " ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "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: 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) -> int: return nth(filter(is_key, range(BIG)), N)\n", "\n", "answer(14.1, 16106, lambda:\n", " nth_key(63))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In **part two**, we do *key stretching*, hashing an additional 2016 times. Everything else is the same:" ] }, { "cell_type": "code", "execution_count": 32, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 14.2: 14,443.9 milliseconds, correct answer: 22423 " ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "@cache\n", "def hashval(i, stretch=2016): \n", " \"\"\"The hash value with key stretching.\"\"\"\n", " h = hashlib.md5(bytes(salt + str(i), 'utf-8')).hexdigest()\n", " for i in range(stretch):\n", " h = hashlib.md5(bytes(h, 'utf-8')).hexdigest()\n", " return h\n", "\n", "answer(14.2, 22423, lambda:\n", " nth_key(63))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This was my highest-scoring day on the leaderboard, finishing #20 on part two. It was also my slowest-running answer. Maybe other people were trying to be clever, while I was just letting my program run." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 15](http://adventofcode.com/2016/day/15): Timing is Everything\n", "\n", "In this puzzle rotating discs with a slot in position 0 spin around. We are asked at what time will all the slots be lined up for a capsule to fall through the slots. The capsule takes one time unit to fall through each disc (not clear why it doesn't accelerate as it falls) and the discs spin one position per time unit." ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 15.1: 53.2 milliseconds, correct answer: 203660 " ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Disc = namedtuple('Disc', 'number, positions, start_time, start_pos')\n", "\n", "def parse_discs(lines) -> list[tuple]:\n", " \"\"\"Parse discs.\"\"\"\n", " return [Disc(*ints(line)) for line in lines]\n", " \n", "def falls(t, discs) -> bool:\n", " \"\"\"If we drop the capsule at time t, does it fall through all slots?\"\"\"\n", " return all((d.start_pos + t + d.number) % d.positions == 0 \n", " for d in discs)\n", "\n", "def all_falling_times(discs) -> Iterable[int]:\n", " \"\"\"Times t that a capsule could fall through all the discs.\"\"\"\n", " 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. " ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 15.2: 634.8 milliseconds, correct answer: 2408135 " ] }, "execution_count": 34, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(15.2, 2408135,\n", " lambda: first(all_falling_times([*parse_discs(Input(15)), Disc(7, 11, 0, 0)])))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 16](http://adventofcode.com/2016/day/16) Dragon Checksum\n", "\n", "Given a bit string of the form `'01...'`, expand it until it fills N bits, then report the checksum.\n", "\n", "The rules for expanding:\n", "- Call the data you have at this point \"a\".\n", "- Make a copy of \"a\"; call this copy \"b\".\n", "- Reverse the order of the characters in \"b\".\n", "- In \"b\", replace all instances of 0 with 1 and all 1s with 0.\n", "- The resulting data is \"a\", then a single 0, then \"b\".\n", "- If this gives N or more bits, take the first N; otherwise repeat the process.\n", "\n", "The rules for the checksum:\n", "- Assume the string is 110010110100\n", "- Consider each pair: 11, 00, 10, 11, 01, 00.\n", "- These are same, same, different, same, different, same, producing 110101.\n", "- The resulting string has length 6, which is even, so we repeat the process.\n", "- The pairs are 11 (same), 01 (different), 01 (different).\n", "- This produces the checksum 100, which has an odd length, so we stop." ] }, { "cell_type": "code", "execution_count": 35, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 16.1: 0.0 milliseconds, correct answer: 01110011101111011" ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "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", " a = a + '0' + b\n", " return a[:N]\n", "\n", "def flip(text, table=str.maketrans('10', '01')): return text.translate(table)\n", "\n", "def checksum(a: str) -> str:\n", " \"\"\"Compute the checksum of `a` by comparing pairs until len is odd.\"\"\"\n", " 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 = '11110010111001001'\n", "\n", "answer(16.1, '01110011101111011', lambda:\n", " checksum(expand(seed, 272)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In **part two**, we take the same seed, but expand it to fill 35Mb of space:" ] }, { "cell_type": "code", "execution_count": 36, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 16.2: 1,560.4 milliseconds, correct answer: 11001111011000111" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(16.2, '11001111011000111', lambda:\n", " checksum(expand(seed, 35651584)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 17](http://adventofcode.com/2016/day/17) Two Steps Forward\n", "\n", "In this puzzle, we move through a 4x4 grid/maze, starting at position (0, 0) and trying to reach (3, 3), but the door from one position to the next is open or not depending on the hash of the path to get there (and my passcode), so doors open and lock themselves as you move around. I'll represent a state as a tuple of `(position, path)` and use `astar_search` to find the shortest path to the goal:\n" ] }, { "cell_type": "code", "execution_count": 37, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 17.1: 0.0 milliseconds, correct answer: RRRLDRDUDD " ] }, "execution_count": 37, "metadata": {}, "output_type": "execute_result" } ], "source": [ "passcode = 'qtetzkpl'\n", "\n", "openchars = 'bcdef'\n", "\n", "grid = set((x, y) for x in range(4) for y in range(4))\n", "\n", "start, goal = (0, 0), (3, 3)\n", "\n", "def to_goal(state: tuple) -> int: \n", " \"\"\"City block distance between state's position and goal.\"\"\"\n", " pos, path = state\n", " return cityblock_distance(pos, goal)\n", "\n", "directions = [(0, 'U', (0, -1)), (1, 'D', (0, 1)), (2, 'L', (-1, 0)), (3, 'R', (1, 0))]\n", "\n", "def moves(state: tuple) -> Iterable[tuple]:\n", " \"\"\"All states reachable from this state.\"\"\"\n", " (x, y), path = state\n", " hashx = hashlib.md5(bytes(passcode + path, 'utf-8')).hexdigest()\n", " for (i, p, (dx, dy)) in directions:\n", " pos2 = (x+dx, y+dy)\n", " if hashx[i] in openchars and pos2 in grid:\n", " yield (pos2, path+p)\n", " \n", "answer(17.1, 'RRRLDRDUDD', lambda:\n", " astar_search((start, ''), to_goal, moves)[-1][1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In **part two**, we're asked for the longest path to the goal. We have to stop when we reach the goal, but we can make repeated visits to positions along the way, as long as the doors are open. I'll use a depth-first search, and keep track of the longest path length:" ] }, { "cell_type": "code", "execution_count": 38, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 17.2: 44.3 milliseconds, correct answer: 706 " ] }, "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ "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", " state = (pos, path) = frontier.pop()\n", " if pos == goal:\n", " longest = max(longest, len(path))\n", " else:\n", " frontier.extend(moves(state))\n", " return longest\n", " \n", "answer(17.2, 706, lambda:\n", " longest_search((start, ''), goal, moves))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 18](http://adventofcode.com/2016/day/18) Like a Rogue\n", "\n", "Here we have a cellular automaton, where a cell is a \"trap\" iff the 3 tiles in the row above, (one to the left above, directly above, and one to the right above) are one of the set `{'^^.', '.^^', '^..', '..^'}`; in other words if the first of the three is different from the last of the three. Given an initial row, we're asked for the count of all the safe tiles in the first 40 rows:\n" ] }, { "cell_type": "code", "execution_count": 39, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 18.1: 0.5 milliseconds, correct answer: 1987 " ] }, "execution_count": 39, "metadata": {}, "output_type": "execute_result" } ], "source": [ "safe, trap = '.', '^' \n", "\n", "def rows(n: int, initial_row: str) -> list[str]:\n", " \"\"\"The first n rows of tiles (given the initial row).\"\"\"\n", " 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", "answer(18.1, 1987, lambda:\n", " cat(rows(40, Input(18).read().strip())).count(safe))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here I reproduce the simple example from the puzzle page:" ] }, { "cell_type": "code", "execution_count": 40, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "['.^^.^.^^^^',\n", " '^^^...^..^',\n", " '^.^^.^.^^.',\n", " '..^^...^^^',\n", " '.^^^^.^^.^',\n", " '^^..^.^^..',\n", " '^^^^..^^^.',\n", " '^..^^^^.^^',\n", " '.^^^..^.^^',\n", " '^^.^^^..^^']" ] }, "execution_count": 40, "metadata": {}, "output_type": "execute_result" } ], "source": [ "rows(10, '.^^.^.^^^^')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In **part two**, we just have to run longer (but only a few seconds):" ] }, { "cell_type": "code", "execution_count": 41, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 18.2: 2,027.1 milliseconds, correct answer: 19984714 " ] }, "execution_count": 41, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(18.2, 19984714, lambda:\n", " cat(rows(400000, Input(18).read().strip())).count(safe))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 19](http://adventofcode.com/2016/day/19) An Elephant Named Joseph\n", "\n", "Elves numbered 1 to *N* sit in a circle. Each Elf brings a present. Then, starting with the first Elf, they take turns stealing all the presents from the Elf to their left. An Elf with no presents is removed from the circle and does not take turns. So, if *N* = 5, then:\n", "\n", " Elf 1 takes Elf 2's present.\n", " Elf 2 has no presents and is skipped.\n", " Elf 3 takes Elf 4's present.\n", " Elf 4 has no presents and is also skipped.\n", " Elf 5 takes Elf 1's two presents.\n", " Neither Elf 1 nor Elf 2 have any presents, so both are skipped.\n", " Elf 3 takes Elf 5's three presents, ending the game.\n", " \n", "Who ends up with all the presents for general case of *N*?\n", "First, I note that I only need to keep track of the Elf number of the remaining elves,\n", "I don't need to count how many presents each one has. I see two representation choices:\n", "- Represent the circle of elves as a list of elf numbers, and everytime an Elf's presents are taken, delete the elf from the list. But this is O(*N*2), where *N* = 3 million, so this will be slow.\n", "- Represent the elves by a range, and instead of deleting elf-by-elf, instead limit the range round-by-round.\n", "If there is an even number of elves, then the elf in position 0 takes from position 1; position 2 takes from position 3, and so on, leaving only the even positions, which we denote `elves[0::2]`. If there is an odd number of elves, then it is the same, except that the last elf takes from the one in position 0, leaving `elves[2::2]`. Here's the code:" ] }, { "cell_type": "code", "execution_count": 42, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 19.1: 0.0 milliseconds, correct answer: 1815603 " ] }, "execution_count": 42, "metadata": {}, "output_type": "execute_result" } ], "source": [ "N_elves = 3004953\n", "\n", "def Elves(N: int) -> range: return range(1, N+1) \n", "\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", "answer(19.1, 1815603, lambda:\n", " winner(Elves(N_elves)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here is a cool thing about representing the elves with a range: the total storage is O(1), not O(*N*).\n", "We never need to make a list of 3 million elements.\n", "Here we see a trace of the calls to `one_round`:\n" ] }, { "cell_type": "code", "execution_count": 43, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "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": [ "1815603" ] }, "execution_count": 43, "metadata": {}, "output_type": "execute_result" } ], "source": [ "one_round = trace1(one_round)\n", "winner(Elves(N_elves))" ] }, { "cell_type": "markdown", "metadata": {}, "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 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", "I will change the `Elves` function to return a `list`, not a `range`. The function `winner` stays the same. The `one_round` function is where the work goes:" ] }, { "cell_type": "code", "execution_count": 44, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 19.2: 318.2 milliseconds, correct answer: 1410630 " ] }, "execution_count": 44, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def Elves(N: int) -> list: return list(range(1, N + 1))\n", "\n", "def one_round(elves: list[int]) -> list[int]:\n", " \"\"\"The first third of elves eliminate ones across the circle from them; who is left?\"\"\"\n", " N = len(elves)\n", " eliminated = 0\n", " for i in range(int(math.ceil(N / 3))):\n", " across = i + eliminated + (N // 2) \n", " elves[across] = None\n", " N -= 1\n", " eliminated += 1\n", " return list(filter(None, elves[i+1:] + elves[:i+1]))\n", "\n", "assert winner(Elves(5)) == 2\n", "\n", "assert one_round(Elves(5)) == [4, 1, 2]\n", "\n", "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 take under a second." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 20](http://adventofcode.com/2016/day/20) Firewall Rules\n", "\n", "We are given a list of blocked IP addresses, in the form `\"2365712272-2390766206\"`, indicating the low and high numbers that are blocked by the firewall. So I'll parse each line into a pair of ints.\n", "\n", "We are asked what is the lowest non-negative integer that is not blocked. I will generate all the unblocked numbers, and just ask for the first one. (Why do it that way? Because it feels like `unblocked` is the fundamental issue of the problem, and we already have a function to compute `first`; there's no need for a `first_unblocked` function that conflates two ideas.) To find unblocked numbers, start a counter, `i` at zero, and increment it past the high value of each range, after yielding any numbers from `i` to the low value of the range:" ] }, { "cell_type": "code", "execution_count": 45, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 20.1: 1.2 milliseconds, correct answer: 32259706 " ] }, "execution_count": 45, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def unblocked(pairs: list[tuple[int, int]]) -> Iterable[int]:\n", " \"\"\"Yield the unblocked integers, given the sorted low-high pairs of blocked number ranges.\"\"\"\n", " i = 0\n", " for (low, high) in pairs:\n", " yield from range(i, low)\n", " i = max(i, high + 1)\n", " \n", "answer(20.1, 32259706, lambda:\n", " first(unblocked(sorted(map(ints, Input(20))))))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In **part two** we are asked how many numbers are unblocked:" ] }, { "cell_type": "code", "execution_count": 46, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 20.2: 0.9 milliseconds, correct answer: 113 " ] }, "execution_count": 46, "metadata": {}, "output_type": "execute_result" } ], "source": [ "answer(20.2, 113, lambda:\n", " len(list(unblocked(sorted(map(ints, Input(20)))))))" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "jupyter": { "outputs_hidden": true } }, "source": [ "# [Day 21](http://adventofcode.com/2016/day/21) Scrambled Letters and Hash \n", "\n", "In this puzzle we are asked to take a password string, scramble it according to a list of instructions, and output the result, which will be a permutation of the original password. This is tedious because there are seven different instructions, but each one is pretty straightforward. I make the following choices:\n", "- I'll transform `password` (a `str`) into `pw` (a `list`), because lists are mutable and easier to manipulate. At the end I'll turn it back into a `str`.\n", "- I'll define functions `rot` and `swap` because they get used multiple times by different instructions.\n", "- I use the variables `A, B` to denote the first two integers anywhere in a line. If there is only one integer (or none), then `B` (and `A`) get as a default value `0`. I accept ill-formed instructions, such as `\"move 1 to 4\"` instead of requiring `\"move position 1 to position 4\"`. \n" ] }, { "cell_type": "code", "execution_count": 47, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 21.1: 0.3 milliseconds, correct answer: gfdhebac " ] }, "execution_count": 47, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def scramble(password: str, instructions: Iterable[str], verbose=False) -> str:\n", " \"\"\"Scramble the password according to the instructions.\"\"\"\n", " 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, = 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", " elif cmd('rotate right'): rot(A)\n", " elif cmd('rotate left'): rot(-A)\n", " elif cmd('reverse'): pw[A:B+1] = pw[A:B+1][::-1]\n", " elif cmd('move'): pw[A:A+1], pw[B:B] = [], pw[A:A+1]\n", " elif cmd('rotate based'):\n", " i = pw.index(words[6])\n", " rot((i + 1 + (i >= 4)) % len(pw))\n", " if verbose: \n", " print(f'{line:37} \"{cat(pw)}\"')\n", " return cat(pw)\n", "\n", "answer(21.1, 'gfdhebac', lambda:\n", " scramble('abcdefgh', Input(21)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When I ran the first version of this code the answer I got was incorrect, and I couldn't see where I went wrong, so I implemented the test case from the problem description and inspected the results line by line." ] }, { "cell_type": "code", "execution_count": 48, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "swap position 4 with position 0 \"ebcda\"\n", "swap letter d with letter b \"edcba\"\n", "reverse positions 0 through 4 \"abcde\"\n", "rotate left 1 step \"bcdea\"\n", "move position 1 to position 4 \"bdeac\"\n", "move position 3 to position 0 \"abdec\"\n", "rotate based on position of letter b \"ecabd\"\n", "rotate based on position of letter d \"decab\"\n" ] }, { "data": { "text/plain": [ "'decab'" ] }, "execution_count": 48, "metadata": {}, "output_type": "execute_result" } ], "source": [ "test = '''swap position 4 with position 0\n", "swap letter d with letter b\n", "reverse positions 0 through 4\n", "rotate left 1 step\n", "move position 1 to position 4\n", "move position 3 to position 0\n", "rotate based on position of letter b\n", "rotate based on position of letter d'''.splitlines()\n", "\n", "scramble('abcde', test, verbose=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That was enough to show me that I had two bugs (which are fixed above): \n", "- For `\"reverse\"`, I thought `\"positions 0 through 4\"` meant `[0:4]`, when actually it means `[0:5]`.\n", "- For `\"rotate based\"`, in the case where the rotation is longer than the password, I need to take the modulo of the password length.\n", "\n", "For **part two**, the task is to find the password that, when scrambled, yields `'fbgdceah'`. I think the puzzle designer was trying to tempt solvers into implementing an `unscramble` function, which would be another 20 or 30 lines of code. Fortunately, I was too lazy to go down that path. I realized there are only 40 thousand permutations of an 8-character password, so we can just brute force them all (which would be infeasible with a 20-character password):" ] }, { "cell_type": "code", "execution_count": 49, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 21.2: 2,384.4 milliseconds, correct answer: dhaegfbc " ] }, "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ "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, "jupyter": { "outputs_hidden": true } }, "source": [ "# [Day 22](http://adventofcode.com/2016/day/22) Grid Computing\n", "\n", "We are given a description of files across a grid computing cluster, like this:\n", "\n", " root@ebhq-gridcenter# df -h\n", " Filesystem Size Used Avail Use%\n", " /dev/grid/node-x0-y0 92T 70T 22T 76%\n", " /dev/grid/node-x0-y1 86T 65T 21T 75%\n", "\n", "For part one, we are asked how many pairs of nodes can viably make a transfer of data. The pair (A, B) is viable if\n", "- Node A is not empty (its Used is not zero).\n", "- Nodes A and B are not the same node.\n", "- The data on node A (its Used) would fit on node B (its Avail).\n", "\n", "I'll represent a node as a `namedtuple` of six integers; the rest is easy:" ] }, { "cell_type": "code", "execution_count": 50, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 22.1: 129.8 milliseconds, correct answer: 1045 " ] }, "execution_count": 50, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Node = namedtuple('Node', 'x, y, size, used, avail, pct')\n", "\n", "nodes = [Node(*ints(line)) for line in Input(22) if line.startswith('/dev')]\n", "\n", "def viable(A, B) -> bool: return A != B and 0 < A.used <= B.avail\n", "\n", "answer(22.1, 1045, lambda:\n", " sum(viable(A, B) for A in nodes for B in nodes))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In **part two**, we are asked to move data from the node in the upper right (the one with maximum `x` value and `y=0`) to the upper left (x=0, y=0). At first I worried about all sorts of complications: could we split the data into two or more pieces, copying different pieces into different nodes, and then recombining them? I spent many minutes thinking about these complications. Eventually, after a more careful reading of the rules, I decided such moves were not allowed, and the answer had to just involve moving the empty square around. So to proceed, we need to find the initial position of the empty node, and the maximum x value, so we know where the data is: " ] }, { "cell_type": "code", "execution_count": 51, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "(Node(x=35, y=27, size=89, used=0, avail=89, pct=0), 35)" ] }, "execution_count": 51, "metadata": {}, "output_type": "execute_result" } ], "source": [ "empty = first(node for node in nodes if node.used == 0)\n", "maxx = max(node.x for node in nodes)\n", "\n", "empty, maxx" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "I will also define the `grid` as a dict of `{(x, y): node}` entries (which will enable me to find neighbors of a node):" ] }, { "cell_type": "code", "execution_count": 52, "metadata": {}, "outputs": [], "source": [ "grid = {(node.x, node.y): node \n", " for node in nodes}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "An `astar_search` seems appropriate. Each state of the search keeps track of the position of the data we are trying to get, and the position of the currently empty node. The heuristic is the city block distance of the data to the origin:" ] }, { "cell_type": "code", "execution_count": 53, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 22.2: 1,212.4 milliseconds, correct answer: 265 " ] }, "execution_count": 53, "metadata": {}, "output_type": "execute_result" } ], "source": [ "State = namedtuple('State', 'datapos, emptypos')\n", "\n", "def distance(state: State) -> int: return cityblock_distance(state.datapos)\n", "\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", " # Try to move contents of `node` at pos into `empty` at emptypos\n", " node, empty = grid[pos], grid[state.emptypos]\n", " if node.used <= empty.size:\n", " newdatapos = (state.emptypos if pos == state.datapos else state.datapos)\n", " yield State(newdatapos, pos)\n", " \n", "answer(22.2, 265, lambda:\n", " astar_step_count(State((maxx, 0), (empty.x, empty.y)), distance, moves))" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "jupyter": { "outputs_hidden": true } }, "source": [ "# [Day 23](http://adventofcode.com/2016/day/23) Safe Cracking\n", "\n", "This day's puzzle is just like Day 12, except there is one more instruction, `tgl`. I made four mistakes in the process of coding this up:\n", "- At first I didn't read the part that says register `'a'` should initially be 7.\n", "- I wasn't sure exactly what constitutes an invalid instruction; it took me a few tries to get that right:\n", "the only thing that is invalid is a `cpy` that does not copy into a register.\n", "- I forgot to subtract one from the `pc` (again!) in the `tgl` instruction.\n", "- I forgot that I had `parse` return instructions as immutable tuples; I had to change that to mutable lists." ] }, { "cell_type": "code", "execution_count": 54, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 23.1: 7.3 milliseconds, correct answer: 10661 " ] }, "execution_count": 54, "metadata": {}, "output_type": "execute_result" } ], "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", " return regs\n", "\n", "def toggle(code, i):\n", " \"\"\"Toggle the instruction at location i.\"\"\"\n", " if 0 <= i < len(code): \n", " inst = code[i]\n", " inst[0] = ('dec' if inst[0] == 'inc' else \n", " 'inc' if len(inst) == 2 else\n", " 'cpy' if inst[0] == 'jnz' else \n", " 'jnz')\n", "\n", "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": 55, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{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": 55, "metadata": {}, "output_type": "execute_result" } ], "source": [ "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": 56, "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": 56, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def profile(code, **regs) -> dict:\n", " \"\"\"Execute instructions until pc goes off the end, yielding the pc each step.\"\"\"\n", " 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": 57, "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 instructions. I can use `cpy 0 0` as a no-op:" ] }, { "cell_type": "code", "execution_count": 58, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Puzzle 23.2: 2.8 milliseconds, correct answer: 479007221 " ] }, "execution_count": 58, "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", " 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", "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": [ "That's 100,000 times faster!" ] }, { "cell_type": "markdown", "metadata": {}, "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`. 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": 59, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "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": 59, "metadata": {}, "output_type": "execute_result" } ], "source": [ "maze = Input(24).read().splitlines()\n", "Counter(cat(maze))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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": 60, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "(1, 19)" ] }, "execution_count": 60, "metadata": {}, "output_type": "execute_result" } ], "source": [ "zero = first((x, y) for y, row in enumerate(maze) for x, c in enumerate(row) if c == '0')\n", "zero" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now I'm ready to go. The state of the search will include the x, y position, and also the digits visited so far, which I can represent as a sorted string (a `frozenset` would also work):" ] }, { "cell_type": "code", "execution_count": 61, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 24.1: 268.3 milliseconds, correct answer: 430 " ] }, "execution_count": 61, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def h(state):\n", " \"\"\"Heuristic: the number of digits not yet visited.\"\"\"\n", " _, visited = state\n", " return 8 - len(visited) # Note: 8 == len('01234567')\n", " \n", "def moves(state):\n", " \"\"\"Move to any neighboring square that is not a wall. Track the digits visited.\"\"\"\n", " pos, visited = state\n", " for x1, y1 in neighbors4(pos):\n", " c = maze[y1][x1]\n", " if c != '#':\n", " visited1 = (visited if (c == '.' or c in visited) else cat(sorted(visited + c)))\n", " yield (x1, y1), visited1\n", "\n", "answer(24.1, 430, lambda:\n", " len(astar_search((zero, '0'), h, moves)) - 1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In **part two** we need to get the robot back to the start square. I'll do that by creating a new heuristic function that still requires us to collect all the digits, and also measures the distance back to the start (`zero`) square." ] }, { "cell_type": "code", "execution_count": 62, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 24.2: 984.7 milliseconds, correct answer: 700 " ] }, "execution_count": 62, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def h2(state):\n", " \"\"\"Heuristic: the number of digits not yet visited, plus the distance back to start.\"\"\"\n", " pos, visited = state\n", " return 8 - len(visited) + cityblock_distance(pos, zero)\n", "\n", "answer(24.2, 700, lambda:\n", " len(astar_search((zero, '0'), h2, moves)) - 1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# [Day 25](http://adventofcode.com/2016/day/25) Clock Signal\n", "\n", "This is another assembly language interpreter puzzle. This time there is one more instruction, `out`, which transmits a signal. We are asked to find the lowest positive integer value for register `a` that causes the program to output an infinite series of `0, 1, 0, 1, 0, 1, ...` signals. Dealing with infinity is difficult, so I'll approximate that by asking: what is the lowest value for register `a` that causes the program to output at least 100 elements in the `0, 1, 0, 1, 0, 1, ...` series, within the first million instructions executed?\n", "\n", "To do that, I'll change `interpret` to be a generator that yields signals, and change it to take an argument saying the number of steps to execute before halting:" ] }, { "cell_type": "code", "execution_count": 63, "metadata": {}, "outputs": [], "source": [ "def interpret(code, steps, **regs):\n", " \"\"\"Execute instructions until pc goes off the end, or until we execute the given number of steps.\"\"\"\n", " 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", " 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 == 'out': yield val(x)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here is my program, and the function `repeats`, which returns True if the code repeats with a given value of the register `a`. Then all we need to do is iterate through integer values for register `a` until we find one that repeats:" ] }, { "cell_type": "code", "execution_count": 64, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "data": { "text/plain": [ "Puzzle 25.1: 904.6 milliseconds, correct answer: 189 " ] }, "execution_count": 64, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def clock_signal(lines) -> int:\n", " \"\"\"Find the first value of register a that causes program to output 0, 1, 0, 1, ...\"\"\"\n", " code = parse_program(lines)\n", " return first(a for a in range(1, BIG) if repeats(a, code))\n", "\n", "def repeats(a, code, steps=10**6, minsignals=100) -> bool:\n", " \"\"\"Does this value for register a cause code to repeat `out` signals of 0, 1, 0, 1, ...?\"\"\"\n", " signals = interpret(code, steps, a=a, b=0, c=0, d=0)\n", " for (i, (signal, expected)) in enumerate(zip(signals, cycle((0, 1)))):\n", " if signal != expected:\n", " return False\n", " # We'll say \"yes\" if the code outputs at least a minimum number of 0, 1, ... signals, and nothing else.\n", " return i >= minsignals\n", " \n", "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. Originally 11.2 and 23.2 took over 10 minutes, but I went back and optimized them and now they're under 1/10 of a second. There are 8 puzzles that still take over a second of run time. But the mean is under a second and the median is just 2.8 milliseconds." ] }, { "cell_type": "code", "execution_count": 65, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Time in milliseconds: sum = 48,367.1, mean = 987.1, median = 2.8, max = 14,443.9\n", "\n", "Puzzle 1.1: 0.4 milliseconds, correct answer: 262 \n", "Puzzle 1.2: 0.3 milliseconds, correct answer: 131 \n", "Puzzle 2.1: 0.5 milliseconds, correct answer: 14894 \n", "Puzzle 2.2: 0.6 milliseconds, correct answer: 26B96 \n", "Puzzle 3.1: 0.2 milliseconds, correct answer: 869 \n", "Puzzle 3.2: 0.3 milliseconds, correct answer: 1544 \n", "Puzzle 4.1: 4.0 milliseconds, correct answer: 409147 \n", "Puzzle 4.2: 1.6 milliseconds, correct answer: 991 \n", "Puzzle 5.1: 7,596.7 milliseconds, correct answer: 1a3099aa \n", "Puzzle 5.2: 11,989.2 milliseconds, correct answer: 694190cd \n", "Puzzle 6.1: 0.6 milliseconds, correct answer: dzqckwsd \n", "Puzzle 6.2: 0.4 milliseconds, correct answer: lragovly \n", "Puzzle 7.1: 12.3 milliseconds, correct answer: 118 \n", "Puzzle 7.2: 119.5 milliseconds, correct answer: 260 \n", "Puzzle 8.1: 0.6 milliseconds, correct answer: 110 \n", "Puzzle 8.2: 0.7 milliseconds, correct answer: ZJHRKCPLYJ \n", "Puzzle 9.1: 0.6 milliseconds, correct answer: 115118 \n", "Puzzle 9.2: 2.0 milliseconds, correct answer: 11107527530 \n", "Puzzle 10.1: 0.6 milliseconds, correct answer: 56 \n", "Puzzle 10.2: 0.6 milliseconds, correct answer: 7847 \n", "Puzzle 11.1: 58.0 milliseconds, correct answer: 31 \n", "Puzzle 11.2: 325.9 milliseconds, correct answer: 55 \n", "Puzzle 12.1: 108.1 milliseconds, correct answer: 318007 \n", "Puzzle 12.2: 3,036.3 milliseconds, correct answer: 9227661 \n", "Puzzle 13.1: 0.3 milliseconds, correct answer: 82 \n", "Puzzle 13.2: 0.2 milliseconds, correct answer: 138 \n", "Puzzle 14.1: 127.3 milliseconds, correct answer: 16106 \n", "Puzzle 14.2: 14,443.9 milliseconds, correct answer: 22423 \n", "Puzzle 15.1: 53.2 milliseconds, correct answer: 203660 \n", "Puzzle 15.2: 634.8 milliseconds, correct answer: 2408135 \n", "Puzzle 16.1: 0.0 milliseconds, correct answer: 01110011101111011\n", "Puzzle 16.2: 1,560.4 milliseconds, correct answer: 11001111011000111\n", "Puzzle 17.1: 0.0 milliseconds, correct answer: RRRLDRDUDD \n", "Puzzle 17.2: 44.3 milliseconds, correct answer: 706 \n", "Puzzle 18.1: 0.5 milliseconds, correct answer: 1987 \n", "Puzzle 18.2: 2,027.1 milliseconds, correct answer: 19984714 \n", "Puzzle 19.1: 0.0 milliseconds, correct answer: 1815603 \n", "Puzzle 19.2: 318.2 milliseconds, correct answer: 1410630 \n", "Puzzle 20.1: 1.2 milliseconds, correct answer: 32259706 \n", "Puzzle 20.2: 0.9 milliseconds, correct answer: 113 \n", "Puzzle 21.1: 0.3 milliseconds, correct answer: gfdhebac \n", "Puzzle 21.2: 2,384.4 milliseconds, correct answer: dhaegfbc \n", "Puzzle 22.1: 129.8 milliseconds, correct answer: 1045 \n", "Puzzle 22.2: 1,212.4 milliseconds, correct answer: 265 \n", "Puzzle 23.1: 7.3 milliseconds, correct answer: 10661 \n", "Puzzle 23.2: 2.8 milliseconds, correct answer: 479007221 \n", "Puzzle 24.1: 268.3 milliseconds, correct answer: 430 \n", "Puzzle 24.2: 984.7 milliseconds, correct answer: 700 \n", "Puzzle 25.1: 904.6 milliseconds, correct answer: 189 \n" ] } ], "source": [ "summary(answers)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That's all folks! Thank you [Eric Wastl](http://was.tl/), that was fun!" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.3" } }, "nbformat": 4, "nbformat_minor": 4 }