11r"""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
55It finds the largest axis-aligned rectangle that can be formed in a tile grid
66using 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
99The module contains a Solution class that inherits from SolutionBase and
1010implements coordinate compression, flood fill, and rectangle validation
2020
2121@dataclass (frozen = True , slots = True )
2222class 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