mirror of
https://github.com/thib8956/advent-of-code.git
synced 2025-08-23 16:01:59 +00:00
chore: create new project structure and aoc.py runner script
This commit is contained in:
19
adventofcode/2015/day1/day1.py
Normal file
19
adventofcode/2015/day1/day1.py
Normal 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())
|
||||
|
24
adventofcode/2018/day1/day1.py
Normal file
24
adventofcode/2018/day1/day1.py
Normal 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())
|
||||
|
30
adventofcode/2018/day2/day2.py
Normal file
30
adventofcode/2018/day2/day2.py
Normal 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()])
|
||||
|
55
adventofcode/2018/day3/day3.py
Normal file
55
adventofcode/2018/day3/day3.py
Normal 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()])
|
||||
|
41
adventofcode/2018/day4/day4.py
Normal file
41
adventofcode/2018/day4/day4.py
Normal 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())
|
||||
|
25
adventofcode/2018/day5/day5.py
Normal file
25
adventofcode/2018/day5/day5.py
Normal 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)
|
||||
|
59
adventofcode/2018/day6/day6.py
Normal file
59
adventofcode/2018/day6/day6.py
Normal 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)
|
||||
|
88
adventofcode/2018/day7/day7.py
Normal file
88
adventofcode/2018/day7/day7.py
Normal 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()])
|
||||
|
0
adventofcode/2019/__init__.py
Normal file
0
adventofcode/2019/__init__.py
Normal file
44
adventofcode/2019/day1/day1.py
Normal file
44
adventofcode/2019/day1/day1.py
Normal 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),
|
||||
)
|
65
adventofcode/2019/day11/day11.py
Normal file
65
adventofcode/2019/day11/day11.py
Normal 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
|
||||
|
0
adventofcode/2019/day13/__init__.py
Normal file
0
adventofcode/2019/day13/__init__.py
Normal file
117
adventofcode/2019/day13/day13.py
Normal file
117
adventofcode/2019/day13/day13.py
Normal 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)
|
||||
|
71
adventofcode/2019/day2/day2.py
Normal file
71
adventofcode/2019/day2/day2.py
Normal 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
|
79
adventofcode/2019/day3/day3.py
Normal file
79
adventofcode/2019/day3/day3.py
Normal 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))
|
37
adventofcode/2019/day4/day4.py
Normal file
37
adventofcode/2019/day4/day4.py
Normal 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))
|
177
adventofcode/2019/day5/day5.py
Normal file
177
adventofcode/2019/day5/day5.py
Normal 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())
|
||||
|
192
adventofcode/2019/day5/day52.py
Normal file
192
adventofcode/2019/day5/day52.py
Normal 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)
|
46
adventofcode/2019/day6/day6.py
Normal file
46
adventofcode/2019/day6/day6.py
Normal 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())
|
||||
|
93
adventofcode/2019/day7/day7.py
Normal file
93
adventofcode/2019/day7/day7.py
Normal 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())
|
||||
|
39
adventofcode/2019/day8/day8.py
Normal file
39
adventofcode/2019/day8/day8.py
Normal 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())
|
||||
|
20
adventofcode/2019/day9/day9.py
Normal file
20
adventofcode/2019/day9/day9.py
Normal 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())
|
||||
|
23
adventofcode/2019/intcode/benchmark.py
Normal file
23
adventofcode/2019/intcode/benchmark.py
Normal 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))
|
||||
|
194
adventofcode/2019/intcode/intcode.py
Normal file
194
adventofcode/2019/intcode/intcode.py
Normal 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
|
||||
|
111
adventofcode/2019/intcode/tests.py
Normal file
111
adventofcode/2019/intcode/tests.py
Normal 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]
|
24
adventofcode/2020/day1/day1.py
Normal file
24
adventofcode/2020/day1/day1.py
Normal 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)
|
||||
|
30
adventofcode/2020/day10/day10.py
Normal file
30
adventofcode/2020/day10/day10.py
Normal 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())
|
||||
|
34
adventofcode/2020/day11/day11.py
Normal file
34
adventofcode/2020/day11/day11.py
Normal 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))
|
28
adventofcode/2020/day11/grid.py
Normal file
28
adventofcode/2020/day11/grid.py
Normal 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)
|
||||
)
|
||||
)
|
117
adventofcode/2020/day11/rules.py
Normal file
117
adventofcode/2020/day11/rules.py
Normal 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}
|
58
adventofcode/2020/day14/day14.py
Normal file
58
adventofcode/2020/day14/day14.py
Normal 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))
|
58
adventofcode/2020/day15/day15.py
Normal file
58
adventofcode/2020/day15/day15.py
Normal 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))
|
108
adventofcode/2020/day16/day16.py
Normal file
108
adventofcode/2020/day16/day16.py
Normal 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())
|
||||
|
14
adventofcode/2020/day2/day2.py
Normal file
14
adventofcode/2020/day2/day2.py
Normal 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)
|
||||
|
24
adventofcode/2020/day2/part1.py
Normal file
24
adventofcode/2020/day2/part1.py
Normal 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}")
|
||||
|
28
adventofcode/2020/day2/part2.py
Normal file
28
adventofcode/2020/day2/part2.py
Normal 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}")
|
||||
|
42
adventofcode/2020/day3/day3.py
Normal file
42
adventofcode/2020/day3/day3.py
Normal 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())
|
||||
|
52
adventofcode/2020/day4/day4.py
Normal file
52
adventofcode/2020/day4/day4.py
Normal 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])
|
||||
|
68
adventofcode/2020/day5/day5.py
Normal file
68
adventofcode/2020/day5/day5.py
Normal 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())
|
27
adventofcode/2020/day5/tests.py
Normal file
27
adventofcode/2020/day5/tests.py
Normal 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()
|
30
adventofcode/2020/day6/day6.py
Normal file
30
adventofcode/2020/day6/day6.py
Normal 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)
|
||||
|
47
adventofcode/2020/day7/day7.py
Normal file
47
adventofcode/2020/day7/day7.py
Normal 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())
|
55
adventofcode/2020/day8/day8.py
Normal file
55
adventofcode/2020/day8/day8.py
Normal 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())
|
||||
|
54
adventofcode/2020/day9/day9.py
Normal file
54
adventofcode/2020/day9/day9.py
Normal 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())
|
||||
|
46
adventofcode/2021/day1/day1.py
Normal file
46
adventofcode/2021/day1/day1.py
Normal 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)
|
44
adventofcode/2021/day2/day2.py
Normal file
44
adventofcode/2021/day2/day2.py
Normal 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)
|
69
adventofcode/2021/day3/day3.py
Normal file
69
adventofcode/2021/day3/day3.py
Normal 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)
|
99
adventofcode/2021/day4/day4.py
Normal file
99
adventofcode/2021/day4/day4.py
Normal 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)
|
41
adventofcode/2021/day5/day5.py
Normal file
41
adventofcode/2021/day5/day5.py
Normal 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)
|
28
adventofcode/2021/day6/day6.py
Normal file
28
adventofcode/2021/day6/day6.py
Normal 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)
|
34
adventofcode/2021/day6/day6_2.py
Normal file
34
adventofcode/2021/day6/day6_2.py
Normal 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)}')
|
35
adventofcode/2021/day7/day7.py
Normal file
35
adventofcode/2021/day7/day7.py
Normal 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)
|
||||
|
48
adventofcode/2021/day8/day8.py
Normal file
48
adventofcode/2021/day8/day8.py
Normal 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)
|
14
adventofcode/2022/day1/day1.py
Normal file
14
adventofcode/2022/day1/day1.py
Normal 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)
|
||||
|
40
adventofcode/2022/day2/day2.py
Normal file
40
adventofcode/2022/day2/day2.py
Normal 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()))
|
||||
|
36
adventofcode/2022/day3/day3.py
Normal file
36
adventofcode/2022/day3/day3.py
Normal 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()))
|
||||
|
45
adventofcode/2022/day4/day4.py
Normal file
45
adventofcode/2022/day4/day4.py
Normal 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()])
|
||||
|
48
adventofcode/2023/day1/day1.py
Normal file
48
adventofcode/2023/day1/day1.py
Normal 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)
|
158
adventofcode/2023/day10/day10.py
Normal file
158
adventofcode/2023/day10/day10.py
Normal 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)
|
45
adventofcode/2023/day2/day2.py
Normal file
45
adventofcode/2023/day2/day2.py
Normal 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)
|
||||
|
116
adventofcode/2023/day3/day3.py
Normal file
116
adventofcode/2023/day3/day3.py
Normal 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)
|
44
adventofcode/2023/day4/day4.py
Normal file
44
adventofcode/2023/day4/day4.py
Normal 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)
|
||||
|
70
adventofcode/2023/day5/day5.py
Normal file
70
adventofcode/2023/day5/day5.py
Normal 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)
|
||||
|
40
adventofcode/2023/day6/day6.py
Normal file
40
adventofcode/2023/day6/day6.py
Normal 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}")
|
70
adventofcode/2023/day7/day7.py
Normal file
70
adventofcode/2023/day7/day7.py
Normal 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}")
|
75
adventofcode/2023/day8/day8.py
Normal file
75
adventofcode/2023/day8/day8.py
Normal 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)
|
33
adventofcode/2023/day9/day9.py
Normal file
33
adventofcode/2023/day9/day9.py
Normal 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)
|
23
adventofcode/2024/day1/day1.py
Normal file
23
adventofcode/2024/day1/day1.py
Normal 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)
|
||||
|
68
adventofcode/2024/day10/day10.py
Normal file
68
adventofcode/2024/day10/day10.py
Normal 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
37
adventofcode/2024/day11/:
Normal 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)
|
||||
|
42
adventofcode/2024/day11/day11.py
Normal file
42
adventofcode/2024/day11/day11.py
Normal 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)
|
||||
|
83
adventofcode/2024/day12/day12.py
Normal file
83
adventofcode/2024/day12/day12.py
Normal 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)
|
||||
|
46
adventofcode/2024/day13/day13.py
Normal file
46
adventofcode/2024/day13/day13.py
Normal 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)
|
||||
|
106
adventofcode/2024/day14/day14.py
Normal file
106
adventofcode/2024/day14/day14.py
Normal 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)
|
||||
|
110
adventofcode/2024/day15/day15.py
Normal file
110
adventofcode/2024/day15/day15.py
Normal 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)
|
||||
|
117
adventofcode/2024/day16/day16.py
Normal file
117
adventofcode/2024/day16/day16.py
Normal 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)
|
||||
|
81
adventofcode/2024/day18/day18.py
Normal file
81
adventofcode/2024/day18/day18.py
Normal 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))
|
||||
|
36
adventofcode/2024/day2/day2.py
Normal file
36
adventofcode/2024/day2/day2.py
Normal 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)
|
||||
|
39
adventofcode/2024/day3/day3.py
Normal file
39
adventofcode/2024/day3/day3.py
Normal 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)
|
||||
|
72
adventofcode/2024/day4/day4.py
Normal file
72
adventofcode/2024/day4/day4.py
Normal 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)
|
||||
|
45
adventofcode/2024/day5/day5.py
Normal file
45
adventofcode/2024/day5/day5.py
Normal 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())
|
80
adventofcode/2024/day6/day6.py
Normal file
80
adventofcode/2024/day6/day6.py
Normal 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)
|
||||
|
36
adventofcode/2024/day7/day7.py
Normal file
36
adventofcode/2024/day7/day7.py
Normal 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)
|
||||
|
71
adventofcode/2024/day8/day8.py
Normal file
71
adventofcode/2024/day8/day8.py
Normal 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())
|
||||
|
120
adventofcode/2024/day9/day9.py
Normal file
120
adventofcode/2024/day9/day9.py
Normal 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
41
adventofcode/aoc.py
Normal 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
73
adventofcode/helper.py
Normal 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)
|
||||
|
Reference in New Issue
Block a user