Skip to content

Commit ec971db

Browse files
committed
Day 16 solution
1 parent 6432a9b commit ec971db

File tree

1 file changed

+150
-0
lines changed

1 file changed

+150
-0
lines changed

solutions/day16.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
from typing import List, Tuple
2+
3+
from aoc.models.base import SolutionBase
4+
5+
6+
class Solution(SolutionBase):
7+
"""Solution for Advent of Code 2024 - Day 16: Reindeer Maze.
8+
9+
This class solves a puzzle involving finding optimal paths through a maze with
10+
movement costs. Reindeer start facing east and can move forward (cost 1) or rotate
11+
90 degrees (cost 1000). Part 1 finds the minimum cost to reach the end, while Part 2
12+
counts all tiles that are part of any optimal path.
13+
14+
Input format:
15+
- Grid of characters where:
16+
'#' represents walls
17+
'S' represents the starting position
18+
'E' represents the ending position
19+
'.' represents empty space
20+
21+
This class inherits from `SolutionBase` and provides methods to find and analyze
22+
optimal paths through the maze considering movement and rotation costs.
23+
"""
24+
25+
def find_start_end(self, grid: List[List[str]]) -> Tuple[Tuple[int, int], Tuple[int, int]]:
26+
"""Find the start and end positions in the maze.
27+
28+
Args:
29+
grid (List[List[str]]): 2D grid representation of the maze where 'S' marks
30+
the start and 'E' marks the end.
31+
32+
Returns:
33+
Tuple containing:
34+
- Tuple[int, int]: Coordinates (y, x) of start position
35+
- Tuple[int, int]: Coordinates (y, x) of end position
36+
"""
37+
start = end = None
38+
for y, row in enumerate(grid):
39+
for x, cell in enumerate(row):
40+
if cell == "S":
41+
start = (y, x)
42+
43+
elif cell == "E":
44+
end = (y, x)
45+
46+
if start and end:
47+
return start, end
48+
49+
return start, end
50+
51+
def find_routes(self, data: List[str]) -> List[Tuple[List[Tuple[int, int]], int]]:
52+
"""Find all possible routes through the maze and their costs.
53+
54+
Args:
55+
data (List[str]): Input lines representing the maze grid.
56+
57+
Returns:
58+
List[Tuple[List[Tuple[int, int]], int]]: List of tuples where each contains:
59+
- List[Tuple[int, int]]: List of coordinates representing the path
60+
- int: Total cost of the path (1 per forward move, 1000 per rotation)
61+
62+
The function uses a breadth-first search approach, tracking:
63+
- Complete path history for each route
64+
- Movement costs (forward=1, rotation=1000)
65+
- Direction facing (0=right, 1=up, 2=left, 3=down)
66+
- Previously visited states to avoid cycles
67+
"""
68+
# Convert input to grid
69+
grid = [list(row) for row in data]
70+
start, end = self.find_start_end(grid)
71+
72+
# Direction vectors: right, up, left, down
73+
directions = [(0, 1), (-1, 0), (0, -1), (1, 0)]
74+
routes = []
75+
visited = {} # (pos, direction) -> min_score
76+
77+
# Queue: (position, path_history, current_score, current_direction)
78+
queue = [(start, [start], 0, 0)] # Start facing right (0)
79+
80+
while queue:
81+
pos, history, curr_score, curr_dir = queue.pop(0)
82+
y, x = pos
83+
84+
# Check if we reached the end
85+
if pos == end:
86+
routes.append((history, curr_score))
87+
continue
88+
89+
# Check if we've seen this state with a better score
90+
state = (pos, curr_dir)
91+
if state in visited and visited[state] < curr_score:
92+
continue
93+
94+
visited[state] = curr_score
95+
96+
# Try each direction
97+
for new_dir, (dy, dx) in enumerate(directions):
98+
# Skip reverse direction
99+
if (curr_dir + 2) % 4 == new_dir:
100+
continue
101+
102+
new_y, new_x = y + dy, x + dx
103+
104+
# Check if new position is valid
105+
if (
106+
0 <= new_y < len(grid)
107+
and 0 <= new_x < len(grid[0])
108+
and grid[new_y][new_x] != "#"
109+
and (new_y, new_x) not in history
110+
):
111+
112+
if new_dir == curr_dir:
113+
# Moving forward
114+
queue.append(
115+
((new_y, new_x), history + [(new_y, new_x)], curr_score + 1, new_dir)
116+
)
117+
else:
118+
# Turning (stay in same position)
119+
queue.append((pos, history, curr_score + 1000, new_dir))
120+
121+
return routes
122+
123+
def part1(self, data: List[str]) -> int:
124+
"""Find the minimum cost to reach the end of the maze.
125+
126+
Args:
127+
data (List[str]): Input lines representing the maze grid.
128+
129+
Returns:
130+
int: Minimum total cost (moves + rotations) to reach the end position
131+
while starting facing east.
132+
"""
133+
possible_routes = self.find_routes(data)
134+
return min(route[1] for route in possible_routes)
135+
136+
def part2(self, data: List[str]) -> int:
137+
"""Count tiles that are part of any optimal (minimum cost) path through the maze.
138+
139+
Args:
140+
data (List[str]): Input lines representing the maze grid.
141+
142+
Returns:
143+
int: Number of unique tiles (positions) that appear in any path that
144+
reaches the end with minimum total cost.
145+
"""
146+
possible_routes = self.find_routes(data)
147+
min_score = min(route[1] for route in possible_routes)
148+
best_routes = [route for route in possible_routes if route[1] == min_score]
149+
optimal_tiles = {tile for route in best_routes for tile in route[0]}
150+
return len(optimal_tiles)

0 commit comments

Comments
 (0)