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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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