mirror of
https://github.com/thib8956/advent-of-code.git
synced 2025-08-24 00:11:57 +00:00
chore: create new project structure and aoc.py runner script
This commit is contained in:
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)
|
||||
|
Reference in New Issue
Block a user