Code
import functools
import itertools
import os
import re
import sys
from collections import defaultdict, deque
import numpy as np
import scipy
1, os.path.join(sys.path[0], ".."))
sys.path.insert(import utils
= utils.year_load(2020) load
import functools
import itertools
import os
import re
import sys
from collections import defaultdict, deque
import numpy as np
import scipy
1, os.path.join(sys.path[0], ".."))
sys.path.insert(import utils
= utils.year_load(2020) load
= load(1, "np")
data 0] * pair[1] for pair in itertools.product(data, data) if sum(pair) == 2020][0] [pair[
[0] * triple[1] * triple[2]
triple[for triple in itertools.product(data, data, data)
if sum(triple) == 2020
0] ][
def is_valid(interval, letters, password):
= [int(x) for x in interval.split("-")]
interval = password.count(letters[0])
occurrences return interval[0] <= occurrences <= interval[1]
= load(2)
lines sum([is_valid(*line.split()) for line in lines])
from operator import xor
def is_valid(interval, letters, password):
= [int(x) - 1 for x in interval.split("-")]
lower, upper return xor(password[lower] == letters[0], password[upper] == letters[0])
sum([is_valid(*line.split()) for line in lines])
= np.array([[0 if char == "." else 1 for char in line] for line in load(3)])
data
def count_trees(data, width_change, height_change):
= data.shape
height, width = 0
result for i in range((height - 1) // height_change + 1):
+= data[i * height_change, (i * width_change) % width]
result return result
3, 1) count_trees(data,
= [[1, 1], [3, 1], [5, 1], [7, 1], [1, 2]]
slopes 0], slope[1]) for slope in slopes]) np.prod([count_trees(data, slope[
= load(4, "raw")
data = data.split("\n\n")
passports = [
passport_dicts dict(list(map(lambda x: x.split(":"), p.replace("\n", " ").split())))
for p in passports
]sum([len(set(p.keys()) - set(["cid"])) == 7 for p in passport_dicts])
def validate_key(key, value):
= {"byr": [1920, 2002], "eyr": [2020, 2030], "iyr": [2010, 2020]}
year_limits try:
if key in ["byr", "eyr", "iyr"]:
= year_limits[key]
minval, maxval return minval <= int(value) <= maxval
if key == "hgt":
= int(value[:-2]), value[-2:]
value, unit return (unit == "cm" and (150 <= value <= 193)) or (
== "in" and (59 <= value <= 76)
unit
)if key == "hcl":
return (
0] == "#"
value[and len(value) == 7
and not (set(value[1:]) - set("0123456789abcdef"))
)if key == "ecl":
return value in ["amb", "blu", "brn", "gry", "grn", "hzl", "oth"]
if key == "pid":
= int(value)
test return len(value) == 9
return True
except ValueError:
return False
def is_valid_passport(p):
return len(set(p.keys()) - set(["cid"])) == 7 and all(
for key in p]
[validate_key(key, p[key])
)
sum([is_valid_passport(p) for p in passport_dicts])
# "BFFFBBFRRR" -> 70, column 7 -> 567
def seat_id(instruction):
return int(instruction.translate(str.maketrans("BFRL", "1010")), 2)
= [seat_id(x) for x in load(5)]
seat_ids max(seat_ids)
set(range(min(seat_ids), max(seat_ids) + 1)) - set(seat_ids)).pop() (
= load(6, "raw")
data = data.split("\n\n")
groups sum(len(set(list(group.replace("\n", "")))) for group in groups)
sum(
len(functools.reduce(lambda x, y: set(x) & set(y), (group.splitlines())))
for group in groups
)
Nothing super groundbreaking for part one. I thought of using a regex to parse the input, but splitting on commas and then into words works just fine.
= load(7)
data = {}
tree for line in data:
= line.split(" bags contain ")
bag, contents if "no other" in contents:
= {}
contents else:
= contents.split(", ")
elements = {
contents " ".join(words[1:-1]): int(words[0]) for words in map(str.split, elements)
}= contents
tree[bag]
@functools.cache
def contains_gold(key):
return "shiny gold" in tree[key] or any(contains_gold(child) for child in tree[key])
sum(contains_gold(key) for key in tree)
The key thing to remember is to include the bag itself, as well as the bags it contains, when calculating the total. That’s what the “+1” is for in the sum
@functools.cache
def count_bags(bag):
return sum(tree[bag][key] * (count_bags(key) + 1) for key in tree[bag])
"shiny gold") count_bags(
= [x.split() for x in load(8)]
data = [(x[0], int(x[1])) for x in data]
data
def terminal_run(program):
= 0, 0
ip, accumulator = {}
seen while ip != len(program):
if ip in seen:
return False, accumulator
= 1
seen[ip] = program[ip]
instruction, operand += 1
ip if instruction == "jmp":
+= operand - 1
ip if instruction == "acc":
+= operand
accumulator return True, accumulator
1] terminal_run(data)[
= {"acc": "acc", "jmp": "nop", "nop": "jmp"}
instruction_map for idx, instruction in enumerate(data):
= (instruction_map[instruction[0]], instruction[1])
new_instruction = terminal_run(data[:idx] + [new_instruction] + data[idx + 1 :])
status, value if status:
break
value
= list(load(9, "np"))
data = len(data) - 25
end for window_start in range(end):
= data[window_start + 25]
target if (
min(
map(
lambda x: abs(target - sum(x)),
+ 25], 2),
itertools.combinations(data[window_start : window_start
)
)!= 0
):break
= target
invalid_number invalid_number
= 0, 1
start_idx, end_idx while start_idx < len(data):
= sum(data[start_idx:end_idx])
total if total == invalid_number:
break
if total < invalid_number:
+= 1
end_idx if total > invalid_number:
+= 1
start_idx = start_idx + 1
end_idx min(data[start_idx:end_idx]) + max(data[start_idx:end_idx])
= [0] + sorted((load(10, "np")))
data == 1).sum() * ((np.diff(data) == 3).sum() + 1) (np.diff(data)
Sorting the values, we see a series of jumps of 1 and jumps of 3. If the value is allowed to jump by at most 3 every time, then we have to include both sides of every jump of 3.
The only interesting thing is then what to do with runs of 1 jumps. In general, we can count the number of ways, \(f\), as follows
\(f(n) = f(n - 1) + g(n-1)\)
The first term comes from saying that we pick the first element, leaving us with a run of length \((n - 1)\), exactly as before. The second comes from saying that we skip the first element, and now have to find the number of ways of choosing for a series of gaps starting with \(2\), followed by \(n - 2\) ones. Similarly
\(g(n - 1) = f(n - 2) + f(n - 3)\)
If we pick the element that resulted in a gap of two, then we just have to choose from a run of n - 2 ones, which is the \(f\) we are looking at. If we don’t pick it, we’ve created a gap of size \(3\) - but then we are forced to pick the next element, leaving us with a run of length \(n - 3\) to distribute.
Putting everything together gives the recurrence
\(f(n) = f(n - 1) + f(n - 2) + f(n - 3)\),
with initial conditions \(f(0) = 1\), \(f(-1) = 0\), \(f(-2) = 0\).
That recurrence can be written in matrix form as
\[ \begin{pmatrix} 1 & 1 & 1 \\ 1 & 0 & 0 \\ 0 & 1 & 0 \\ \end{pmatrix}\]
And iterating the function is then just a question of matrix powers
def total_ways(n_ones):
= np.array([[1, 1, 1], [1, 0, 0], [0, 1, 0]])
matrix return (np.linalg.matrix_power(matrix, n_ones) @ [1, 0, 0])[0]
np.product(len(x)) for x in "".join(str(x) for x in np.diff(data)).split("3")]
[total_ways( )
For the first part, we are taking the convolution of our original grid with a weights array that looks like \[ \begin{pmatrix} 1 & 1 & 1\\ 1 & 0 & 1\\ 1 & 1 & 1\\ \end{pmatrix} \] Scipy has nice routines that handle all that indexing for us, so we’ll cheat and use them. The only slight issue is what to do at the edge of the grid, but using a constant value of 0 for any cells that would fall outside the grid works out of the box.
= np.array([[1 if char == "." else 0 for char in line] for line in load(11)])
data = np.where(data)
mask = np.zeros(data.shape, dtype=int)
board = np.ones(board.shape, dtype=int)
new_board = 0
new_board[mask] = [[1, 1, 1], [1, 0, 1], [1, 1, 1]]
weights while (board != new_board).any():
= new_board.copy()
board = scipy.ndimage.convolve(new_board, weights, mode="constant")
convolution == 0)] = 1
new_board[np.where(convolution >= 4)] = 0
new_board[np.where(convolution = 0
new_board[mask] sum() board.
For part 2 I was unable to find a nice way of expressing the condition of “Look for the first grid position in a given direction which is not floor”. That means that I have to manually loop over the grid instead of using the convolution routine - and that really slows down the runtime!
= np.zeros(data.shape, dtype=int)
board = np.ones(board.shape, dtype=int)
new_board = 0
new_board[mask]
def update(board):
= board.copy()
new_board = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]]
directions for row, col in itertools.product(*[range(x) for x in board.shape]):
= 0
total for direction in directions:
= False
done = row, col
new_row, new_col while not done:
= new_row + direction[0], new_col + direction[1]
new_row, new_col if (
< 0
new_row or new_row >= board.shape[0]
or new_col < 0
or new_col >= board.shape[1]
):break
if not data[new_row, new_col]:
= True
done else: # no break - so a valid position
+= board[new_row, new_col]
total if total == 0:
= 1
new_board[row, col] if total >= 5:
= 0
new_board[row, col] = 0
new_board[mask] return new_board
while (new_board != board).any():
= new_board
board = update(new_board)
new_board sum() new_board.
I’ll use the usual trick of modelling the 2d grid as the complex plane.
= {"N": 1j, "E": 1, "S": -1j, "W": -1}
directions = {"L": 1j, "R": -1j}
turns = 0, 1
position, direction
def update_state(position, direction, instruction, value, part=1):
if instruction in directions:
= directions[instruction] * value
offset return (
+ offset, direction)
(position if part == 1
else (position, direction + offset)
)if instruction in turns:
return position, direction * turns[instruction] ** (int(value // 90))
return position + value * direction, direction
= load(12)
instructions for instruction, value in map(lambda x: (x[0], int(x[1:])), instructions):
= update_state(position, direction, instruction, value)
position, direction int(abs(position.real) + abs(position.imag))
Part 2 can be done basically the same way as part 1, with only a small change to the update state function. In part 1, nesw move the ship; in part 2 they move the waypoint. Everything else is the same.
= 0, 10 + 1j
position, waypoint for instruction, value in map(lambda x: (x[0], int(x[1:])), instructions):
= update_state(position, waypoint, instruction, value, part=2)
position, waypoint int(abs(position.real) + abs(position.imag))
This one looks like a bunch of modular arithmetic. The first one is just whichever bus number n has the smallest -x (mod n)
= 1006726
x = (
buses "23,x,x,x,x,x,x,x,x,x,x,x,x,41,x,x,x,x,x,x,x,x,x,647,"
"x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,13,19,x,x,x,x,x,x,x"
",x,x,29,x,557,x,x,x,x,x,37,x,x,x,x,x,x,x,x,x,x,17"
)= [
bus_moduli int(_), (-idx) % int(_)) for idx, _ in enumerate(buses.split(",")) if _ != "x"
(
]= [n for n, a in bus_moduli]
bus_numbers = [-x % bus for bus in bus_numbers]
waits min(waits) * bus_numbers[np.argmin(waits)]
For the second, it’s the Chinese remainder theorem to the rescue. Code shamelessly copied from wikipedia.
from utils import crt
crt(bus_moduli)
= [line.split(" = ") for line in load(14)]
data = defaultdict(int)
result = ""
mask for operation, operand in data:
if operation == "mask":
= operand
mask else:
= "".join(
new_operand if (b1 != "X") else b2 for b1, b2 in zip(mask, f"{int(operand):036b}")]
[b1
)4:-1]] = int(new_operand, 2)
result[operation[sum(result.values())
= defaultdict(int)
result = ""
mask for operation, operand in data:
if operation == "mask":
= operand
mask else:
= int(operand)
operand = operation[4:-1]
operation = [""]
addresses for b1, b2 in zip(mask, f"{int(operation):036b}"):
if b1 == "X":
= [a + "0" for a in addresses] + [a + "1" for a in addresses]
addresses elif b1 == "1":
= [a + "1" for a in addresses]
addresses else:
= [a + b2 for a in addresses]
addresses for address in addresses:
int(address, 2)] = operand
result[sum(result.values())
= load(15, "int")[0]
prefix = {i: idx + 1 for idx, i in enumerate(prefix[:-1])}
state = prefix[-1]
current
def update(current, i):
if current not in state:
= 0
next_n else:
= i - state[current]
next_n = i
state[current] return next_n
= 2020
n_turns for i in range(len(prefix), n_turns):
= update(current, i)
current current
30 million is just within range where brute force would be plausible. Let’s try it:
for i in range(n_turns, 30_000_000):
= update(current, i)
current current
I could do this by trying to merge ranges of valid values. Or I could just instantiate a set with every valid value…
= load(16, "raw")
data = data[:-1].split("\n\n")
rules, own, nearby = set()
valid for rule in rules.split("\n"):
= [int(x) for x in re.findall(r"\d+", rule)]
limits for lower, upper in zip(limits[::2], limits[1::2]):
|= set(range(lower, upper + 1))
valid sum(
map(
lambda x: 0 if int(x) in valid else int(x),
"\n") + 1 :].replace("\n", ",").split(","),
nearby[nearby.index(
) )
I’m not entirely surprised that that’s where he went for part 2
= defaultdict(set)
ranges for rule in rules.split("\n"):
= rule.split(":")
name, _ = [int(x) for x in re.findall(r"\d+", rule)]
limits for lower, upper in zip(limits[::2], limits[1::2]):
|= set(range(lower, upper + 1))
ranges[name] = [int(x) for x in own.split("\n")[1].split(",")]
own_ticket = [[int(x) for x in line.split(",")] for line in nearby.split("\n")[1:]]
nearby_tickets = np.array([x for x in nearby_tickets if all(y in valid for y in x)])
nearby_tickets = [0] * len(own_ticket)
assignments while ranges:
for column in range(len(assignments)):
if assignments[column]:
continue
= [
candidates
keyfor key in ranges.keys()
if all(x in ranges[key] for x in nearby_tickets[:, column])
]if len(candidates) == 1:
= candidates[0]
assignments[column] del ranges[candidates[0]]
np.product(
[
own_ticket[idx]for idx, assignment in enumerate(assignments)
if "departure" in assignment
] )
This looks like another good time to use scipy’s handy correlate/convolve functions. At some point I should probably learn what the difference between those two is.
= np.array(
data 1 if char == "#" else 0 for char in line] for line in load(17)], dtype=int
[[
)
def simulate(data, dimensions=3, ncycles=6):
= np.ones((3,) * dimensions)
weights = dimensions - len(data.shape)
missing_dimensions = data.reshape(data.shape + (1,) * missing_dimensions)
data = np.pad(data, ncycles)
data for i in range(ncycles):
= scipy.ndimage.correlate(data, weights, mode="constant")
convolution = (convolution == 3) | ((convolution == 4) & data)
data return data.sum()
simulate(data)
4) simulate(data,
import string
= {"*": lambda x, y: x * y, "+": lambda x, y: x + y}
operators
def find_closing_paren(s):
assert s[0] == "("
= 0
count for idx, char in enumerate(s):
+= 1 if char == "(" else -1 if char == ")" else 0
count if count == 0:
return idx + 1
def evaluate(expression, part=1):
= 0
i = deque()
ops = deque()
operands while i < len(expression):
= expression[i]
char if char in "+*":
ops.append(char)+= 1
i continue
if char == "(":
= find_closing_paren(expression[i:])
delta + 1 : i + delta - 1], part=part))
operands.append(evaluate(expression[i += delta
i elif char in string.digits:
int(char))
operands.append(+= 1
i
if part == 2 and ops and ops[-1] == "+":
ops.pop()+ operands.pop())
operands.append(operands.pop()
while ops:
= ops.popleft()
op
operands.appendleft(operators[op](operands.popleft(), operands.popleft()))return operands.pop()
= [x.replace(" ", "") for x in load(18)]
lines sum(evaluate(line) for line in lines)
The change for part 2 is so small that it can be included in part 1 as a flag
sum(evaluate(line, part=2) for line in lines)
= load(19, "raw")
data = map(lambda x: x.split("\n"), data.split("\n\n"))
relations, strings = {}
dependencies = {}
rules for relation in relations:
= relation.split(":")
lhs, rhs int(lhs)] = (
rules['"', "").strip()}
{rhs.replace(if '"' in rhs
else {tuple([int(y) for y in x.split()]) for x in rhs.split("|")}
)
= {
dependencies set().union(*rules[x]) if x not in [54, 20] else set() for x in rules
x:
}
def concat(l1, l2):
return {s1 + s2 for s1 in l1 for s2 in l2}
def expand(rule, rules, mappings):
= set()
result for option in rules[rule]:
if isinstance(option, str):
return rules[rule]
= (
new_elements 0]]
mappings[option[if len(option) == 1
else concat(*[mappings[i] for i in option])
)|= new_elements
result return result
def free_elements(mydict):
return [x for x in mydict if not mydict[x]]
= {}
mappings while dependencies:
= free_elements(dependencies)[0]
k1 = expand(k1, rules, mappings)
mappings[k1] for k2 in dependencies:
dependencies[k2].discard(k1)del dependencies[k1]
len(list(filter(lambda x: x in mappings[0], strings[:-1])))
We have loops now! Analysing the new rules, we see that the system should accept all strings of the form 42m 13n, with m > n > 0. Looking at the previous part shows that rule 42 and rule 31 never overlap, and all the strings they match have length 8. That gives the following logic for part 2:
from more_itertools import chunked
def part2(s):
= ["".join(x).strip() for x in chunked(s, 8)]
chunks = 0, 0
r42, r31 while chunks[r42] in mappings[42]:
+= 1
r42 if r42 == len(chunks):
break
while r42 + r31 < len(chunks):
if chunks[r42 + r31] not in mappings[31]:
break
+= 1
r31
return r31 >= 1 and r42 > r31 and r42 + r31 == len(chunks)
sum(part2(s) for s in strings[:-1])
= load(20, "raw").split("\n\n")
data 0]
data[= {}
tiles
def edge_hashes(t):
= [
edges 0],
t[0][::-1],
t[-1],
t[:, -1][::-1],
t[:, -1][::-1],
t[-1],
t[0][::-1],
t[:, 0],
t[:,
]return {
reduce(lambda x, y: 2 * x + y, edge[::-1])
idx: functools.for idx, edge in enumerate(edges)
}
def fingerprint(tile):
return tile, edge_hashes(tile), {v: k for k, v in edge_hashes(tile).items()}
for entry in data:
*tile = entry.split("\n")
header, = np.array(
tile 1 if char == "#" else 0 for char in line] for line in tile if line],
[[="int",
dtype
)= int(re.findall(r"\d+", header)[0])
tile_id = fingerprint(tile)
tiles[tile_id] = defaultdict(set)
matches = sorted(tiles.keys())
keys for x in range(len(keys)):
for y in range(x + 1, len(keys)):
if set(tiles[keys[x]][1].values()) & set(tiles[keys[y]][1].values()):
matches[keys[x]].add(keys[y])
matches[keys[y]].add(keys[x])= [x for x in matches if len(matches[x]) == 2]
corners np.product(corners)
Direct inspection shows that there are no false matches in the edge pairings, so we can proceed to place all the tiles without taking that into account. We’ll start by placing the tiles in the correct location without worrying about their orientation, and then rotate and flip them afterwards.
There are eight possible ways to fill the board (four different corners to put in the top left, and then for each of those, two different choices for how to flip around the main diagonal), so we’ll arbitrarily pick one of them.
We’ll start by placing a corner piece in the top left, and then one of its neighbors below it.
When we place a tile, we mark the candidates for its unplaced neighbors: these are the intersection of whatever candidates were there before, and the unplaced matches of the current tile. We also remove the current tile as a candidate from any other open location, since it’s just been placed. Whenever a location has only one candidate, we can place that, and proceed until the whole board is filled.
= corners[0]
corner = next(iter(matches[corner]))
match = defaultdict(lambda: set(keys))
locations = set()
placed
def place(tile, location):
= tile
locations[location]
placed.add(tile)= location
x, y if x < 11 and isinstance(locations[x + 1, y], set):
+ 1, y] &= matches[tile] - placed
locations[x if y < 11 and isinstance(locations[x, y + 1], set):
+ 1] &= matches[tile] - placed
locations[x, y = []
to_place for location in locations:
if isinstance(locations[location], set):
locations[location].discard(tile)if len(locations[location]) == 1:
to_place.append(location)for placement in to_place:
if isinstance(locations[placement], set):
next(iter(locations[placement])), placement)
place(
0, 0))
place(corner, (1, 0))
place(match, (= np.zeros((12, 12), dtype=int)
coords for location in locations:
= locations[location] coords[location]
With that out of the way, we need to orient the tiles correctly. We start by making sure the top left corner is oriented correctly, and then we match all of the other tiles to that structure.
For each row in the grid, we match the first cell to the first cell in the row above, and then we match all the other cells to the neighbor to their left.
# start off by making sure that the right hand side matches
= [
overlap 0, 0]][2][key]
tiles[coords[for key in set(tiles[coords[0, 0]][2].keys()) & set(tiles[coords[0, 1]][2].keys())
0]
][= (((overlap - overlap % 2) - 2) // 2) % 4
delta if delta:
= np.rot90(tiles[coords[0, 0]][0], -delta)
tile 0, 0]] = fingerprint(tile)
tiles[coords[# Then check if the bottom of the tile is the one that matches [1, 0]. If not, flip it vertically.
= [
overlap 0, 0]][2][key]
tiles[coords[for key in set(tiles[coords[0, 0]][2].keys()) & set(tiles[coords[1, 0]][2].keys())
0]
][if overlap // 2 != 2:
= tile[::-1]
tile 0, 0]] = fingerprint(tile) tiles[coords[
All the edges are labelled (not well, but they are labelled). To be oriented correctly, the 7 edge on a tile should match the 2 edge on the tile to its left, and/or the 0 edge on a tile should match the 5 edge on the tile above it.
for y in range(12):
for x in range(12):
if not x and not y:
continue
= tiles[coords[y, x]][0]
tile if not x:
= 5
old_edge = 0
target = (y - 1, x)
comparison else:
= 2
old_edge = 7
target = (y, x - 1)
comparison = tiles[coords[comparison]][1][old_edge]
value = tiles[coords[y, x]][2][value]
current if (target - current) % 2:
= tile.T
tile = 7 - current
current = (current - target) // 2
rotation = np.rot90(tile, rotation)
tile = fingerprint(tile) tiles[coords[y, x]]
We can now reconstruct the board, and go hunting for the sea monsters. The convolve/correlate functions are handy here as well, since we’re looking for an area where all of a subset of the neighboring cells are lit up. So we correlate with that mask, and check which correlations have the full set.
The following code will fail if any of the sea monsters overlap, but luckily they don’t.
= np.zeros((8 * 12, 8 * 12), dtype=int)
board for y in range(12):
for x in range(12):
* 8 : (y + 1) * 8, x * 8 : (x + 1) * 8] = tiles[coords[y, x]][0][
board[y 1:-1, 1:-1
]= [
pattern 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1],
[0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0],
[
]
= np.sum(pattern)
total = 0
monsters for reflection in range(2):
for rotation in range(4):
= scipy.ndimage.correlate(board, pattern, mode="constant")
convolution += (convolution == total).sum()
monsters = np.rot90(board)
board = board.T
board sum() - monsters * 15 board.
Nice and easy one today, with just a bit of set intersection logic
= defaultdict(int)
counts = {}
mappings for line in load(21):
= [
ingredients, allergens for x in line[:-2].replace(",", "").split("(contains")
x.split()
]for ingredient in ingredients:
+= 1
counts[ingredient] for allergen in allergens:
if allergen in mappings:
&= set(ingredients)
mappings[allergen] else:
= set(ingredients)
mappings[allergen] = set().union(*mappings.values())
potential_allergens sum(counts[x] for x in counts if x not in potential_allergens)
And part 2 isn’t much harder. Any allergens which have only one matching ingredient are fixed; and this ingredient can then be removed from all the other allergens. And then it’s a question of continuing until we have the full map
= {}
assignments = {x for x in mappings if len(mappings[x]) == 1}
assignable while mappings:
= assignable.pop()
key = mappings[key].pop()
value del mappings[key]
= value
assignments[key] for mapping in mappings:
mappings[mapping].discard(value)if len(mappings[mapping]) == 1:
assignable.add(mapping)print(",".join(assignments[x] for x in sorted(list(assignments.keys()))))
= map(deque, np.array(load(22, "int")).reshape(2, -1)[:, 1:])
p1, p2 while p1 and p2:
= p1[0], p2[0]
c1, c2 -1)
p1.rotate(-1)
p2.rotate(= (p1, p2) if (c1 > c2) else (p2, p1)
winner, loser
winner.append(loser.pop())= p1 if p1 else p2
result * (np.arange(len(result)) + 1)[::-1]).sum() (np.array(result)
A fairly straightforward recursive implementation of the requirements
= map(deque, np.array(load(22, "int")).reshape(2, -1)[:, 1:])
p1, p2
def play(p1, p2):
= set()
seen while p1 and p2:
= (tuple(p1), tuple(p2))
hashed if hashed in seen:
return 1, 0
seen.add(hashed)= p1[0], p2[0]
c1, c2 -1)
p1.rotate(-1)
p2.rotate(if c1 < len(p1) and c2 < len(p2):
= deque(list(p1)[:c1])
new_p1 = deque(list(p2)[:c2])
new_p2 = play(new_p1, new_p2)
recursive_battle = (p1, p2) if recursive_battle[0] else (p2, p1)
winner, loser else:
= (p1, p2) if (c1 > c2) else (p2, p1)
winner, loser
winner.append(loser.pop())return p1, p2
= play(p1, p2)
result = result[0] if result[0] else result[1]
result * np.arange(1, len(result) + 1)[::-1]).sum() (np.array(result)
= [int(x) for x in "583976241"]
data = deque(data)
s = len(s)
l for _ in range(100):
= (s[0] - 2) % l + 1
target -1)
s.rotate(= [s.popleft() for i in range(3)]
pickup while target in pickup:
= (target - 2) % l + 1
target
= 0
count while s[0] != target:
-1)
s.rotate(+= 1
count -1)
s.rotate(for value in pickup[::-1]:
s.appendleft(value)+ 1)
s.rotate(count while s[0] != 1:
s.rotate()
s.popleft()"".join(str(x) for x in s)
This approach is way too slow for part 2. Instead, we can build an implicit linked list in a numpy array, so that a[node] = next node for all nodes. To make that work with zero based indexing we relabel all the nodes. A single step can now be accomplished by just updating three array elements: the next pointer of the current node, the next pointer of the destination node, and the next pointer of the last of the moved elements.
= data + list(range(10, 1_000_001))
initial_state = np.zeros(len(initial_state), dtype=int)
a = min(initial_state)
minval for c, n in zip(initial_state, initial_state[1:] + [initial_state[0]]):
- 1] = n - 1
a[c = a[-1]
current for _ in range(10_000_000):
= [a[current], a[a[current]], a[a[a[current]]]]
to_move = current - 1
destination while destination < 0 or destination in to_move:
-= 1
destination if destination < 0:
= 999_999
destination # next pointer of current is the one after the moved ones
= a[to_move[-1]]
a[current] # The next one after the moved three is the one after the destination
-1]] = a[destination]
a[to_move[# After the destination comes the first of the moved ones
= to_move[0]
a[destination] # And we move to the next node
= a[current]
current 0] + 1) * (a[a[0]] + 1) (a[
= {
vectors "e": (1, 0),
"ne": (1, 1),
"nw": (0, 1),
"w": (-1, 0),
"sw": (-1, -1),
"se": (0, -1),
}= ""
current = "ew"
terminals = defaultdict(bool)
tiles for s in load(24):
= []
result for char in s:
+= char
current if char in terminals:
result.append(vectors[current])= ""
current = tuple(np.array(result).sum(axis=0))
coords ^= 1
tiles[coords] sum(list(tiles.values())) np.
This is a fairly simple convolution problem again. We’ll be working in the basis defined above, and in that basis, the neighbors are as given in the vectors dictionary. So the array of weights is the one below.
= list(tiles.keys())
keys = [[1, 1, 0], [1, 0, 1], [0, 1, 1]]
weights = 100
border = 2 * border + np.max(keys) - np.min(keys)
size = np.zeros((size, size), dtype=int)
board for x, y in tiles:
+ size // 2, y + size // 2] = tiles[x, y]
board[x for i in range(100):
= scipy.ndimage.correlate(board, weights, mode="constant")
neighbors = ((board == 0) & (neighbors == 2)) | (
flips == 1) & ((neighbors == 0) | (neighbors > 2))
(board
)= (board + flips) % 2
board sum() board.
= 14222596
d = 4057428
c = 0, 0
s1, s2 = 7
f = 1
v = 20201227
mod = 0
i while not s1 or not s2:
= v * f % mod
v += 1
i if v == d and not s1:
= i
s1 if v == c and not s2:
= i
s2 = 1
r1 for i in range(s1):
= r1 * c % mod
r1 r1