Skip to content

Commit 37800e1

Browse files
committed
chore: aoc 2025 day 9 docstrings on additional class methods
1 parent 7c82a8d commit 37800e1

File tree

1 file changed

+160
-2
lines changed

1 file changed

+160
-2
lines changed

_2025/solutions/day09.py

Lines changed: 160 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
r"""Day 9: Movie Theater
22
3-
This module provides the solution for Advent of Code 2025 - Day 9
3+
This module provides the solution for Advent of Code 2025 - Day 9.
44
55
It finds the largest axis-aligned rectangle that can be formed in a tile grid
66
using red tiles as opposite corners (Part 1), then extends the search to allow
7-
rectangles that include both red and \"green\" tiles (Part 2).
7+
rectangles that include both red and "green" tiles (Part 2).
88
99
The module contains a Solution class that inherits from SolutionBase and
1010
implements coordinate compression, flood fill, and rectangle validation
@@ -20,20 +20,41 @@
2020

2121
@dataclass(frozen=True, slots=True)
2222
class CompressedTiles:
23+
"""Coordinate-compressed representation of the input tile set.
24+
25+
The original problem coordinates may be large and sparse. This structure
26+
compresses the coordinate space down to the unique x- and y-values that
27+
occur in the input, allowing grid-based operations to run on a much smaller
28+
grid.
29+
30+
Attributes
31+
----------
32+
xs:
33+
Sorted unique x-coordinates present in the input.
34+
ys:
35+
Sorted unique y-coordinates present in the input.
36+
coords:
37+
List of (x_idx, y_idx) pairs, where each index refers into `xs`/`ys`.
38+
These represent the red tile positions in compressed space.
39+
"""
40+
2341
xs: list[int]
2442
ys: list[int]
2543
coords: list[tuple[int, int]] # (x_idx, y_idx)
2644

2745
@property
2846
def width(self) -> int:
47+
"""Return the compressed grid width (number of unique x-values)."""
2948
return len(self.xs)
3049

3150
@property
3251
def height(self) -> int:
52+
"""Return the compressed grid height (number of unique y-values)."""
3353
return len(self.ys)
3454

3555
@property
3656
def N(self) -> int: # noqa: N802
57+
"""Return the number of red tiles in the input."""
3758
return len(self.coords)
3859

3960

@@ -54,6 +75,20 @@ class Solution(SolutionBase):
5475
DIRECTIONS: ClassVar[tuple[tuple[int, int], ...]] = ((1, 0), (-1, 0), (0, 1), (0, -1))
5576

5677
def parse_data(self, data: list[str]) -> CompressedTiles:
78+
r"""Parse input lines and build a coordinate-compressed tile set.
79+
80+
Each input line contains an integer coordinate pair in the form \"X,Y\".
81+
This method extracts all red tile positions, builds the sorted unique
82+
x- and y-coordinate lists, and converts each coordinate into its
83+
corresponding compressed index pair.
84+
85+
Args:
86+
data: List of \"X,Y\" strings representing red tile positions.
87+
88+
Returns
89+
-------
90+
CompressedTiles: Compressed coordinate lists and red tile indices.
91+
"""
5792
tiles = [tuple(map(int, line.split(","))) for line in data if line.strip()]
5893

5994
xs = sorted({x for x, _ in tiles})
@@ -66,21 +101,90 @@ def parse_data(self, data: list[str]) -> CompressedTiles:
66101
return CompressedTiles(xs=xs, ys=ys, coords=coords)
67102

68103
def calculate_area(self, x1: int, y1: int, x2: int, y2: int) -> int:
104+
r"""Compute the inclusive area of an axis-aligned rectangle on the input grid.
105+
106+
The rectangle is defined by two opposite corners (x1, y1) and (x2, y2),
107+
and includes both boundary lines (hence the +1 in each dimension).
108+
109+
Args:
110+
x1: X-coordinate of the first corner.
111+
y1: Y-coordinate of the first corner.
112+
x2: X-coordinate of the opposite corner.
113+
y2: Y-coordinate of the opposite corner.
114+
115+
Returns
116+
-------
117+
int: Inclusive rectangle area \( (|x2-x1|+1) * (|y2-y1|+1) \).
118+
"""
69119
return (abs(x2 - x1) + 1) * (abs(y2 - y1) + 1)
70120

71121
def construct_grid(self, height: int, width: int, value: int = 0) -> list[list[int]]:
122+
"""Construct an integer grid initialized to a constant value.
123+
124+
This grid is used as a compact mask over the compressed coordinate
125+
space. Cell values are interpreted as:
126+
- 0: empty
127+
- 1: red tile
128+
- 2: green tile (boundary or filled interior)
129+
130+
Args:
131+
height: Number of rows.
132+
width: Number of columns.
133+
value: Initial integer value for all cells.
134+
135+
Returns
136+
-------
137+
list[list[int]]: A height x width integer grid.
138+
"""
72139
return [[value] * width for _ in range(height)]
73140

74141
def construct_bool_grid(
75142
self, height: int, width: int, *, value: bool = False
76143
) -> list[list[bool]]:
144+
"""Construct a boolean grid initialized to a constant value.
145+
146+
This is used for marking reachability during flood fill (e.g. which
147+
empty cells are reachable from the exterior boundary).
148+
149+
Args:
150+
height: Number of rows.
151+
width: Number of columns.
152+
value: Initial boolean value for all cells.
153+
154+
Returns
155+
-------
156+
list[list[bool]]: A height x width boolean grid.
157+
"""
77158
return [[value] * width for _ in range(height)]
78159

79160
def mark_red_tiles(self, grid: list[list[int]], coords: list[tuple[int, int]]) -> None:
161+
"""Mark red tiles on the compressed grid.
162+
163+
Sets grid cells corresponding to red tile coordinates to 1.
164+
165+
Args:
166+
grid: Integer grid to modify in-place.
167+
coords: List of (x_idx, y_idx) red tile positions in compressed space.
168+
"""
80169
for x_idx, y_idx in coords:
81170
grid[y_idx][x_idx] = 1
82171

83172
def mark_green_boundary(self, grid: list[list[int]], coords: list[tuple[int, int]]) -> None:
173+
"""Trace the loop edges between red tiles and mark boundary tiles as green.
174+
175+
The input red tiles are treated as vertices of a closed polygonal loop
176+
in the given order. Consecutive tiles (including last->first) must be
177+
axis-aligned. Any empty grid cells along these connecting segments are
178+
marked as green (2), leaving red cells intact.
179+
180+
Args:
181+
grid: Integer grid to modify in-place.
182+
coords: List of (x_idx, y_idx) red tile positions in loop order.
183+
184+
Raises
185+
------
186+
ValueError: If a segment between consecutive points is not axis-aligned.
187+
"""
84188
N = len(coords) # noqa: N806
85189
for i in range(N):
86190
x1, y1 = coords[i]
@@ -110,11 +214,38 @@ def seed_if_outside_empty(
110214
x: int,
111215
y: int,
112216
) -> None:
217+
"""Seed the flood fill queue with an exterior empty cell if eligible.
218+
219+
This helper is used to initialize the outside flood fill from boundary
220+
positions. A cell is added if it is empty (0) and has not already been
221+
marked as outside.
222+
223+
Args:
224+
grid: Integer grid containing tile markings.
225+
outside: Boolean grid marking cells known to be reachable from outside.
226+
queue: Flood fill queue of (x, y) positions to explore.
227+
x: Column index to check.
228+
y: Row index to check.
229+
"""
113230
if grid[y][x] == 0 and not outside[y][x]:
114231
outside[y][x] = True
115232
queue.append((x, y))
116233

117234
def flood_fill_outside_zeros(self, grid: list[list[int]]) -> list[list[bool]]:
235+
"""Mark all empty cells reachable from the grid boundary.
236+
237+
This performs a BFS flood fill over cells with value 0, starting from
238+
all boundary positions. The result is a boolean mask indicating which
239+
empty cells are connected to the exterior and therefore not part of the
240+
enclosed interior.
241+
242+
Args:
243+
grid: Integer grid where 0 denotes empty and non-zero denotes blocked.
244+
245+
Returns
246+
-------
247+
list[list[bool]]: Boolean grid where True indicates an outside-reachable empty cell.
248+
"""
118249
height = len(grid)
119250
width = len(grid[0])
120251

@@ -145,6 +276,16 @@ def flood_fill_outside_zeros(self, grid: list[list[int]]) -> list[list[bool]]:
145276
return outside
146277

147278
def fill_interior_as_green(self, grid: list[list[int]], outside: list[list[bool]]) -> None:
279+
"""Convert enclosed empty cells into green tiles.
280+
281+
After flood-filling the outside, any remaining empty (0) cells that are
282+
not marked outside are interior to the loop. These are re-labeled as
283+
green (2) in-place.
284+
285+
Args:
286+
grid: Integer grid to modify in-place.
287+
outside: Boolean grid marking which empty cells are reachable from outside.
288+
"""
148289
height = len(grid)
149290
width = len(grid[0])
150291

@@ -161,6 +302,23 @@ def rectangle_all_non_zero(
161302
y_top: int,
162303
y_bottom: int,
163304
) -> bool:
305+
"""Check whether a rectangle contains no empty cells.
306+
307+
The rectangle bounds are inclusive and are expressed in compressed grid
308+
indices. A rectangle is valid if every cell within the bounds is
309+
non-zero (i.e. red or green).
310+
311+
Args:
312+
grid: Integer grid containing 0 for empty and non-zero for occupied.
313+
x_left: Left boundary (inclusive) in x index space.
314+
x_right: Right boundary (inclusive) in x index space.
315+
y_top: Top boundary (inclusive) in y index space.
316+
y_bottom: Bottom boundary (inclusive) in y index space.
317+
318+
Returns
319+
-------
320+
bool: True if all cells in the rectangle are non-zero, otherwise False.
321+
"""
164322
for y in range(y_top, y_bottom + 1):
165323
row = grid[y]
166324
for x in range(x_left, x_right + 1):

0 commit comments

Comments
 (0)