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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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