instructions = load(1)[0].split(", ")position = np.array([0, 0])direction = np.array([0, 1])rotations = {"R": np.array([[0, 1], [-1, 0]], dtype=int),"L": np.array([[0, -1], [1, 0]], dtype=int),}for instruction in instructions: direction = rotations[instruction[0]] @ direction position += direction *int(instruction[1:])sum(abs(position))
Part 2
Then, you notice the instructions continue on the back of the Recruiting Document. Easter Bunny HQ is actually at the first location you visit twice.
Code
position = np.array([0, 0])direction = np.array([0, 1])seen = {tuple(position): True}for instruction in instructions: direction = rotations[instruction[0]] @ directionfor i inrange(int(instruction[1:])): position += directioniftuple(position) in seen:break seen[tuple(position)] =Trueelse:continuebreaksum(abs(position))
data = np.array(load(3, "int"), dtype=int)def is_valid(triangle): x, y, z = trianglereturn x + y > z and x + z > y and y + z > xsum(map(is_valid, data))
def parse_line(room): checksum = room[-6:-1] sector_id =int(room[:-7].split("-")[-1]) name ="-".join(room.split("-")[:-1])return name, sector_id, checksumdef calculate_checksum(name): occurrences =list(zip(*np.unique(list(name.replace("-", "")), return_counts=True)))return"".join(x[0] for x insorted(occurrences, key=lambda x: [-x[1], x[0]])[:5])data = [parse_line(l) for l in load(4)]sum( sector_idfor name, sector_id, checksum in dataif calculate_checksum(name) == checksum)
Part 2
Code
real_rooms = [room[:2] for room in data if calculate_checksum(room[0]) == room[2]]def decrypt(name, offset): alphabet ="abcdefghijklmnopqrstuvwxyz" shifted_alphabet ="".join(x for x in np.roll(list(alphabet), -offset %26))return name.translate(str.maketrans(alphabet, shifted_alphabet)), offset[answer for room in real_rooms if"north"in (answer := decrypt(*room))[0]]
messages = load(6)"".join(pd.DataFrame([list(x) for x in messages]).mode().values[0])
Part 2
Code
foo = np.array([list(x) for x in messages])s =""for i inrange(foo.shape[1]): letters, counts = np.unique(foo[:, i], return_counts=True) s += letters[counts.argmin()]s
data = load(7)abba = re.compile(r"(.)(?!\1)(.)\2\1")bracketed_abba = re.compile(r"\[[^]]*(.)(?!\1)(.)\2\1.*?\]")def supports_tls(haystack):returnbool(re.search(abba, haystack)) andnotbool( re.search(bracketed_abba, haystack) )sum(supports_tls(line) for line in data)
Part 2
Part two is more regex wrangling, except the patterns can overlap now. We could spend time figuring out exactly how to account for that, or we can import the third party regex module which does it for us automagically.
Code
import regexdef supports_ssl(haystack): aba = regex.compile(r"(.)(?!\1)(.)\1") bracket_split = [x.split("[") for x in haystack.split("]")] outside, inside = itertools.zip_longest(*bracket_split, fillvalue="") abas = [ matchfor fragment in outsidefor match in regex.findall(aba, fragment, overlapped=True) ]for a, b in abas: bab =f"{b}{a}{b}"ifany(bab in fragment for fragment in inside):returnTruereturnFalsesum(supports_ssl(line) for line in data)
data = load(9)[0]part1 = datadef count(s, part2=False): total =0while s:if s[0] !="(": total +=1 s = s[1:]continue end = s.index(")") chars, repeat =map(int, s[1:end].split("x")) s = s[end +1 :]if part2: total += repeat * count(s[:chars], True)else: total += repeat * chars s = s[chars:]return totalcount(data)
data = load(10)wiring = {}state = defaultdict(list)for line in data: command = re.findall("(bot|value|output) (\d+)", line) numbers = [int(x[1]) for x in command] names = [x[0] for x in command]iflen(command) ==2: state[numbers[1]].append(numbers[0])else: wiring[numbers[0]] = [x for x inzip(names[1:], numbers[1:])]queue = deque([x for x in start iflen(state[x]) ==2])output = [0] *21def step(): current = queue.popleft() values =sorted(state[current]) state[current] = [] left, right = wiring[current]for idx, (name, value) inenumerate(wiring[current]):if name =="bot": state[value].append(values[idx])iflen(state[value]) ==2: queue.append(value)else: output[value] = values[idx]return current, valueswhileTrue: current, values = step()if values == [17, 61]:breakcurrent
This one looks difficult, but I don’t think it is too tricky. Given that we are in floor \(n\), the valid next positions are us at floor \(n+1\) or \(n - 1\), with up to two items moved; with the items moved being subject to the puzzle constraints.
So I think the way to go is A*.
Code
from more_itertools import groupern_floors =4def distance_estimate(state, end): items = state[1]returnsum((val /2) * (n_floors - i -1) for i, val inenumerate(items))def is_valid(items): generators, chips = state[::2], state[1::2]returnall( (chip == generator) or (chip notin generators)for chip, generator inzip(chips, generators) )def normalize(items):returntuple(x for pair insorted(list(grouper(items, 2))) for x in pair)def constrained_neighbors(state): floor, items = state active_indices = [index for index, val inenumerate(items) if val == floor] neighbors =set()for new_floor in [floor +1, floor -1]:ifnot (0<= new_floor < n_floors):continue moves = [[x] for x in active_indices]if new_floor == floor +1: moves = itertools.chain(moves, itertools.combinations(active_indices, 2))for move in moves: new_items =list(items)for index in move: new_items[index] = new_floorif is_valid(new_items): neighbors.add((new_floor, normalize(new_items)))return neighborsstate =0, (0, 0, 0, 0, 1, 1, 1, 1, 1, 2)target =3, (3,) *len(state[1])utils.astar(state, target, constrained_neighbors, distance_estimate)
Part 2
Extending this to part 2 without changing anything is possible, but the whole thing takes about a minute and a half to run. When I have time, I’ll come back and look at it again.
Reducing the search space by only letting the elevator move down with one item at a time reduced the runtime to about half. I’m not 100% convinced the restriction is always valid, but it did work in this case.
This is a fairly straightforward implementation of the problem description, with no particular cleverness going on. We have two types of instructions - ones that take two operands, and ones that take only one, and we can treat those together.
Code
def run(program, registers=None):if registers isNone: registers = defaultdict(int) ip =0while ip <len(program): instruction = program[ip] operator, operands = instruction[0], instruction[1:]if operator in ["cpy", "jnz"]: source, destination = operands value =int(source) if source notin"abcd"else registers[source]if operator =="cpy": registers[destination] = valueif operator =="jnz"and value !=0: ip +=int(destination) -1elif operator in ["inc", "dec"]: registers[operands[0]] +=2* (operator =="inc") -1 ip +=1return registers["a"]data = [line.split(" ") for line in load(12)]run(data)
from utils import astardef is_valid(x, y, secret=1362):if x <0or y <0:returnFalse val = x * x +3* x +2* x * y + y + y * y + secret ones =f"{val:b}".count("1")return (ones %2) ==0def neighbors(state): x, y = state candidates = [(x -1, y), (x +1, y), (x, y -1), (x, y +1)]return [candidate for candidate in candidates if is_valid(*candidate)]def distance_function(point, target):returnabs(point[0] - target[0]) +abs(point[1] - target[1])start = (1, 1)target =31, 39utils.astar(start, target, neighbors, distance_function)
import hashlibdef infinite_triples(prefix, part=1): r1 =r"(.)\1\1" r2 =r"(.)\1\1\1\1" n =1whileTrue: s = hashlib.md5((prefix +str(n)).encode()).hexdigest()if part ==2:for i inrange(2016): s = hashlib.md5(s.encode()).hexdigest()if r := re.search(r1, s):yield (r.groups(1)[0], re.findall(r2, s))else:yieldFalse n +=1def nth_key_index(prefix, n=64, part=1): triples =filter(lambda x: x[1], enumerate(infinite_triples(prefix, part))) window = [next(triples)] current =0while current < n: idx, (triple, _) = window.pop(0)whilenot window or window[-1][0] < idx +1000: window.append(next(triples)) active_quints = [char for triple in window[:-1] for char in triple[1][1]]if triple in active_quints: current +=1return idx +1nth_key_index("yjdafjpo")
Part 2
I was a little uncertain about how to write this cleanly – all of the logic from part one is the same, the only difference is how the hash is generated. In the end, I made a toggle in the infinite_triples function, which is why part 2 can be solved by writing just:
from utils import crtdata = [[int(x) for x in re.findall(r"\d+", line)] for line in load(15)]remainders = [(x[1], -(x[-1] + x[0])) for x in data]crt(remainders)
BFS to the rescue. I wanted to do A*, but the “distance from 3,3” heuristic didn’t seem like it would give much. Then I dropped to Dijkstra, but realised that if all steps cost the same, that’s just BFS.
data = np.array([1if char =="^"else0for char in load(18)[0]], dtype=int)left_right = [1, 0, 1]rows = []for i inrange(40): rows.append(data) data = (scipy.ndimage.convolve(data, left_right, mode="constant") ==1).astype(int)(np.array(rows) ==0).sum()
Part 2
For part 2 I should probably check to see if I ever hit a row that I’ve seen before, and then use the repeated cycle to avoid having to calculate that many rows. Or I can just brute force it and not care:
Code
for i inrange(400000-40): rows.append(data) data = (scipy.ndimage.convolve(data, left_right, mode="constant") ==1).astype(int)(np.array(rows) ==0).sum()
Unfortunately, the same approach won’t work here, since the rotations to the middle of the queue really ruin everything.
What we can do instead is notice that the pattern of deletions correspond to leaving every third elf alive, starting just after halfway around the circle. To avoid interference from potentially dead elves, we can play the game in rounds, with one round ending whenever an elf at the start of the line would have died. In each round then, a number of elves at the start of the line get to take presents, a number in the middle do nothing, and a number at the end are eliminated from the game. What each of these lists looks like is not too hard to determine:
Code
def one_round(mylist): l =len(mylist) n =int((l +2) /3) middle = mylist[int(l /2) +2- l %2 :: 3]return mylist[n : int(l /2)] + middle + mylist[:n]i =3005290x =list(range(1, i +1))whilelen(x) >1: x = one_round(x)x[0]
data =sorted( [[int(x) for x in line.split("-")] for line in load(20)], key=lambda x: x[0])low, high = data[0]for new_low, new_high in data[1:]:if high +1< new_low:breakelse: high =max(high, new_high)high +1
Part 2
We’ll start by merging the overlapping banned ranges together. Then, the high point of one range and the low point of the next range define a range of allowed values (endpoints not included). We can sum the length of these to get the total number of allowed values.
Code
def merge_ranges(data): result = [] initial, final = data[0]for low, high in data[1:]:if final +1>= low: final =max(high, final)else: result += [initial, final] initial, final = low, high result += [initial, final]return result(0- ranges[0]+sum([high - low -1for low, high inzip(ranges[1::2], ranges[2::2])])+4294967295- ranges[-1])
data = [x.split() for x in load(21)]s ="abcdefgh"def update(s, line, part=1): operands = line[2], line[-1]if line[0] =="reverse": l, r =sorted(map(int, operands)) s = s[:l] + s[l : r +1][::-1] + s[r +1 :]elif line[0] =="swap":if line[1] =="letter": l, r =map(lambda x: s.index(x), operands)else: l, r =map(int, operands) l, r =sorted([l, r]) s = s[:l] + s[r] + s[l +1 : r] + s[l] + s[r +1 :]elif line[0] =="rotate":if line[1] =="left": rotation =-int(operands[0])elif line[1] =="right": rotation =int(operands[0])else:if part ==1: index = s.index(operands[1]) rotation =1+ index + (index >=4)if part ==2: rotation = reverse_rotation(s, operands[1])if part ==2: rotation =-rotation rotation = rotation %len(s) s = s[-rotation:] + s[:-rotation]elif line[0] =="move": source, dest =map(int, operands)if part ==2: source, dest = dest, source tmp = s[:source] + s[source +1 :] s = tmp[:dest] + s[source] + tmp[dest:]return sfor line in data: s = update(s, line)s
Part 2
Ouch. move, swap and reverse should be easy to do backwards, since they’re self-inverses (potentially with the arguments swapped). The issue is rotate. When we have to go left/right a fixed number of steps there’s no problem, since we just go the other way. For the last one the issue is that the amount we have to rotate by depends on what the state was prior to the rotation. Luckily there aren’t that many possible rotations, so the best approach seems to be to just to see which potential preimage string gives the correct answer when rotated.
Code
def rotate_based_on(s, char): index = s.index(char) rotation =1+ index + (index >=4) rotation = rotation %len(s)return s[-rotation:] + s[:-rotation]def reverse_rotation(s, char):for i, original_string in [(i, s[-i:] + s[:-i]) for i inrange(len(s))]:if rotate_based_on(original_string, char) == s:return-is ="fbgdceah"for line in data[::-1]: s = update(s, line, part=2)s
data = load(22, "int")data.sort(key=lambda x: x[-2])def binary_search(key, haystack): key = key[3] left, right =0, len(haystack)while (right - left) >1: midpoint =int((left + right) /2)if haystack[midpoint][-2] >= key: right = midpointelse: left = midpointreturn rightresult =0for idx1, val inenumerate(data):if val[3] ==0:continue idx2 = binary_search(val, data) result +=len(data[idx2:]) + (idx2 <= idx1)result
Part 2
Just another graph search. There’s only one empty space, so we can uniquely define the current state by the location of the empty space, and the location of the data we’re trying to move.
toggle_map = {"inc": "dec", "tgl": "inc", "dec": "inc", "cpy": "jnz", "jnz": "cpy"}def run(program, registers=None):if registers isNone: registers = {"a": 0, "b": 0, "c": 0, "d": 0}def extract_operands(ip): instruction = program[ip]return instruction[0], instruction[1:]def evaluate_operand(x):returnint(x) if x notin"abcd"else registers[x] ip =0 i =0while ip <len(program): i +=1 operator, operands = extract_operands(ip)if operator in ["cpy", "jnz"]: source, destination = operands value = evaluate_operand(source)if operator =="cpy": registers[destination] = valueif operator =="jnz"and value !=0: ip += evaluate_operand(destination) -1elif operator in ["inc", "dec"]: registers[operands[0]] +=2* (operator =="inc") -1elif operator =="tgl": destination = ip + evaluate_operand(operands[0])try: operator, operands = extract_operands(destination) operator = toggle_map[operator] program[destination] = [operator] + operandsexceptIndexError:pass ip +=1return registers["a"]data = [line.split(" ") for line in load("23")]registers = {"a": 7, "b": 0, "c": 0, "d": 0}run(data, registers)
Part 2
Just setting the registers to registers = {"a": 12, "b": 0, "c": 0, "d": 0} didn’t work, since the code was running incredibly slowly. I ended up analysing my input script instead. The first line copied a to b, and lines 2-16 multiplied a by (b - 1), decreased b by one and set c to 2b (ish). Then came the toggle instruction, and the two instructions after that sent us back to line 2.
Some things that stood out here were that c was an even number, decreasing by 2 each iteration so tgl c tried to toggle every other instruction, starting at the end of the program and moving back towards the tgl instruction itself. That means that the loop before the toggle instruction is unaffected for a long time, and so after the ith iteration we have a = n * (n - 1) * (n - 2) * … * (n - i). This continues until b = 2, when the tgl instruction finally toggles the jnz on line 17. At that point we have a = factorial(n). The (now-toggled) last section of the program then just adds the product of the numeric arguments on line 21 and 22.
This was a bit of a roller coaster. I originally used my pre-existing bfs code to search the maze, and it was unbelievably slow. Instead of investigating I decided to try and transform the maze, and found a conceptual approach which was horribly over-engineered, but probably would have worked. Before I finished implementing it, I thought of trying another BFS, less general and hence faster, and it ran more than fast enough. Oh well.
I still liked the original approach though. The idea was to simplify the graph of the maze via the following transformations:
Delete all empty (non-goal) nodes with only two neighbors by directly connecting their neighbors with a single edge, with a weight \(w = w_1 + w_2\)
Recursively remove all empty dead ends.
Identify bottlenecks in the graph, i.e. nodes whose removal would disconnect the graph. From there, generate the block-cut tree of the graph, and simplify each component of the block-cut to just the cut vertices and the goal nodes. This basically means that the problem of finding the shortest path X->Y is reduced to finding the shortest path to and from a given bottleneck, and then stitching the paths together.
It was fun to think about even though I didn’t use it in the end.
Code
from scipy.cluster.hierarchy import DisjointSetparse =lambda x: -2if x =="#"else-1if x =="."elseint(x)data = np.array([[parse(char) for char in line] for line in load(24)])def encode(mask):returnset([x +1j* y for x, y inzip(*np.where(mask))])nodes = np.where(data >=0)order = data[nodes].argsort()nodes = [x +1j* y for x, y inzip(*[index[order] for index in nodes])]distances = np.ones((len(nodes), len(nodes))) * np.infnode_indices = {n: idx for idx, n inenumerate(nodes)}open_squares = encode(data >-2)graph = defaultdict(list)for square in open_squares:for delta in (1, 1j):if square + delta in open_squares: graph[square].append(square + delta) graph[square + delta].append(square)for node in nodes: idx = node_indices[node] queue = deque([(0, node)]) visited =set()while queue and (distances[idx] == np.inf).any(): cost, state = queue.popleft()if state in visited:continue visited.add(state)if state in nodes: new_idx = node_indices[state] distances[idx, new_idx] = cost distances[new_idx, idx] = costfor neighbor in graph[state]:if neighbor notin visited: queue.append((cost +1, neighbor))minval = np.inffor permutation in itertools.permutations(range(1, len(nodes))): indices = (0,) + permutation[:-1], permutationif (total :=sum(distances[indices])) < minval: minval = totalint(minval)
This is an interesting problem, because it requires more thinking and less coding. Blindly running the code provided in my input leads to infinite loops, and the question is then how to analyse these. In particular, we’re asked for an input that matches an infinite sequence of alternating ones and zeros, and we don’t really have any way of knowing that a sequence that looks promising doesn’t start to diverge after 100, 1000 or even 1,000,000 terms. Instead, I decided to analyse the provided code and understand what it was doing. After a bit of conversion, I found it to be equivalent to the following snippet:
def clock(start): a =0whileTrue:if a ==0: a = start +2550yield a %2 a = a //2
But that’s just the binary representation of (start + 2550) reversed and repeated endlessly. So we’re looking for the smallest number \(n\) such that n + 2550 has a binary representation of the form \(101010\ldots\)
Code
x =2while x <2550: x =4* x +2x -2550
Source Code
---title: 2016 Solutions---# Imports```{python}import functoolsimport hashlibimport itertoolsimport osimport reimport sysfrom collections import defaultdict, dequefrom pathlib import Pathfrom queue import PriorityQueueimport more_itertoolsimport numpy as npimport pandas as pdimport scipysys.path.insert(1, os.path.join(sys.path[0], ".."))import utilsload = utils.year_load(2016)```# [Day 1: No Time for a Taxicab](https://adventofcode.com/2016/day/1)## Part 1How many blocks away is Easter Bunny HQ?```{python}instructions = load(1)[0].split(", ")position = np.array([0, 0])direction = np.array([0, 1])rotations = {"R": np.array([[0, 1], [-1, 0]], dtype=int),"L": np.array([[0, -1], [1, 0]], dtype=int),}for instruction in instructions: direction = rotations[instruction[0]] @ direction position += direction *int(instruction[1:])sum(abs(position))```## Part 2Then, you notice the instructions continue on the back of the Recruiting Document. Easter Bunny HQ is actually at the first location you visit twice.```{python}position = np.array([0, 0])direction = np.array([0, 1])seen = {tuple(position): True}for instruction in instructions: direction = rotations[instruction[0]] @ directionfor i inrange(int(instruction[1:])): position += directioniftuple(position) in seen:break seen[tuple(position)] =Trueelse:continuebreaksum(abs(position))```# [Day 2: Bathroom Security](https://adventofcode.com/2016/day/2)## Part 1```{python}commands = {"U": -1j, "D": 1j, "L": -1, "R": 1}grid = {(x %3) +1j* (x //3): x +1for x inrange(9)}def update(point, grid, instructions):for instruction in instructions: new_point = point + commands[instruction]if new_point in grid: point = new_pointreturn pointpoint =1+1jpassword =""instructions = load(2)for line in instructions: point = update(point, grid, line) password +=str(grid[point])password```## Part 2```{python}grid = {0j+2: 1,1j+1: 2,1j+2: 3,1j+3: 4,2j: 5,2j+1: 6,2j+2: 7,2j+3: 8,2j+4: 9,3j+1: "A",3j+2: "B",3j+3: "C",4j+2: "D",}point =2jpassword =""for line in instructions: point = update(point, grid, line) password +=str(grid[point])password```# [Day 3: Squares with three sides](https://adventofcode.com/2016/day/3)## Part 1```{python}data = np.array(load(3, "int"), dtype=int)def is_valid(triangle): x, y, z = trianglereturn x + y > z and x + z > y and y + z > xsum(map(is_valid, data))```## Part 2```{python}sum(map(is_valid, data.T.ravel().reshape(-1, 3)))```# [Day 4: Security Through Obscurity](https://adventofcode.com/2016/day/4)## Part 1```{python}def parse_line(room): checksum = room[-6:-1] sector_id =int(room[:-7].split("-")[-1]) name ="-".join(room.split("-")[:-1])return name, sector_id, checksumdef calculate_checksum(name): occurrences =list(zip(*np.unique(list(name.replace("-", "")), return_counts=True)))return"".join(x[0] for x insorted(occurrences, key=lambda x: [-x[1], x[0]])[:5])data = [parse_line(l) for l in load(4)]sum( sector_idfor name, sector_id, checksum in dataif calculate_checksum(name) == checksum)```## Part 2```{python}real_rooms = [room[:2] for room in data if calculate_checksum(room[0]) == room[2]]def decrypt(name, offset): alphabet ="abcdefghijklmnopqrstuvwxyz" shifted_alphabet ="".join(x for x in np.roll(list(alphabet), -offset %26))return name.translate(str.maketrans(alphabet, shifted_alphabet)), offset[answer for room in real_rooms if"north"in (answer := decrypt(*room))[0]]```# [Day 5: How About a Nice Game of Chess?](https://adventofcode.com/2016/day/5)## Part 1```{python}import hashlibh = hashlib.md5()prefix ="wtnhxymk"password =""i =0whilelen(password) <8: s = hashlib.md5((prefix +str(i)).encode(encoding="UTF-8")).hexdigest()if s[:5] =="0"*5: password = password + s[5] i +=1password```## Part 2```{python}password = [None] *8i =0whileany([x isNonefor x in password]): s = hashlib.md5((prefix +str(i)).encode(encoding="UTF-8")).hexdigest()if s[:5] =="0"*5and s[5] in"01234567"and password[int(s[5])] isNone: password[int(s[5])] = s[6] i +=1"".join(password)```# [Day 6: Signals and Noise](https://adventofcode.com/2016/day/6)## Part 1```{python}messages = load(6)"".join(pd.DataFrame([list(x) for x in messages]).mode().values[0])```## Part 2```{python}foo = np.array([list(x) for x in messages])s =""for i inrange(foo.shape[1]): letters, counts = np.unique(foo[:, i], return_counts=True) s += letters[counts.argmin()]s```# [Day 7: Internet Protocol Version 7](https://adventofcode.com/2016/day/7)## Part 1```{python}data = load(7)abba = re.compile(r"(.)(?!\1)(.)\2\1")bracketed_abba = re.compile(r"\[[^]]*(.)(?!\1)(.)\2\1.*?\]")def supports_tls(haystack):returnbool(re.search(abba, haystack)) andnotbool( re.search(bracketed_abba, haystack) )sum(supports_tls(line) for line in data)```## Part 2Part two is more regex wrangling, except the patterns can overlap now. We could spend time figuring out exactly how to account for that, or we can import the third party regex module which does it for us automagically.```{python}import regexdef supports_ssl(haystack): aba = regex.compile(r"(.)(?!\1)(.)\1") bracket_split = [x.split("[") for x in haystack.split("]")] outside, inside = itertools.zip_longest(*bracket_split, fillvalue="") abas = [ matchfor fragment in outsidefor match in regex.findall(aba, fragment, overlapped=True) ]for a, b in abas: bab =f"{b}{a}{b}"ifany(bab in fragment for fragment in inside):returnTruereturnFalsesum(supports_ssl(line) for line in data)```# [Day 8: Two-Factor Authentication](https://adventofcode.com/2016/day/8)## Part 1```{python}array = np.zeros((6, 50), dtype=int)lines = [x.split() for x in load(8)]for instructions in lines:if instructions[0] =="rect": row, col = [int(a) for a in instructions[1].split("x")] array[:col, :row] =1continue row =int(instructions[2].split("=")[1]) amount =int(instructions[-1])if instructions[1] =="column": array = array.T array[row] = np.roll(array[row], amount)if instructions[1] =="column": array = array.Tarray.sum()```## Part 2```{python}[["".join("█"if char else" "for char in line)] for line in array]```# [Day 9: Explosives in Cyberspace](https://adventofcode.com/2016/day/9)## Part 1```{python}data = load(9)[0]part1 = datadef count(s, part2=False): total =0while s:if s[0] !="(": total +=1 s = s[1:]continue end = s.index(")") chars, repeat =map(int, s[1:end].split("x")) s = s[end +1 :]if part2: total += repeat * count(s[:chars], True)else: total += repeat * chars s = s[chars:]return totalcount(data)```## Part 2```{python}count(data, part2=True)```# [Day 10: Balance Bots](https://adventofcode.com/2016/day/10)## Part 1```{python}data = load(10)wiring = {}state = defaultdict(list)for line in data: command = re.findall("(bot|value|output) (\d+)", line) numbers = [int(x[1]) for x in command] names = [x[0] for x in command]iflen(command) ==2: state[numbers[1]].append(numbers[0])else: wiring[numbers[0]] = [x for x inzip(names[1:], numbers[1:])]queue = deque([x for x in start iflen(state[x]) ==2])output = [0] *21def step(): current = queue.popleft() values =sorted(state[current]) state[current] = [] left, right = wiring[current]for idx, (name, value) inenumerate(wiring[current]):if name =="bot": state[value].append(values[idx])iflen(state[value]) ==2: queue.append(value)else: output[value] = values[idx]return current, valueswhileTrue: current, values = step()if values == [17, 61]:breakcurrent```## Part 2With Part 1 out of the way, part 2 is just```{python}while queue: step()np.product(output[:3])```# [Day 11: Radioisotope Thermoelectric Generators](https://adventofcode.com/2016/day/11)## Part 1This one looks difficult, but I don't think it is too tricky. Given that we are in floor $n$, the valid next positions are us at floor $n+1$ or $n - 1$, with up to two items moved; with the items moved being subject to the puzzle constraints.So I think the way to go is A\*.```{python}from more_itertools import groupern_floors =4def distance_estimate(state, end): items = state[1]returnsum((val /2) * (n_floors - i -1) for i, val inenumerate(items))def is_valid(items): generators, chips = state[::2], state[1::2]returnall( (chip == generator) or (chip notin generators)for chip, generator inzip(chips, generators) )def normalize(items):returntuple(x for pair insorted(list(grouper(items, 2))) for x in pair)def constrained_neighbors(state): floor, items = state active_indices = [index for index, val inenumerate(items) if val == floor] neighbors =set()for new_floor in [floor +1, floor -1]:ifnot (0<= new_floor < n_floors):continue moves = [[x] for x in active_indices]if new_floor == floor +1: moves = itertools.chain(moves, itertools.combinations(active_indices, 2))for move in moves: new_items =list(items)for index in move: new_items[index] = new_floorif is_valid(new_items): neighbors.add((new_floor, normalize(new_items)))return neighborsstate =0, (0, 0, 0, 0, 1, 1, 1, 1, 1, 2)target =3, (3,) *len(state[1])utils.astar(state, target, constrained_neighbors, distance_estimate)```## Part 2Extending this to part 2 without changing anything is possible, but the whole thing takes about a minute and a half to run. When I have time, I'll come back and look at it again.Reducing the search space by only letting the elevator move down with one item at a time reduced the runtime to about half. I'm not 100% convinced the restriction is always valid, but it did work in this case.```{python}state =0, (0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2)target =3, (3,) *len(state[1])utils.astar(state, target, constrained_neighbors, distance_estimate)```# [Day 12: Leonardo's Monorail](https://adventofcode.com/2016/day/12)## Part 1This is a fairly straightforward implementation of the problem description, with no particular cleverness going on. We have two types of instructions - ones that take two operands, and ones that take only one, and we can treat those together.```{python}def run(program, registers=None):if registers isNone: registers = defaultdict(int) ip =0while ip <len(program): instruction = program[ip] operator, operands = instruction[0], instruction[1:]if operator in ["cpy", "jnz"]: source, destination = operands value =int(source) if source notin"abcd"else registers[source]if operator =="cpy": registers[destination] = valueif operator =="jnz"and value !=0: ip +=int(destination) -1elif operator in ["inc", "dec"]: registers[operands[0]] +=2* (operator =="inc") -1 ip +=1return registers["a"]data = [line.split(" ") for line in load(12)]run(data)```## Part 2```{python}registers = defaultdict(int)registers["c"] =1run(data, registers)```# [Day 13: A Maze of Twisty Little Cubicles](https://adventofcode.com/2016/day/13)## Part 1```{python}from utils import astardef is_valid(x, y, secret=1362):if x <0or y <0:returnFalse val = x * x +3* x +2* x * y + y + y * y + secret ones =f"{val:b}".count("1")return (ones %2) ==0def neighbors(state): x, y = state candidates = [(x -1, y), (x +1, y), (x, y -1), (x, y +1)]return [candidate for candidate in candidates if is_valid(*candidate)]def distance_function(point, target):returnabs(point[0] - target[0]) +abs(point[1] - target[1])start = (1, 1)target =31, 39utils.astar(start, target, neighbors, distance_function)```## Part 2```{python}len(utils.bfs((1, 1), lambda cost, state: cost >50, neighbors, return_visited=True))```# [Day 14: One-Time Pad](https://adventofcode.com/2016/day/14)## Part 1```{python}import hashlibdef infinite_triples(prefix, part=1): r1 =r"(.)\1\1" r2 =r"(.)\1\1\1\1" n =1whileTrue: s = hashlib.md5((prefix +str(n)).encode()).hexdigest()if part ==2:for i inrange(2016): s = hashlib.md5(s.encode()).hexdigest()if r := re.search(r1, s):yield (r.groups(1)[0], re.findall(r2, s))else:yieldFalse n +=1def nth_key_index(prefix, n=64, part=1): triples =filter(lambda x: x[1], enumerate(infinite_triples(prefix, part))) window = [next(triples)] current =0while current < n: idx, (triple, _) = window.pop(0)whilenot window or window[-1][0] < idx +1000: window.append(next(triples)) active_quints = [char for triple in window[:-1] for char in triple[1][1]]if triple in active_quints: current +=1return idx +1nth_key_index("yjdafjpo")```## Part 2I was a little uncertain about how to write this cleanly – all of the logic from part one is the same, the only difference is how the hash is generated. In the end, I made a toggle in the `infinite_triples` function, which is why part 2 can be solved by writing just:```{python}nth_key_index("yjdafjpo", part=2)```# [Day 15: Timing is Everything](https://adventofcode.com/2016/day/15)## Part 1Another round of the chinese remainder theorem.```{python}from utils import crtdata = [[int(x) for x in re.findall(r"\d+", line)] for line in load(15)]remainders = [(x[1], -(x[-1] + x[0])) for x in data]crt(remainders)```## Part 2```{python}remainders.append([11, -(len(remainders) +1)])crt(remainders)```# [Day 16: Dragon Checksum](https://adventofcode.com/2016/day/16)## Part 1```{python}start = [1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1]length =272def solve(prefix, length):whilelen(prefix) < length: prefix = prefix + [0] + [1^ x for x in prefix[::-1]] s = prefix[:length]whilelen(s) %2==0: s =abs(np.diff(s))[::2] ^1return sprint(*solve(start, length), sep="")```## Part 2```{python}print(*solve(start, 35651584), sep="")```# [Day 17: Two Steps Forward](https://adventofcode.com/2016/day/17)## Part 1BFS to the rescue. I wanted to do A\*, but the "distance from 3,3" heuristic didn't seem like it would give much. Then I dropped to Dijkstra, but realised that if all steps cost the same, that's just BFS.```{python}start = (0, "bwnlcvfs")def neighbors(position, path): chars = hashlib.md5(path.encode()).hexdigest()[:4] directions ="UDLR" deltas =-1j, 1j, -1, 1 candidates = [ (position + delta, path + direction)for delta, direction, char inzip(deltas, directions, chars)if char in"bcdef" ]return [ candidatefor candidate in candidatesif0<= candidate[0].real <4and0<= candidate[0].imag <4 ]q = deque([start])while q: position, path = q.popleft()if position ==3+3j: result = path[len(start[1]) :]break q += deque(neighbors(position, path))result```## Part 2```{python}q = deque([start])i =0while q: position, path = q.popleft()if position ==3+3j: result =len(path) -len(start[1])continue q += deque(neighbors(position, path)) i +=1result```# [Day 18: Like a Rogue](https://adventofcode.com/2016/day/18)## Part 2```{python}data = np.array([1if char =="^"else0for char in load(18)[0]], dtype=int)left_right = [1, 0, 1]rows = []for i inrange(40): rows.append(data) data = (scipy.ndimage.convolve(data, left_right, mode="constant") ==1).astype(int)(np.array(rows) ==0).sum()```## Part 2For part 2 I should probably check to see if I ever hit a row that I've seen before, and then use the repeated cycle to avoid having to calculate that many rows. Or I can just brute force it and not care:```{python}for i inrange(400000-40): rows.append(data) data = (scipy.ndimage.convolve(data, left_right, mode="constant") ==1).astype(int)(np.array(rows) ==0).sum()```# [Day 19: An Elephant Named Joseph](https://adventofcode.com/2016/day/19)## Part 1This problem – with rotations by 1 and deletions only of neighboring elves is definitely calling for a deque:```{python}limit =100numbers =list(range(1, limit +1))queue = deque(numbers)while queue: queue.rotate(-1) s = queue.popleft()s```## Part 2Unfortunately, the same approach won't work here, since the rotations to the middle of the queue really ruin everything.What we can do instead is notice that the pattern of deletions correspond to leaving every third elf alive, starting just after halfway around the circle. To avoid interference from potentially dead elves, we can play the game in rounds, with one round ending whenever an elf at the start of the line would have died. In each round then, a number of elves at the start of the line get to take presents, a number in the middle do nothing, and a number at the end are eliminated from the game. What each of these lists looks like is not too hard to determine:```{python}def one_round(mylist): l =len(mylist) n =int((l +2) /3) middle = mylist[int(l /2) +2- l %2 :: 3]return mylist[n : int(l /2)] + middle + mylist[:n]i =3005290x =list(range(1, i +1))whilelen(x) >1: x = one_round(x)x[0]```# [Day 20: Firewall Rules](https://adventofcode.com/2016/day/20)## Part 1```{python}data =sorted( [[int(x) for x in line.split("-")] for line in load(20)], key=lambda x: x[0])low, high = data[0]for new_low, new_high in data[1:]:if high +1< new_low:breakelse: high =max(high, new_high)high +1```## Part 2We'll start by merging the overlapping banned ranges together. Then, the high point of one range and the low point of the next range define a range of allowed values (endpoints not included). We can sum the length of these to get the total number of allowed values.```{python}def merge_ranges(data): result = [] initial, final = data[0]for low, high in data[1:]:if final +1>= low: final =max(high, final)else: result += [initial, final] initial, final = low, high result += [initial, final]return result(0- ranges[0]+sum([high - low -1for low, high inzip(ranges[1::2], ranges[2::2])])+4294967295- ranges[-1])```# [Day 21: Scrambled Letters and Hash](https://adventofcode.com/2016/day/21)## Part 1```{python}data = [x.split() for x in load(21)]s ="abcdefgh"def update(s, line, part=1): operands = line[2], line[-1]if line[0] =="reverse": l, r =sorted(map(int, operands)) s = s[:l] + s[l : r +1][::-1] + s[r +1 :]elif line[0] =="swap":if line[1] =="letter": l, r =map(lambda x: s.index(x), operands)else: l, r =map(int, operands) l, r =sorted([l, r]) s = s[:l] + s[r] + s[l +1 : r] + s[l] + s[r +1 :]elif line[0] =="rotate":if line[1] =="left": rotation =-int(operands[0])elif line[1] =="right": rotation =int(operands[0])else:if part ==1: index = s.index(operands[1]) rotation =1+ index + (index >=4)if part ==2: rotation = reverse_rotation(s, operands[1])if part ==2: rotation =-rotation rotation = rotation %len(s) s = s[-rotation:] + s[:-rotation]elif line[0] =="move": source, dest =map(int, operands)if part ==2: source, dest = dest, source tmp = s[:source] + s[source +1 :] s = tmp[:dest] + s[source] + tmp[dest:]return sfor line in data: s = update(s, line)s```## Part 2Ouch. `move`, `swap` and `reverse` should be easy to do backwards, since they're self-inverses (potentially with the arguments swapped). The issue is `rotate`. When we have to go left/right a fixed number of steps there's no problem, since we just go the other way. For the last one the issue is that the amount we have to rotate by depends on what the state was prior to the rotation. Luckily there aren't that many possible rotations, so the best approach seems to be to just to see which potential preimage string gives the correct answer when rotated.```{python}def rotate_based_on(s, char): index = s.index(char) rotation =1+ index + (index >=4) rotation = rotation %len(s)return s[-rotation:] + s[:-rotation]def reverse_rotation(s, char):for i, original_string in [(i, s[-i:] + s[:-i]) for i inrange(len(s))]:if rotate_based_on(original_string, char) == s:return-is ="fbgdceah"for line in data[::-1]: s = update(s, line, part=2)s```# [Day 22: Grid Computing](https://adventofcode.com/2016/day/22)## Part 1```{python}data = load(22, "int")data.sort(key=lambda x: x[-2])def binary_search(key, haystack): key = key[3] left, right =0, len(haystack)while (right - left) >1: midpoint =int((left + right) /2)if haystack[midpoint][-2] >= key: right = midpointelse: left = midpointreturn rightresult =0for idx1, val inenumerate(data):if val[3] ==0:continue idx2 = binary_search(val, data) result +=len(data[idx2:]) + (idx2 <= idx1)result```## Part 2Just another graph search. There's only one empty space, so we can uniquely define the current state by the location of the empty space, and the location of the data we're trying to move.```{python}size = np.array(data)[:, :2].max(axis=0)grid = np.ones(size +1, dtype=int)data = np.array(data)grid[tuple(data[np.where(data[:, 2] >200)][:, :2].T)] =-1source =tuple(data[-1][:2])grid[source] =0target = size[0], 0def heuristic(x, y): to_data =abs(x[0] - y[0]) +abs(x[1] - y[1])return4* (abs(y[0]) +abs(y[1]) -1) + to_data -1def neighbors(x, y): new_states = []for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]: new_x = x[0] + dx, x[1] + dyif ( new_x[0] <0or new_x[1] <0or new_x[0] > size[0]or new_x[1] > size[1]or grid[new_x] ==-1 ):continueif new_x == y: new_states.append((new_x, (x[0], x[1])))else: new_states.append((new_x, y))return new_statesqueue = PriorityQueue()queue.put((0, source, target))costs = defaultdict(lambda: np.inf)costs[source, target] =0i =0while queue.qsize() >0: i +=1 _, x, y = queue.get()if y == (0, 0): result = costs[x, y]breakfor a, b in neighbors(x, y): current_cost = costs[x, y] +1if current_cost < costs[a, b]: costs[a, b] = current_cost queue.put((current_cost + heuristic(a, b), a, b))result```# [Day 23: Safe Cracking](https://adventofcode.com/2016/day/23)## Part 1```{python}toggle_map = {"inc": "dec", "tgl": "inc", "dec": "inc", "cpy": "jnz", "jnz": "cpy"}def run(program, registers=None):if registers isNone: registers = {"a": 0, "b": 0, "c": 0, "d": 0}def extract_operands(ip): instruction = program[ip]return instruction[0], instruction[1:]def evaluate_operand(x):returnint(x) if x notin"abcd"else registers[x] ip =0 i =0while ip <len(program): i +=1 operator, operands = extract_operands(ip)if operator in ["cpy", "jnz"]: source, destination = operands value = evaluate_operand(source)if operator =="cpy": registers[destination] = valueif operator =="jnz"and value !=0: ip += evaluate_operand(destination) -1elif operator in ["inc", "dec"]: registers[operands[0]] +=2* (operator =="inc") -1elif operator =="tgl": destination = ip + evaluate_operand(operands[0])try: operator, operands = extract_operands(destination) operator = toggle_map[operator] program[destination] = [operator] + operandsexceptIndexError:pass ip +=1return registers["a"]data = [line.split(" ") for line in load("23")]registers = {"a": 7, "b": 0, "c": 0, "d": 0}run(data, registers)```## Part 2Just setting the registers to `registers = {"a": 12, "b": 0, "c": 0, "d": 0}` didn't work, since the code was running incredibly slowly. I ended up analysing my input script instead. The first line copied a to b, and lines 2-16 multiplied a by (b - 1), decreased b by one and set c to 2b (ish). Then came the toggle instruction, and the two instructions after that sent us back to line 2.Some things that stood out here were that c was an even number, decreasing by 2 each iteration so `tgl c` tried to toggle every other instruction, starting at the end of the program and moving back towards the `tgl` instruction itself. That means that the loop before the toggle instruction is unaffected for a long time, and so after the ith iteration we have a = n \* (n - 1) \* (n - 2) \* … \* (n - i). This continues until b = 2, when the `tgl` instruction finally toggles the `jnz` on line 17. At that point we have `a = factorial(n)`. The (now-toggled) last section of the program then just adds the product of the numeric arguments on line 21 and 22.And that's the final answer.# [Day 24: Air Duct Spelunking](https://adventofcode.com/2016/day/24)## Part 1This was a bit of a roller coaster. I originally used my pre-existing bfs code to search the maze, and it was unbelievably slow. Instead of investigating I decided to try and transform the maze, and found a conceptual approach which was horribly over-engineered, but probably would have worked. Before I finished implementing it, I thought of trying another BFS, less general and hence faster, and it ran more than fast enough. Oh well.I still liked the original approach though. The idea was to simplify the graph of the maze via the following transformations:1. Delete all empty (non-goal) nodes with only two neighbors by directly connecting their neighbors with a single edge, with a weight $w = w_1 + w_2$2. Recursively remove all empty dead ends.3. Identify bottlenecks in the graph, i.e. nodes whose removal would disconnect the graph. From there, generate the block-cut tree of the graph, and simplify each component of the block-cut to just the cut vertices and the goal nodes. This basically means that the problem of finding the shortest path X-\>Y is reduced to finding the shortest path to and from a given bottleneck, and then stitching the paths together.It was fun to think about even though I didn't use it in the end.```{python}from scipy.cluster.hierarchy import DisjointSetparse =lambda x: -2if x =="#"else-1if x =="."elseint(x)data = np.array([[parse(char) for char in line] for line in load(24)])def encode(mask):returnset([x +1j* y for x, y inzip(*np.where(mask))])nodes = np.where(data >=0)order = data[nodes].argsort()nodes = [x +1j* y for x, y inzip(*[index[order] for index in nodes])]distances = np.ones((len(nodes), len(nodes))) * np.infnode_indices = {n: idx for idx, n inenumerate(nodes)}open_squares = encode(data >-2)graph = defaultdict(list)for square in open_squares:for delta in (1, 1j):if square + delta in open_squares: graph[square].append(square + delta) graph[square + delta].append(square)for node in nodes: idx = node_indices[node] queue = deque([(0, node)]) visited =set()while queue and (distances[idx] == np.inf).any(): cost, state = queue.popleft()if state in visited:continue visited.add(state)if state in nodes: new_idx = node_indices[state] distances[idx, new_idx] = cost distances[new_idx, idx] = costfor neighbor in graph[state]:if neighbor notin visited: queue.append((cost +1, neighbor))minval = np.inffor permutation in itertools.permutations(range(1, len(nodes))): indices = (0,) + permutation[:-1], permutationif (total :=sum(distances[indices])) < minval: minval = totalint(minval)```## Part 2```{python}minval = np.inffor permutation in itertools.permutations(range(1, len(nodes))): indices = (0,) + permutation, permutation + (0,)if (total :=sum(distances[indices])) < minval: minval = totalint(minval)```# [Day 25: Clock Signal](https://adventofcode.com/2016/day/25)## Part 1This is an interesting problem, because it requires more thinking and less coding. Blindly running the code provided in my input leads to infinite loops, and the question is then how to analyse these. In particular, we're asked for an input that matches an infinite sequence of alternating ones and zeros, and we don't really have any way of knowing that a sequence that looks promising doesn't start to diverge after 100, 1000 or even 1,000,000 terms. Instead, I decided to analyse the provided code and understand what it was doing. After a bit of conversion, I found it to be equivalent to the following snippet:``` pythondef clock(start): a =0whileTrue:if a ==0: a = start +2550yield a %2 a = a //2```But that's just the binary representation of (start + 2550) reversed and repeated endlessly. So we're looking for the smallest number $n$ such that n + 2550 has a binary representation of the form $101010\ldots$```{python}x =2while x <2550: x =4* x +2x -2550```