1- """Boilerplate solution template for Advent of Code daily challenges.
1+ r """Day 9: Movie Theater
22
3- This module provides a template class for solving Advent of Code puzzle problems.
4- It includes a base structure with two method stubs (part1 and part2) that can be
5- implemented for specific day's challenges.
3+ This module provides the solution for Advent of Code 2025 - Day 9
64
7- The template follows the SolutionBase pattern used across the Advent of Code solutions,
8- allowing for consistent handling of input parsing and solution execution.
5+ It finds the largest axis-aligned rectangle that can be formed in a tile grid
6+ 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).
8+
9+ The module contains a Solution class that inherits from SolutionBase and
10+ implements coordinate compression, flood fill, and rectangle validation
11+ to efficiently search the space of candidate rectangles.
912"""
1013
1114from collections import deque
1417
1518
1619class Solution (SolutionBase ):
17- """Solution template for Advent of Code daily puzzle .
20+ """Find largest valid rectangles in a movie theater tile grid .
1821
19- This class provides a standardized structure for implementing solutions to
20- daily Advent of Code challenges. It inherits from SolutionBase and includes
21- method stubs for part1 and part2 of the puzzle.
22+ The input is a list of coordinates where tiles are red. Part 1 treats the
23+ rest of the grid as unconstrained and finds the largest possible rectangle
24+ using any two red tiles as opposite corners. Part 2 connects the red tiles
25+ into a loop of red+green tiles and flood-fills the interior to mark all
26+ green tiles, then searches for the largest rectangle that only contains
27+ red or green tiles.
2228
23- Subclasses should override these methods with specific implementation logic
24- for parsing input and solving the puzzle requirements .
29+ Coordinate compression is used to keep the grid small, and a brute-force
30+ rectangle search is combined with a grid mask to validate candidate areas .
2531 """
2632
2733 def part1 (self , data : list [str ]) -> int :
28- """Solve the first part of the daily puzzle.
34+ r"""Find largest rectangle area using two red tiles as opposite corners.
35+
36+ For every pair of red tiles that differ in both x and y, this method
37+ computes the area of the axis-aligned rectangle they define and tracks
38+ the maximum. The actual grid size does not matter due to the use of
39+ red tile coordinates only.
2940
3041 Args:
31- data: List of input strings to be processed
42+ data: List of \"X,Y\" strings representing red tile positions
3243
3344 Returns
3445 -------
35- int: Solution for part 1 of the puzzle
46+ int: Largest rectangle area using two red tiles as opposite corners
3647 """
3748 tiles = [tuple (map (int , line .split ("," ))) for line in data ]
3849
3950 if len (tiles ) < 2 :
4051 return 0
4152
42- # Coordinate compression (" space distortion" )
53+ # Coordinate compression (space distortion)
4354 xs = sorted ({x for x , _ in tiles })
4455 ys = sorted ({y for _ , y in tiles })
4556
4657 x_to_idx = {x : i for i , x in enumerate (xs )}
4758 y_to_idx = {y : i for i , y in enumerate (ys )}
4859
49- # Compressed positions of red tiles
5060 compressed_tiles : list [tuple [int , int ]] = [(x_to_idx [x ], y_to_idx [y ]) for x , y in tiles ]
5161
52- # Group tiles by compressed row/column if you want small pruning later
53- # but the brute-force over all pairs is already fine for AoC sizes.
5462 max_area = 0
5563 n = len (compressed_tiles )
5664
@@ -79,35 +87,45 @@ def part1(self, data: list[str]) -> int:
7987 return max_area
8088
8189 def part2 (self , data : list [str ]) -> int :
82- """Solve the second part of the daily puzzle.
90+ r"""Find largest rectangle area using only red and green tiles.
91+
92+ First, the red tiles are connected in input order with axis-aligned
93+ segments forming a loop; these segments become green tiles. Then, a
94+ flood fill from the outside marks all empty tiles reachable from the
95+ boundary as \"outside\". Any remaining empty tiles inside the loop are
96+ also marked green. Finally, the method checks all red-opposite-corner
97+ rectangles and keeps the largest whose interior contains only red or
98+ green tiles (no empty tiles).
8399
84100 Args:
85- data: List of input strings to be processed
101+ data: List of \"X,Y\" strings representing red tile positions in loop order
86102
87103 Returns
88104 -------
89- int: Solution for part 2 of the puzzle
105+ int: Largest rectangle area that uses red tiles as opposite corners
106+ and includes only red or green tiles inside
90107 """
91108 tiles = [tuple (map (int , line .split ("," ))) for line in data if line .strip ()]
92109
93110 if len (tiles ) < 2 :
94111 return 0
95112
113+ # Coordinate compression
96114 xs = sorted ({x for x , _ in tiles })
97115 ys = sorted ({y for _ , y in tiles })
98116 x_to_idx = {x : i for i , x in enumerate (xs )}
99- y_to_idx = {y : i for i , y in tiles }
100117 y_to_idx = {y : i for i , y in enumerate (ys )}
101118 compressed = [(x_to_idx [x ], y_to_idx [y ]) for x , y in tiles ]
102119
103120 w , h = len (xs ), len (ys )
104- grid = [[0 ] * w for _ in range (h )] # 0 = empty, 1 = red, 2 = green
121+ # 0 = empty, 1 = red, 2 = green
122+ grid = [[0 ] * w for _ in range (h )]
105123
106124 # Mark red tiles
107125 for cx , cy in compressed :
108126 grid [cy ][cx ] = 1
109127
110- # Draw green boundary segments between consecutive reds (wrap)
128+ # Draw green boundary segments between consecutive reds (wrap around )
111129 n = len (compressed )
112130 for i in range (n ):
113131 x1 , y1 = compressed [i ]
@@ -123,19 +141,22 @@ def part2(self, data: list[str]) -> int:
123141 if grid [y1 ][x ] == 0 :
124142 grid [y1 ][x ] = 2
125143 else :
126- raise ValueError ("Non axis-aligned segment in input" )
144+ err_msg = "Non axis-aligned segment in input"
145+ raise ValueError (err_msg )
127146
128147 # Flood-fill outside empty cells
129148 outside = [[False ] * w for _ in range (h )]
130149 q : deque [tuple [int , int ]] = deque ()
131150
151+ # Seed flood fill from outer boundary
132152 for x in range (w ):
133153 if grid [0 ][x ] == 0 :
134154 outside [0 ][x ] = True
135155 q .append ((x , 0 ))
136156 if grid [h - 1 ][x ] == 0 :
137157 outside [h - 1 ][x ] = True
138158 q .append ((x , h - 1 ))
159+
139160 for y in range (h ):
140161 if grid [y ][0 ] == 0 :
141162 outside [y ][0 ] = True
@@ -158,6 +179,7 @@ def part2(self, data: list[str]) -> int:
158179 if grid [y ][x ] == 0 and not outside [y ][x ]:
159180 grid [y ][x ] = 2
160181
182+ # Search for largest valid rectangle (only red/green inside)
161183 max_area = 0
162184 n = len (compressed )
163185
0 commit comments