Skip to content

Commit 74080c7

Browse files
committed
Day 18 solution
1 parent 2022222 commit 74080c7

File tree

1 file changed

+168
-0
lines changed

1 file changed

+168
-0
lines changed

solutions/day18.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
from collections import deque
2+
from typing import List, Tuple
3+
4+
from aoc.models.base import SolutionBase
5+
6+
7+
class Solution(SolutionBase):
8+
"""Solution for Advent of Code 2024 - Day 18: RAM Run.
9+
10+
This class solves a puzzle involving pathfinding in a corrupted memory space.
11+
The memory space is represented as a grid where bytes fall and corrupt specific
12+
coordinates. Part 1 finds the shortest path from start to exit through uncorrupted
13+
spaces, while Part 2 determines which falling byte first makes the exit unreachable.
14+
15+
Input format:
16+
- Multiple lines of comma-separated coordinates (x,y)
17+
- Each coordinate represents where a byte will fall and corrupt
18+
- Coordinates start from (0,0) at top-left
19+
- Grid size is 7x7 for test data, 71x71 for real data
20+
21+
This class inherits from `SolutionBase` and provides methods to construct grids,
22+
find paths, and analyze byte corruption patterns.
23+
"""
24+
25+
def construct_grid(
26+
self, size: int, coordinates: List[Tuple[int, int]], limit: int
27+
) -> List[str]:
28+
"""Construct a grid representation of the memory space with corrupted bytes.
29+
30+
Args:
31+
size (int): Size of the grid (both width and height)
32+
coordinates (List[Tuple[int, int]]): List of (x,y) coordinates where bytes fall
33+
limit (int): Number of coordinates to process (12 for test, 1024 for real data)
34+
35+
Returns:
36+
List[str]: Grid representation where '.' is safe and '#' is corrupted
37+
"""
38+
grid = [["."] * size for _ in range(size)]
39+
40+
for col, row in coordinates[:limit]:
41+
grid[row][col] = "#"
42+
43+
return ["".join(row) for row in grid]
44+
45+
def has_path(self, grid: List[str]) -> bool:
46+
"""Check if there exists any path from start to end through uncorrupted spaces.
47+
48+
Uses breadth-first search to efficiently determine path existence without
49+
calculating the actual path length.
50+
51+
Args:
52+
grid (List[str]): Current state of the memory space
53+
54+
Returns:
55+
bool: True if a path exists to the exit, False otherwise
56+
"""
57+
size = len(grid)
58+
start = (0, 0)
59+
end = (size - 1, size - 1)
60+
queue = deque([start])
61+
seen = {start}
62+
63+
while queue:
64+
x, y = queue.popleft()
65+
66+
if (x, y) == end:
67+
return True
68+
69+
for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
70+
new_x, new_y = x + dx, y + dy
71+
if (
72+
0 <= new_x < size
73+
and 0 <= new_y < size
74+
and grid[new_y][new_x] == "."
75+
and (new_x, new_y) not in seen
76+
):
77+
seen.add((new_x, new_y))
78+
queue.append((new_x, new_y))
79+
80+
return False
81+
82+
def find_shortest_path(self, grid: List[str]) -> int:
83+
"""Find the shortest path from start to end through uncorrupted spaces.
84+
85+
Uses breadth-first search to guarantee the shortest path is found.
86+
Returns -1 if no path exists.
87+
88+
Args:
89+
grid (List[str]): Current state of the memory space
90+
91+
Returns:
92+
int: Length of shortest path to exit, or -1 if no path exists
93+
"""
94+
size = len(grid)
95+
start = (0, 0)
96+
end = (size - 1, size - 1)
97+
queue = deque([(start, 0)]) # (position, path_length)
98+
seen = {start}
99+
100+
while queue:
101+
position, length = queue.popleft()
102+
103+
if position == end:
104+
return length
105+
106+
x, y = position
107+
for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
108+
new_x, new_y = x + dx, y + dy
109+
new_pos = (new_x, new_y)
110+
if (
111+
0 <= new_x < size
112+
and 0 <= new_y < size
113+
and grid[new_y][new_x] == "."
114+
and new_pos not in seen
115+
):
116+
seen.add(new_pos)
117+
queue.append((new_pos, length + 1))
118+
119+
return -1
120+
121+
def part1(self, data: List[str]) -> int:
122+
"""Find shortest path to exit after initial byte corruption.
123+
124+
For test data (7x7 grid), simulates first 12 bytes falling.
125+
For real data (71x71 grid), simulates first 1024 bytes falling.
126+
127+
Args:
128+
data (List[str]): Input lines containing byte fall coordinates
129+
130+
Returns:
131+
int: Length of shortest path to exit, or -1 if no path exists
132+
"""
133+
coordinates = [tuple(map(int, row.split(","))) for row in data]
134+
limit = 12 if len(coordinates) == 25 else 1024
135+
grid_size = max(n for pair in coordinates for n in pair) + 1
136+
grid = self.construct_grid(grid_size, coordinates, limit)
137+
return self.find_shortest_path(grid)
138+
139+
def part2(self, data: List[str]) -> str:
140+
"""Find coordinates of first byte that makes exit unreachable.
141+
142+
Uses binary search to efficiently find the critical byte that blocks
143+
all possible paths to the exit.
144+
145+
Args:
146+
data (List[str]): Input lines containing byte fall coordinates
147+
148+
Returns:
149+
str: Coordinates of blocking byte as "x,y" string
150+
"""
151+
coordinates = [tuple(map(int, row.split(","))) for row in data]
152+
grid_size = max(n for pair in coordinates for n in pair) + 1
153+
154+
# Binary search for the first coordinate that blocks all paths
155+
left = 0
156+
right = len(coordinates)
157+
158+
while right - left > 1:
159+
mid = (left + right) // 2
160+
grid = self.construct_grid(grid_size, coordinates, mid)
161+
162+
if self.has_path(grid):
163+
left = mid
164+
else:
165+
right = mid
166+
167+
# right is now the index of the first coordinate that blocks all paths
168+
return f"{coordinates[right-1][0]},{coordinates[right-1][1]}"

0 commit comments

Comments
 (0)