From Dec. 1 to Dec. 25, I will be solving the puzzles that appear each day at Advent of Code . The two-part puzzles are released at midnight EST (9:00PM my time); 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:
On days when I start at 9:00PM and am competing against the clock, I take shortcuts. I use shorter names, because I'm not a fast typer. I run test cases in the Jupyter Notebook, but don't make them into assert statements. Even then, I'm not really competitive with the fastest solvers. On days when I don't start until all the points are gone, what you see here is pretty much exactly what I did, or at least what I ended up with after ccorrecting typos and other errors.To understand the problems completely, you will have to read the full description in the " Day 1 :" link in each day's section header.
Day 0: Getting ReadyOn November 30th, I spent some time preparing:
I'll import my favorite modules and functions, so I don't have to do it each day.
From looking at last year's puzzles, I knew that there would be a data file on most days, so I defined Input to open the file.
From working on another puzzle site, Project Euler , I had built up a collection of utility functions, shown below:
In[1]: import re import numpy as np import math from collections import Counter, defaultdict, namedtuple, deque from functools import lru_cache from itertools import permutations, combinations, chain, cycle, product from heapq import heappop, heappush def Input(day): "Open this day's input file." return open('advent2016/input{}.txt'.format(day)) def transpose(matrix): return zip(*matrix) def first(iterable): return next(iter(iterable)) def firsttrue(iterable): return first(it for it in iterable if it) def counttrue(iterable): return sum(bool(it) for it in iterable) cat = ''.join = frozenset() # Empty set inf = float('inf') BIG = 10 ** 999 def grep(pattern, lines): "Print lines that match pattern." for line in lines: if re.search(pattern, line): print(line) def groupby(iterable, key=lambda it: it): "Return a dic whose keys are key(it) and whose values are all the elements of iterable with that key." dic = defaultdict(list) for it in iterable: dic[key(it)].append(it) return dic def powerset(iterable): "Yield all subsets of items." items = list(iterable) for r in range(len(items)+1): for c in combinations(items, r): yield c # 2-D points implemented using (x, y) tuples def X(point): return point[0] def Y(point): return point[1] def neighbors4(point): "The four neighbors (without diagonals)." x, y = point return ((x+1, y), (x-1, y), (x, y+1), (x, y-1)) def neighbors8(point): "The eight neifhbors (with diagonals)." x, y = point return ((x+1, y), (x-1, y), (x, y+1), (x, y-1), (X+1, y+1), (x-1, y-1), (x+1, y-1), (x-1, y+1)) def cityblock_distance(p, q=(0, 0)): "City block distance between two points." return abs(X(p) - X(q)) + abs(Y(p) - Y(q)) def euclidean_distance(p, q=(0, 0)): "Euclidean (hypotenuse) distance between two points." return math.hypot(X(p) - X(q), Y(p) - Y(q)) def trace1(f): "Print a trace of the input and output of a function on one line." def traced_f(*args): result = f(*args) print('{} = {}'.format(_callstr(f, args), result)) return result return traced_f def trace(f): "Print a trace of the call and args on one line, and the return on another." def traced_f(*args): print(_callstr(f, args)) trace.indent += 1 try: result = f(*args) finally: trace.indent -= 1 print('{} = {}'.format(_callstr(f, args), result)) return result return traced_f trace.indent = 0 def _callstr(f, args): "Return a string representing f(*args)." return '{}{}({})'.format('> ' * trace.indent, f.__name__, ', '.join(map(str, args))) def astar_search(start, h_func, move_func): "Find a shortest sequence of states from start to a goal state (a state s with h_func(s) == 0)." frontier = [(h_func(start), start)] # A priority queue, ordered by path length, f = g + h previous = {start: None} # start state has no previous state; other states will path_cost = {start: 0} # The cost of the best path to a state. while frontier: (f, s) = heappop(frontier) if h_func(s) == 0: return Path(previous, s) for s2 in move_func(s): new_cost = path_cost[s] + 1 if s2 not in path_cost or new_cost < path_cost[s2]: heappush(frontier, (new_cost + h_func(s2), s2)) path_cost[s2] = new_cost previous[s2] = s return dict(fail=True, front=len(frontier), prev=len(previous)) def Path(previous, s): "Return a list of states that lead to state s, according to the previous dict." return ([] if (s is None) else Path(previous, previous[s]) + [s])Some tests/examples for these:
In[2]: assert tuple(transpose(((1, 2, 3), (4, 5, 6)))) == ((1, 4), (2, 5), (3, 6)) assert first('abc') == first(['a', 'b', 'c']) == 'a' assert cat(['a', 'b', 'c']) == 'abc' assert (groupby(['test', 'one', 'two', 'three', 'four'], key=len) == {3: ['one', 'two'], 4: ['test', 'four'], 5: ['three']}) Day 1 : No Time for a TaxicabI make the following choices:
Intersection Points in the city grid will be represented as points on the complex plane. Headings and turns can be represented by unit vectors in the complex plane: if you are heading east (along the positive real axis), then a left turn means you head north, and a right turn means you head south, and in general a left or right turn is a multiplication of your current heading by the North or South unit vectors, respectively. Moves of the form "R53" will be parsed into a (turn, distance) pair, e.g. (South, 53) .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.
In[3]: Point = complex N, S, E, W = 1j, -1j, 1, -1 # Unit vectors for hea