From 269974a6f26155bde98a405aa837a5eb1a6d6dbb Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Sun, 24 Dec 2017 22:39:24 -0800 Subject: [PATCH] Advent 2017 through Day 25 (the end) --- ipynb/Advent 2017.ipynb | 1416 ++++++++++++++++++++++++++++++--------- 1 file changed, 1115 insertions(+), 301 deletions(-) diff --git a/ipynb/Advent 2017.ipynb b/ipynb/Advent 2017.ipynb index 5cfd240..8acbe02 100644 --- a/ipynb/Advent 2017.ipynb +++ b/ipynb/Advent 2017.ipynb @@ -8,13 +8,15 @@ "\n", "Peter Norvig\n", "\n", - "I'm doing the [Advent of Code](https://adventofcode.com) puzzles, just like [last year](https://github.com/norvig/pytudes/blob/master/ipynb/Advent%20of%20Code.ipynb). This time, my terms of engagement are:\n", + "I'm doing the [Advent of Code](https://adventofcode.com) puzzles, just like [last year](https://github.com/norvig/pytudes/blob/master/ipynb/Advent%20of%20Code.ipynb). My terms of engagement are:\n", "\n", - "* I won't write a summary of each day's puzzle description. Follow the links in the section headers (e.g. **[Day 1](https://adventofcode.com/2017/day/1)**) to understand what each puzzle is asking. \n", - "* What you see is mostly the algorithm I first came up with first, although sometimes I go back and refactor if I think the original is unclear.\n", - "* I do clean up the code a bit even after I solve the puzzle: adding docstrings, changing variable names, changing input boxes to `assert` statements.\n", - "* I will describe my errors that slowed me down.\n", - "* Some days I start on time and try to code very quickly (although I know that people at the top of the leader board will be much faster than me); other days I end up starting late and don't worry about going quickly.\n", + "* You'll need to follow the links in the section headers (e.g. **[Day 1](https://adventofcode.com/2017/day/1)**) to understand what each puzzle is asking; I won't repeat the puzzle description.\n", + "* What you see is mostly the algorithm I came up with first, although sometimes I go back and refactor for clarity.\n", + "* I'll clean up the code a bit: adding docstrings, making variable names longer and more descriptive, adding `assert` statements.\n", + "* I will discuss any errors I made along the way; usually I won't show the erroneous code, just a description of what I did wrong.\n", + "* The way Advent of Code works is that you read the puzzle descriotion for Part One, but only when you correctly solve it do you get to see Part Two. This is typical in software development: you deploy some code, and then some new requirements arise. So it makes sense to program by creating small functions and data types that form a *vocabulary* for the domain at hand, and can be recombined to solve new problems in the domain.\n", + "* Each day's code should run in a few seconds; certainly less than a minute.\n", + "* There is a contest to see who can solve each day's puzzle fastest; I do not expect to be competitive.\n", "\n", "\n", "\n", @@ -28,7 +30,9 @@ { "cell_type": "code", "execution_count": 1, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "# Python 3.x Utility Functions\n", @@ -40,6 +44,7 @@ "import numpy as np\n", "import math\n", "import random\n", + "import time\n", "\n", "from collections import Counter, defaultdict, namedtuple, deque, abc, OrderedDict\n", "from functools import lru_cache\n", @@ -49,7 +54,6 @@ "from heapq import heappop, heappush\n", "from numba import jit\n", "\n", - "identity = lambda x: x\n", "letters = 'abcdefghijklmnopqrstuvwxyz'\n", "\n", "cache = lru_cache(None)\n", @@ -119,6 +123,8 @@ " # Why <= maxval rather than < maxval? In part because that's how Ruby's upto does it.\n", " return takewhile(lambda x: x <= maxval, iterable)\n", "\n", + "identity = lambda x: x\n", + "\n", "def groupby(iterable, key=identity):\n", " \"Return a dict of {key(item): [items...]} grouping all items in iterable by keys.\"\n", " groups = defaultdict(list)\n", @@ -179,11 +185,30 @@ " \n", "flatten = chain.from_iterable\n", "\n", - "def repeat(n, fn, arg):\n", - " \"Repeat arg=fn(arg) n times.\"\n", - " for i in range(n):\n", - " arg = fn(arg)\n", - " return arg\n", + "################ Functional programming\n", + "\n", + "def mapt(fn, *args): \n", + " \"Do a map, and make the results into a tuple.\"\n", + " return tuple(map(fn, *args))\n", + "\n", + "def map2d(fn, grid):\n", + " \"Apply fn to every element in a 2-dimensional grid.\"\n", + " return tuple(mapt(fn, row) for row in grid)\n", + "\n", + "def repeat(n, fn, arg, *args, **kwds):\n", + " \"Repeat arg = fn(arg) n times, return arg.\"\n", + " return nth(repeatedly(fn, arg, *args, **kwds), n)\n", + "\n", + "def repeatedly(fn, arg, *args, **kwds):\n", + " \"Yield arg, fn(arg), fn(fn(arg)), ...\"\n", + " yield arg\n", + " while True:\n", + " arg = fn(arg, *args, **kwds)\n", + " yield arg\n", + " \n", + "def compose(f, g): \n", + " \"The function that computes f(g(x)).\"\n", + " return lambda x: f(g(x))\n", "\n", "################ Making immutable objects\n", " \n", @@ -195,10 +220,6 @@ " \"Canonicalize these order-independent items into a hashable canonical form.\"\n", " typ = typ or (cat if isinstance(items, str) else tuple)\n", " return typ(sorted(items))\n", - "\n", - "def mapt(fn, *args): \n", - " \"Do a map, and make the results into a tuple.\"\n", - " return tuple(map(fn, *args))\n", " \n", "################ Math Functions\n", " \n", @@ -208,9 +229,9 @@ " \"Integer square root (rounds down).\"\n", " return int(n ** 0.5)\n", "\n", - "def ints(start, end):\n", + "def ints(start, end, step=1):\n", " \"The integers from start to end, inclusive: range(start, end+1)\"\n", - " return range(start, end + 1)\n", + " return range(start, end + 1, step)\n", "\n", "def floats(start, end, step=1.0):\n", " \"Yield floats from start to end (inclusive), by increments of step.\"\n", @@ -235,11 +256,19 @@ "\n", "################ 2-D points implemented using (x, y) tuples\n", "\n", - "def X(point): x, y = point; return x\n", - "def Y(point): x, y = point; return y\n", + "def X(point): return point[0]\n", + "def Y(point): return point[1]\n", "\n", "origin = (0, 0)\n", - "UP, DOWN, LEFT, RIGHT = (0, 1), (0, -1), (-1, 0), (1, 0)\n", + "HEADINGS = UP, LEFT, DOWN, RIGHT = (0, -1), (-1, 0), (0, 1), (1, 0)\n", + "\n", + "def turn_right(heading): return HEADINGS[HEADINGS.index(heading) - 1]\n", + "def turn_around(heading):return HEADINGS[HEADINGS.index(heading) - 2]\n", + "def turn_left(heading): return HEADINGS[HEADINGS.index(heading) - 3]\n", + "\n", + "def add(A, B): \n", + " \"Element-wise addition of two n-dimensional vectors.\"\n", + " return mapt(sum, zip(A, B))\n", "\n", "def neighbors4(point): \n", " \"The four neighboring squares.\"\n", @@ -260,9 +289,13 @@ " return sum(abs(p - q) for p, q in zip(P, Q))\n", "\n", "def distance(P, Q=origin): \n", - " \"Hypotenuse distance between two points.\"\n", + " \"Straight-line (hypotenuse) distance between two points.\"\n", " return sum((p - q) ** 2 for p, q in zip(P, Q)) ** 0.5\n", "\n", + "def king_distance(P, Q=origin):\n", + " \"Number of chess King moves between two points.\"\n", + " return max(abs(p - q) for p, q in zip(P, Q))\n", + "\n", "################ Debugging \n", "\n", "def trace1(f):\n", @@ -332,11 +365,15 @@ ], "source": [ "def tests():\n", + " \"Tests for my utility functions.\"\n", + " \n", " # Functions for Input, Parsing\n", " assert Array('''1 2 3\n", " 4 5 6''') == ((1, 2, 3), \n", " (4, 5, 6))\n", " assert Vector('testing 1 2 3.') == ('testing', 1, 2, 3.0)\n", + " assert Integers('test1 (2, -3), #4') == (2, -3, 4)\n", + " assert Atom('123.4') == 123.4 and Atom('x') == 'x'\n", " \n", " # Functions on Iterables\n", " assert first('abc') == first(['a', 'b', 'c']) == 'a'\n", @@ -353,7 +390,6 @@ " assert sequence((i**2 for i in range(5))) == (0, 1, 4, 9, 16)\n", " assert join(range(5)) == '01234'\n", " assert join(range(5), ', ') == '0, 1, 2, 3, 4'\n", - " assert multiply([1, 2, 3, 4]) == 24\n", " assert transpose(((1, 2, 3), (4, 5, 6))) == ((1, 4), (2, 5), (3, 6))\n", " assert isqrt(9) == 3 == isqrt(10)\n", " assert ints(1, 100) == range(1, 101)\n", @@ -363,9 +399,18 @@ " assert quantify(['testing', 1, 2, 3, int, len], callable) == 2 # int and len are callable\n", " assert quantify([0, False, None, '', [], (), {}, 42]) == 1 # Only 42 is truish\n", " assert set(shuffled('abc')) == set('abc')\n", + " \n", + " # Functional programming\n", + " assert mapt(math.sqrt, [1, 9, 4]) == (1, 3, 2)\n", + " assert map2d(abs, ((1, -2, -3), (-4, -5, 6))) == ((1, 2, 3), (4, 5, 6))\n", + " assert repeat(3, isqrt, 256) == 2\n", + " assert compose(isqrt, abs)(-9) == 3\n", + " \n", + " # Making immutable objects\n", + "\n", + " assert Set([1, 2, 3, 3]) == {1, 2, 3}\n", " assert canon('abecedarian') == 'aaabcdeeinr'\n", " assert canon([9, 1, 4]) == canon({1, 4, 9}) == (1, 4, 9)\n", - " assert mapt(math.sqrt, [1, 9, 4]) == (1, 3, 2)\n", " \n", " # Math\n", " assert transpose([(1, 2, 3), (4, 5, 6)]) == ((1, 4), (2, 5), (3, 6))\n", @@ -379,6 +424,7 @@ " assert X(P) == 3 and Y(P) == 4\n", " assert cityblock_distance(P) == cityblock_distance(P, origin) == 7\n", " assert distance(P) == distance(P, origin) == 5\n", + " assert turn_right(UP) == turn_left(DOWN) == turn_around(LEFT) == RIGHT\n", " \n", " # Search\n", " assert Astar((4, 4), neighbors8, distance) == [(4, 4), (3, 3), (2, 2), (1, 1), (0, 0)]\n", @@ -609,15 +655,15 @@ "data": { "text/plain": [ "[(0, 0),\n", - " (0, 1),\n", - " (-1, 1),\n", - " (-1, 0),\n", - " (-1, -1),\n", " (0, -1),\n", - " (1, -1),\n", - " (1, 0),\n", + " (-1, -1),\n", + " (-1, 0),\n", + " (-1, 1),\n", + " (0, 1),\n", " (1, 1),\n", - " (1, 2)]" + " (1, 0),\n", + " (1, -1),\n", + " (1, -2)]" ] }, "execution_count": 10, @@ -655,7 +701,7 @@ { "data": { "text/plain": [ - "(263, 212)" + "(263, -212)" ] }, "execution_count": 11, @@ -939,6 +985,7 @@ } ], "source": [ + "@jit\n", "def run2(program, verbose=False):\n", " memory = list(program)\n", " pc = steps = 0\n", @@ -970,8 +1017,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 6.2 s, sys: 8.37 ms, total: 6.21 s\n", - "Wall time: 6.21 s\n" + "CPU times: user 5.3 s, sys: 18.9 ms, total: 5.31 s\n", + "Wall time: 5.33 s\n" ] }, { @@ -1703,6 +1750,14 @@ "execution_count": 46, "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 14 ms, sys: 209 µs, total: 14.2 ms\n", + "Wall time: 14.3 ms\n" + ] + }, { "data": { "text/plain": [ @@ -1751,7 +1806,7 @@ "\n", "assert knothash2('') == 'a2582a3a0e66e6e86e3812dcb672a272'\n", "\n", - "knothash2(stream2)" + "%time knothash2(stream2)" ] }, { @@ -1767,7 +1822,7 @@ "source": [ "# [Day 11](https://adventofcode.com/2017/day/11): Hex Ed\n", "\n", - "The first thing I did was search [`[hex coordinates]`](https://www.google.com/search?source=hp&ei=Ft4xWoOqKcy4jAOs76a4CQ&q=hex+coordinates), and the #1 result (as I expected) was Amit Patel's \"[Hexagonal Grids](https://www.redblobgames.com/grids/hexagons/)\" page. I chose his \"odd-q vertical layout\" to define the six directions as (dx, dy) deltas:" + "The first thing I did was search [`[hex coordinates]`](https://www.google.com/search?source=hp&ei=Ft4xWoOqKcy4jAOs76a4CQ&q=hex+coordinates), and the #1 result (as I expected) was Amit Patel's \"[Hexagonal Grids](https://www.redblobgames.com/grids/hexagons/)\" page. I chose his \"odd-q vertical layout\" to define the six headings as (dx, dy) deltas:" ] }, { @@ -1778,14 +1833,14 @@ }, "outputs": [], "source": [ - "directions6 = dict(n=(0, -1), ne=(1, 0), se=(1, 1), s=(0, 1), sw=(-1, 0), nw=(-1, -1))" + "headings6 = dict(n=(0, -1), ne=(1, 0), se=(1, 1), s=(0, 1), sw=(-1, 0), nw=(-1, -1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now I can read the path, follow it, and see where it ends up. If the end point is `(x, y)`, then it will take `max(abs(x), abs(y))` steps to get back to the origin, because each step can increment or decrement either `x` or `y` or both." + "Now I can read the path, follow it, and see where it ends up. From there, we have to compute how far we are from the origin: I can use my `king_distance` function for that—the number of moves a Chess King would take." ] }, { @@ -1809,10 +1864,10 @@ "\n", "def follow(path):\n", " \"Follow each step of the path; return final distance to origin.\"\n", - " x, y = (0, 0)\n", - " for (dx, dy) in map(directions6.get, path):\n", - " x += dx; y += dy\n", - " return max(abs(x), abs(y))\n", + " pos = origin\n", + " for dir in path:\n", + " pos = add(pos, headings6[dir])\n", + " return king_distance(pos)\n", "\n", "follow(path)" ] @@ -1846,11 +1901,12 @@ ], "source": [ "def follow2(path):\n", - " \"Follow each step of the path; return max steps to origin.\"\n", - " x = y = maxsteps = 0\n", - " for (dx, dy) in map(directions6.get, path):\n", - " x += dx; y += dy\n", - " maxsteps = max(maxsteps, abs(x), abs(y))\n", + " \"Follow each step of the path; return the farthest away we ever got.\"\n", + " pos = origin\n", + " maxsteps = 0\n", + " for dir in path:\n", + " pos = add(pos, headings6[dir])\n", + " maxsteps = max(maxsteps, king_distance(pos))\n", " return maxsteps\n", "\n", "follow2(path)" @@ -2119,6 +2175,14 @@ "execution_count": 60, "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 515 ms, sys: 3.8 ms, total: 519 ms\n", + "Wall time: 519 ms\n" + ] + }, { "data": { "text/plain": [ @@ -2136,7 +2200,7 @@ " hash = knothash2(key + '-' + str(i))\n", " return format(int(hash, base=16), '0128b')\n", "\n", - "sum(bits(key, i).count('1') for i in range(128))" + "%time sum(bits(key, i).count('1') for i in range(128))" ] }, { @@ -2151,7 +2215,9 @@ { "cell_type": "code", "execution_count": 61, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "def Grid(key, N=128+2):\n", @@ -2213,6 +2279,14 @@ "execution_count": 64, "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 512 ms, sys: 2.21 ms, total: 514 ms\n", + "Wall time: 515 ms\n" + ] + }, { "data": { "text/plain": [ @@ -2225,7 +2299,7 @@ } ], "source": [ - "flood_all(Grid(key))" + "%time flood_all(Grid(key))" ] }, { @@ -2246,8 +2320,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 15.1 s, sys: 26.4 ms, total: 15.1 s\n", - "Wall time: 15.2 s\n" + "CPU times: user 15.4 s, sys: 56.6 ms, total: 15.4 s\n", + "Wall time: 15.5 s\n" ] }, { @@ -2262,22 +2336,23 @@ } ], "source": [ - "@jit # This was the slowest solution; @jit helps a bit.\n", + "@jit\n", "def gen(prev, factor, m=2147483647):\n", " \"Generate a sequence of numbers according to the rules; stop at 0.\"\n", " while prev:\n", " prev = (prev * factor) % m\n", " yield prev\n", " \n", - "def judge(A, B, N=40*10**6, mask=2**16-1): \n", + "def judge(A, B, N, mask=2**16-1): \n", " \"How many of the first N numbers from A and B agree in the masked bits (default last 16)?\"\n", " return quantify(a & mask == b & mask\n", " for (a, b, _) in zip(A, B, range(N)))\n", "\n", + "\n", "def A(): return gen(516, 16807)\n", "def B(): return gen(190, 48271)\n", "\n", - "%time judge(A(), B())" + "%time judge(A(), B(), 40*10**6)" ] }, { @@ -2300,8 +2375,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 9.39 s, sys: 8.62 ms, total: 9.4 s\n", - "Wall time: 9.4 s\n" + "CPU times: user 10.2 s, sys: 47.2 ms, total: 10.2 s\n", + "Wall time: 10.3 s\n" ] }, { @@ -2574,7 +2649,9 @@ { "cell_type": "code", "execution_count": 74, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "class Node:\n", @@ -2670,14 +2747,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 1.35 s, sys: 5.54 ms, total: 1.35 s\n", - "Wall time: 1.35 s\n" + "CPU times: user 1.66 s, sys: 12.9 ms, total: 1.68 s\n", + "Wall time: 1.68 s\n" ] }, { "data": { "text/plain": [ - "<__main__.Node at 0x110c44be0>" + "<__main__.Node at 0x1171a9f98>" ] }, "execution_count": 77, @@ -2693,7 +2770,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Bad news! It takes over a second to do just 100,000 insertions, which means about 10 minutes for 50 million insertions. I did in fact try\n", + "Bad news! More than a second for just 100,000 insertions, which projects to over 10 minutes for 50 million insertions. I did in fact try\n", "\n", " spinlock2(N=50000000).find(0).next.data\n", " \n", @@ -2710,8 +2787,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 5.47 s, sys: 6.62 ms, total: 5.48 s\n", - "Wall time: 5.48 s\n" + "CPU times: user 5.63 s, sys: 20.1 ms, total: 5.65 s\n", + "Wall time: 5.66 s\n" ] }, { @@ -2943,7 +3020,9 @@ { "cell_type": "code", "execution_count": 83, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "diagram = Inputstr(19)\n", @@ -3024,7 +3103,7 @@ "source": [ "# [Day 20](https://adventofcode.com/2017/day/20): Particle Swarm\n", "\n", - "I'll create structures for particles, each will have fields for particle's number (`id`), position (`p`), velocity(`v`), and acceleration (`a`):" + "I'll create structures for particles, each will have fields for particle's number (`id`), position (`p`), velocity(`v`), and acceleration (`a`). I have `particles` as a function that creartes a collection, and not a collection in its own right, because I anticipate that I will want to mutate particles, so I'll need a fresh copy every time I want to do something with them." ] }, { @@ -3048,12 +3127,12 @@ } ], "source": [ - "def particles(lines=list(Input(20))):\n", - " \"Parse the list of particles.\"\n", + "def particles(lines=tuple(Input(20))):\n", + " \"Parse the input file into a list of particles.\"\n", " return [Particle(id, *grouper(Integers(line), 3)) \n", " for id, line in enumerate(lines)]\n", "\n", - "def Particle(id, p, v, a): return Struct(id=id, p=p, v=v, a=a) \n", + "def Particle(id, p, v, a): return Struct(id=id, p=p, v=v, a=a) \n", "\n", "particles()[:5]" ] @@ -3062,7 +3141,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "I'm not quite sure how to determine what \"in the long run\" means, so I'll just interpret it as meaning \"after 1000 time steps.\" On each step we update velocity and position of each particle. At the end, we determine the id number of the particle closest to the origin. Note that `evolve` generates an infinite sequence of particles, and we use `nth` to pick out the 1000th generation." + "I'm not quite sure how to determine what \"in the long run\" means, so I'll just interpret it as meaning \"after 1000 updates.\" " ] }, { @@ -3082,24 +3161,19 @@ } ], "source": [ - "def evolve(particles):\n", - " \"Continually update particles, yielding them after each time step.\"\n", - " while True:\n", - " yield particles\n", - " for p in particles:\n", - " p.v = add(p.v, p.a)\n", - " p.p = add(p.p, p.v)\n", - " \n", - "def add(A, B): \n", - " \"Add two n-dimensional vectors.\"\n", - " return mapt(sum, zip(A, B))\n", + "def update(particles):\n", + " \"Update velocity and position of all particles.\"\n", + " for r in particles:\n", + " r.v = add(r.v, r.a)\n", + " r.p = add(r.p, r.v) \n", + " return particles\n", "\n", "def closest(particles):\n", " \"Find the particle closest to origin.\"\n", - " return min(particles, key=lambda p: sum(map(abs, p.p)))\n", + " return min(particles, key=lambda r: sum(map(abs, r.p)))\n", "\n", - "# Answer: the id of the particle closest to origin in the 1000th generation of `evolve`\n", - "closest(nth(evolve(particles()), 1000)).id" + "# Answer: the id of the particle closest to origin after 1000 updates\n", + "closest(repeat(1000, update, particles())).id" ] }, { @@ -3108,7 +3182,7 @@ "source": [ "**Part Two**\n", "\n", - "I can still use `evolve` unchanged, and I will add the function `remove_collisions` to eliminate particles that are in the same position as another particle. I use `map` to apply this to every generation. Note that `remove_collisions` both mutates the argument, `particles`, and also returns it. " + "I'll add the function `remove_collisions`, and now the thing we repeatedly do is the composition of `remove_collisions` and `update`. Also, instead of finding the `id` of the `closest` particle, now we just need to count the number of surviving particles:" ] }, { @@ -3129,14 +3203,19 @@ ], "source": [ "def remove_collisions(particles):\n", - " \"Mutate (and return) particles, keep only ones that have a unique position.\"\n", - " poscount = Counter(p.p for p in particles)\n", - " particles[:] = [p for p in particles if poscount[p.p] == 1]\n", - " return particles\n", + " \"Eliminate particles that are in the same place as another.\"\n", + " num_particles_at = Counter(r.p for r in particles)\n", + " return [r for r in particles if num_particles_at[r.p] == 1]\n", " \n", - "# Answer: remove collisions from every generation of evolving particles, \n", - "# select the 1000th generation, and say how many particles are in that generation.\n", - "len(nth(map(remove_collisions, evolve(particles())), 1000))" + "# Answer: number of particles remaining after collisions removed\n", + "len(repeat(1000, compose(remove_collisions, update), particles()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "I got the right answer both times, so my assumption that \"in the long run\" means \"1000 updates\" turned out to work for my input data, but I feel bad that it is not guaranteed to work for all input data." ] }, { @@ -3145,41 +3224,52 @@ "source": [ "# [Day 21](https://adventofcode.com/2017/day/21): Fractal Art\n", "\n", - "First I'll create a `dict` of enhancement rules. I'll translate `'#'` and `'.'` pixels to 1 and 0, and represent a grid as a tuple of tuples (e.g. `((0, 0), (1, 1))`), so that it can be a `dict` key. I have to deal with rotations and flips; I could do that at lookup time, but it will be faster to do it at rule creation time:" + "Today looks like a complex one, so I'll break the code up into more chunks and have more test assertions than usual. I can identify the following important data types:\n", + "\n", + "- `Enhancements`: a `dict` of `{grid: larger_grid}` rewrite rules.\n", + "- `grid`: a square of 0-or-1 pixels, such as `((0, 1), (0, 1))`. The function `Pixels` translates text into this form.\n", + "\n", + "I define the functions `rotate` and `flip`; the puzzle descriptions says \"When searching for a rule to use, rotate and flip the pattern as necessary,\" but I'm going to be doing many searches, and only one initialization of the rule set, so it will be more efficient to do the rotating and flipping just once:" ] }, { "cell_type": "code", "execution_count": 89, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "def Enhancements(lines):\n", - " \"Create a dict of {square: enhanced_square}\"\n", + " \"Create a dict of {grid: enhanced_grid}; include all rotations/flips.\"\n", " enhancements = {}\n", " for line in lines:\n", - " lhs, rhs = map(Key, line.split('=>'))\n", - " for r in range(4):\n", + " lhs, rhs = map(Pixels, line.split('=>'))\n", + " for rot in range(4):\n", " enhancements[lhs] = enhancements[flip(lhs)] = rhs\n", " lhs = rotate(lhs)\n", " return enhancements\n", - " \n", - "def Key(line): \n", - " \"Key('../##') => ((0, 0), (1, 1))\"\n", + "\n", + "def Pixels(text): \n", + " \"Translate the str '.#/.#' to the grid ((0, 1), (0, 1))\"\n", " bits = {'#': 1, '.': 0}\n", " return tuple(tuple(bits[p] for p in row.strip())\n", - " for row in line.split('/'))\n", + " for row in text.split('/'))\n", " \n", - "def rotate(key): return tuple(zip(*reversed(key)))\n", + "def rotate(subgrid): \n", + " \"Rotate a subgrid 90 degrees clockwise.\"\n", + " return tuple(zip(*reversed(subgrid)))\n", "\n", - "def flip(L): return tuple(tuple(reversed(row)) for row in L)" + "def flip(subgrid): \n", + " \"Reverse every row of the subgrid.\"\n", + " return tuple(tuple(reversed(row)) for row in subgrid)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's test this on just the enhancement rules with 2x2 squares; with rotations and flips there should be 16 entries:" + "Let's test some assertions, and then look at all the 2x2 enhancement rulesfrom my input file; with rotations and flips there should be 24 = 16 entries:" ] }, { @@ -3214,6 +3304,10 @@ } ], "source": [ + "assert Pixels('../##') == ((0, 0), (1, 1))\n", + "assert rotate(((0, 0), (1, 1))) == ((1, 0), (1, 0))\n", + "assert flip(((0, 0, 1), (1, 1, 0))) == ((1, 0, 0), (0, 1, 1))\n", + "\n", "Enhancements('''\n", "../.. => .../.#./.#.\n", "#./.. => .../#../#..\n", @@ -3227,34 +3321,27 @@ { "cell_type": "code", "execution_count": 91, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "16" - ] - }, - "execution_count": 91, - "metadata": {}, - "output_type": "execute_result" - } - ], + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ - "len(_)" + "assert len(_) == 2 ** 4" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Looks good; let's create my complete table. There should be 24 + 29 entries:" + "Looks good; let's create the complete `enhancements` for my data. There should be 24 + 29 = 528 entries:" ] }, { "cell_type": "code", "execution_count": 92, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "enhancements = Enhancements(Input(21))\n", @@ -3266,65 +3353,63 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now on each iteration we `enhance` the grid by first `expand`-ing it (looking up each `slice` in the `enhancements` rules) and then `stitch` the pieces together:" + "Now on each iteration we `enhance` the grid by first dividing it into pieces with `divide_grid`, then using my utility function `map2d` to apply `enhancements` to each piece, and then call `stitch_grid` to put all the pieces back together into a bigger grid:" ] }, { "cell_type": "code", "execution_count": 93, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "def enhance(grid): \n", " \"Expand small pieces into bigger ones and stitch them together.\"\n", - " return stitch(expand(grid))\n", + " return stitch_grid(map2d(enhancements.get, divide_grid(grid)))\n", "\n", - "def expand(grid):\n", + "def divide_grid(grid):\n", " \"Slice the grid into d x d pieces and enhance each piece.\"\n", " N = len(grid[0])\n", " d = (2 if N % 2 == 0 else 3 if N % 3 == 0 else error())\n", - " return [[enhancements[slice(grid, r, c, d)]\n", + " return [[tuple(row[c:c+d] for row in grid[r:r+d])\n", " for c in range(0, N, d)]\n", " for r in range(0, N, d)]\n", "\n", - "def stitch(pieces): \n", + "def stitch_grid(pieces): \n", " \"Stitch the pieces back into one big grid.\"\n", " N = sum(map(len, pieces[0]))\n", - " return tuple(tuple(get(pieces, r, c) \n", + " return tuple(tuple(getpixel(pieces, r, c) \n", " for c in range(N))\n", " for r in range(N))\n", "\n", - "def get(pieces, r, c):\n", + "def getpixel(pieces, r, c):\n", " \"The pixel at location (r, c), from a matrix of d x d pieces.\"\n", + " # Use `//` to find the right piece, and `%` to find the pixel within the piece\n", " d = len(pieces[0][0])\n", - " row = pieces[r // d]\n", - " cell = row[c // d]\n", - " return cell[r % d][c % d]\n", - "\n", - "def slice(grid, r, c, d):\n", - " \"The d x d slice of grid starting at position (r, c)\"\n", - " return tuple(row[c:c+d] for row in grid[r:r+d])" + " piece = pieces[r // d][c // d]\n", + " return piece[r % d][c % d]" ] }, { "cell_type": "code", "execution_count": 94, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "((0, 1, 0), (0, 0, 1), (1, 1, 1))" - ] - }, - "execution_count": 94, - "metadata": {}, - "output_type": "execute_result" - } - ], + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ - "grid = Key('.#./..#/###')\n", - "grid" + "# Some tests\n", + "corners = Pixels('#..#/..../..../#..#')\n", + "pieces = [[((1, 0), \n", + " (0, 0)), ((0, 1), \n", + " (0, 0))], \n", + " [((0, 0), \n", + " (1, 0)), ((0, 0), \n", + " (0, 1))]]\n", + "\n", + "assert divide_grid(corners) == pieces\n", + "assert stitch_grid(pieces) == corners" ] }, { @@ -3335,7 +3420,7 @@ { "data": { "text/plain": [ - "[[((1, 0, 1, 0), (0, 0, 1, 0), (0, 1, 0, 1), (0, 1, 0, 0))]]" + "((0, 1, 0), (0, 0, 1), (1, 1, 1))" ] }, "execution_count": 95, @@ -3344,7 +3429,9 @@ } ], "source": [ - "expand(_)" + "# An extended test \n", + "grid = Pixels('.#./..#/###')\n", + "grid" ] }, { @@ -3355,7 +3442,7 @@ { "data": { "text/plain": [ - "((1, 0, 1, 0), (0, 0, 1, 0), (0, 1, 0, 1), (0, 1, 0, 0))" + "[[((0, 1, 0), (0, 0, 1), (1, 1, 1))]]" ] }, "execution_count": 96, @@ -3364,7 +3451,7 @@ } ], "source": [ - "stitch(_)" + "divide_grid(_)" ] }, { @@ -3375,8 +3462,7 @@ { "data": { "text/plain": [ - "[[((0, 0, 0), (1, 0, 0), (1, 0, 0)), ((1, 0, 1), (0, 1, 0), (0, 1, 0))],\n", - " [((1, 0, 1), (0, 1, 0), (0, 1, 0)), ((0, 0, 0), (1, 0, 0), (1, 0, 0))]]" + "((((1, 0, 1, 0), (0, 0, 1, 0), (0, 1, 0, 1), (0, 1, 0, 0)),),)" ] }, "execution_count": 97, @@ -3385,13 +3471,74 @@ } ], "source": [ - "expand(_)" + "map2d(enhancements.get, _)" ] }, { "cell_type": "code", "execution_count": 98, "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "((1, 0, 1, 0), (0, 0, 1, 0), (0, 1, 0, 1), (0, 1, 0, 0))" + ] + }, + "execution_count": 98, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "stitch_grid(_)" + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[[((1, 0), (0, 0)), ((1, 0), (1, 0))], [((0, 1), (0, 1)), ((0, 1), (0, 0))]]" + ] + }, + "execution_count": 99, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "divide_grid(_)" + ] + }, + { + "cell_type": "code", + "execution_count": 100, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "((((0, 0, 0), (1, 0, 0), (1, 0, 0)), ((1, 0, 1), (0, 1, 0), (0, 1, 0))),\n", + " (((1, 0, 1), (0, 1, 0), (0, 1, 0)), ((0, 0, 0), (1, 0, 0), (1, 0, 0))))" + ] + }, + "execution_count": 100, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "map2d(enhancements.get, _)" + ] + }, + { + "cell_type": "code", + "execution_count": 101, + "metadata": {}, "outputs": [ { "data": { @@ -3404,18 +3551,18 @@ " (0, 1, 0, 1, 0, 0))" ] }, - "execution_count": 98, + "execution_count": 101, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "stitch(_)" + "stitch_grid(_)" ] }, { "cell_type": "code", - "execution_count": 99, + "execution_count": 102, "metadata": {}, "outputs": [ { @@ -3424,7 +3571,7 @@ "12" ] }, - "execution_count": 99, + "execution_count": 102, "metadata": {}, "output_type": "execute_result" } @@ -3437,12 +3584,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "That looks right; Let's try to solve the puzzle:" + "That looks right; Let's try to solve the whole puzzle:" ] }, { "cell_type": "code", - "execution_count": 100, + "execution_count": 103, "metadata": {}, "outputs": [ { @@ -3451,7 +3598,7 @@ "147" ] }, - "execution_count": 100, + "execution_count": 103, "metadata": {}, "output_type": "execute_result" } @@ -3464,136 +3611,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "That's correct!\n", + "\n", "**Part Two**\n", "\n", - "It looked like I didn't need to change any code to do Part Two, just change the `5` to an `18`. I ran it, and got an answer in a few seconds—but the answer was wrong. I went back and carefully looked over my code, and realized there was a place in `expand` where I had swapped the order of `r` and `c` (the code there is fixed now). Apparently there is some symmetry over the first 5 enhancements that yields the correct answer either way, but that breaks down over 18 enhancements. (Also: if I had guessed that `sum(flatten(repeat(n, enhance, grid)))` would be used in both parts, I probably would have created a function for it. But I was guessing that Part Two would involve `enhance`, but not necessarily in the same way as Part One.)" - ] - }, - { - "cell_type": "code", - "execution_count": 101, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "1936582" - ] - }, - "execution_count": 101, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sum(flatten(repeat(18, enhance, grid)))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# A Note on Reuse\n", + "Huh — It looks like I don't need to change any code for Part Two, just do `18` repetitions instead of `5`. \n", "\n", - "One interesting question about these two-part problems: how does the code in Part Two use the code in Part One? \n", - "Here are my answers:\n", - "\n", - "* **All new**: All the code for Part Two (except possibly reading and parsing the input) is brand new: \n", - "
Days 1, 2, 4, 7\n", - "\n", - "* **Reuse as is**: The major function defined in Part One is called again in Part Two (and some new code\n", - "is also written):\n", - "
Days 3 (`spiral`), 6 (`spread`, but `realloc2` is copy-paste-edit), 9, 12, 14 (`bits`), \n", - "15 (`A, B, gen, judge`), 16 (`perform`), 19 (`follow_tubes`), 20 (`evolve, particles`), 21 (`expands`, `gridsum`)\n", - "\n", - "* **Generalize**: A major function from Part One is generalized in Part Two (e.g. by adding an optional parameter):\n", - "
Days 13 (`caught`)\n", - "\n", - "* **Copy-paste-edit**: The major function from Part One is copied and edited for Part Two:\n", - "
Days 5 (`run2`), 8 (`run82`), 10 (`knothash2`), 11 (`follow2`), 17 (`spinlock2`), 18 (`step18`)\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Wrapping Up: Verification and Timing\n", - "\n", - "Here is a little test harness to verify that I still get the right answers (even if I refactor some of the code):" - ] - }, - { - "cell_type": "code", - "execution_count": 106, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 54.6 s, sys: 156 ms, total: 54.7 s\n", - "Wall time: 54.9 s\n" - ] - } - ], - "source": [ - "%%time\n", - "\n", - "def day(d, compute1, answer1, compute2, answer2):\n", - " \"Assert that we get the right answers for this day.\"\n", - " assert compute1 == answer1\n", - " assert compute2 == answer2\n", - "\n", - "day(1, sum(digits[i] for i in range(N) if digits[i] == digits[i - 1]), 1158,\n", - " sum(digits[i] for i in range(N) if digits[i] == digits[i - N // 2]), 1132)\n", - "day(2, sum(abs(max(row) - min(row)) for row in rows2), 46402, \n", - " sum(map(evendiv, rows2)), 265)\n", - "day(3, cityblock_distance(nth(spiral(), M - 1)), 475, \n", - " first(x for x in spiralsums() if x > M), 279138)\n", - "day(4, quantify(Input(4), is_valid), 337, \n", - " quantify(Input(4), is_valid2), 231)\n", - "day(5, run(program), 364539, \n", - " run2(program), 27477714)\n", - "day(6, realloc(banks), 12841, \n", - " realloc2(banks), 8038)\n", - "day(7, first(programs - set(flatten(above.values()))), 'wiapj', \n", - " correct(wrongest(programs)), 1072)\n", - "day(8, max(run8(program8).values()), 6828, \n", - " run8_2(program8), 7234)\n", - "day(9, total_score(text2), 9662, \n", - " len(text1) - len(text3), 4903)\n", - "day(10, knothash(stream), 4480, \n", - " knothash2(stream2), 'c500ffe015c83b60fad2e4b7d59dabc4')\n", - "day(11, follow(path), 705, \n", - " follow2(path), 1469)\n", - "day(12, len(G[0]), 115, \n", - " len({Set(G[i]) for i in G}), 221)\n", - "day(13, trip_severity(scanners), 1504, \n", - " safe_delay(scanners), 3823370)\n", - "day(14, sum(bits(key, i).count('1') for i in range(128)), 8316, \n", - " flood_all(Grid(key)), 1074)\n", - "day(15, judge(A(), B()), 597, \n", - " judge(criteria(4, A()), criteria(8, B()), 5*10**6), 303)\n", - "day(16, perform(dance), 'lbdiomkhgcjanefp', \n", - " whole(48, dance), 'ejkflpgnamhdcboi')\n", - "day(17, spinlock2().find(2017).next.data, 355,\n", - " spinlock3(N=50*10**6)[1], 6154117)\n", - "day(18, run18(program18), 7071, \n", - " run18_2(program18)[1].sends, 8001)\n", - "day(19, cat(filter(str.isalpha, follow_tubes(diagram))), 'VEBTPXCHLI', \n", - " quantify(follow_tubes(diagram)), 18702)\n", - "day(20, closest(nth(evolve(particles()), 1000)).id, 243,\n", - " len(nth(map(remove_collisions, evolve(particles())), 1000)), 648)\n", - "day(21, sum(flatten(repeat(5, enhance, grid))), 147,\n", - " sum(flatten(repeat(18, enhance, grid))), 1936582)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And here is a plot of the time taken to program solutions to both parts of each puzzle each day, for me, the first person to finish, and the hundredth person. On days when I started late, the times are my estimates, not official times (these include days 1, 3, 8, 13, 14, 17, 18, 21)." + "Well, almost. Doing that gave an answer (in a few seconds); but the answer was wrong. I carefully looked over all my code, and realized there was a place where I had swapped the order of `r` and `c`. Once I fixed that (the fix is already incorporated above), I got the right answer:" ] }, { @@ -3601,11 +3625,801 @@ "execution_count": 104, "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 5.35 s, sys: 90.8 ms, total: 5.44 s\n", + "Wall time: 5.44 s\n" + ] + }, { "data": { - "image/png": "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\n", "text/plain": [ - "" + "1936582" + ] + }, + "execution_count": 104, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time sum(flatten(repeat(18, enhance, grid)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 22](https://adventofcode.com/2017/day/22): Sporifica Virus\n", + "\n", + "This one looks to be of medium difficulty. One important choice: since we are dealing with \"a seemingly-infinite two-dimensional grid of compute nodes,\" and I think it will be sparse, I'll represent the grid with a `set` of the positions of infected nodes, rather than with a 2-dimensional array. I'll define a `namedtuple` to hold the state of the network: the current position of the virus, its heading, the number of infections caused so far, and the set of infected nodes:" + ] + }, + { + "cell_type": "code", + "execution_count": 105, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "Net = namedtuple('Net', 'current, heading, caused, infected')\n", + "\n", + "def parse_net(lines):\n", + " \"Read the initial state of the network.\"\n", + " lines = list(lines)\n", + " current = (len(lines) // 2, len(lines[0].strip()) // 2)\n", + " return Net(current, UP, 0,\n", + " {(x, y) \n", + " for (y, row) in enumerate(lines) \n", + " for (x, node) in enumerate(row)\n", + " if node == '#'})" + ] + }, + { + "cell_type": "code", + "execution_count": 106, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Net(current=(1, 1), heading=(0, -1), caused=0, infected={(0, 1), (2, 0)})" + ] + }, + "execution_count": 106, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "test = '''\n", + "..#\n", + "#..\n", + "...\n", + "'''.strip().splitlines()\n", + " \n", + "parse_net(test)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now the logic for one step of the simulation, called a *burst*:" + ] + }, + { + "cell_type": "code", + "execution_count": 107, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def burst(net):\n", + " \"Simulate the virus through one step and return the new state of the network.\"\n", + " (current, heading, caused, infected) = net\n", + " heading = (turn_right if current in infected else turn_left)(heading)\n", + " if current in infected:\n", + " infected.remove(current)\n", + " else:\n", + " caused += 1\n", + " infected.add(current)\n", + " return Net(add(current, heading), heading, caused, infected)" + ] + }, + { + "cell_type": "code", + "execution_count": 108, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Net(current=(1, 0), heading=(1, 0), caused=5, infected={(0, 1), (-1, 1), (-1, 0), (2, 0), (1, 1)})" + ] + }, + "execution_count": 108, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# We're supposed to get 5 infections caused in the first 7 steps:\n", + "repeat(7, burst, parse_net(test))" + ] + }, + { + "cell_type": "code", + "execution_count": 109, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Net(current=(2, 0), heading=(0, -1), caused=41, infected={(5, -1), (3, 2), (-1, 0), (3, -3), (1, 0), (1, -2), (4, -2), (-1, 1), (2, -3), (5, 0), (2, 2), (0, -1), (4, 1), (1, 1)})" + ] + }, + "execution_count": 109, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# And 41 out of 70:\n", + "repeat(70, burst, parse_net(test))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This testing revealed a problem: I had (yet again) messed up the order of (x, y). (I think it is confusing that we have two traditional orders: (x, y) and (row, col), and they are not the same.) After fixing that, I was\n", + "ready to solve the problem:" + ] + }, + { + "cell_type": "code", + "execution_count": 110, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "5460" + ] + }, + "execution_count": 110, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "repeat(10000, burst, parse_net(Input(22))).caused" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Part Two**\n", + "\n", + "It looks like I can't re-use any of my code from Part One (except by copy-and-paste), because I want to replace my `set` of `infected` nodes with a `dict` of node `status`, which can be `I`, `F`, `C`, or `W`:" + ] + }, + { + "cell_type": "code", + "execution_count": 111, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "Net2 = namedtuple('Net2', 'current, heading, caused, status')\n", + "\n", + "def parse_net2(lines):\n", + " \"Read the initial state of the network.\"\n", + " lines = list(lines)\n", + " current = (len(lines) // 2, len(lines[0].strip()) // 2)\n", + " return Net2(current, UP, 0,\n", + " {(x, y): 'I'\n", + " for (y, row) in enumerate(lines) \n", + " for (x, node) in enumerate(row)\n", + " if node == '#'})\n", + "\n", + "def burst2(net):\n", + " \"Simulate the evolved virus through one step and return the new state of the network.\"\n", + " (current, heading, caused, status) = net\n", + " cur = status.get(current, 'C')\n", + " if cur == 'C':\n", + " heading = turn_left(heading)\n", + " status[current] = 'W'\n", + " elif cur == 'W':\n", + " # heading unchanged\n", + " status[current] = 'I'\n", + " caused += 1\n", + " elif cur == 'I':\n", + " heading = turn_right(heading)\n", + " status[current] = 'F'\n", + " elif cur == 'F':\n", + " heading = turn_around(heading)\n", + " status[current] = 'C'\n", + " return Net2(add(current, heading), heading, caused, status)" + ] + }, + { + "cell_type": "code", + "execution_count": 112, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "26" + ] + }, + "execution_count": 112, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Of the first 100 bursts, 26 will result in infection\n", + "repeat(100, burst2, parse_net2(test)).caused" + ] + }, + { + "cell_type": "code", + "execution_count": 113, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 21.2 s, sys: 38.9 ms, total: 21.3 s\n", + "Wall time: 21.3 s\n" + ] + }, + { + "data": { + "text/plain": [ + "2511702" + ] + }, + "execution_count": 113, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Ready to answer Part Two\n", + "# (A little nervous about 10,000,000 repetitions, but I think it will be under a minute.)\n", + "%time repeat(10000000, burst2, parse_net2(Input(22))).caused" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "I had another bug here that gave me the wrong answer the first time: I had put the \"`caused += 1`\" line under the condition where the status *was* `'I'`, whereas it actually belongs under the condition where the status *becomes* `'I'`.\n", + "\n", + "# [Day 23](https://adventofcode.com/2017/day/23): Coprocessor Conflagration\n", + "\n", + "Part One looks straightforward. I won't make the \"X might be an integer\" mistake again:\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 114, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "9409" + ] + }, + "execution_count": 114, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def run23(program):\n", + " regs = {L: 0 for L in 'abcdefgh'}\n", + " pc = 0\n", + " mulcount = 0\n", + " while 0 <= pc < len(program):\n", + " op, X, Y = program[pc]\n", + " pc += 1\n", + " if op == 'set': regs[X] = value(regs, Y)\n", + " elif op == 'sub': regs[X] -= value(regs, Y)\n", + " elif op == 'mul': regs[X] *= value(regs, Y); mulcount += 1\n", + " elif op == 'jnz' and value(regs, X): pc += value(regs, Y) - 1\n", + " return mulcount\n", + "\n", + "run23(Array(Input(23)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Part Two**\n", + "\n", + "The hint of \"You'll need to **optimize the program**\" reminded me of a puzzle from 2016 where I had to understand what the program was doing and make it more efficient. It wasn't obvious what Day 23's program was doing, but I began the process of re-writing it as a Python program, converting the `jnz` instructions to `if` and `while` statements. Eventually I realized that the inner loop was doing \"`b % d`\", and my program became the following:" + ] + }, + { + "cell_type": "code", + "execution_count": 115, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 950 ms, sys: 3.65 ms, total: 953 ms\n", + "Wall time: 953 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "913" + ] + }, + "execution_count": 115, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "@jit\n", + "def run23_2():\n", + " a = 1\n", + " d = e = f = g = h = 0\n", + " b = 99\n", + " c = b\n", + " if a:\n", + " b *= 100\n", + " b -= -100000\n", + " c = b\n", + " c -= -17000\n", + " while True:\n", + " f = 1\n", + " d = 2\n", + " e = 2\n", + " while True:\n", + " if b % d == 0:\n", + " f = 0\n", + " d -= -1\n", + " g = d - b\n", + " if g == 0:\n", + " if f == 0:\n", + " h -= -1\n", + " g = b - c\n", + " if g == 0:\n", + " return h\n", + " b -= -17\n", + " break\n", + " \n", + "%time run23_2()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `numba.jit` decorator really helps here, speeding up execution from 13 seconds to 1 second. It also helped on Day 15, but not as dramatically, and was not able to help on Day 22.\n", + "\n", + "\n", + "# [Day 24](https://adventofcode.com/2017/day/24): Electromagnetic Moat\n", + "\n", + "First I will read the data and store it as a table of `{port_number: [all_components_with_that_number_on_either_side]}`. I also define two simple utility functions:" + ] + }, + { + "cell_type": "code", + "execution_count": 116, + "metadata": {}, + "outputs": [], + "source": [ + "def component_table(pairs):\n", + " \"Make a table of {port: {components_with_that_port}\"\n", + " ctable = defaultdict(set)\n", + " for pair in pairs:\n", + " ctable[pair[0]].add(pair)\n", + " ctable[pair[1]].add(pair)\n", + " return ctable\n", + "\n", + "ctable = component_table(map(Integers, Input(24)))\n", + "\n", + "def other_port(component, port):\n", + " \"The other port in a two-port component.\"\n", + " return (component[1] if component[0] == port else component[0])\n", + "\n", + "def strength(chain): return sum(flatten(chain))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We are dealing with an optimization problem involving paths in a graph (called *chains* in this problem), and we're looking for the chain that maximizes `strength`. I'll represent a chain as a tuple of components. I could have defined a single function that traverses the graph and also keeeps track of the maximum, but I think it is cleaner to keep the two aspects of the problem separate. First a function to generate all possible chains:" + ] + }, + { + "cell_type": "code", + "execution_count": 117, + "metadata": {}, + "outputs": [], + "source": [ + "def chains(chain=(), port=0, ctable=ctable):\n", + " \"Given a partial chain ending in `port`, yield all chains that extend it.\"\n", + " yield chain\n", + " for c in ctable[port]:\n", + " if c not in chain: \n", + " # Extend with components, c, that match port but are not already in chain\n", + " yield from chains(chain + (c,), other_port(c, port), ctable)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And then asking for the strength of the strongest chain:" + ] + }, + { + "cell_type": "code", + "execution_count": 118, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 2.38 s, sys: 5.91 ms, total: 2.39 s\n", + "Wall time: 2.39 s\n" + ] + }, + { + "data": { + "text/plain": [ + "1695" + ] + }, + "execution_count": 118, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time strength(max(chains(), key=strength))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "I was worried it was going to be slow, so I measured the `%time`, but it turned out not too bad.\n", + "\n", + "**Part Two**\n", + "\n", + "Now we want to find the strength of the longest chain, but if there is a tie, pick the strongest one:" + ] + }, + { + "cell_type": "code", + "execution_count": 119, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 2.45 s, sys: 3.48 ms, total: 2.45 s\n", + "Wall time: 2.45 s\n" + ] + }, + { + "data": { + "text/plain": [ + "1673" + ] + }, + "execution_count": 119, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def length_and_strength(c): return len(c), strength(c)\n", + "\n", + "%time strength(max(chains(), key=length_and_strength))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "I think I made the right choice in defining things the way I did. My code is simple, and gets the right answers in a few seconds. But I realize there are some inefficiencies:\n", + "\n", + "- Calculating the strength of a chain is O(N), but since we always form new chains by extending an old chain (for which we know the strength) with one new component, calculating the strength of the new chain could be O(1).\n", + "- A chain is a `tuple`, so checking \"`c not in chain`\" is O(N). If the chain were a `set`, it would be O(1).\n", + "- A new chain is created by *copying* the previous chain and appending a new component. A more efficient approach is to *mutate* the chain by adding a component, and then removing the component when it is time to consider other possibilities. This is called *backtracking*.\n", + "\n", + "Here is a backtracking implementation. It keeps track of a single `chain`, `port`, and `strength`. A call to `recurse(best_strength)` returns the best strength, either the one passed in, or one found by adding components to the current chain. When `recurse` returns, `chain`, `port`, and `strength` are reset to their original values, and the best strength found is returned as the value of the call to `recurse`. This is indeed faster (and gives the same answer): " + ] + }, + { + "cell_type": "code", + "execution_count": 120, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 635 ms, sys: 1.79 ms, total: 637 ms\n", + "Wall time: 637 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "1695" + ] + }, + "execution_count": 120, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def strongest_chain(ctable=ctable):\n", + " \"\"\"Return the strength of the strongest chain, using backtracking.\"\"\"\n", + " chain = set()\n", + " port = 0\n", + " strength = 0\n", + " def recurse(best_strength):\n", + " nonlocal chain, port, strength\n", + " for c in ctable[port] - chain:\n", + " # Update chain, port, strength\n", + " # then recurse and possibly update best_strength\n", + " # then backtrack and rest chain, port, strength\n", + " chain.add(c)\n", + " port = other_port(c, port)\n", + " strength += sum(c)\n", + " best_strength = max(strength, recurse(best_strength))\n", + " chain.remove(c)\n", + " port = other_port(c, port)\n", + " strength -= sum(c)\n", + " return best_strength\n", + " return recurse(0)\n", + "\n", + "%time strongest_chain()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can decide whether the saving in time is worth the complication in code." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# [Day 25](https://adventofcode.com/2017/day/25): The Halting Problem\n", + "\n", + "I won't write a parser for my input; instead I'll translate it into a `dict`:" + ] + }, + { + "cell_type": "code", + "execution_count": 121, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def machine():\n", + " \"machine()[state][value] == (new_value, move, new_state)}\"\n", + " L, R = -1, +1\n", + " A, B, C, D, E, F = 'ABCDEF'\n", + " return {\n", + " A: ((1, R, B), (0, L, C)),\n", + " B: ((1, L, A), (1, R, D)),\n", + " C: ((0, L, B), (0, L, E)),\n", + " D: ((1, R, A), (0, R, B)),\n", + " E: ((1, L, F), (1, L, C)),\n", + " F: ((1, R, D), (1, R, A))}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now a simple interpreter for machines like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 122, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "4769" + ] + }, + "execution_count": 122, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def turing(machine, state='A', steps=12667664):\n", + " \"Run the Turing machine for given number of steps, then return tape.\"\n", + " tape = defaultdict(int)\n", + " cursor = 0\n", + " for step in range(steps):\n", + " tape[cursor], move, state = machine[state][tape[cursor]]\n", + " cursor += move\n", + " return tape\n", + "\n", + "sum(turing(machine()).values())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# A Note on Reuse\n", + "\n", + "One interesting question: for what days did my Part Two code reuse the Part One code? How so?\n", + "Here are my answers:\n", + "\n", + "\n", + "* **Total Reuse**: The major function defined in Part One is called again in Part Two:\n", + "
Days 3 (`spiral`), 6 (`spread`, but `realloc2` is copy-paste-edit), 9, 12, 14 (`bits`), \n", + "15 (`A, B, gen, judge`), 16 (`perform`), 19 (`follow_tubes`), 20 (`update, particles`), 21 (`enhance`),\n", + "24 (`chains`, `strength`)\n", + "\n", + "* **Generalization**: A major function from Part One is generalized in Part Two (e.g. by adding an optional parameter):\n", + "
Days 13 (`caught`)\n", + "\n", + "* **Copy-edit**: The major function from Part One is copied and edited for Part Two:\n", + "
Days 5 (`run2`), 8 (`run8_2`), 10 (`knothash2`), 11 (`follow2`), 17 (`spinlock2`), 18 (`run18_2`), 22 (`parse_net2`, `burst2`)\n", + "\n", + "* **All new**: All the code for Part Two (except possibly reading and parsing the input) is brand new: \n", + "
Days 1, 2, 4, 7, 23\n", + "\n", + "I think I did a reasonably good job of facilitating reuse. It seems like using generators and higher-order functions like `repeat` helps.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Verification and Timing\n", + "\n", + "A little test harness and a report on run times:" + ] + }, + { + "cell_type": "code", + "execution_count": 123, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Day 5: 5.1 sec\n", + "Day 15: 25.6 sec\n", + "Day 17: 5.8 sec\n", + "Day 21: 5.4 sec\n", + "Day 22: 21.2 sec\n", + "CPU times: user 1min 19s, sys: 262 ms, total: 1min 19s\n", + "Wall time: 1min 19s\n" + ] + } + ], + "source": [ + "%%time\n", + "\n", + "def run_tests(tests, short=5.0):\n", + " \"Run daily test assertions; report times > `short` seconds.\"\n", + " for day in sorted(tests):\n", + " t0 = time.time()\n", + " assert tests[day]()\n", + " dt = time.time() - t0\n", + " if dt > short: \n", + " print('Day {:2d}: {:4.1f} sec'.format(day, dt))\n", + " \n", + "run_tests({\n", + " 1: lambda: sum(digits[i] for i in range(N) if digits[i] == digits[i - 1]) == 1158 and\n", + " sum(digits[i] for i in range(N) if digits[i] == digits[i - N // 2]) == 1132,\n", + " 2: lambda: sum(abs(max(row) - min(row)) for row in rows2) == 46402 and\n", + " sum(map(evendiv, rows2)) == 265,\n", + " 3: lambda: cityblock_distance(nth(spiral(), M - 1)) == 475 and\n", + " first(x for x in spiralsums() if x > M) == 279138,\n", + " 4: lambda: quantify(Input(4), is_valid) == 337 and\n", + " quantify(Input(4), is_valid2) == 231,\n", + " 5: lambda: run(program) == 364539 and\n", + " run2(program) == 27477714,\n", + " 6: lambda: realloc(banks) == 12841 and\n", + " realloc2(banks) == 8038,\n", + " 7: lambda: first(programs - set(flatten(above.values()))) == 'wiapj' and\n", + " correct(wrongest(programs)) == 1072,\n", + " 8: lambda: max(run8(program8).values()) == 6828 and\n", + " run8_2(program8) == 7234,\n", + " 9: lambda: total_score(text2) == 9662 and\n", + " len(text1) - len(text3) == 4903,\n", + " 10: lambda: knothash(stream) == 4480 and\n", + " knothash2(stream2) == 'c500ffe015c83b60fad2e4b7d59dabc4',\n", + " 11: lambda: follow(path) == 705 and\n", + " follow2(path) == 1469,\n", + " 12: lambda: len(G[0]) == 115 and\n", + " len({Set(G[i]) for i in G}) == 221,\n", + " 13: lambda: trip_severity(scanners) == 1504 and\n", + " safe_delay(scanners) == 3823370,\n", + " 14: lambda: sum(bits(key, i).count('1') for i in range(128)) == 8316 and\n", + " flood_all(Grid(key)) == 1074,\n", + " 15: lambda: judge(A(), B(), 40*10**6) == 597 and\n", + " judge(criteria(4, A()), criteria(8, B()), 5*10**6) == 303,\n", + " 16: lambda: perform(dance) == 'lbdiomkhgcjanefp' and\n", + " whole(48, dance) == 'ejkflpgnamhdcboi',\n", + " 17: lambda: spinlock2().find(2017).next.data == 355 and\n", + " spinlock3(N=50*10**6)[1] == 6154117,\n", + " 18: lambda: run18(program18) == 7071 and\n", + " run18_2(program18)[1].sends == 8001,\n", + " 19: lambda: cat(filter(str.isalpha, follow_tubes(diagram))) == 'VEBTPXCHLI' and\n", + " quantify(follow_tubes(diagram)) == 18702,\n", + " 20: lambda: closest(repeat(1000, update, particles())).id == 243 and\n", + " len(repeat(1000, compose(remove_collisions, update), particles())) == 648,\n", + " 21: lambda: sum(flatten(repeat(5, enhance, grid))) == 147 and\n", + " sum(flatten(repeat(18, enhance, grid))) == 1936582,\n", + " 22: lambda: repeat(10000, burst, parse_net(Input(22))).caused == 5460 and\n", + " repeat(10000000, burst2, parse_net2(Input(22))).caused == 2511702,\n", + " 23: lambda: run23(Array(Input(23))) == 9409 and\n", + " run23_2() == 913,\n", + " 24: lambda: strongest_chain() == 1695 and\n", + " strength(max(chains(), key=length_and_strength)) == 1673,\n", + " 25: lambda: sum(turing(machine()).values()) == 4769\n", + "})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Development Time\n", + "\n", + "Here is a plot of the time taken to program solutions to both parts of each puzzle each day, for me, the first person to finish, and the hundredth person. I'm usually about triple the time of the first solver, and a little slower than the 100th." + ] + }, + { + "cell_type": "code", + "execution_count": 124, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\nAAwGg2Gl/H827qoWdm5ZaQAAAABJRU5ErkJggg==\n", + "text/plain": [ + "" ] }, "metadata": {}, @@ -3616,16 +4430,16 @@ "def plot_times(times):\n", " plt.style.use('seaborn-whitegrid')\n", " X = ints(1, len(times[0]) - 2)\n", - " for (label, mark, *Y) in times:\n", - " plt.plot(X, Y, mark, label=label)\n", - " plt.xlabel('Day Number'); plt.ylabel('Minutes to Solve Both Parts')\n", + " for (label, c, *Y) in times:\n", + " plt.plot(X, Y, c+':', label=label)\n", + " plt.xlabel('Day Number'); \n", + " plt.ylabel('Minutes to Solve Both Parts')\n", " plt.legend(loc='upper left')\n", "\n", - "x = None\n", "plot_times([\n", - " ('Me', 'd:', 4, 6, 20, 5, 12, 30, 33, 10, 21, 40, 13, 12, 30, 41, 13, 64, 54, 74, 50, 18, 40),\n", - " ('100th', 'v:', 6, 6, 23, 4, 5, 9, 25, 8, 12, 25, 12, 9, 22, 25, 10, 27, 16, 41, 18, 21, 45),\n", - " ('1st', '^:', 1, 1, 4, 1, 2, 3, 10, 3, 4, 6, 3, 2, 6, 5, 2, 5, 5, 10, 5, 7, 10)])" + " ('Me', 'o', 4, 6, 20, 5, 12, 30, 33, 10, 21, 40, 13, 12, 30, 41, 13, 64, 54, 74, 50, 18, 40, 25, 50, 10, 10),\n", + " ('100th','v', 6, 6, 23, 4, 5, 9, 25, 8, 12, 25, 12, 9, 22, 25, 10, 27, 16, 41, 18, 21, 45, 20, 54, 21, 11),\n", + " ('1st', '^', 1, 1, 4, 1, 2, 3, 10, 3, 4, 6, 3, 2, 6, 5, 2, 5, 5, 10, 5, 7, 10, 6, 19, 6, 2)])" ] } ],