chore: create new project structure and aoc.py runner script

This commit is contained in:
2025-08-04 16:23:06 +02:00
parent f76375d835
commit e2964c6c36
91 changed files with 177 additions and 113 deletions

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env python3
def main(inp):
floor = 0
basement = -1
for i, c in enumerate(inp):
if c == "(": floor += 1
elif c == ")": floor -= 1
if basement == -1 and floor == -1:
basement = i + 1
print("Part 1: ", floor)
print("Part 2: ", basement)
if __name__ == '__main__':
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
with open(infile) as inp:
main(inp.read().rstrip())

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env python3
import itertools
def main(inp):
# Part 1
changes = [int(n) for n in inp]
print(sum(changes))
freq = 0
seen = {0}
for num in itertools.cycle(changes):
freq += num
if freq in seen:
print(freq)
break
seen.add(freq)
if __name__ == '__main__':
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
with open(infile) as inp:
main(inp.readlines())

View File

@@ -0,0 +1,30 @@
from collections import Counter
def part2(ids):
for id_a in ids:
for id_b in ids:
common_letters = [a for a, b in zip(id_a, id_b) if a == b]
if len(common_letters) == len(id_a) - 1:
res = "".join(common_letters)
print("Part 2: ", res)
return
def main(lines):
total_twice, total_thrice = 0, 0
for line in lines:
c = Counter(line)
total_twice += 1 if [k for k,v in c.items() if v == 2] != [] else 0
total_thrice += 1 if [k for k,v in c.items() if v == 3] != [] else 0
print(f"Part 1: ", total_twice * total_thrice)
part2(lines)
if __name__ == "__main__":
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
with open(infile) as inp:
main([l.rstrip() for l in inp.readlines()])

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python3
import re
import itertools
from dataclasses import dataclass
from collections import defaultdict
@dataclass
class Rectangle:
x: int
y: int
width: int
height: int
@property
def x2(self):
return self.x + self.width
@property
def y2(self):
return self.y + self.height
def parse_line(l):
parsed = re.findall(r"\d+", l)
id_, x, y, width, height = map(int, parsed)
return id_, Rectangle(x, y, width, height)
def main(inp):
regions = defaultdict(set)
for id_, region in map(parse_line, inp):
for x in range(region.x, region.x2):
for y in range(region.y, region.y2):
regions[x, y].add(id_)
total = sum(len(x) > 1 for x in regions.values())
print(f"Part 1: ", total)
all_ids = set()
overlapping_ids = set()
for region in regions.values():
all_ids.update(region)
if len(region) > 1:
overlapping_ids.update(region)
difference = all_ids - overlapping_ids
print(f"Part 2: {difference.pop()}")
if __name__ == '__main__':
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
with open(infile) as inp:
main([l.rstrip() for l in inp.readlines()])

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env python3
import re
from collections import defaultdict, Counter
from pprint import pprint
def main(inp):
# total minutes asleep per guard
guards = defaultdict(int)
# list of guards sleeping for each minute
minutes = defaultdict(lambda: defaultdict(int))
for l in sorted(inp):
minute = re.search(r":(\d+)", l).group(1)
if "#" in l:
current_id = re.search(r"#(\d+)", l).group(1)
elif "asleep" in l:
start = int(minute)
elif "wakes" in l:
end = int(minute)
guards[current_id] += end - start
for m in range(start, end):
minutes[m][current_id] += 1
# Find the guard that has the most minutes asleep. What minute does that guard spend asleep the most?
guard_id = max(guards.items(), key=lambda x: x[1])[0]
minute = max([(k, v[guard_id]) for k, v in minutes.items() if guard_id in v], key=lambda x: x[1])[0]
print("Part 1: ", int(guard_id) * minute)
# Of all guards, which guard is most frequently asleep on the same minute?
maxs = { m: max(minutes[m].items(), key=lambda x: x[1]) for m in minutes.keys()}
minute, rest = max(maxs.items(), key=lambda x: x[1][1])
id_, _ = rest
print("Part 2: ", int(id_) * minute)
if __name__ == '__main__':
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
with open(infile) as inp:
main(inp.readlines())

View File

@@ -0,0 +1,25 @@
def reduce_polymer(p):
res = [""]
for x in p:
if res[-1].swapcase() == x:
res.pop()
else:
res.append(x)
return "".join(res)
def main(inp):
print("Part 1: ", len(reduce_polymer(inp)))
letters = set(x.lower() for x in inp)
min_ = 2 << 32
for l in letters:
p = inp.replace(l.lower(), "").replace(l.upper(), "")
min_ = min(len(reduce_polymer(p)), min_)
print("Part 2: ", min_)
if __name__ == "__main__":
import fileinput
inp = next(l.rstrip() for l in fileinput.input())
main(inp)

View File

@@ -0,0 +1,59 @@
from collections import defaultdict, namedtuple
Point = namedtuple('Point', ['x', 'y'])
def dist(a, b):
"manhattan distance"
return abs(a.x - b.x) + abs(a.y - b.y)
def iter_grid(bounds):
min_, max_ = bounds
for x in range(min_.x, max_.x + 1):
for y in range(min_.y, max_.y + 1):
yield Point(x, y)
def is_edge(p, bounds):
start, end = bounds
return p.x == start.x or p.x == end.x or p.y == start.y or p.y == end.y
def main(inp):
grid = [Point(*map(int, l.split(", "))) for l in inp]
bounds = (Point(min(p.x for p in grid), min(p.y for p in grid)),
Point(max(p.x for p in grid), max(p.y for p in grid)))
areas = defaultdict(int)
infinite_regions = set()
for p in iter_grid(bounds):
# find dist to every point of grid
distances = sorted((dist(p2, p), i) for i, p2 in enumerate(grid))
# equally far from two or more coordinates, don't count
if distances[0][0] == distances[1][0]:
continue
_, index = distances[0]
areas[index] += 1
if is_edge(p, bounds):
infinite_regions.add(index)
# remove all infinite regions by index
for i in infinite_regions:
del areas[i]
print("Part 1: ", max(areas.values()))
count = 0
for cur in iter_grid(bounds):
s = sum(dist(cur, p) for p in grid)
if s < 10_000:
count += 1
print("Part 2: ", count)
if __name__ == "__main__":
import fileinput
inp = list(l.rstrip() for l in fileinput.input())
main(inp)

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python3
import re
from bisect import insort
from collections import defaultdict
def topological_sort(graph, reverse_deps):
# find starting nodes: with no incoming edges (aka indegree 0)
queue = sorted([task for task in graph.keys() if reverse_deps[task] == set()])
order = []
seen = set()
while queue != []:
current = queue.pop(0)
if current not in seen:
seen.add(current)
order.append(current)
# add dependencies if all prerequisites are already visited,
# insert them in alphabetical order
for d in graph[current]:
if all(x in order for x in reverse_deps[d]):
insort(queue, d)
return order
def main(inp):
dependencies = defaultdict(set)
reverse_deps = defaultdict(set)
for l in inp:
first, second = re.findall(r"[sS]tep (\w)", l)
dependencies[first].add(second)
reverse_deps[second].add(first)
order = topological_sort(dependencies, reverse_deps)
print("Part 1: ", "".join(order))
done = []
doing = dict()
workers = 5
step = 0
number_of_tasks = len(order)
while len(done) != number_of_tasks:
assert len(doing) <= workers
for i in range(workers):
# check if the worker has a pending task
if i in doing:
task = doing[i]
if is_task_done(task, step):
#print(f"{step}: worker #{i}, task {task} done")
del doing[i]
done.append(task[0])
else:
continue
next_task = get_task(dependencies, reverse_deps, done, doing)
if next_task is not None:
#print(f"{step}: worker #{i}, starting task {next_task}")
doing[i] = (next_task, step)
#print(f"{step}: {doing} {done}")
if len(done) == number_of_tasks:
break
step += 1
print(f"{step}\t{'\t'.join(x[0] for x in doing.values())}")
print(step)
def get_task(graph, reverse_deps, done, doing):
queue = sorted([task for task in graph.keys() if all(x in done for x in reverse_deps[task])])
doingg = [x[0] for x in doing.values()]
for t in queue:
if t not in done and t not in doingg:
return t
return None
def is_task_done(task, step):
letter, start_t = task
duration = ord(letter) - ord("A") + 61
if step - start_t >= duration:
return True
return False
if __name__ == '__main__':
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
with open(infile) as inp:
main([l.rstrip() for l in inp.readlines()])

View File

View File

@@ -0,0 +1,44 @@
#! /usr/bin/env python3
def calculate_fuel_iterative(mass):
total = 0
new_mass = mass
while True:
new_mass = new_mass // 3 - 2
if new_mass < 0:
break
total += new_mass
return total
def calculate_total_fuel_mass(input_file, mass_function=lambda x: x // 3 - 2):
total = 0
with open(input_file) as masses:
for mass in masses:
total += mass_function(int(mass))
return total
def test_part2():
inputs = (14, 1969, 100756)
expected = (2, 966, 50346)
for i, inp in enumerate(inputs):
result = calculate_fuel_iterative(inp)
assert result == expected[i], "Result for {} should be {}, was {}".format(
inp, expected[i], result
)
print("All tests passed for part 2.")
if __name__ == "__main__":
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
print("Part 1 - total mass: ", calculate_total_fuel_mass(infile))
test_part2()
print(
"Part 2 -- total mass: ",
calculate_total_fuel_mass(infile, calculate_fuel_iterative),
)

View File

@@ -0,0 +1,65 @@
# TODO replace PYTHONPATH hack with a proper solution, like making intcode an
# installed module https://stackoverflow.com/a/50194143
import sys
from pathlib import Path
sys.path.append(str(Path(__file__).absolute().parent.parent / "intcode"))
from intcode import interpret_intcode, Interpreter
def paint(program, initial_state):
interpreter = Interpreter(program)
pos = 0 + 0j
direction = 0 - 1j # initially facing up ^
colors = {}
colors[pos] = initial_state
while True:
interpreter.stdin.append(colors.get(pos, 0))
interpreter.interpret(break_on_output=True)
interpreter.interpret(break_on_output=True)
if interpreter.halted:
return colors
color = interpreter.stdout.pop(0)
colors[pos] = color
turn = interpreter.stdout.pop(0)
if turn == 0:
direction *= -1j # turn left
elif turn == 1:
direction *= 1j # turn right
else:
assert False
pos += direction
def main(data, part=1):
program = list(map(int, data.readline().rstrip().split(",")))
if part == 1:
colors = paint(program, initial_state=0)
print("Part 1: ", len(colors))
else:
colors = paint(program, initial_state=1)
part2(colors)
def part2(colors):
min_x = int(min(x.real for x in colors.keys()))
max_x = int(max(x.real for x in colors.keys()))
min_y = int(min(x.imag for x in colors.keys()))
max_y = int(max(x.imag for x in colors.keys()))
for y in range(min_y, max_y + 1):
l = []
for x in range(min_x, max_x + 1):
point = complex(x, y)
l.append("\u2588" if point in colors and colors[point] == 1 else " ")
print("".join(l))
if __name__ == "__main__":
import fileinput
with fileinput.input() as f:
main(f, part=1)
#with fileinput.input() as f:
#main(f, part=2) # FIXME unable to run both parts simultaneously

View File

View File

@@ -0,0 +1,117 @@
#!/usr/bin/env python3
# TODO replace PYTHONPATH hack with a proper solution, like making intcode an
# installed module https://stackoverflow.com/a/50194143
import sys
import time
from itertools import zip_longest
from pathlib import Path
from collections import defaultdict, Counter
from dataclasses import dataclass
sys.path.append(str(Path(__file__).absolute().parent.parent / "intcode"))
from intcode import interpret_intcode, Interpreter
@dataclass
class State:
grid: ...
score = 0
bounds = None
paddle_pos = None
ball_pos = None
stopped = False
def grouper(n, iterable):
"grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
args = [iter(iterable)] * n
return zip_longest(*args)
def part1(program):
interpreter = interpret_intcode(program)
game = { (x, y): tile for x, y, tile in grouper(3, interpreter.stdout) }
print("Part 1: ", len([x for x in game.values() if x == 2]))
def parse_map(output):
grid = {}
score = None
for x, y, tile in grouper(3, output):
if (x, y) == (-1, 0):
score = tile
else:
grid[x,y] = tile
paddle = next((k for k, v in grid.items() if v == 3), None)
ball = next((k for k, v in grid.items() if v == 4), None)
bounds = (max(x for x, y in grid.keys()),
max(y for x, y in grid.keys()))
return grid, bounds, score, paddle, ball
def update_state(state: State, stdout):
grid, bounds, score, paddle, ball = parse_map(stdout)
if ball is None:
state.stopped = True
if state.bounds is None: # only set bounds the first time
state.bounds = bounds
if score is not None:
state.score = score
if paddle is not None:
state.paddle_pos = paddle
if ball is not None:
state.ball_pos = ball
if grid is None:
state.grid = grid
else:
# merge grid
for (x, y), v in grid.items():
state.grid[x,y] = v
return state
def part2(program):
program[0] = 2
interpreter = Interpreter(program)
state = State({})
while not interpreter.halted:
interpreter.interpret(break_on_output=False, break_on_input=True)
state = update_state(state, interpreter.stdout)
if state.stopped:
break
#print_map(state)
interpreter.stdout.clear()
paddle_x, ball_x = state.paddle_pos[0], state.ball_pos[0]
if paddle_x < ball_x: # move right
interpreter.stdin.append(1)
elif paddle_x > ball_x: # move left
interpreter.stdin.append(-1)
else:
interpreter.stdin.append(0)
print("Part 2: ", state.score)
def print_map(state):
clear = "\033[2J"
tiles = { 0: " ", 1: "#", 2: "~", 3: "_", 4: "O" }
max_x, max_y = state.bounds
print(clear)
for y in range(max_y + 1):
l = []
for x in range(max_x + 1):
tile = state.grid[x,y]
l.append(tiles[tile])
print("".join(l))
time.sleep(1/60)
def main(inp):
program = [int(x) for x in inp.readline().rstrip().split(",")]
part1(program)
part2(program)
if __name__ == "__main__":
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
with open(infile) as raw_input:
main(raw_input)

View File

@@ -0,0 +1,71 @@
def interpret_intcode(input_prog):
# Instruction pointer: index of the next element to be executed
ip = 0
while ip < len(input_prog):
instruction = input_prog[ip]
if instruction == 99:
break
elif instruction == 1:
# The operands to sum are at the memory location ip+1 and ip+2.
operands = (input_prog[input_prog[ip + 1]], input_prog[input_prog[ip + 2]])
result = sum(operands)
target = input_prog[ip + 3]
input_prog[target] = result
ip += 4
elif instruction == 2:
# The operands to multiply are at the memory location ip+1 and ip+2.
operands = (input_prog[input_prog[ip + 1]], input_prog[input_prog[ip + 2]])
result = operands[0] * operands[1]
target = input_prog[ip + 3]
input_prog[target] = result
ip += 4
def tests():
inputs = (
[1, 0, 0, 0, 99], # ADD 1 + 1 to location 0
[2, 3, 0, 3, 99], # MUL 2 * 3 to location 3
[2, 4, 4, 5, 99, 0],
[1, 1, 1, 4, 99, 5, 6, 0, 99],
)
expected_outputs = (
[2, 0, 0, 0, 99], # 1 + 1 = 2
[2, 3, 0, 6, 99], # 3 * 2 = 6
[2, 4, 4, 5, 99, 9801], # 99 * 99 = 9801
[30, 1, 1, 4, 2, 5, 6, 0, 99],
)
for i, inp in enumerate(inputs):
result = inp[:]
interpret_intcode(result)
assert (
result == expected_outputs[i]
), f"Expected output for {inp} is {expected_outputs[i]}, but found {result} instead."
print("All tests passed.")
def run_program(memory, noun, verb):
memory[1] = noun
memory[2] = verb
interpret_intcode(memory)
return memory[0]
if __name__ == "__main__":
tests()
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
with open(infile) as inp:
memory = [int(x) for x in inp.readline().strip().split(",")]
# Pass a copy to avoid modifying the original memory
print("Part 1 answer: ", run_program(memory[:], 12, 2))
# Part 2
result = 0
for verb in range(99):
for noun in range(99):
if run_program(memory[:], noun, verb) == 19690720:
print(f"Part 2: noun={noun}, verb={verb}")
print("Result = ", 100 * noun + verb)
break

View File

@@ -0,0 +1,79 @@
#!/usr/bin/env python3
def manhattan_distance(p):
return abs(p[0]) + abs(p[1])
def points_for_wire(wire):
x, y, count = 0, 0, 0
points = {}
# (x, y)
directions = {"R": (1, 0), "L": (-1, 0), "U": (0, 1), "D": (0, -1)}
for p in wire:
# D42 -> for _ in range(42)
for _ in range(int(p[1:])):
offset = directions[p[0]]
x += offset[0]
y += offset[1]
count += 1
points[(x ,y)] = count
return points
def find_min_distance(wire1, wire2):
points1 = points_for_wire(wire1)
points2 = points_for_wire(wire2)
intersections = points1.keys() & points2.keys()
closest = min((intersection for intersection in intersections), key=manhattan_distance)
return manhattan_distance(closest)
def find_least_steps(wire1, wire2):
points1 = points_for_wire(wire1)
points2 = points_for_wire(wire2)
intersections = points1.keys() & points2.keys()
# Intersection with the least steps
least_steps = min(intersections, key=lambda x: points1[x] + points2[x])
return points1[least_steps] + points2[least_steps]
def tests():
inputs = (
(("R8", "U5", "L5", "D3"), ("U7", "R6", "D4", "L4")),
(("R75","D30","R83", "U83", "L12", "D49", "R71", "U7", "L72"), ("U62", "R66", "U55", "R34", "D71", "R55", "D58", "R83")),
(("R98", "U47", "R26", "D63", "R33", "U87", "L62", "D20", "R33", "U53", "R51"), ("U98", "R91", "D20", "R16", "D67", "R40", "U7", "R15", "U6", "R7"))
)
# Part 1
expected = (6, 159, 135)
for i, inp in enumerate(inputs):
result = find_min_distance(inp[0], inp[1])
assert result == expected[i], "Result for {} should be {}, was {}".format(
inp, expected[i], result
)
# Part 2
# expected number of steps
expected_part2 = (30, 610, 410)
print("All tests passed.")
for i, inp in enumerate(inputs):
result = find_least_steps(inp[0], inp[1])
assert result == expected_part2[i], "Result for {} should be {}, was {}".format(
inp, expected_part2[i], result
)
if __name__ == "__main__":
tests()
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
with open(infile) as raw_input:
lines = raw_input.readlines()
wire1, wire2 = [line.strip("\n").split(",") for line in lines]
print("Part 1 -- distance = ", find_min_distance(wire1, wire2))
print("Part 2 -- steps = ", find_least_steps(wire1, wire2))

View File

@@ -0,0 +1,37 @@
#! /usr/bin/env python3
def check_increase(number):
num = str(number)
for i in range(len(num) - 1):
if num[i+1] < num[i]:
return False
return True
def check_adjacent(number):
num = str(number)
for digit in num:
count = num.count(digit)
if count == 2: # Part one : <= 2
return True
return False
def tests():
assert check_increase(123456) == True
assert check_increase(123454) == False
assert check_adjacent(112345) == True
assert check_adjacent(123445) == True
assert check_adjacent(123456) == False
def main(start, end):
matches = 0
for n in range(start, end + 1):
if check_increase(n) and check_adjacent(n):
matches += 1
return matches
if __name__ == "__main__":
tests()
print("Matches : ", main(367479, 893698))

View File

@@ -0,0 +1,177 @@
#! /usr/bin/env python3
from collections import namedtuple
from enum import Enum
import logging
logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.WARN)
def get_nth_digit(n, number):
"Returns the nth digit of the input number"
return number // 10 ** n % 10
class Operation(Enum):
ADDITION = 1
MULTIPLICATION = 2
INPUT = 3
OUTPUT = 4
JMP_IF_TRUE = 5
JMP_IF_FALSE = 6
LESS_THAN = 7
EQUALS = 8
TERMINATION = 99
class Mode(Enum):
POSITION = 0
IMMEDIATE = 1
class Instruction:
def __init__(self, opcode):
self.operation = Operation(opcode % 100)
self.modes = [Mode(get_nth_digit(n, opcode)) for n in range(2, 5)]
self.handler_name = f"handle_{self.operation.name.lower()}"
self.handler = getattr(self, self.handler_name, self.handle_termination)
self.input = 0
self.output = 0
def __repr__(self):
return f"Instruction({self.operation}, {self.modes})"
def handle(self, program, ip):
return self.handler(program, ip)
def handle_addition(self, program, ip):
first, second = self._get_parameters(program, ip)
logging.debug(f"ADD {first} {second}")
result = first + second
# the last mode should *always* be POSITION
program[program[ip + 3]] = result
ip += 4
return ip
def handle_multiplication(self, program, ip):
first, second = self._get_parameters(program, ip)
logging.debug(f"MUL {first} {second}")
result = first * second
# the last mode should *always* be POSITION
program[program[ip + 3]] = result
ip += 4
return ip
def handle_input(self, program, ip):
program[program[ip + 1]] = self.input
ip += 2
return ip
def handle_output(self, program, ip):
param = (
program[ip + 1]
if self.modes[0] is Mode.IMMEDIATE
else program[program[ip + 1]]
)
self.output = param
print("OUT ", param)
ip += 2
return ip
def handle_jmp_if_true(self, program, ip):
first, second = self._get_parameters(program, ip)
logging.debug(f"JMPT {first} {second}")
return second if first != 0 else ip + 3
def handle_jmp_if_false(self, program, ip):
first, second = self._get_parameters(program, ip)
logging.debug(f"JMPF {first} {second}")
return second if first == 0 else ip + 3
def handle_less_than(self, program, ip):
first, second = self._get_parameters(program, ip)
logging.debug(f"LT {first} {second}")
program[program[ip + 3]] = int(first < second)
ip += 4
return ip
def handle_equals(self, program, ip):
first, second = self._get_parameters(program, ip)
logging.debug(f"EQ {first} {second}")
program[program[ip + 3]] = int(first == second)
ip += 4
return ip
def handle_termination(self, program, ip):
print("HALT")
return ip
def _get_parameters(self, program, ip):
first = (
program[ip + 1]
if self.modes[0] is Mode.IMMEDIATE
else program[program[ip + 1]]
)
second = (
program[ip + 2]
if self.modes[1] is Mode.IMMEDIATE
else program[program[ip + 2]]
)
return first, second
def interpret_intcode(program, stdin=[]):
ip = 0
while program[ip] != 99:
opcode = program[ip]
instruction = Instruction(opcode)
if instruction.operation == Operation.INPUT:
instruction.input=stdin.pop(0)
ip = instruction.handle(program, ip)
return instruction.output
def tests():
inputs = (
[1, 0, 0, 0, 99], # ADD 1 + 1 to location 0
[2, 3, 0, 3, 99], # MUL 2 * 3 to location 3
[2, 4, 4, 5, 99, 0],
[1, 1, 1, 4, 99, 5, 6, 0, 99],
[1101, 1, 1, 0, 99], # ADD 1 + 1 to location 0 (direct access)
)
expected_outputs = (
[2, 0, 0, 0, 99], # 1 + 1 = 2
[2, 3, 0, 6, 99], # 3 * 2 = 6
[2, 4, 4, 5, 99, 9801], # 99 * 99 = 9801
[30, 1, 1, 4, 2, 5, 6, 0, 99],
[2, 1, 1, 0, 99], # 1 + 1 = 2 (direct access)
)
for i, inp in enumerate(inputs):
result = inp[:]
interpret_intcode(result)
assert (
result == expected_outputs[i]
), f"Expected output for {inp} is {expected_outputs[i]}, but found {result} instead."
# factorial test
fac = [3,29,1007,29,2,28,1005,28,24,2,27,29,27,1001,29,-1,29,1101,0,0,28,1006,28,2,4,27,99,1,0,0]
res = interpret_intcode(fac, [4])
assert res == 24, f"Expected 24 but got {res}"
print("\nAll tests passed.\n")
def run_input_program(inp):
print("Start of input program.")
memory = [int(x) for x in inp.readline().strip().split(",")]
part1 = interpret_intcode(memory[::], [1])
print("Part 1: ", part1)
part2 = interpret_intcode(memory[::], [5])
print("Part 2: ", part2)
if __name__ == "__main__":
tests()
import fileinput
run_input_program(fileinput.input())

View File

@@ -0,0 +1,192 @@
import logging
logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.DEBUG)
class Instruction:
code, argument_num = 0, 0
def execute(self, intcode, arguments):
pass
def new_pc(self, intcode):
return intcode.pc + self.argument_num + 1
class PlusInstruction(Instruction):
code, argument_num = 1, 3
def execute(self, intcode, arguments):
logging.debug(f"ADD {arguments[0].value} {arguments[1].value}")
intcode.set(arguments[2].address, arguments[0].value + arguments[1].value)
return self.new_pc(intcode)
class MultiplyInstruction(Instruction):
code, argument_num = 2, 3
def execute(self, intcode, arguments):
logging.debug(f"MUL {arguments[0].value} {arguments[1].value}")
intcode.set(arguments[2].address, arguments[0].value * arguments[1].value)
return self.new_pc(intcode)
class InputInstruction(Instruction):
code, argument_num = 3, 1
def execute(self, intcode, arguments):
intcode.set(arguments[0].address, intcode.get_input())
return self.new_pc(intcode)
class OutputInstruction(Instruction):
code, argument_num = 4, 1
def execute(self, intcode, arguments):
intcode.output = arguments[0].value
return self.new_pc(intcode)
class JumpIfTrueInstruction(Instruction):
code, argument_num = 5, 2
def execute(self, intcode, arguments):
logging.debug(f"JMPT {arguments[0].value} {arguments[1].value}")
return arguments[1].value if arguments[0].value != 0 else self.new_pc(intcode)
class JumpIfFalseInstruction(Instruction):
code, argument_num = 6, 2
def execute(self, intcode, arguments):
logging.debug(f"JMPF {arguments[0].value} {arguments[1].value}")
return arguments[1].value if arguments[0].value == 0 else self.new_pc(intcode)
class LessThanInstruction(Instruction):
code, argument_num = 7, 3
def execute(self, intcode, arguments):
logging.debug(f"LT {arguments[0].value} {arguments[1].value}")
intcode.set(arguments[2].address, int(arguments[0].value < arguments[1].value))
return self.new_pc(intcode)
class EqualsInstruction(Instruction):
code, argument_num = 8, 3
def execute(self, intcode, arguments):
intcode.set(arguments[2].address, int(arguments[0].value == arguments[1].value))
return self.new_pc(intcode)
class RelativeBaseOffsetInstruction(Instruction):
code, argument_num = 9, 1
def execute(self, intcode, arguments):
intcode.relative_base += arguments[0].value
return self.new_pc(intcode)
class HaltInstruction(Instruction):
code, argument_num = 99, 0
def execute(self, intcode, arguments):
intcode.halted = True
return None
class Argument:
def __init__(self, value, address):
self.value = value
self.address = address
class IntCode:
def __init__(self, program, inputs=[], input_func=None):
self.program = program[:]
self.inputs = inputs[::-1]
self.input_func = input_func
self.relative_base = 0
self.memory = {}
self.output = None
self.halted = False
self.pc = 0
def _get_instruction(self, instruction_code):
return next(
cls for cls in Instruction.__subclasses__() if cls.code == instruction_code
)
def _parse_arguments(self, argument_num):
modes = str(self.program[self.pc]).zfill(5)[:3][::-1]
arguments = []
for i in range(argument_num):
value = self.program[self.pc + i + 1] + (
self.relative_base if modes[i] == "2" else 0
)
if modes[i] == "1":
arguments.append(Argument(value, value))
else:
arguments.append(
Argument(
self.program[value]
if value < len(self.program)
else self.memory.get(value, 0),
value,
)
)
return arguments
def run(self):
self.output = None
while not self.halted and self.output is None:
instruction = self._get_instruction(self.program[self.pc] % 100)
arguments = self._parse_arguments(instruction.argument_num)
self.pc = instruction().execute(self, arguments)
return self.output
def execute(self):
last_output = None
while not self.halted:
output = self.run()
if not self.halted:
last_output = output
return last_output
def clone(self):
cloned = IntCode(self.program)
cloned.inputs = self.inputs[:]
cloned.input_func = self.input_func
cloned.relative_base = self.relative_base
cloned.memory = {key: value for key, value in self.memory.items()}
cloned.output = self.output
cloned.halted = self.halted
cloned.pc = self.pc
return cloned
def get(self, address):
return (
self.program[address]
if address < len(self.program)
else self.memory.get(value + self.relative_base, 0)
)
def set(self, address, value):
target = self.program if address < len(self.program) else self.memory
target[address] = value
def input(self, value):
self.inputs = [value] + self.inputs
def get_input(self):
return self.inputs.pop() if self.inputs else self.input_func()
import os
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
code = list(map(int, open(os.path.join(SCRIPT_DIR, "input.txt")).read().split(",")))
part2 = IntCode(code, [5]).execute()
print(part2)

View File

@@ -0,0 +1,46 @@
from collections import defaultdict
def path(graph, start, goal):
path = []
node = start
while node != goal:
node = graph[node]
path.append(node)
return path
def find_first_common(x, y):
"returns the indices i and j of the first common element of x and y"
for i, xx in enumerate(x):
for j, yy in enumerate(y):
if xx == yy:
return i, j
def main(inp):
graph = {}
for l in inp:
left, right = l.rstrip().split(")")
graph[right] = left
total = 0
for node, child in graph.items():
cnt = 1
while child != "COM":
child = graph[child]
cnt += 1
total += cnt
print("Part 1: ", total)
p1 = path(graph, "YOU", "COM")
p2 = path(graph, "SAN", "COM")
a, b = find_first_common(p1, p2)
total_path = p1[:a] + p2[b::-1] # p1 + p2 - p1 n p2
print("Part 2: ", len(total_path) - 1)
if __name__ == "__main__":
import fileinput
main(fileinput.input())

View File

@@ -0,0 +1,93 @@
import sys
from pathlib import Path
sys.path.append(str(Path(__file__).absolute().parent.parent / "intcode"))
from itertools import cycle, permutations
from intcode import interpret_intcode, Interpreter
def main(inp):
mem = list(map(int, inp.readline().rstrip().split(",")))
max_ret = 0
for seq in permutations([0, 1, 2, 3, 4], 5):
ret = amplifiers(mem, list(seq))
max_ret = max(ret.stdout[0], max_ret)
print("Part 1: ", max_ret)
max_ret = 0
for seq in permutations((5, 6, 7, 8, 9)):
ret = part2(mem, list(seq))
max_ret = max(max_ret, ret)
print("Part 2: ", max_ret)
def amplifiers(program, sequence):
ret = interpret_intcode(program[::], [sequence.pop(0), 0])
ret = interpret_intcode(program[::], [sequence.pop(0), ret.stdout.pop()])
ret = interpret_intcode(program[::], [sequence.pop(0), ret.stdout.pop()])
ret = interpret_intcode(program[::], [sequence.pop(0), ret.stdout.pop()])
ret = interpret_intcode(program[::], [sequence.pop(0), ret.stdout.pop()])
return ret
def part2(program, sequence):
amplifiers = [Interpreter(program[::], [sequence.pop(0)]) for _ in range(5)]
it = cycle(enumerate(amplifiers))
id_, amp = next(it)
inp = 0
max_signal = 0
while True:
max_signal = max(max_signal, inp)
amp.stdin.append(inp)
amp.interpret()
out = amp.stdout
if amp.halted:
break
next_id, next_amp = next(it)
inp = out.pop(0)
amp = next_amp
id_= next_id
return max_signal
def tests():
program = [3, 15, 3, 16, 1002, 16, 10, 16, 1, 16, 15, 15, 4, 15, 99, 0, 0]
sequence = [4, 3, 2, 1, 0]
res = amplifiers(program, sequence)
assert res.stdout == [43210]
program = [3,23,3,24,1002,24,10,24,1002,23,-1,23,
101,5,23,23,1,24,23,23,4,23,99,0,0]
sequence = [0,1,2,3,4]
res = amplifiers(program, sequence)
assert res.stdout == [54321]
program = [3,31,3,32,1002,32,10,32,1001,31,-2,31,1007,31,0,33,
1002,33,7,33,1,33,31,31,1,32,31,31,4,31,99,0,0,0]
sequence = [1,0,4,3,2]
res = amplifiers(program, sequence)
assert res.stdout == [65210]
def tests2():
program = [3,26,1001,26,-4,26,3,27,1002,27,2,27,1,27,26,
27,4,27,1001,28,-1,28,1005,28,6,99,0,0,5]
sequence = [9,8,7,6,5]
assert part2(program, sequence) == 139629729
program = [3,52,1001,52,-5,52,3,53,1,52,56,54,1007,54,5,55,1005,55,26,1001,54,
-5,54,1105,1,12,1,53,54,53,1008,54,0,55,1001,55,1,55,2,53,55,53,4,
53,1001,56,-1,56,1005,56,6,99,0,0,0,0,10]
sequence = [9,7,8,5,6]
assert part2(program, sequence) == 18216
if __name__ == "__main__":
import fileinput
tests()
tests2()
main(fileinput.input())

View File

@@ -0,0 +1,39 @@
from itertools import zip_longest
def grouper(n, iterable):
"grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
args = [iter(iterable)] * n
return zip_longest(*args)
def main(inp):
data = inp.readline().rstrip()
width, height = 25, 6
layers = [x for x in grouper(width*height, data)]
layer_i, _ = min(((i, x.count("0")) for i, x in enumerate(layers)), key=lambda x: x[1])
ones = layers[layer_i].count("1")
twos = layers[layer_i].count("2")
print("Part 1: ", ones * twos)
image = ["2"] * width * height
layers = [x for x in grouper(width*height, data)]
layer_i, _ = min(((i, x.count("0")) for i, x in enumerate(layers)), key=lambda x: x[1])
for l in layers[::-1]:
# 0 is black, 1 is white, and 2 is transparent.
for i, x in enumerate(l):
if x == "2":
continue
image[i] = x
print("Part 2:")
for i in range(height):
b, w = " ", "\u2588"
im = [b if x == "0" else w for x in image[width*i:width*(i+1)]]
print("".join(im))
if __name__ == "__main__":
import fileinput
main(fileinput.input())

View File

@@ -0,0 +1,20 @@
# TODO replace PYTHONPATH hack with a proper solution, like making intcode an
# installed module https://stackoverflow.com/a/50194143
import sys
from pathlib import Path
sys.path.append(str(Path(__file__).absolute().parent.parent / "intcode"))
from intcode import interpret_intcode
def main(inp):
mem = list(map(int, inp.readline().rstrip().split(",")))
print("Part 1: ", interpret_intcode(mem[::], stdin=[1]).stdout[0])
print("Part 2: ", interpret_intcode(mem[::], stdin=[2]).stdout[0])
if __name__ == "__main__":
import fileinput
main(fileinput.input())

View File

@@ -0,0 +1,23 @@
# https://www.reddit.com/r/adventofcode/comments/egq9xn/2019_day_9_intcode_benchmarking_suite/
from intcode import interpret_intcode
def isqrt(n):
program = [3,1,109,149,21101,0,15,0,20101,0,1,1,1105,1,18,204,1,99,22101,0,1,2,22101,0,1,1,21101,0,43,3,22101,0,1,4,22101,0,2,5,109,3,1105,1,78,109,-3,22102,-1,1,1,22201,1,4,3,22102,-1,1,1,1208,3,0,62,2105,-1,0,1208,3,1,69,2105,-1,0,22101,0,4,1,1105,1,26,1207,1,1,83,2105,-1,0,21101,0,102,3,22101,0,2,4,22101,0,1,5,109,3,1105,1,115,109,-3,22201,1,4,1,21101,0,2,2,1105,1,115,2102,-1,2,140,2101,0,2,133,22101,0,1,2,20001,133,140,1,1207,2,-1,136,2105,-1,0,21201,2,-1,2,22101,1,1,1,1105,1,131]
res = interpret_intcode(program, stdin=[n])
return res.stdout.pop()
def sum_of_primes(n):
program = [3,100,1007,100,2,7,1105,-1,87,1007,100,1,14,1105,-1,27,101,-2,100,100,101,1,101,101,1105,1,9,101,105,101,105,101,2,104,104,101,1,102,102,1,102,102,103,101,1,103,103,7,102,101,52,1106,-1,87,101,105,102,59,1005,-1,65,1,103,104,104,101,105,102,83,1,103,83,83,7,83,105,78,1106,-1,35,1101,0,1,-1,1105,1,69,4,104,99]
res = interpret_intcode(program, stdin=[n])
return res.stdout.pop()
if __name__ == '__main__':
#import timeit
#print(timeit.timeit("test()", globals=locals()))
print(isqrt(111))
print(sum_of_primes(10))

View File

@@ -0,0 +1,194 @@
#! /usr/bin/env python3
from collections import defaultdict
from enum import Enum
import logging
import os
loglevel = (os.getenv("LOGLEVEL") or "WARN").upper()
logging.basicConfig(format="%(levelname)s:%(message)s", level=loglevel)
def get_nth_digit(n, number):
"Returns the nth digit of the input number"
return number // 10 ** n % 10
class Operation(Enum):
ADDITION = 1
MULTIPLICATION = 2
INPUT = 3
OUTPUT = 4
JMP_IF_TRUE = 5
JMP_IF_FALSE = 6
LESS_THAN = 7
EQUALS = 8
BASE = 9
TERMINATION = 99
class Mode(Enum):
POSITION = 0
IMMEDIATE = 1
RELATIVE = 2
class Instruction:
def __init__(self, memory, ip, base):
self.ip = ip
self.memory = memory
self.opcode = memory[ip]
# A B C D E
# 0 1 1 0 3
# A B C modes, DE opcode
self.operation = Operation(self.opcode % 100)
self.modes = [Mode(get_nth_digit(n, self.opcode)) for n in range(2, 5)]
self.handler_name = f"handle_{self.operation.name.lower()}"
self.handler = getattr(self, self.handler_name, self.handle_unknown)
self.input = None
self.output = None
self.halted = False
self.base = base
def __repr__(self):
return f"[{self.opcode}] Instruction({self.operation}, {self.modes})"
def handle(self):
return self.handler()
def handle_addition(self):
first, second = self._get_parameters()
logging.debug(f"ADD {first} {second}")
result = first + second
address = self._get_write_addr(3)
logging.debug(f"{address}")
self._write(address, result)
self.ip += 4
return self.ip
def handle_multiplication(self):
first, second = self._get_parameters()
logging.debug(f"MUL {first} {second}")
result = first * second
address = self._get_write_addr(3)
self._write(address, result)
self.ip += 4
return self.ip
def handle_input(self):
address = self._get_write_addr(1)
self._write(address, self.input)
logging.debug(f"INP {address} {self.input}")
self.ip += 2
return self.ip
def handle_output(self):
self.output = self._get_value(1)
logging.debug(f"OUT {self.output}")
self.ip += 2
return self.ip
def handle_jmp_if_true(self):
first, second = self._get_parameters()
logging.debug(f"JMPT {first} {second} {first != 0}")
return second if first != 0 else self.ip + 3
def handle_jmp_if_false(self):
first, second = self._get_parameters()
logging.debug(f"JMPF {first} {second}")
return second if first == 0 else self.ip + 3
def handle_less_than(self):
first, second = self._get_parameters()
logging.debug(f"LT {first} {second} {first < second}")
address = self._get_write_addr(3)
self._write(address, int(first < second))
self.ip += 4
return self.ip
def handle_equals(self):
first, second = self._get_parameters()
logging.debug(f"EQ {first} {second}")
address = self._get_write_addr(3)
self._write(address, int(first == second))
self.ip += 4
return self.ip
def handle_termination(self):
logging.debug("HALT")
self.halted = True
return self.ip
def handle_base(self):
self.base += self._get_value(1)
logging.debug(f"BASE {self.base}")
self.ip += 2
return self.ip
def handle_unknown(self):
raise ValueError(f"Unknown operation <{self.operation}> @ [{self.ip}]")
def _get_value(self, offset):
value = self.memory[self.ip + offset]
match self.modes[offset - 1]:
case Mode.POSITION: return self.memory[value] if value < len(self.memory) else 0
case Mode.IMMEDIATE: return value
case Mode.RELATIVE: return self.memory[value + self.base]
case _: raise ValueError(f"{self.modes[i]}")
def _get_write_addr(self, offset):
value = self.memory[self.ip + offset]
match self.modes[offset - 1]:
case Mode.POSITION: return value
case Mode.RELATIVE: return value + self.base
case _: raise ValueError(f"{self.modes[i]}")
def _get_parameters(self):
first = self._get_value(1)
second = self._get_value(2)
return first, second
def _write(self, address, value):
while address >= len(self.memory):
self.memory += [0] * len(self.memory)
self.memory[address] = value
class Interpreter:
def __init__(self, program, stdin=[]):
self.ip = 0
self.stdin = stdin
self.stdout = []
self.memory = program[::]
self.halted = False
self.base = 0
def __repr__(self):
return f"Interpreter(ip={self.ip}, stdin={self.stdin}, stdout={self.stdout}, halted={self.halted})"
def interpret(self, break_on_input=False, break_on_output=True):
while not self.halted:
# fetch next instruction
instruction = Instruction(self.memory, self.ip, self.base)
# pause if INP + break on input
if instruction.operation == Operation.INPUT:
if break_on_input and self.stdin == []:
break
else:
instruction.input = self.stdin.pop(0)
# execute instruction
self.ip = instruction.handle()
self.base = instruction.base
logging.debug(f"IP {self.ip}")
if instruction.output is not None:
self.stdout.append(instruction.output)
if break_on_output:
break
if instruction.halted:
self.halted = True
def interpret_intcode(program, stdin=[]):
interpreter = Interpreter(program, stdin)
interpreter.interpret(break_on_output=False)
return interpreter

View File

@@ -0,0 +1,111 @@
# Run with pytest
import logging
from intcode import *
def run_test(program, expected_mem=None, stdin=[], expected_out=None):
mem = program[::]
out = interpret_intcode(mem, stdin=stdin)
if expected_mem is not None:
assert expected_mem == out.memory
if expected_out is not None:
assert expected_out == out.stdout
def test_day2():
tests = [
[[1,0,0,0,99], [2,0,0,0,99]], # ADD 1 + 1 to location 0
[[2,3,0,3,99], [2,3,0,6,99]], # MUL 2 * 3 to location 3
[[2,4,4,5,99,0], [2,4,4,5,99,9801]], # MUL 99 * 99 to location 5 (9801)
[[1,1,1,4,99,5,6,0,99], [30,1,1,4,2,5,6,0,99]],
]
for program, expected in tests:
run_test(program, expected)
def test_day5_basic():
# Using position mode, consider whether the input is equal to 8; output 1
# (if it is) or 0 (if it is not).
prog = [3,9,8,9,10,9,4,9,99,-1,8]
run_test(prog, stdin=[0], expected_out=[0])
run_test(prog, stdin=[8], expected_out=[1])
# Using position mode, consider whether the input is less than 8; output 1
# (if it is) or 0 (if it is not).
prog = [3,9,7,9,10,9,4,9,99,-1,8]
run_test(prog, stdin=[7], expected_out=[1])
run_test(prog, stdin=[9], expected_out=[0])
# Using immediate mode, consider whether the input is equal to 8; output 1
# (if it is) or 0 (if it is not).
prog = [3,3,1108,-1,8,3,4,3,99]
run_test(prog, stdin=[8], expected_out=[1])
run_test(prog, stdin=[9], expected_out=[0])
# Using immediate mode, consider whether the input is less than 8; output 1
# (if it is) or 0 (if it is not).
prog = [3,3,1107,-1,8,3,4,3,99]
run_test(prog, stdin=[7], expected_out=[1])
run_test(prog, stdin=[9], expected_out=[0])
# Here are some jump tests that take an input, then output 0 if the input
# was zero or 1 if the input was non-zero:
prog = [3,12,6,12,15,1,13,14,13,4,13,99,-1,0,1,9] # using position mode
run_test(prog, stdin=[0], expected_out=[0])
run_test(prog, stdin=[4], expected_out=[1])
prog = [3,12,6,12,15,1,13,14,13,4,13,99,-1,0,1,9] # using immediate mode
run_test(prog, stdin=[0], expected_out=[0])
run_test(prog, stdin=[4], expected_out=[1])
def test_day5_larger():
"""
The above example program uses an input instruction to ask for a single
number. The program will then output 999 if the input value is below 8,
output 1000 if the input value is equal to 8, or output 1001 if the input
value is greater than 8.
"""
prog = [3,21,1008,21,8,20,1005,20,22,107,8,21,20,1006,20,31,
1106,0,36,98,0,0,1002,21,125,20,4,20,1105,1,46,104,
999,1105,1,46,1101,1000,1,20,4,20,1105,1,46,98,99]
run_test(prog, stdin=[7], expected_out=[999])
run_test(prog, stdin=[8], expected_out=[1000])
run_test(prog, stdin=[9], expected_out=[1001])
def test_day9_base():
program = [109, 19, 109, 6, 99]
interpreter = Interpreter(program)
interpreter.base = 2000
interpreter.interpret(break_on_output=False)
assert interpreter.base == 2025
def test_day9_relative():
program = [109,6,21001,9,25,1,104,0,99,49]
ret = interpret_intcode(program)
assert ret.stdout == [74]
def test_day9_quine():
quine = [109,1,204,-1,1001,100,1,100,1008,100,16,101,1006,101,0,99]
run_test(quine, expected_out=quine)
def test_day9_relative():
run_test([109, -1, 4, 1, 99], expected_out=[-1])
run_test([109, -1, 104, 1, 99], expected_out=[1])
run_test([109, -1, 204, 1, 99], expected_out=[109])
run_test([109, 1, 9, 2, 204, -6, 99], expected_out=[204])
run_test([109, 1, 109, 9, 204, -6, 99], expected_out=[204])
run_test([109, 1, 209, -1, 204, -106, 99], expected_out=[204])
run_test([109, 1, 3, 3, 204, 2, 99], stdin=[69], expected_out=[69])
run_test([109, 1, 203, 2, 204, 2, 99], stdin=[1337], expected_out=[1337])
def test_fac():
# factorial test
fac = [3,29,1007,29,2,28,1005,28,24,2,27,29,27,1001,29,-1,29,1101,0,0,28,1006,28,2,4,27,99,1,0,0]
res = interpret_intcode(fac, [4])
assert res.stdout == [24]

View File

@@ -0,0 +1,24 @@
#! /usr/bin/env python3
from itertools import product
def part1(inp):
inp = [int(x) for x in inp]
result_pairs = [x for x in list(product(inp, inp)) if sum(x) == 2020]
print(result_pairs)
print(result_pairs[0][0] * result_pairs[0][1])
def part2(inp):
inp = [int(x) for x in inp]
result_pairs = [x for x in list(product(inp, repeat=3)) if sum(x) == 2020]
print(result_pairs)
print(result_pairs[0][0] * result_pairs[0][1] * result_pairs[0][2])
if __name__ == "__main__":
import fileinput
lines = [x for x in fileinput.input()]
part1(lines)
part2(lines)

View File

@@ -0,0 +1,30 @@
#!/usr/bin/env python3
from collections import Counter
def part1(adapters):
counts = Counter()
# 1 for the socket, of 3 for the device
for current, next in zip([0] + adapters, adapters + [3]):
counts[next - current] += 1
return counts[1] * counts[3]
def part2(adapters):
counts = Counter({0: 1})
for jolt in adapters:
s = counts[jolt - 1] + counts[jolt - 2] + counts[jolt - 3]
counts[jolt] = s
return max(counts.values())
def main(f):
adapters = sorted(int(l.rstrip()) for l in f)
print(part1(adapters))
print(part2(adapters))
if __name__ == "__main__":
import fileinput
main(fileinput.input())

View File

@@ -0,0 +1,34 @@
#!/usr/bin/env python3
from rules import part1_rules, part2_rules
def main(grid, rules):
generation = 0
while True:
changes, next_grid = step(grid, rules)
generation += 1
grid = next_grid
assert generation < 1000
if generation % 10 == 0:
print(f"Generation {generation}, changes: {changes}")
if changes == 0:
return next_grid.count("#")
def step(grid, rules):
changes = 0
next_grid = grid[:]
for index, cell in enumerate(grid):
try:
changes += rules[cell](index, grid, next_grid)
except KeyError:
pass
return changes, next_grid
if __name__ == "__main__":
import sys
with open(sys.argv[1]) as infile:
grid = list("".join(infile.read().splitlines()))
print("Part 1 ", main(grid, rules=part1_rules))
print("Part 2 ", main(grid, rules=part2_rules))

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env python3
class Grid:
def __init__(self, inp):
lines = inp.read().splitlines()
self.cells = list("".join(lines))
self.height = len(lines)
self.width = len(lines[0])
def __getitem__(self, key):
x, y = key
return cells[y * self.width + x]
def __setitem__(self, key, value):
x, y = key
cells[y * self.width + x] = value
def __iter__(self):
return self.cells.__iter__()
def __str__(self):
"\n".join(
"".join(
grid[pos : pos + grid_width]
for pos in range(0, self.width, self.height)
)
)

View File

@@ -0,0 +1,117 @@
#!/usr/bin/env python3
from itertools import count, takewhile
directions = (
(-1, -1),
(-1, 0),
(-1, 1),
(0, -1),
(0, 1),
(1, -1),
(1, 0),
(1, 1),
)
grid_width = 98
grid_height = 97
def handle_empty(index, grid, next_grid):
"""
If a seat is empty (L) and there are no occupied seats adjacent to it, the seat becomes occupied.
"""
neighbors = count_neighbors(index, grid)
if neighbors == 0:
next_grid[index] = "#"
return 1
return 0
def handle_occupied(index, grid, next_grid):
"""
If a seat is occupied (#) and four or more seats adjacent to it are also occupied, the seat becomes empty.
"""
neighbors = count_neighbors(index, grid)
if neighbors >= 4:
next_grid[index] = "L"
return 1
return 0
def count_neighbors(pos, grid):
neighbors = 0
x = pos % grid_width
y = pos // grid_width
for (dx, dy) in directions:
xx = x + dx
yy = y + dy
if not in_bounds((xx, yy)):
continue
if grid[yy * grid_width + xx] == "#":
neighbors += 1
return neighbors
def handle_empty_2(index, grid, next_grid):
"""
If a seat is empty and there are no occupied seat visible in neither direction,
the seat becomes occupied
"""
neighbors = 0
x = index % grid_width
y = index // grid_width
for direction in directions:
# keep moving in the specified direction, while checking
# that we are in bounds of the grid
for xx, yy in takewhile(in_bounds, move(x, y, direction)):
cell = grid[yy * grid_width + xx]
if cell == "#":
neighbors += 1
elif cell == "L":
break # No occupied seat in that direction, we can break
if neighbors == 0:
next_grid[index] = "#"
return True
return False
def handle_occupied_2(index, grid, next_grid):
"""
An occupied seat becomes empty if there are five or more visible occupied
seats in either direction.
"""
occupied = 0
x = index % grid_width
y = index // grid_width
for direction in directions:
for xx, yy in takewhile(in_bounds, move(x, y, direction)):
cell = grid[yy * grid_width + xx]
if cell == "#":
occupied += 1
elif cell != ".":
break
if occupied >= 5:
next_grid[index] = "L"
return True
return False
def in_bounds(pos):
x, y = pos
return 0 <= x < grid_width and 0 <= y < grid_height
def move(x, y, direction):
xx = x
yy = y
while True:
yield xx, yy
xx += direction[0]
yy += direction[1]
part1_rules = {"L": handle_empty, "#": handle_occupied}
part2_rules = {"L": handle_empty_2, "#": handle_occupied_2}

View File

@@ -0,0 +1,58 @@
#!/usr/bin/env python3
from collections import defaultdict
def part1(infile):
memory = defaultdict(int)
for line in infile:
left, right = line.split("=")
if left.startswith("mask"):
one_mask = right.translate(right.maketrans("X", "0"))
zero_mask = right.translate(right.maketrans("X10", "001"))
else:
address = int(left.split("[")[1].rstrip("] ")) # mem[42] -> 42
value = int(right.rstrip())
memory[address] = value & ~int(zero_mask, 2) | int(one_mask, 2)
return sum(memory.values())
def part2(infile):
memory = defaultdict(int)
for line in infile:
left, right = line.split(" = ")
if left.startswith("mask"):
mask = right.rstrip()
else:
value = right.rstrip()
address = apply_mask(left.split("[")[1].rstrip("] "), mask)
for addr in generate_floating_addresses(address):
memory[int(addr, 2)] = int(value)
return sum(memory.values())
def apply_mask(address, mask):
address = bin(int(address)).lstrip("0b")
address = address.zfill(36)
for index, bit in enumerate(mask):
if bit == "1":
address = address[:index] + "1" + address[index + 1 :]
elif bit == "X":
address = address[:index] + "X" + address[index + 1 :]
return address
def generate_floating_addresses(address):
index = address.find("X")
if index == -1:
return [address]
a1 = generate_floating_addresses(address[:index] + "0" + address[index + 1 :])
a2 = generate_floating_addresses(address[:index] + "1" + address[index + 1 :])
return a1 + a2
if __name__ == "__main__":
import fileinput
lines = [x for x in fileinput.input()]
print(part1(lines))
print(part2(lines))

View File

@@ -0,0 +1,58 @@
#!/usr/bin/env python3
from collections import defaultdict
from array import array
def main(initial_suite, max_iteration):
iteration = 1
seen = defaultdict(int)
# init
for number in initial_suite:
seen[number] = iteration
iteration += 1
current = 0
while iteration < max_iteration:
last_seen = seen[current]
if last_seen == 0:
seen[current] = iteration
current = 0 # next
else:
seen[current] = iteration
current = iteration - last_seen # next
iteration += 1
return current
def main_array(initial_suite, max_iteration):
iteration = 1
seen = array('I', [0] * max_iteration)
# init
for number in initial_suite:
seen[number] = iteration
iteration += 1
current = 0
while iteration < max_iteration:
last_seen = seen[current]
if last_seen == 0:
seen[current] = iteration
current = 0 # next
else:
seen[current] = iteration
current = iteration - last_seen # next
iteration += 1
return current
if __name__ == "__main__":
inp = [6, 3, 15, 13, 1, 0]
# 423 µs ± 53.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
print(main(inp, 2020))
# 13.6 s ± 2.89 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
#print(main(inp, 30000000))
print(main_array(inp, 30000000))

View File

@@ -0,0 +1,108 @@
# /usr/bin/env python3
import re
from collections import defaultdict
"""
identify invalid nearby tickets by considering only whether tickets contain values that are not valid for any field.
"""
def main(content):
rules, my_ticket, other_tickets = content.split("\n\n")
rules = parse_fields(rules)
my_ticket = my_ticket.splitlines()[1]
other_tickets = other_tickets.splitlines()[1:]
print("Ticket scanning error rate ", part1(other_tickets, rules))
part2(my_ticket, other_tickets, rules)
def parse_fields(fields):
fields_dict = {}
for field in fields.splitlines():
k, v = field.split(": ")
ranges = re.findall(r"(\d+)-(\d+)", v)
fields_dict[k] = [range(int(r[0]), int(r[1]) + 1) for r in ranges]
return fields_dict
def part1(tickets, rules):
scanning_error_rate = 0
for ticket in tickets:
scanning_error_rate += sum(validate_ticket(ticket, rules))
return scanning_error_rate
def validate_ticket(ticket, rules):
invalid_fields = []
for value in ticket.split(","):
value = int(value)
if not validate_field(value, *rules.values()):
invalid_fields.append(value)
return invalid_fields
def validate_field(field, *rules):
validations = (any(field in r for r in rule) for rule in rules)
return any(validations)
def part2(my_ticket, other_tickets, rules):
# filter only valid tickets
valid_tickets = [ticket for ticket in other_tickets if validate_ticket(ticket, rules) == []]
valid_tickets.append(my_ticket) # my ticket is valid
# possible field for each index of a ticket
candidates = defaultdict(set)
for index in range(len(rules)):
def inner():
for rule_name, constraints in rules.items():
for ticket in valid_tickets:
field_value = int(ticket.split(",")[index])
if not validate_field(field_value, constraints):
return
candidates[index].add(rule_name)
inner()
sorted_candidates = sort_candidates(candidates)
fields_indexes = {}
try:
while len(fields_indexes) != len(rules):
index, found = sorted_candidates.popitem()
found = next(iter(found))
fields_indexes[index] = found
sorted_candidates = remove_item(sorted_candidates, found)
except:
pass
fields_indexes = {k: v for k,v in fields_indexes.items() if v.startswith('departure')}
total = 1
my_ticket = my_ticket.split(',')
for index in fields_indexes:
total *= int(my_ticket[index])
a = 1
def sort_candidates(c):
return {x: c[x] for x in sorted(c, key=lambda k: len(c[k]), reverse=True)}
def remove_item(candidates, item):
ret = {}
for key, value in candidates.items():
try:
value.remove(item)
except ValueError:
pass
ret[key] = value
#candidates = {k: set(v - item) for k,v in candidates.items()}
return ret
if __name__ == "__main__":
import sys
with open(sys.argv[1]) as f:
main(f.read())

View File

@@ -0,0 +1,14 @@
import part1
import part2
def main(lines):
part1.main(lines)
part2.main(lines)
if __name__ == "__main__":
import fileinput
lines = [x for x in fileinput.input()]
main(lines)

View File

@@ -0,0 +1,24 @@
#! /usr/bin/env python3
def parse_line(line):
repeat_range, letter, pwd = line.split(' ')
letter = letter[0]
repeat_min, repeat_max = repeat_range.split('-')
repeat_min, repeat_max = int(repeat_min), int(repeat_max)
return letter, range(repeat_min, repeat_max + 1), pwd
def test_password(line):
letter, repeat_range, pwd = parse_line(line)
count = pwd.count(letter)
return count in repeat_range
def main(passwds):
valid_counter = 0
for l in passwds:
if test_password(l):
valid_counter += 1
print(f"Number of valid password in input : {valid_counter}")

View File

@@ -0,0 +1,28 @@
#! /usr/bin/env python3
def xor(a, b):
return (a and not b) or (not a and b)
def parse_line(line):
repeat_range, letter, pwd = line.split(' ')
letter = letter[0]
first_pos, second_pos = repeat_range.split('-')
first_pos, second_pos = int(first_pos), int(second_pos)
return letter, first_pos, second_pos, pwd
def test_password(line):
letter, first_pos, second_pos, pwd = parse_line(line)
return xor(pwd[first_pos - 1] == letter,
pwd[second_pos - 1] == letter)
def main(passwds):
valid_counter = 0
for l in passwds:
if test_password(l):
valid_counter += 1
print(f"Number of valid password in input : {valid_counter}")

View File

@@ -0,0 +1,42 @@
def check_slope_for_trees(plan, x_right=3, y_down=1):
x_max = len(plan[0])
y_max = len(plan)
x, y = 0, 0
tree_count = 0
while y < y_max - 1:
x += x_right
x %= x_max # wrap x
y += y_down
pos = plan[y][x]
if pos == '#':
tree_count += 1
return tree_count
def part1(plan):
tree_count = check_slope_for_trees(plan)
print(f"number of trees : {tree_count}")
def part2(plan):
slopes = [{"x": 1, "y": 1}, {"x": 3, "y": 1}, {"x": 5, "y": 1}, {"x": 7, "y": 1}, {"x": 1, "y": 2}]
tree_product = 1
for slope in slopes:
tree_count = check_slope_for_trees(plan, slope["x"], slope["y"])
tree_product *= tree_count
print(f"slope {slope} number of trees : {tree_count}")
print(f"Cumulative product of tress : {tree_product}")
def main(f):
lines = [line.rstrip() for line in f]
part1(lines)
part2(lines)
if __name__ == "__main__":
import fileinput
main(fileinput.input())

View File

@@ -0,0 +1,52 @@
#! /usr/bin/env python3
import re
def main(inp):
inp = open(inp).read().rstrip().split('\n\n')
valid_passeports_1, valid_passeports_2 = 0, 0
for l in inp:
l = re.split(r'[\n\s]', l)
passeport = dict(p.split(':') for p in l)
if check_passeport(passeport):
valid_passeports_1 += 1
if check_passeport(passeport, run_validators=True):
valid_passeports_2 += 1
print("Part 1: valid passeports: ", valid_passeports_1)
print("Part 2: valid passeports: ", valid_passeports_2)
def check_passeport(passeport, run_validators=False):
fields = [
('byr', lambda v: 1920 <= int(v) <= 2002), # (Birth Year)
('iyr', lambda v: 2010 <= int(v) <= 2020), # (Issue Year)
('eyr', lambda v: 2020 <= int(v) <= 2030), # (Expiration Year)
('hgt', validate_height), # (Height)
('hcl', lambda v: re.search("#[0-9a-f]{6}", v)), # (Hair Color)
('ecl', lambda v: v in ('amb', 'blu', 'brn', 'gry', 'grn', 'hzl', 'oth')), # (Eye Color)
('pid', lambda v: len(v) == 9 and v.isdecimal()), # (Passport ID)
#'cid' # Country id, ignored
]
for field, validator in fields:
value = passeport.get(field)
if value is None:
return False
elif run_validators and not validator(value):
return False
return True
def validate_height(v):
unit = v[-2:]
height = int(v[:-2])
if unit == 'cm':
return 150 <= height <= 193
if unit == 'in':
return 59 <= height <= 76
return False
if __name__ == "__main__":
import sys
main(sys.argv[1])

View File

@@ -0,0 +1,68 @@
#! /usr/bin/env python3
from itertools import product
from bisect import bisect
def main(inp):
results = {}
for line in inp:
boarding_pass = line.rstrip()
row, col = parse_boarding_pass(boarding_pass)
seat_id = get_seat_id(row, col)
results[boarding_pass] = (row, col, seat_id)
part1(results)
part2(results)
def part1(results):
max_seat_id = max(x[2] for x in results.values())
print("Max seat ID: ", max_seat_id)
def part2(results):
seat_ids = sorted(x[2] for x in results.values())
missing_seat_ids = set(range(max(seat_ids))) - set(seat_ids)
print("Your seat id : ", max(missing_seat_ids))
def parse_boarding_pass(boarding_pass, strategy="binary"):
"Poor man's dispatcher"
try:
to_call = globals()[f"parse_boarding_pass_{strategy}"]
return to_call(boarding_pass)
except KeyError:
raise KeyError(f"Bad strategy name {strategy}")
def parse_boarding_pass_binary(boarding_pass):
"Parse boarding pass using a binary conversion"
boarding_pass = boarding_pass.translate(str.maketrans("FLBR", "0011"))
row = boarding_pass[:7]
col = boarding_pass[7:]
return int(row, base=2), int(col, base=2)
def parse_boarding_pass_bisect(boarding_pass):
"Pass boarding pass using bisection algorithm"
row = bisect(boarding_pass[:7], lower_option="F", upper_option="B", max=127)
col = bisect(boarding_pass[7:], lower_option="L", upper_option="R", max=7)
return row, col
def bisect(inp, lower_option, upper_option, max):
min_v, max_v = 0, max
for l in inp:
length = max_v - min_v
if l == lower_option:
max_v = min_v + length // 2
elif l == upper_option:
min_v = 1 + min_v + length // 2
return min_v
def get_seat_id(row, col):
return 8 * row + col
if __name__ == "__main__":
import fileinput
main(fileinput.input())

View File

@@ -0,0 +1,27 @@
#! /usr/bin/env python3
from day5 import *
def tests():
inputs = {
"FBFBBFFRLR": (44, 5, 357),
"BFFFBBFRRR": (70, 7, 567),
"FFFBBBFRRR": (14, 7, 119),
"BBFFBBFRLL": (102, 4, 820)
}
test("bisect", inputs)
test("binary", inputs)
def test(strategy, inputs):
for boarding_pass, expected in inputs.items():
row, col = parse_boarding_pass(boarding_pass, strategy=strategy)
seat_id = get_seat_id(row, col)
assert row == expected[0]
assert col == expected[1]
assert seat_id == expected[2]
print(row, col, seat_id, expected)
if __name__ == "__main__":
tests()

View File

@@ -0,0 +1,30 @@
#! /usr/bin/env python3
from collections import Counter
def part1(groups):
number_of_questions = 0
for group in groups:
unique_questions = set(group.replace("\n", ""))
number_of_questions += len(unique_questions)
print(number_of_questions)
def part2(groups):
# number of questions for which everyone in a group answered 'yes'
number_of_questions = 0
for group in groups:
group_length = group.count("\n") + 1
group_counter = Counter(group.replace("\n", ""))
everyone_answered = [k for (k, v) in group_counter.items() if v == group_length]
number_of_questions += len(everyone_answered)
print(number_of_questions)
if __name__ == "__main__":
import sys
inp = sys.argv[1]
groups = open(inp).read().split("\n\n")
part1(groups)
part2(groups)

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env python3
import re
from collections import defaultdict, deque
def main(input_rules):
rules = parse_rules(input_rules)
reverse_rules = build_reverse_rules(rules)
print(part1(reverse_rules))
print(part2(rules, "shiny gold"))
def parse_rules(input_rules):
rules = {}
for input_rule in input_rules:
color, rule = input_rule.split(" bags contain ")
rules[color] = {color: int(number) for number, color in re.findall(r'(\d+) (\w+ \w+)', rule)}
return rules
def build_reverse_rules(rules):
reverse_rules = defaultdict(list)
for bag, inner_rules in rules.items():
for c in inner_rules:
reverse_rules[c].append(bag)
return reverse_rules
def part1(reverse_rules):
queue = deque(("shiny gold",))
may_contain_shiny_gold = set()
while queue:
color = queue.pop()
for c in reverse_rules.get(color, []):
if c not in may_contain_shiny_gold:
may_contain_shiny_gold.add(c)
queue.appendleft(c)
return len(may_contain_shiny_gold)
def part2(rules, color):
return sum(number + number * part2(rules, c) for c, number in rules[color].items())
if __name__ == "__main__":
import fileinput
main(fileinput.input())

View File

@@ -0,0 +1,55 @@
#! /usr/bin/env python3
def part1(instructions):
instruction_pointer = 0
accumulator = 0
visited_instructions = set()
while instruction_pointer not in visited_instructions: # return before executing any instruction a second time
if instruction_pointer >= len(instructions): # stop the program when ip is out of bounds
break
visited_instructions.add(instruction_pointer)
instruction, argument = instructions[instruction_pointer].split(" ")
if instruction == "acc":
accumulator += int(argument)
instruction_pointer += 1
elif instruction == "jmp":
value = int(argument)
instruction_pointer += value
else:
instruction_pointer += 1
return instruction_pointer, accumulator
def part2(instructions):
for index, line in enumerate(instructions):
permutation = generate_permutation(instructions, line, index)
if permutation is None:
continue
instruction_pointer, accumulator = part1(permutation)
if instruction_pointer == len(permutation):
return accumulator
def generate_permutation(instructions, line, index):
permutation = instructions[:]
instruction, arg = line.split(" ")
if instruction == "acc": # don't replace acc operations
return
elif instruction == "nop":
permutation[index] = f"jmp {arg}"
elif instruction == "jmp":
permutation[index] = f"nop {arg}"
return permutation
def main(inp):
instructions = [line.rstrip() for line in fileinput.input()]
print("Part 1 : (ip, acc) ", part1(instructions)[1])
print("Part 2 : (ip, acc) ", part2(instructions))
if __name__ == "__main__":
import fileinput
main(fileinput.input())

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
import itertools
def part1(lines):
preamble_size = 25
for nums in window(lines, preamble_size + 1):
candidate = nums[-1]
if not test_number(candidate, nums[:-1]):
return candidate
return -1
def window(seq, n):
it = iter(seq)
result = tuple(itertools.islice(it, n))
if len(result) == n:
yield result
for elem in it:
result = result[1:] + (elem,)
yield result
def test_number(num, previous):
sums = set(sum(x) for x in itertools.combinations(previous, 2))
return num in sums
def part2(lines, target: int):
total = 0
visited = []
for index, _ in enumerate(lines):
i = index
while total < target:
total += lines[i]
visited.append(lines[i])
i += 1
if total == target:
return max(visited) + min(visited)
visited.clear()
total = 0
def main(f):
lines = [int(l.rstrip()) for l in f]
invalid_number = part1(lines)
print("part1 ", invalid_number)
print("part2 ", part2(lines, invalid_number))
if __name__ == "__main__":
import fileinput
main(fileinput.input())

View File

@@ -0,0 +1,46 @@
from itertools import islice
def window(seq, n=3):
"Returns a sliding window (of width n) over data from the iterable"
" s -> (s0,s1,...s[n-1]), (s1,s2,...,sn), ... "
it = iter(seq)
result = tuple(islice(it, n))
if len(result) == n:
yield result
for elem in it:
result = result[1:] + (elem,)
yield result
def part1(infile):
with open(infile) as f:
lines = f.readlines()
previous = int(lines[0])
i = 0
for line in lines[1:]:
if int(line) > previous:
i += 1
previous = int(line)
print("Part 1 ", i)
def part2(infile):
with open(infile) as f:
lines = f.readlines()
previous = None
i = 0
for w in window(lines):
measure = sum(int(x) for x in w)
if previous is not None and measure > previous:
i += 1
previous = measure
print("Part 2 ", i)
if __name__ == "__main__":
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
part1(infile)
part2(infile)

View File

@@ -0,0 +1,44 @@
def part1(inp):
horizontal_pos = 0
depth = 0
for line in inp:
command, amount = line.split(" ")
if command == "forward":
horizontal_pos += int(amount)
elif command == "up":
depth -= int(amount)
elif command == "down":
depth += int(amount)
#print(f"horizontal {horizontal_pos}, depth {depth}")
print("Part 1", horizontal_pos * depth)
def part2(inp):
horizontal_pos = 0
depth = 0
aim = 0
for line in inp:
command, amount = line.split(" ")
if command == "forward":
horizontal_pos += int(amount)
# It increases your depth by your aim multiplied by X.
depth += aim * int(amount)
elif command == "up":
aim -= int(amount)
elif command == "down":
aim += int(amount)
#print(f"horizontal {horizontal_pos}, depth {depth}")
print("Part 2", horizontal_pos * depth)
def main(input_file):
with open(input_file) as f:
entries = f.readlines()
part1(entries)
part2(entries)
if __name__ == "__main__":
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
main(infile)

View File

@@ -0,0 +1,69 @@
def calculate_gamma(inp):
gamma_rate = [0] * len(inp[0])
for line in inp:
for index, char in enumerate(line):
gamma_rate[index] += int(char)
gamma_rate = [0 if x < len(inp) // 2 else 1 for x in gamma_rate]
return gamma_rate
def part1(inp):
gamma = calculate_gamma(inp)
epsilon = [0 if x == 1 else 1 for x in gamma]
# power consumption = dec(gamma_rate) * dec(epsilon_rate)
power = int("".join(str(x) for x in gamma), 2) * int("".join(str(x) for x in epsilon), 2)
print("Part 1, power consumption : ", power)
def calculate_most_common(inp, pos):
sum = 0
for line in inp:
sum += int(line[pos])
return 0 if sum < len(inp) // 2 else 1
def filter_oxygen(inp, pos, most_common):
result = []
for line in inp:
if int(line[pos]) == most_common:
result.append(line)
return result
def oxygen_rating(inp):
result = inp[:]
for pos in range(len(inp[0])):
most_common = calculate_most_common(result, pos)
result = filter_oxygen(result, pos, most_common)
if len(result) == 1:
return result
def co2_rating(inp):
result = inp[:]
for pos in range(len(inp[0])):
least_common = 1 - calculate_most_common(result, pos)
result = filter_oxygen(result, pos, least_common)
if len(result) == 1:
return result
def part2(inp):
oxygen = oxygen_rating(inp)
co2 = co2_rating(inp)
res = int("".join(str(x) for x in oxygen), 2) * int("".join(str(x) for x in co2), 2)
print(f"Part 2 : {res}")
def main(input_file):
with open(input_file) as f:
entries = [x.rstrip() for x in f.readlines()]
part1(entries)
part2(entries)
if __name__ == "__main__":
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
main(infile)

View File

@@ -0,0 +1,99 @@
from dataclasses import dataclass
from collections import deque
@dataclass
class BingoItem:
value: int
marked: bool = False
def parse_grid(inp):
raw_grid = [inp.popleft() for _ in range(5)]
grid = [[BingoItem(int(y)) for y in x.rstrip().split(" ") if y != ''] for x in raw_grid]
return grid
def parse_grids(inp):
grids = []
while len(inp) >= 5:
grid = parse_grid(inp)
grids.append(grid)
try:
inp.popleft()
except IndexError:
break
return grids
def check_line_win(grid):
for line in grid:
if all(n.marked for n in line):
return True
return False
def check_column_win(grid):
for col_number in range(len(grid[0])):
column = [line[col_number] for line in grid]
if all(x.marked for x in column):
return True
return False
def calculate_score(grid, final_num):
unmarked = sum([sum([n.value for n in line if not n.marked]) for line in grid])
return final_num * unmarked
def print_green(text, end):
print(f"\033[1;32;40m{text}\033[0;37;40m", end=end)
def print_grid(grid):
for line in grid:
for col in line:
if col.marked:
print_green(f"{str(col.value).ljust(2)}", " ")
else:
print(f"{str(col.value).ljust(2)}", end=" ")
print()
print()
def play_bingo(numbers, grids):
winning_grids = []
for number in numbers:
print(number)
for grid in grids:
for line in grid:
for grid_number in line:
if grid_number.value == number:
grid_number.marked = True
for grid in grids:
win = [check_line_win(grid), check_column_win(grid)]
if any(win):
winning_grids.append((grid, number))
# the grid won, remove it from the game
grids.remove(grid)
first_winning_grid, number = winning_grids[0]
first_score = calculate_score(first_winning_grid, number)
print(f"Part 1, score = {first_score}")
last_winning_grid, number = winning_grids[-1]
last_score = calculate_score(last_winning_grid, number)
print(f"Part 2, score {last_score}")
def main(input_file):
with open(input_file) as f:
inp = deque(f.readlines())
numbers = [int(x) for x in inp.popleft().split(",")]
inp.popleft()
grids = parse_grids(inp)
play_bingo(numbers, grids)
if __name__ == "__main__":
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
main(infile)

View File

@@ -0,0 +1,41 @@
from collections import defaultdict
def main(infile):
points = defaultdict(int)
with open(infile) as f:
for line in f:
start, end = line.split(" -> ")
start_x, start_y = [int(x) for x in start.split(",")]
end_x, end_y = [int(x) for x in end.split(",")]
# column |
if start_x == end_x:
step = 1 if start_y < end_y else -1
for y in range(start_y, end_y + step, step):
points[(start_x, y)] = points[(start_x, y)] + 1
# line -
elif start_y == end_y:
step = 1 if start_x < end_x else -1
for x in range(start_x, end_x + step, step):
points[(x, start_y)] = points[(x, start_y)] + 1
# diagonal \
elif ((start_x < end_x and start_y > end_y)
or (start_x > end_x and start_y < end_y)):
step = 1 if start_y > end_y else -1
for dx in range(0, end_x - start_x + step, step):
points[(start_x + dx, start_y - dx)] = points[(start_x + dx, start_y + dx)] + 1
# diagonal /
elif ((start_x < end_x and start_y < end_y)
or (start_x > end_x and start_y > end_y)):
step = 1 if start_y < end_y else -1
for dx in range(0, end_x - start_x + step, step):
points[(start_x + dx, start_y + dx)] = points[(start_x + dx, start_y + dx)] + 1
res = len([x for x in points.values() if x >= 2])
print(res)
if __name__ == "__main__":
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
main(infile)

View File

@@ -0,0 +1,28 @@
from collections import defaultdict, Counter
def calculate_fishes(inp, days):
fishes = Counter(inp)
for day in range(days):
fishes_new = defaultdict(int)
for fish, cnt in fishes.items():
if fish == 0:
fishes_new[8] += cnt
fishes_new[6] += cnt
else:
fishes_new[fish - 1] += cnt
fishes = fishes_new
return sum(fishes.values())
def main(infile):
with open(infile) as f:
inp = [int(x) for x in f.readline().split(",")]
res = calculate_fishes(inp, 80)
print(f"Part 1, {res}")
res = calculate_fishes(inp, 256)
print(f"Part 2, {res}")
if __name__ == "__main__":
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
main(infile)

View File

@@ -0,0 +1,34 @@
total = 0
numbers = [0, 0, 0, 0, 0, 0, 0, 0, 0]
with open("input.txt") as f:
data = [int(x) for x in f.readline().split(',')]
for i in data:
if i == 0:
numbers[0] += 1
if i == 1:
numbers[1] += 1
if i == 2:
numbers[2] += 1
if i == 3:
numbers[3] += 1
if i == 4:
numbers[4] += 1
if i == 5:
numbers[5] += 1
if i == 6:
numbers[6] += 1
if i == 7:
numbers[7] += 1
if i == 8:
numbers[8] += 1
def rotate(l):
return l[1:] + l[:1]
for j in range(256):
numbers = rotate(numbers)
numbers[6] += numbers[8]
print(f'DAY {j+1} AMOUNT OF FISH: {sum(numbers)}')

View File

@@ -0,0 +1,35 @@
from collections import OrderedDict
def part1(crabs):
moves = OrderedDict()
for pos in range(min(crabs), max(crabs) + 1):
# calculate total fuel required to move to pos:
for crab in crabs:
fuel_cost = abs(pos - crab)
moves[pos] = moves.get(pos, 0) + fuel_cost
min_move = min(moves, key=moves.get)
print(f"Part 1, min move {min_move}, cost {moves[min_move]}")
def part2(crabs):
moves = OrderedDict()
for pos in range(min(crabs), max(crabs) + 1):
# calculate total fuel required to move to pos:
for crab in crabs:
dx = abs(pos - crab)
# S = (n+1)(u0 + un)/2
fuel_cost = dx * (dx+1)//2
moves[pos] = moves.get(pos, 0) + fuel_cost
min_move = min(moves, key=moves.get)
print(f"Part 1, min move {min_move}, cost {moves[min_move]}")
if __name__ == "__main__":
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
with open(infile) as f:
crabs = [int(x) for x in f.readline().split(",")]
part1(crabs)
part2(crabs)

View File

@@ -0,0 +1,48 @@
def part1(inp):
total = 0
for display in inp:
_, output_values = display
for value in output_values:
if len(value) in [2, 4, 3, 7]:
total += 1
print(f"Part 1 : {total}")
def part2(inp):
for display in inp:
patterns, values = display
patterns = sorted(patterns, key=lambda x: len(x))
print(patterns)
# easy
d1 = [x for x in patterns if len(x) == 2][0]
print("1", d1)
d4 = [x for x in patterns if len(x) == 4][0]
print("4", d4)
d7 = [x for x in patterns if len(x) == 3][0]
print("7", d7)
d8 = [x for x in patterns if len(x) == 7][0]
print("8", d8)
# 3 is the only digit that has all common segments with 1
breakpoint()
d3 = [x for x in patterns if set(d1).issubset(set(x)) and len(x) == 5][0]
print("3", d3)
break
def main(infile):
inp = []
with open(infile) as f:
for display in f:
display = display.rstrip().split(" | ")
signal_patterns = display[0].split(" ")
output_values = display[1].split(" ")
inp.append([signal_patterns, output_values])
part1(inp)
part2(inp)
if __name__ == "__main__":
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
main(infile)

View File

@@ -0,0 +1,14 @@
def main(content):
inventories = [x.rstrip().split("\n") for x in content.split("\n\n")]
calories = sorted((sum(int(y) for y in x) for x in inventories), reverse=True)
print("Part 1: ", calories[0])
print("Part 2: ", sum(calories[:3]))
if __name__ == "__main__":
import sys
infile = sys.argv[1]
with open(infile) as f:
content = f.read()
main(content)

View File

@@ -0,0 +1,40 @@
def main(content):
table = str.maketrans("XYZ", "ABC")
score = 0
for c in content:
c = c.translate(table)
w = ord(c[-1]) - ord("A") + 1
match c:
case "A A" | "B B" | "C C": score += 3 + w
case "A B" | "B C" | "C A": score += 6 + w
case "A C" | "B A" | "C B": score += w
case _: assert False, c
print("Part 1: ", score)
# x = lose, y = draw, z = win
score = 0
for c in content:
outcome = c[-1]
if outcome == "Y":
w = ord(c[0]) - ord("A") + 1
score += 3 + w
elif outcome == "Z":
index = ord(c[0]) - ord("A")
play = "ABC"[(index + 1) % 3]
w = ord(play) - ord("A") + 1
score += 6 + w
elif outcome == "X":
index = ord(c[0]) - ord("A")
w = ord("ABC"[index - 1]) - ord("A") + 1
score += w
else:
assert False, outcome
print(score)
if __name__ == "__main__":
import fileinput
#main(['A Y', 'B X', 'C Z'])
main(list(l.rstrip() for l in fileinput.input()))

View File

@@ -0,0 +1,36 @@
from itertools import zip_longest
from functools import reduce
from operator import and_
def grouper(n, iterable):
"grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
args = [iter(iterable)] * n
return zip_longest(*args)
def split(x):
return set(x[:len(x)//2]), set(x[len(x)//2:])
def get_priority(x):
x = x.pop()
prio = ord(x)
if "a" <= x <= "z":
prio -= ord("a") - 1
else:
prio -= ord("A") - 27
return prio
def main(content):
total = sum(get_priority(reduce(and_, split(l))) for l in content)
print("Part 1: ", total)
total = sum(get_priority(reduce(and_, map(set, x))) for x in grouper(3, content))
print("Part 2: ", total)
if __name__ == "__main__":
import fileinput
main(list(l.rstrip() for l in fileinput.input()))

View File

@@ -0,0 +1,45 @@
def contains(first, second):
"True if first ⊂ second or second ⊂ first"
start_a, end_a = first
start_b, end_b = second
if start_b >= start_a and end_b <= end_a:
return True
start_a, end_a = second
start_b, end_b = first
if start_b >= start_a and end_b <= end_a:
return True
return False
def overlaps(first, second):
start_a, end_a = first
start_b, end_b = second
if start_a <= start_b <= end_a:
return True
if start_b <= start_a <= end_b:
return True
return False
def main(content):
total = 0
for l in content:
first, second = [tuple(map(int, x.split("-"))) for x in l.split(",")]
if contains(first, second):
total += 1
print("Part 1: ", total)
total = 0
for l in content:
first, second = [tuple(map(int, x.split("-"))) for x in l.split(",")]
if overlaps(first, second):
total += 1
print("Part 2: ", total)
if __name__ == "__main__":
import fileinput
main([l.rstrip() for l in fileinput.input()])

View File

@@ -0,0 +1,48 @@
def part1(lines):
res = []
for line in lines:
digits = [c for c in line if c.isnumeric()]
res.append(int(digits[0] + digits[-1]))
print(f"Part 1: {sum(res)}")
spelled_digits = { "one": "o1e", "two": "t2o", "three": "t3e", "four": "f4r", "five": "f5e", "six": "s6x", "seven": "s7n", "eight": "e8t", "nine": "n9e" }
def substitute_digits(s, trans):
res = s
for word, digit in trans.items():
res = res.replace(word, digit)
return res
def part2(lines):
res = []
for line in lines:
line = line.rstrip()
res_line = []
nline = ""
for c in line:
nline += c
nline = substitute_digits(nline, spelled_digits)
digits = [c for c in nline if c.isnumeric()]
r = int(digits[0] + digits[-1])
print(f"{line} => {nline}, {r}")
res.append(r)
print(f"Part 2: {sum(res)}")
if __name__ == "__main__":
import sys
infile = sys.argv[1]
with open(infile) as f:
lines = f.readlines()
part1(lines)
part2(lines)

View File

@@ -0,0 +1,158 @@
from typing import Dict, List, Set, Tuple
from dataclasses import dataclass
from collections import defaultdict
from enum import Enum
@dataclass(frozen=True)
class Vec2d:
x: int
y: int
def __add__(self, other):
return Vec2d(self.x + other.x, self.y + other.y)
class Direction(Enum):
NORTH = Vec2d(0, -1) # [0, 0] is the top-left corner, so y increases going downwards
SOUTH = Vec2d(0, 1)
EAST = Vec2d(1, 0)
WEST = Vec2d(-1, 0)
"""
| is a vertical pipe connecting north and south.
- is a horizontal pipe connecting east and west.
L is a 90-degree bend connecting north and east.
J is a 90-degree bend connecting north and west.
7 is a 90-degree bend connecting south and west.
F is a 90-degree bend connecting south and east.
. is ground; there is no pipe in this tile.
S is the starting position of the animal; there is a pipe on this tile, but your sketch doesn't show what shape the pipe has.
"""
PIPES = {
"|": (Direction.SOUTH, Direction.NORTH),
"-": (Direction.EAST, Direction.WEST),
"L": (Direction.NORTH, Direction.EAST),
"J": (Direction.NORTH, Direction.WEST),
"7": (Direction.SOUTH, Direction.WEST),
"F": (Direction.SOUTH,Direction.EAST),
}
PIPES_REVERSE_LOOKUP = {v: k for k,v in PIPES.items()}
def find_start_position(grid: List[str]) -> Vec2d:
for y, row in enumerate(grid):
for x, c in enumerate(row):
if c == "S":
return Vec2d(x, y)
raise RuntimeError("The start position was not found")
def update_start_symbol(grid: List[str], start_pos: Vec2d):
"""
Updates the map by replacing the start symbol "S" with its actual corresponding pipe
"""
# check which neighbors are connected to the start position
connections = []
north = start_pos + Direction.NORTH.value
south = start_pos + Direction.SOUTH.value
east = start_pos + Direction.EAST.value
west = start_pos + Direction.WEST.value
if grid[north.y][north.x] in "|7F":
connections.append(Direction.NORTH)
if grid[south.y][south.x] in "|LJ":
connections.append(Direction.SOUTH)
if grid[east.y][east.x] in "-7J":
connections.append(Direction.EAST)
if grid[west.y][west.x] in "-LF":
connections.append(Direction.WEST)
print("Start symbol has the following connections: ", connections)
assert len(connections) == 2, "start symbol has invalid connections"
pipe = PIPES_REVERSE_LOOKUP[tuple(connections)]
print(f"Start symbol is a {pipe} pipe")
# replace it in the grid accordingly
grid[start_pos.y] = grid[start_pos.y].replace("S", pipe)
def parse_graph(grid: List[str]) -> Dict[Vec2d, List[Vec2d]]:
graph = defaultdict(list)
for y, row in enumerate(grid):
for x, pipe in enumerate(row):
pos = Vec2d(x, y)
if pipe in PIPES:
for direction in PIPES[pipe]:
next_pos = pos + direction.value
graph[pos].append(next_pos)
return graph
def traverse_graph(graph, start_pos) -> Tuple[int, Set[Vec2d]]:
"""
traverse the graph using BFS, return the path and the
find the length of the longest path in the graph
"""
queue = [(start_pos, 0)] # (pos, distance from start)
max_dist = 0
visited = {start_pos}
while queue != []:
cur, dist = queue.pop(0)
max_dist = max(max_dist, dist)
for next_pos in graph[cur]:
if next_pos not in visited:
visited.add(next_pos)
queue.append((next_pos, dist+1))
return max_dist, visited
def count_enclosed_tiles(grid, edges):
"""
count the number of enclosed tiles in the loop by casting a ray on each row
and counting the number of intersections with the edges of the loop
"""
enclosed_count = 0
for y, row in enumerate(grid):
crossings = 0
for x, pipe in enumerate(row):
pos = Vec2d(x, y)
if pos in edges:
if pipe in "L|J":
crossings += 1
elif crossings % 2 == 1:
enclosed_count += 1
return enclosed_count
def main(grid):
rows, cols = len(grid), len(grid[0])
start_pos = find_start_position(grid)
print("Start pos ", start_pos)
update_start_symbol(grid, start_pos)
graph = parse_graph(grid)
max_dist, visited = traverse_graph(graph, start_pos)
print("Part 1: ", max_dist)
# visited edges are the ones that are part of the loop
inside_count = count_enclosed_tiles(grid, visited)
print("Part 2: ", inside_count)
if __name__ == "__main__":
import sys
infile = sys.argv[1]
with open(infile) as f:
lines = [l.rstrip() for l in f.readlines()]
main(lines)

View File

@@ -0,0 +1,45 @@
from math import prod
def part1(lines):
result = set()
for index, line in enumerate(lines):
game_id = index + 1
_, line = line.split(": ")
result.add(int(game_id))
hands = line.rstrip().split("; ")
for hand in hands:
colors = {"red": 0, "green": 0, "blue": 0}
cubes = hand.split(", ")
for cube in cubes:
n, color = cube.split()
colors[color] = int(n)
if colors["red"] > 12 or colors["green"] > 13 or colors["blue"] > 14:
# impossible configuration, remove this game_id from the result (if present)
result.discard(int(game_id))
print(f"Part 1: {sum(result)}")
def part2(lines):
result = []
for line in lines:
colors = {"red": 0, "green": 0, "blue": 0}
_, line = line.split(": ")
hands = line.rstrip().split("; ")
for hand in hands:
cubes = hand.split(", ")
for cube in cubes:
n, color = cube.split()
colors[color] = max(colors[color], int(n))
result.append(prod(colors.values()))
print(f"Part 2: {sum(result)}")
if __name__ == "__main__":
import sys
infile = sys.argv[1]
with open(infile) as f:
lines = f.readlines()
part1(lines)
part2(lines)

View File

@@ -0,0 +1,116 @@
from typing import Tuple, List, Set
from dataclasses import dataclass
@dataclass(frozen=True)
class Item:
pos: Tuple[int, int]
symbol: str
def browse_schema(schema):
total_parts = 0
buf = []
max_row, max_col = len(schema), len(schema[0])
symbols: List[Tuple[Item, Set[Item]]] = []
numbers: List[Item] = []
for y in range(max_row):
for x in range(max_col):
item = schema[y][x]
if item.isnumeric():
# continue parsing full number
buf.append(item)
else:
neighbors = get_neighbors_of((x, y), schema)
symbols.append((Item((x, y), item), set(neighbors)))
if buf and not item.isnumeric():
# end of a number, do the engine part check
number = "".join(buf)
neighbors = get_neighbors((x, y), len(buf), schema)
start_pos = (x-len(number), y)
symbols.append((Item((x, y), number), get_neighbors_of((x, y), schema)))
numbers.append(Item(start_pos, number))
if is_engine_part(neighbors):
total_parts += int(number)
buf.clear() # reached end of a number, clear buffer
print(f"Part 1, sum of the parts numbers = {total_parts}")
part2(symbols, numbers)
def part2(symbols, numbers):
total_gears = 0
stars = [(s, neighbors) for s, neighbors in symbols if s.symbol == "*"]
for _, neighbors in stars:
corresponding_numbers = set()
digits = [n for n in neighbors if n.symbol.isdigit()]
for digit in digits:
# find full number (number.start_pos < digit.pos < number.end_pos)
for number in numbers:
if number.pos[1] - 1 <= digit.pos[1] <= number.pos[1] + 1 and number.pos[0] <= digit.pos[0] <= number.pos[0]+len(number.symbol):
corresponding_numbers.add(number.symbol)
if len(corresponding_numbers) == 2:
a, b = corresponding_numbers
total_gears += int(a) * int(b)
#print(f"star: {star.pos} {corresponding_numbers}")
print(f"Part 2, sum of gear ratios = {total_gears}")
def is_engine_part(neighbors: List[Item]) -> bool:
# get list of symbols (not '.', \n or a number)
symbols = filter(lambda x: not x.symbol.isnumeric() and not x.symbol in (".", "\n"), neighbors)
return next(symbols, None) is not None
def get_neighbors(pos: Tuple[int, int], length: int, schema: List[List[str]]) -> List[Item]:
x, y = pos
start_x = x - length
neighbors = [get_neighbors_of((x, y), schema) for x in range(start_x, x)]
neighbors = [item for sublist in neighbors for item in sublist] # flatten list of list
return neighbors
def get_neighbors_of(pos: Tuple[int, int], schema: List[List[str]]) -> List[Item]:
max_row, max_col = len(schema), len(schema[0])
x, y = pos
neighbors: List[Item] = []
# top
if y-1 >= 0:
neighbors.append(Item((x, y-1), schema[y-1][x]))
# bottom:
if y+1 < max_row:
neighbors.append(Item((x, y+1), schema[y+1][x]))
# left
if x-1 >= 0:
neighbors.append(Item((x-1, y), schema[y][x-1]))
# right
if x+1 < max_col:
neighbors.append(Item((x+1, y), schema[y][x+1]))
# top-left
if y-1 >= 0 and x-1 >= 0:
neighbors.append(Item((x-1, y-1), schema[y-1][x-1]))
# top-right
if y-1 >= 0 and x+1 < max_col:
neighbors.append(Item((x+1, y-1), schema[y-1][x+1]))
# bottom-left
if y+1 < max_row and x-1 >= 0:
neighbors.append(Item((x-1, y+1), schema[y+1][x-1]))
# bottom-right
if y+1 < max_row and x+1 < max_col:
neighbors.append(Item((x+1, y+1), schema[y+1][x+1]))
return neighbors
if __name__ == "__main__":
import sys
infile = sys.argv[1]
with open(infile) as f:
schema = [[c for c in line] for line in f.readlines()]
browse_schema(schema)

View File

@@ -0,0 +1,44 @@
def parse_tickets(lines):
tickets = []
for line in lines:
_, nums = line.rstrip().split(": ")
winning, played = nums.split(" | ")
winning, played = set(winning.split()), set(played.split())
tickets.append((winning, played))
return tickets
def part1(tickets):
total = 0
for ticket in tickets:
winning, played = ticket
num_wins = len(winning.intersection(played))
points = 0 if num_wins == 0 else 2**(num_wins-1)
total += points
print(f"part 1, total={total}")
def part2(tickets):
tickets = [[1, t] for t in tickets]
for index, ticket in enumerate(tickets):
mult = ticket[0]
winning, played = ticket[1]
num_wins = len(winning.intersection(played))
for i in range(index+1, index+1+num_wins):
tickets[i][0] += mult
num_tickets = sum(n for n, _ in tickets)
print(f"part 2, number of tickets: {num_tickets}")
def main(f):
tickets = parse_tickets(f)
part1(tickets)
part2(tickets)
if __name__ == "__main__":
import sys
infile = sys.argv[1]
with open(infile) as f:
main(f)

View File

@@ -0,0 +1,70 @@
from itertools import zip_longest
def grouper(n, iterable):
"grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
args = [iter(iterable)] * n
return zip_longest(*args)
def part1(sections):
# consume seed section
_, seeds = next(sections)
seeds = [int(s) for s in seeds]
for _, mapping in sections:
seeds = [apply_mapping(s, mapping) for s in seeds]
print(f"Part 1, lowest location number = {min(seeds)}")
def part2(sections):
# consume seed section
_, seeds = next(sections)
seeds = [int(s) for s in seeds]
min_seed = 2**128
for start_seed, length in grouper(2, seeds):
subseeds = range(start_seed, start_seed + length)
print(f"calculate_for_subseeds(subseeds len={len(subseeds)})")
res = calculate_for_subseeds(subseeds, sections)
mini = min(res)
if mini < min_seed:
min_seed = mini
print(f"Part 2 {min_seed}")
def calculate_for_subseeds(seeds, sections):
new_seeds = seeds
for _, mapping in sections:
new_seeds = [apply_mapping(s, mapping) for s in new_seeds]
return new_seeds
def apply_mapping(seed, mapping):
for dst, src, length in grouper(3, mapping):
src, length, dst = int(src), int(length), int(dst)
end = src + length
if src <= seed < end:
return seed + (dst-src)
return seed
def parse_input(infile):
with open(infile) as f:
sections = f.read().split("\n\n")
sections = ((title, numbers) for title, numbers in (s.split(":") for s in sections))
sections = ((title, numbers.split()) for title, numbers in sections)
return sections
if __name__ == "__main__":
import sys
import os
SCRIPTPATH = os.path.dirname(os.path.realpath(__file__))
infile = next(iter(sys.argv[1:]), None)
sections = parse_input(infile or os.path.join(SCRIPTPATH, "example.txt"))
part1(sections)
sections = parse_input(infile or os.path.join(SCRIPTPATH, "example.txt"))
part2(sections)

View File

@@ -0,0 +1,40 @@
def parse_part1(path):
with open(path) as f:
time, distance = f.readlines()
time = [int(x) for x in time.split()[1:]]
distance = [int(x) for x in distance.split()[1:]]
return zip(time, distance)
def calculate_wins(data):
total = 1
for time, record in data:
ways = 0
for n in range(0, time+1):
speed = n
distance = (time-n) * speed
if distance > record:
ways += 1
total *= ways
return total
def parse_part2(path):
with open(path) as f:
time, distance = f.readlines()
time = time.split(":")[1].replace(" ", "").rstrip()
distance = distance.split(":")[1].replace(" ", "").rstrip()
return int(time), int(distance)
if __name__ == "__main__":
assert calculate_wins(zip(*[[7, 15, 30], [9, 40, 200]])) == 288 # part 1 example
assert calculate_wins([[71530, 940200]]) == 71503 # part 2 example
import sys
if len(sys.argv) == 2:
data = parse_part1(sys.argv[1])
res = calculate_wins(data)
print(f"Part 1, res={res}")
data = parse_part2(sys.argv[1])
res = calculate_wins([data])
print(f"Part 2, res={res}")

View File

@@ -0,0 +1,70 @@
from collections import Counter
def calculate_rank(hand, part2=False):
card_ranks = {'2': 1, '3': 2, '4': 3, '5': 4, '6': 5, '7': 6, '8': 7, '9': 8, 'T': 9, 'J': 10, 'Q': 11, 'K': 12, 'A': 13}
if part2:
# in part2, jokers are the weakest card
card_ranks['J'] = 0
# substitute cards with their ranks to make them sortable
hand = [card_ranks[c] for c in hand]
cnt = Counter(hand)
if part2 and cnt[0] != 5: # edge case if hand == 'JJJJJ'
# substitute jokers with the most common card that isn't a joker
most_common_card = cnt.most_common(2)[0][0] if cnt.most_common(2)[0][0] != 0 else cnt.most_common(2)[1][0]
new_hand = [most_common_card if c == 0 else c for c in hand]
cnt = Counter(new_hand)
rank = 0
match sorted(cnt.values()):
case [5]: rank = 7
case [1, 4]: rank = 6
case [2, 3]: rank = 5
case [1, 1, 3]: rank = 4
case [1, 2, 2]: rank = 3
case [1, 1, 1, 2]: rank = 2
case [1, 1, 1, 1, 1]: rank = 1
# return rank, and hand as a tiebreaker
return (rank, hand)
def parse_input(inp):
hands = [l.strip().split() for l in inp.strip().split("\n")]
return hands
def calculate_wins(hands, part2=False):
total = 0
hands = sorted(hands, key=lambda hb: calculate_rank(hb[0], part2=part2))
for rank, hand in enumerate(hands):
hand, bid = hand
total += (rank + 1) * int(bid)
return total
if __name__ == "__main__":
sample_input = """
32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483
"""
res = calculate_wins(parse_input(sample_input))
print(f"part 1 example: {res}")
assert res == 6440
res = calculate_wins(parse_input(sample_input), part2=True)
print(f"part 2 example: {res}")
assert res == 5905
import sys
if len(sys.argv) == 2:
with open(sys.argv[1]) as f:
inp = parse_input(f.read())
res = calculate_wins(inp)
print(f"Part 1, res={res}")
res = calculate_wins(inp, part2=True)
print(f"Part 2, res={res}")

View File

@@ -0,0 +1,75 @@
import re
import math
from itertools import cycle
def parse_input(infile):
with open(infile) as f:
content = f.read().rstrip()
directions, nodes = content.split("\n\n")
directions = directions.strip()
nodes = nodes.split("\n")
nodes = [n.split(" = ") for n in nodes]
nodes = {k: re.findall(r"\w{3}", v) for k, v in nodes}
return directions, nodes
def part1(directions, nodes):
iterations = 0
current_node = "AAA"
for d in cycle(directions):
if current_node == "ZZZ":
break
iterations += 1
if d == "L":
current_node = nodes[current_node][0]
else:
current_node = nodes[current_node][1]
print(f"Part 1: reached 'ZZZ' in {iterations} iterations")
def part2(directions, nodes):
current_nodes = [k for k in nodes.keys() if k.endswith("A")]
# keep track of iterations number for each visited node
# (the number will stop to beeing incremented once the node n_i value reached the target value 'xxZ')
iterations = [0] * len(current_nodes)
for d in cycle(directions):
if all(c.endswith("Z") for c in current_nodes):
break
if d == "L":
new_nodes = []
for i, n in enumerate(current_nodes):
if n.endswith("Z"): # end condition already reached for this node
new_nodes.append(n)
else:
new_nodes.append(nodes[n][0])
iterations[i] += 1
current_nodes = new_nodes
else:
new_nodes = []
for i, n in enumerate(current_nodes):
if n.endswith("Z"): # end condition already reached for this node
new_nodes.append(n)
else:
new_nodes.append(nodes[n][1])
iterations[i] += 1
current_nodes = new_nodes
# the result is the lowest common multiple between the number of iterations
# for each node
result = math.lcm(*iterations)
print(f"Part 2: reached all nodes such that 'xxZ' in {result} iterations")
if __name__ == "__main__":
import sys
import os
SCRIPTPATH = os.path.dirname(os.path.realpath(__file__))
infile = sys.argv[1] if len(sys.argv) == 2 else "example.txt"
directions, nodes = parse_input(os.path.join(SCRIPTPATH, infile))
part1(directions, nodes)
directions, nodes = parse_input(os.path.join(SCRIPTPATH, infile))
part2(directions, nodes)

View File

@@ -0,0 +1,33 @@
def parse_input(infile):
with open(infile) as f:
return [[int(x) for x in l.strip().split()] for l in f.readlines()]
def process_line(line):
if set(line) == {0}:
return 0
else:
next_line = [cur - next for next, cur in zip(line, line[1:])]
return line[-1] + process_line(next_line)
def process_line_back(line):
if set(line) == {0}:
return 0
else:
next_line = [cur - next for next, cur in zip(line, line[1:])]
return line[0] - process_line_back(next_line)
def solve(data):
print(f"Part 1: {sum(process_line(l) for l in data)}")
print(f"Part 2: {sum(process_line_back(l) for l in data)}")
if __name__ == "__main__":
import sys
import os
SCRIPTPATH = os.path.dirname(os.path.realpath(__file__))
infile = sys.argv[1] if len(sys.argv) == 2 else "example.txt"
data = parse_input(os.path.join(SCRIPTPATH, infile))
solve(data)

View File

@@ -0,0 +1,23 @@
from collections import Counter
def main(lines):
firsts, seconds = list(sorted(x[0] for x in lines)), list(sorted(x[1] for x in lines))
total = sum(abs(a - b) for (a, b) in zip(firsts, seconds))
print("Part 1: ", total)
counts = Counter(seconds)
total = sum(x * counts[x] for x in firsts)
print("Part 2: ", total)
if __name__ == "__main__":
import sys
infile = sys.argv[1]
with open(infile) as f:
lines = f.readlines()
lines = [list(map(int, x.rstrip().split())) for x in lines]
main(lines)

View File

@@ -0,0 +1,68 @@
from dataclasses import dataclass
@dataclass(frozen=True)
class Vec2d:
x: int
y: int
def __add__(self, other):
return Vec2d(self.x + other.x, self.y + other.y)
DIRECTIONS = (
Vec2d(0, -1), # N
Vec2d(1, 0), # E
Vec2d(0, 1), # S
Vec2d(-1, 0), # W
)
def in_bounds(grid, pos):
return 0 <= pos.y < len(grid) and 0 <= pos.x < len(grid[0])
def get_pos(grid, pos):
return grid[pos.y][pos.x]
def bfs(grid, start_pos):
visited = [start_pos]
goals = []
while visited != []:
current_pos = visited.pop()
current_val = get_pos(grid, current_pos)
if current_val == 9:
goals.append(current_pos)
for d in DIRECTIONS:
next_pos = current_pos + d
# next node can be reached if it's value is current + 1
if in_bounds(grid, next_pos) and get_pos(grid, next_pos) == current_val + 1:
if next_pos not in visited:
visited.append(next_pos)
return goals
def main(grid):
trailheads = []
for y, row in enumerate(grid):
for x, c in enumerate(row):
if c == 0:
trailheads.append(Vec2d(x, y))
total = 0
total2 = 0
for start_pos in trailheads:
trails = bfs(grid, start_pos)
total += len(set(trails))
total2 += len(trails)
print("Part 1: ", total)
print("Part 2: ", total2)
if __name__ == "__main__":
import sys
infile = sys.argv[1] if 1 < len(sys.argv) else "example.txt"
with open(infile) as f:
grid = [list(map(int, list(l.rstrip()))) for l in f.readlines()]
main(grid)

37
adventofcode/2024/day11/: Normal file
View File

@@ -0,0 +1,37 @@
from collections import Counter
def blink(data, steps):
counter = Counter(data)
for i in range(steps):
new_counter = Counter()
#data = tuple(x for y in (blink_stone(s) for s in data) for x in y)
for stone, count in counter.items():
s = str(stone)
if stone == 0:
new_counter[1] += count
elif len(s) % 2 == 0:
first, second = int(s[:len(s)//2]), int(s[len(s)//2:])
new_counter[first] += count
new_counter[second] += count
else:
new_counter[2024*stone] += count
counter = new_counter
print(len(counter.items()))
return counter.total()
if __name__ == "__main__":
res = blink((125, 17), 25)
assert res == 55312, f"expected 55312, but was {res}"
import sys
if len(sys.argv) > 1:
infile = sys.argv[1]
with open(infile) as f:
data = tuple(map(int, f.read().rstrip().split()))
part1 = blink(data, 25)
print("Part 1: ", part1)
part2 = blink(data, 75)
print("Part 2: ", part2)

View File

@@ -0,0 +1,42 @@
from collections import Counter
def main(data):
part1 = run(data, 25)
print("Part 1: ", part1)
part2 = run(data, 75)
print("Part 2: ", part2)
def run(data, steps):
data = Counter(data)
for _ in range(steps):
data = blink(data)
return data.total()
def blink(data):
new_counter = Counter()
for stone, count in data.items():
s = str(stone)
if stone == 0:
new_counter[1] += count
elif len(s) % 2 == 0:
first, second = int(s[:len(s)//2]), int(s[len(s)//2:])
new_counter[first] += count
new_counter[second] += count
else:
new_counter[2024*stone] += count
return new_counter
if __name__ == "__main__":
res = run((125, 17), 25)
assert res == 55312, f"expected 55312, but was {res}"
import sys
if len(sys.argv) > 1:
infile = sys.argv[1]
with open(infile) as f:
data = tuple(map(int, f.read().rstrip().split()))
main(data)

View File

@@ -0,0 +1,83 @@
from dataclasses import dataclass
from collections import defaultdict
@dataclass(frozen=True)
class Vec2d:
x: int
y: int
def __add__(self, other):
return Vec2d(self.x + other.x, self.y + other.y)
DIRECTIONS = (
Vec2d(0, -1), # N
Vec2d(1, 0), # E
Vec2d(0, 1), # S
Vec2d(-1, 0), # W
)
def in_bounds(grid, pos):
return 0 <= pos.y < len(grid) and 0 <= pos.x < len(grid[0])
def visit_region(grid, root):
region = set()
# BFS
queue = [root]
visited = set()
region.add(root)
visited.add(root)
while queue != []:
node = queue.pop(0)
for d in DIRECTIONS:
new_pos = node + d
if in_bounds(grid, new_pos) and new_pos not in visited:
visited.add(new_pos)
if grid[root.y][root.x] == grid[new_pos.y][new_pos.x]:
queue.append(new_pos)
region.add(new_pos)
return region
def get_perimeter(grid, region):
perimeter = 0
for node in region:
for d in DIRECTIONS:
new_pos = node + d
if not in_bounds(grid, new_pos) or grid[new_pos.y][new_pos.x] != grid[node.y][node.x]:
perimeter += 1
return perimeter
def main(grid):
# build list of regions using BFS
regions = []
visited = set()
for y, row in enumerate(grid):
for x, c in enumerate(row):
pos = Vec2d(x, y)
if pos not in visited:
region = visit_region(grid, pos)
visited.update(region)
regions.append((c, region))
total = 0
for region in regions:
c, nodes = region
area = len(nodes)
perimeter = get_perimeter(grid, nodes)
#print(f"Region {c} area {area} perimeter {perimeter}")
total += area * perimeter
print("Part 1: ", total)
if __name__ == "__main__":
import sys
infile = sys.argv[1] if 1 < len(sys.argv) else "example.txt"
with open(infile) as f:
grid = [l.rstrip() for l in f.readlines()]
main(grid)

View File

@@ -0,0 +1,46 @@
def parse_machine(machine):
btn_a, btn_b, prize = [x.split(": ")[1].split(", ") for x in machine]
btn_a = [int(x.split("+")[1]) for x in btn_a]
btn_b = [int(x.split("+")[1]) for x in btn_b]
prize = [int(x.split("=")[1]) for x in prize]
return (btn_a, btn_b, prize)
def solve(btn_a, btn_b, prize, offset=0):
a_x, a_y = btn_a
b_x, b_y = btn_b
p_x, p_y = [p + offset for p in prize]
# apply Cramer's rule to solve the 2x2 system
A = (p_x*b_y - p_y*b_x) / (a_x*b_y - a_y*b_x)
B = (a_x*p_y - a_y*p_x) / (a_x*b_y - a_y*b_x)
if A.is_integer() and B.is_integer():
return int(A), int(B)
return None, None
def main(content):
part1 = 0
part2 = 0
for machine in content:
btn_a, btn_b, prize = parse_machine(machine)
A, B = solve(btn_a, btn_b, prize)
if A is not None and B is not None:
part1 += 3*A + B
A, B = solve(btn_a, btn_b, prize, 10000000000000)
if A is not None and B is not None:
part2 += 3*A + B
print("Part 1: ", part1)
print("Part 2: ", part2)
if __name__ == "__main__":
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
with open(infile) as f:
content = f.read().split("\n\n")
content = [x.rstrip().split("\n") for x in content]
main(content)

View File

@@ -0,0 +1,106 @@
from math import prod
from collections import Counter
def parse_bots(lines):
lines = [l.rstrip().split(" ") for l in lines]
lines = [complex(*map(int, x.split("=")[1].split(","))) for l in lines for x in l]
return [lines[i:i+2] for i in range(0, len(lines), 2)] # [(pos, velocity), ...]
def simulate_bots(bots, grid_size, steps=100, part2=False):
step = 0
width, height = grid_size
stats = []
while step < steps:
new_bots = []
for pos, velocity in bots:
pos = pos + velocity
if pos.real >= width:
pos -= width
if pos.real < 0:
pos += width
if pos.imag >= height:
pos -= height * 1j
if pos.imag < 0:
pos += height * 1j
new_bots.append((pos, velocity))
bots = new_bots
step += 1
if part2:
# search step which maximizes safety value
safety = calculate_safety(Counter([p for p, _ in bots]), grid_size)
stats.append((safety, step))
return [pos for pos, _ in bots], stats
def determine_quadrant(pos, grid_size):
width, height = grid_size
q = None
if pos.real < width // 2 and pos.imag < height // 2:
q = 0
elif pos.real > width // 2 and pos.imag < height //2:
q = 1
elif pos.real < width // 2 and pos.imag > height // 2:
q = 2
elif pos.real > width // 2 and pos.imag > height // 2:
q = 3
return q
def calculate_safety(bots, grid_size):
total_quadrants = [0, 0, 0, 0]
for pos, count in bots.items():
q = determine_quadrant(pos, grid_size)
if q is None: # ignore middle row and col
continue
total_quadrants[q] += count
return prod(total_quadrants)
def part1(bots, grid_size):
bots, _ = simulate_bots(bots, grid_size)
c = Counter(bots)
return calculate_safety(c, grid_size)
def part2(bots, grid_size):
max_step = grid_size[0] * grid_size[1] # input is periodic
_, stats = simulate_bots(bots, grid_size, max_step, part2=True)
return sorted(stats)[0][1]
def main(lines):
bots = parse_bots(lines)
total = part1(bots, grid_size)
print("Part 1: ", total)
p2 = part2(bots, grid_size)
print("Part 2: ", p2)
def print_grid(c, grid_size):
width, height = grid_size
for y in range(height):
for x in range(width):
a = c.get(complex(x, y))
if a is None:
print(".", end="")
else:
print(a, end="")
print()
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
infile = "example.txt"
grid_size = (11, 7)
else:
infile = sys.argv[1]
grid_size = (101, 103)
with open(infile) as f:
lines = f.readlines()
main(lines)

View File

@@ -0,0 +1,110 @@
DIRECTIONS = {
"^": 0 - 1j, # up
">": 1 + 0j, # right
"v": 0 + 1j, # down
"<": -1 + 0j, # left
}
def find_start_pos(grid):
for y, row in enumerate(grid):
for x, _ in enumerate(row):
if grid[y][x] == "@":
return complex(x, y)
def get_pos(grid, pos):
x, y = int(pos.real), int(pos.imag)
if 0 <= x < len(grid[0]) and 0 <= y < len(grid):
return grid[y][x]
return None
def set_pos(grid, pos, val):
x, y = int(pos.real), int(pos.imag)
grid[y][x] = val
def debug_print(grid, move):
print("Move ", move)
for row in grid:
print("".join(row))
def push(grid, pos, movement):
direction = DIRECTIONS[movement]
start_pos = pos + direction
# Find the end pos of a consecutive "O" chain
end_pos = start_pos
while get_pos(grid, end_pos + direction) == "O":
end_pos += direction
if get_pos(grid, end_pos + direction) == ".":
if movement == ">":
start_x, end_x = int(start_pos.real), int(end_pos.real)
y = int(start_pos.imag)
for i in range(end_x, start_x - 1, -1):
grid[y][i+1] = "O" # shift "O" to the right
grid[y][start_x] = "."
elif movement == "<":
start_x, end_x = int(start_pos.real), int(end_pos.real)
y = int(start_pos.imag)
for i in range(start_x, end_x - 1, -1):
grid[y][i-1] = "O" # shift "O" to the left
grid[y][start_x] = "."
elif movement == "v":
start_y, end_y = int(start_pos.imag), int(end_pos.imag)
x = int(start_pos.real)
for i in range(start_y, end_y + 1):
grid[i+1][x] = "O" # shift "O" down
grid[start_y][x] = "."
elif movement == "^":
start_y, end_y = int(start_pos.imag), int(end_pos.imag)
x = int(start_pos.real)
for i in range(start_y, end_y - 1, -1):
grid[i-1][x] = "O" # shift "O" up
grid[start_y][x] = "."
def calculate_gps_coords(grid):
total = 0
for y, row in enumerate(grid):
for x, _ in enumerate(row):
if grid[y][x] == "O":
total += 100*y + x
return total
def main(content):
grid, movements = content.split("\n\n")
grid = [list(x) for x in grid.split("\n")]
movements = movements.replace("\n", "")
pos = find_start_pos(grid)
for movement in movements:
new_pos = pos + DIRECTIONS[movement]
v = get_pos(grid, new_pos)
match v:
case ".":
set_pos(grid, pos, ".")
set_pos(grid, new_pos, "@")
pos = new_pos
case "O":
push(grid, pos, movement)
if get_pos(grid, new_pos) == ".":
set_pos(grid, new_pos, "@")
set_pos(grid, pos, ".")
pos = new_pos
case "#": pass
case c: raise RuntimeError("This should never happen", c)
#debug_print(grid, movement)
part1 = calculate_gps_coords(grid)
print("Part 1: ", part1)
if __name__ == "__main__":
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
with open(infile) as f:
content = f.read()
main(content)

View File

@@ -0,0 +1,117 @@
from dataclasses import dataclass
from heapq import heappop, heappush
DIRECTIONS = {
">": 1 + 0j, # EAST
"v": 0 + 1j, # SOUTH
"<": -1 + 0j, # WEST
"^": 0 - 1j, # NORTH
}
DIRECTIONS_RV = {v: k for k,v in DIRECTIONS.items()}
@dataclass(frozen=True)
class Node:
pos: complex
direction: complex
cost: int = 0
parent: "Node" = None
def __lt__(self, other):
return self.cost < other.cost
def find_start_and_goal(grid):
start, goal = None, None
for y, row in enumerate(grid):
for x, _ in enumerate(row):
if grid[y][x] == "S":
start = complex(x, y)
elif grid[y][x] == "E":
goal = complex(x, y)
return start, goal
def get_pos(grid, pos):
x, y = int(pos.real), int(pos.imag)
if 0 <= x < len(grid[0]) and 0 <= y < len(grid):
return grid[y][x]
return None
def search(grid, start_node, end_pos):
"""
Returns the shortest path between start and end using Dijkstra's algorithm
"""
queue = [start_node]
visited = set()
best_costs = {}
while queue != []:
node = heappop(queue)
visited.add(node)
if node.pos == end_pos:
return node
if node.cost > best_costs.get((node.pos, node.direction), 99999999): # already found a better path to this pos
continue
best_costs[(node.pos, node.direction)] = node.cost
# visit each neighbor
# go in the same direction
n1 = Node(node.pos + node.direction, node.direction, node.cost + 1, node)
if get_pos(grid, n1.pos) != "#" and n1 not in visited:
heappush(queue, n1)
# turn clockwise
turned = node.direction * 1j
n2 = Node(node.pos + turned, turned, node.cost + 1000 + 1, node)
if get_pos(grid, n2.pos) != "#" and n2 not in visited:
heappush(queue, n2)
# turn counterclockwise
turned = node.direction * -1j
n3 = Node(node.pos + turned, turned, node.cost + 1000 + 1, node)
if get_pos(grid, n3.pos) != "#" and n3 not in visited:
heappush(queue, n3)
return None
def print_grid(grid):
for row in grid:
print("".join(row))
def main(grid, debug=False):
start, goal = find_start_and_goal(grid)
direction = 1 + 0j # initial direction is east
end_node = search(grid, Node(start, direction), goal)
total_cost = end_node.cost
print("Part 1: ", total_cost)
if debug:
# compute path
n = end_node
path = []
if n is not None:
while n.parent is not None:
path.insert(0, n)
n = n.parent
for n in path:
x, y = int(n.pos.real), int(n.pos.imag)
grid[y][x] = "O"
print(f"Pos {x},{y} Direction {DIRECTIONS_RV[n.direction]} Cost {n.cost}")
print_grid(grid)
input()
print(chr(27) + "[2J") # clear terminal
if __name__ == "__main__":
import sys
infile = sys.argv[1] if 1 < len(sys.argv) else "example.txt"
with open(infile) as f:
grid = [list(l.rstrip()) for l in f.readlines()]
main(grid)

View File

@@ -0,0 +1,81 @@
from dataclasses import dataclass
from heapq import heappush, heappop
DIRECTIONS = (
1 + 0j, # EAST
0 + 1j, # SOUTH
-1 + 0j, # WEST
0 - 1j, # NORTH
)
@dataclass(frozen=True)
class Node:
pos: complex
cost: int = 0
def __lt__(self, other):
return self.cost < other.cost
def can_reach(pos, obstacles, grid_size):
height, width = grid_size
x, y = int(pos.real), int(pos.imag)
if 0 <= x < width and 0 <= y < height:
if (x, y) not in obstacles:
return True
return False
def search(obstacles, start, goal, grid_size): # could just use bfs?
queue = [Node(start)]
visited = set()
i = 0
while queue != []:
node = heappop(queue)
visited.add(node.pos)
if node.pos == goal:
return node.cost
for direction in DIRECTIONS:
new_pos = node.pos + direction
n = Node(new_pos, node.cost + 1)
if n not in queue and n.pos not in visited and can_reach(n.pos, obstacles, grid_size):
heappush(queue, n)
return -1
def find_path(coords, limit, grid_size):
obstacles = coords[:limit]
start = 0+0j
goal = complex(grid_size[0] - 1, grid_size[1] - 1)
cost = search(obstacles, start, goal, grid_size)
return cost
def main(coords, limit, grid_size):
part1 = find_path(coords, limit, grid_size)
print("Part 1: ", part1)
# binary search for part 2
low, high = limit, len(coords)
while high - low > 1:
i = (low + high) // 2
if find_path(coords, i, grid_size) != -1:
low = i
else:
high = i
print("Part 2: ", coords[low])
if __name__ == "__main__":
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
with open(infile) as f:
lines = f.readlines()
lines = [tuple(map(int, l.rstrip().split(","))) for l in lines]
if infile == "example.txt":
main(lines, 12, (7, 7))
else:
main(lines, 1024, (71, 71))

View File

@@ -0,0 +1,36 @@
from itertools import pairwise
def is_safe(report):
diffs = [b - a for a, b in pairwise(report)]
return all(1 <= d <= 3 for d in diffs) or all(-3 <= d <= -1 for d in diffs)
def is_safe_with_problem_damper(report):
for i, _ in enumerate(report):
copy = report[::]
del copy[i]
if is_safe(copy):
return True
return False
def main(reports):
safe_reports = sum(1 for x in reports if is_safe(x))
print("Part 1: ", safe_reports)
safe_reports = 0
for report in reports:
if is_safe_with_problem_damper(report):
safe_reports += 1
print("Part 2: ", safe_reports)
if __name__ == "__main__":
import sys
infile = sys.argv[1]
with open(infile) as f:
lines = f.readlines()
lines = [list(map(int, x.rstrip().split())) for x in lines]
main(lines)

View File

@@ -0,0 +1,39 @@
import re
def main(content):
operations = re.findall(r"(?:mul\((\d+),(\d+)\))|(do\(\))|(don't\(\))", content)
# filter only mul instructions for part1, format: ('498', '303', '', '')
mul_operations = [x for x in operations if x[0].isnumeric()]
total = sum(int(a) * int(b) for a, b, *_rest in mul_operations)
print("Part 1: ", total)
do_mul = True
total = 0
for op in operations:
token = "".join(op)
print(token)
if token.startswith("don't"):
do_mul = False
print("disable_mul")
elif token.startswith("do"):
do_mul = True
elif token.isnumeric():
if do_mul:
a, b, *_rest = op
total += int(a) * int(b)
else:
raise RuntimeError(f"Invalid token {token}")
print("Part 2: ", total)
if __name__ == "__main__":
import sys
infile = sys.argv[1]
with open(infile) as f:
content = f.read()
main(content)

View File

@@ -0,0 +1,72 @@
DIRECTIONS = (
(0, -1), # N
(-1, -1), # NW
(1, -1), # NE
(-1, 0), # W
(1, 0), # E
(0, 1), # S
(-1, 1), # SW
(1, 1), # SE
)
def get_grid_pos(grid, pos):
x, y = pos
if 0 <= x < len(grid):
if 0 <= y < len(grid[0]):
return grid[y][x]
return None
def part1(grid):
count = 0
for y, row in enumerate(grid):
for x, letter in enumerate(row):
if letter != "X":
continue
for direction in DIRECTIONS:
acc = letter
for i in range(1, 4):
dx, dy = direction
pos = (x + i*dx, y + i*dy)
next_letter = get_grid_pos(grid, pos)
if next_letter is not None:
acc += next_letter
else:
break # out-of-bounds, go to next direction
if acc == "XMAS":
count += 1
print("Part 1: ", count)
def part2(grid):
count = 0
for y, row in enumerate(grid):
for x, _ in enumerate(row):
if y + 2 >= len(grid) or x + 2 >= len(grid[0]):
continue
if grid[y+1][x+1] != "A": # center letter is always "A"
continue
# M.S / .A. / M.S
if grid[y][x] == "M" and grid[y][x+2] == "S" and grid[y+2][x] == "M" and grid[y+2][x+2] == "S":
count += 1
# M.M / .A. / S.S
if grid[y][x] == "M" and grid[y][x+2] == "M" and grid[y+2][x] == "S" and grid[y+2][x+2] == "S":
count += 1
# S.M / .A. / S.M
if grid[y][x] == "S" and grid[y][x+2] == "M" and grid[y+2][x] == "S" and grid[y+2][x+2] == "M":
count += 1
# S.S / .A. / M.M
if grid[y][x] == "S" and grid[y][x+2] == "S" and grid[y+2][x] == "M" and grid[y+2][x+2] == "M":
count += 1
print("Part 2: ", count)
if __name__ == "__main__":
import sys
infile = sys.argv[1] if 1 < len(sys.argv) else "example.txt"
with open(infile) as f:
grid = [l.rstrip() for l in f.readlines()]
part1(grid)
part2(grid)

View File

@@ -0,0 +1,45 @@
from collections import defaultdict
from itertools import pairwise
def check_update(upd, rules):
for a, b in pairwise(upd):
if [a, b] not in rules:
return False
return True
def fix_update(upd, rules):
while not check_update(upd, rules):
for i in range(len(upd)):
for j in range(i+1, len(upd)):
if [upd[j], upd[i]] in rules:
upd[j], upd[i] = upd[i], upd[j]
def main(content):
rules, updates = content.split("\n\n")
rules = [x.split("|") for x in rules.split("\n")]
updates = [x.split(",") for x in updates.rstrip().split("\n")]
part1 = 0
incorrect_updates = []
for update in updates:
if check_update(update, rules):
middle = update[len(update)//2]
part1 += int(middle)
else:
incorrect_updates.append(update)
print("Part 1: ", part1)
part2 = 0
for update in incorrect_updates:
fix_update(update, rules)
middle = update[len(update)//2]
part2 += int(middle)
print("Part 2: ", part2)
if __name__ == "__main__":
import sys
infile = sys.argv[1] if 1 < len(sys.argv) else "example.txt"
with open(infile) as f:
main(f.read())

View File

@@ -0,0 +1,80 @@
from itertools import cycle
from dataclasses import dataclass
from pprint import pprint
@dataclass(frozen=True)
class Vec2d:
x: int
y: int
def __add__(self, other):
return Vec2d(self.x + other.x, self.y + other.y)
DIRECTIONS = (
Vec2d(0, -1), # N
Vec2d(1, 0), # E
Vec2d(0, 1), # S
Vec2d(-1, 0), # W
)
def find_start_pos(grid):
for y, row in enumerate(grid):
for x, c in enumerate(row):
if c == "^":
return Vec2d(x, y)
raise RuntimeError("No start position found")
def find_path(grid, pos):
directions = cycle(DIRECTIONS)
direction = next(directions)
path = {(pos, direction)}
while True:
new_pos = pos + direction
if 0 <= new_pos.y < len(grid) and 0 <= new_pos.x < len(grid[0]):
while grid[new_pos.y][new_pos.x] == "#":
direction = next(directions)
new_pos = pos + direction
pos = new_pos
if (pos, direction) in path: # if we visited this position while going the same direction, we are in a loop
visited = [x for x, _ in path]
return visited, True
path.add((pos, direction))
else:
visited = [x for x, _ in path]
return visited, False
raise RuntimeError("Should not happen")
def main(grid):
pos = find_start_pos(grid)
path, _ = find_path(grid, pos)
print("Part 1: ", len(set(path)))
loops = []
last_obstacle_pos = None
for obstacle_pos in path:
if pos == obstacle_pos:
continue
grid[obstacle_pos.y][obstacle_pos.x] = "#"
path, is_loop = find_path(grid, pos)
if is_loop:
loops.append(obstacle_pos)
grid[obstacle_pos.y][obstacle_pos.x] = "."
print("Part 2: ", len(set(loops)))
if __name__ == "__main__":
import sys
infile = sys.argv[1] if 1 < len(sys.argv) else "example.txt"
with open(infile) as f:
grid = [list(l.rstrip()) for l in f.readlines()]
main(grid)

View File

@@ -0,0 +1,36 @@
def test(values, part2=False):
if len(values) == 1:
yield values[0]
else:
for rest in test(values[1:], part2):
yield values[0] + rest
yield values[0] * rest
if part2:
yield int(str(rest) + str(values[0])) # concatenation
def main(data):
part1 = 0
part2 = 0
for expected, values in data:
for res in test(values[::-1]):
if res == expected:
part1 += res
break
for res in test(values[::-1], part2=True):
if res == expected:
part2 += res
break
print("Part 1: ", part1)
print("Part 2: ", part2)
if __name__ == "__main__":
import sys
infile = sys.argv[1] if 1 < len(sys.argv) else "example.txt"
with open(infile) as f:
data = [l.rstrip().split(": ") for l in f.readlines()]
data = [(int(a), tuple(map(int, b.split(" ")))) for a, b in data]
main(data)

View File

@@ -0,0 +1,71 @@
from itertools import combinations
def in_bounds(pos, grid):
x, y = int(pos.real), int(pos.imag)
if 0 <= y < len(grid) and 0 <= x < len(grid[0]):
return True
return False
def part1(locations, grid):
antinodes = set()
for first, second in combinations(locations.items(), 2):
first_loc, first_freq = first
second_loc, second_freq = second
if first_freq == second_freq:
slope = first_loc - second_loc
a = first_loc + slope
if in_bounds(a, grid):
antinodes.add(a)
b = second_loc - slope
if in_bounds(b, grid):
antinodes.add(b)
return antinodes
def part2(locations, grid):
antinodes = set()
for first, second in combinations(locations.items(), 2):
first_loc, first_freq = first
second_loc, second_freq = second
antinodes.update([first_loc, second_loc])
if first_freq == second_freq:
slope = first_loc - second_loc
i = 1
while True:
a = first_loc + i*slope
if not in_bounds(a, grid):
break
antinodes.add(a)
i += 1
j = 0
while True:
b = second_loc - j*slope
if not in_bounds(b, grid):
break
antinodes.add(b)
j += 1
return antinodes
def main(lines):
grid = [l.rstrip() for l in lines]
locations = {}
for y, row in enumerate(grid):
for x, c in enumerate(row):
if c != '.':
locations[complex(x, y)] = c
print("Part 1: ", len(part1(locations, grid)))
print("Part 2: ", len(part2(locations, grid)))
if __name__ == "__main__":
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else "example.txt"
with open(infile) as f:
main(f.readlines())

View File

@@ -0,0 +1,120 @@
from dataclasses import dataclass
from typing import Literal
@dataclass()
class Item:
kind: Literal["file", "free"]
size: int
id_: int = 0
def parse_disk(data):
disk = {}
disk2 = []
id_cnt = 0
offset = 0
for pos, size in enumerate(data):
if pos % 2 == 0: # file
id = id_cnt
for i in range(int(size)):
disk[offset + i] = id
disk2.append(Item("file", int(size), id))
id_cnt += 1
offset += int(size)
else: # free space
if int(size) > 0:
disk2.append(Item("free", int(size)))
offset += int(size)
return disk, disk2
def part1(disk):
max_file_location = max(disk.keys())
write_ptr = 0
read_ptr = max_file_location
while True:
while write_ptr in disk:
write_ptr += 1
while read_ptr not in disk:
read_ptr -= 1
if write_ptr >= read_ptr:
break
disk[write_ptr] = disk[read_ptr]
del disk[read_ptr]
checksum = sum(i * disk.get(i, 0) for i in range(max_file_location))
return checksum
def part2(disk):
max_id = max(f.id_ for f in disk)
for i in range(max_id, -1, -1):
file, file_index = next((file, index) for index, file in enumerate(disk) if file.id_ == i)
# find index of the first gap large enough
free_index, free_space = next(((i, b) for i, b in enumerate(disk) if b.kind == "free" and b.size >= file.size), (None, None))
if free_index is None:
continue
if free_index >= file_index: # always move file to the left
continue
# decrease free space by file size (in case free space is larger)
disk[free_index].size -= file.size
# add a free space in place of the file
disk[file_index] = Item("free", file.size)
# insert file just before free space
disk.insert(free_index, file)
#debug = debug_print(disk)
#print(debug)
# calculate checksum for part2
total_checksum = 0
offset = 0
#print(disk)
debug_print(disk)
#print(len(disk))
for f in disk:
if f.kind != "file":
offset += f.size
continue
# S(n) = n*(n+1) // 2
#print(f"checksum = {f.id_} * ({offset} * {f.size} + ({f.size} * ({f.size - 1})) // 2")
checksum = f.id_ * (offset * f.size + (f.size * (f.size - 1)) // 2)
#print(f, checksum, total_checksum)
offset += f.size
total_checksum += checksum
return total_checksum
def main(inp):
disk, disk2 = parse_disk(inp)
print("Part 1: ", part1(disk))
print("Part 2: ", part2(disk2))
def debug_print(disk):
res = []
for item in disk:
if item.kind == "free" and item.size > 0:
res.extend(["."] * item.size)
else:
res.extend([str(item.id_)] * item.size)
return "".join(res)
if __name__ == "__main__":
import sys
infile = sys.argv[1] if len(sys.argv) > 1 else None
content = "2333133121414131402"
if infile is not None:
with open(infile) as f:
content = f.read().rstrip()
main(content)

41
adventofcode/aoc.py Normal file
View File

@@ -0,0 +1,41 @@
import argparse
from adventofcode.helper import run, get_input_file
def main():
parser = argparse.ArgumentParser(description="Advent of Code CLI")
subparsers = parser.add_subparsers(dest='command')
# Sous-commande init
init_parser = subparsers.add_parser('init', help='Init an aoc day')
init_parser.add_argument('year', type=int)
init_parser.add_argument('day', type=int)
# Sous-commande run
run_parser = subparsers.add_parser('run', help='Run an aoc day')
run_parser.add_argument('year', type=int)
run_parser.add_argument('day', type=int)
args = parser.parse_args()
if args.command == 'init':
handle_init(args.year, args.day)
elif args.command == 'run':
handle_run(args.year, args.day)
else:
parser.print_help()
def handle_init(year, day):
# TODO initialize directory if needed, download input file and create
# dayX.py from a template
raise NotImplementedError("init")
def handle_run(year, day):
run(year, day)
if __name__ == "__main__":
main()

73
adventofcode/helper.py Normal file
View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
import urllib.request
import getpass
import sys
import time
import subprocess
import os
from pathlib import Path
ROOTPATH = Path(os.path.dirname(os.path.realpath(__file__)))
_auth = None
def get_auth():
global _auth
if _auth is None:
if "AUTH" in os.environ:
_auth = os.environ["AUTH"]
else:
_auth = getpass.getpass(prompt="Cookie: ")
def get_input_file(year, day):
url = f"https://adventofcode.com/{year}/day/{day}/input"
r = urllib.request.Request(url)
r.add_header("Cookie", f"session={_auth}")
res = urllib.request.urlopen(r)
return res
def run(year, day):
if day is not None:
path = ROOTPATH / Path(f"{year}/day{day}")
script_path = path / Path(f"day{day}.py")
input_path = path / Path("input.txt")
if not script_path.exists():
print(f"Invalid day {day}", file=sys.stderr)
exit(1)
if not input_path.exists():
print(f"Downloading input file {input_path}")
get_auth()
with open(input_path, "wb") as f:
res = get_input_file(year, day)
f.write(res.read())
run_day(script_path, input_path)
else:
for day in range(1, 26):
path = ROOTPATH / Path(f"{year}/day{day}")
script_path = path / Path(f"day{day}.py")
input_path = path / Path("input.txt")
if script_path.exists():
if not input_path.exists():
print(f"- downloading input file {input_path}")
get_auth()
with open(input_path, "wb") as f:
res = get_input_file(year, day)
f.write(res.read())
run_day(script_path, input_path)
def run_day(script_path, input_path):
try:
print(f"> running {script_path}")
start = time.time()
res = subprocess.run([sys.executable, script_path.absolute(), input_path.absolute()], check=True, stdout=subprocess.PIPE, timeout=30)
elapsed = time.time() - start
print(res.stdout.decode())
print(f"> ran {script_path} in {elapsed:.3f}s")
except subprocess.TimeoutExpired:
print(f"> timeout {script_path} after 30s", file=sys.stderr)