1212"""
1313
1414from collections import deque
15+ from dataclasses import dataclass
16+ from typing import ClassVar
1517
1618from aoc .models .base import SolutionBase
1719
1820
21+ @dataclass (frozen = True , slots = True )
22+ class CompressedTiles :
23+ xs : list [int ]
24+ ys : list [int ]
25+ coords : list [tuple [int , int ]] # (x_idx, y_idx)
26+
27+ @property
28+ def width (self ) -> int :
29+ return len (self .xs )
30+
31+ @property
32+ def height (self ) -> int :
33+ return len (self .ys )
34+
35+ @property
36+ def N (self ) -> int : # noqa: N802
37+ return len (self .coords )
38+
39+
1940class Solution (SolutionBase ):
2041 """Find largest valid rectangles in a movie theater tile grid.
2142
@@ -30,6 +51,124 @@ class Solution(SolutionBase):
3051 rectangle search is combined with a grid mask to validate candidate areas.
3152 """
3253
54+ DIRECTIONS : ClassVar [tuple [tuple [int , int ], ...]] = ((1 , 0 ), (- 1 , 0 ), (0 , 1 ), (0 , - 1 ))
55+
56+ def parse_data (self , data : list [str ]) -> CompressedTiles :
57+ tiles = [tuple (map (int , line .split ("," ))) for line in data if line .strip ()]
58+
59+ xs = sorted ({x for x , _ in tiles })
60+ ys = sorted ({y for _ , y in tiles })
61+
62+ x_to_idx = {x : i for i , x in enumerate (xs )}
63+ y_to_idx = {y : i for i , y in enumerate (ys )}
64+
65+ coords = [(x_to_idx [x ], y_to_idx [y ]) for x , y in tiles ]
66+ return CompressedTiles (xs = xs , ys = ys , coords = coords )
67+
68+ def calculate_area (self , x1 : int , y1 : int , x2 : int , y2 : int ) -> int :
69+ return (abs (x2 - x1 ) + 1 ) * (abs (y2 - y1 ) + 1 )
70+
71+ def construct_grid (self , height : int , width : int , value : int = 0 ) -> list [list [int ]]:
72+ return [[value ] * width for _ in range (height )]
73+
74+ def construct_bool_grid (
75+ self , height : int , width : int , * , value : bool = False
76+ ) -> list [list [bool ]]:
77+ return [[value ] * width for _ in range (height )]
78+
79+ def mark_red_tiles (self , grid : list [list [int ]], coords : list [tuple [int , int ]]) -> None :
80+ for x_idx , y_idx in coords :
81+ grid [y_idx ][x_idx ] = 1
82+
83+ def mark_green_boundary (self , grid : list [list [int ]], coords : list [tuple [int , int ]]) -> None :
84+ N = len (coords ) # noqa: N806
85+ for i in range (N ):
86+ x1 , y1 = coords [i ]
87+ x2 , y2 = coords [(i + 1 ) % N ]
88+
89+ if x1 == x2 :
90+ y_min , y_max = sorted ((y1 , y2 ))
91+ for y in range (y_min , y_max + 1 ):
92+ if grid [y ][x1 ] == 0 :
93+ grid [y ][x1 ] = 2
94+
95+ elif y1 == y2 :
96+ x_min , x_max = sorted ((x1 , x2 ))
97+ for x in range (x_min , x_max + 1 ):
98+ if grid [y1 ][x ] == 0 :
99+ grid [y1 ][x ] = 2
100+
101+ else :
102+ err_msg = "Non axis-aligned segment in input"
103+ raise ValueError (err_msg )
104+
105+ def seed_if_outside_empty (
106+ self ,
107+ grid : list [list [int ]],
108+ outside : list [list [bool ]],
109+ queue : deque [tuple [int , int ]],
110+ x : int ,
111+ y : int ,
112+ ) -> None :
113+ if grid [y ][x ] == 0 and not outside [y ][x ]:
114+ outside [y ][x ] = True
115+ queue .append ((x , y ))
116+
117+ def flood_fill_outside_zeros (self , grid : list [list [int ]]) -> list [list [bool ]]:
118+ height = len (grid )
119+ width = len (grid [0 ])
120+
121+ outside = self .construct_bool_grid (height , width , value = False )
122+ queue : deque [tuple [int , int ]] = deque ()
123+
124+ for x in range (width ):
125+ self .seed_if_outside_empty (grid , outside , queue , x , 0 )
126+ self .seed_if_outside_empty (grid , outside , queue , x , height - 1 )
127+
128+ for y in range (height ):
129+ self .seed_if_outside_empty (grid , outside , queue , 0 , y )
130+ self .seed_if_outside_empty (grid , outside , queue , width - 1 , y )
131+
132+ while queue :
133+ x , y = queue .popleft ()
134+ for dx , dy in self .DIRECTIONS :
135+ nx , ny = x + dx , y + dy
136+ if (
137+ nx in range (width )
138+ and ny in range (height )
139+ and not outside [ny ][nx ]
140+ and grid [ny ][nx ] == 0
141+ ):
142+ outside [ny ][nx ] = True
143+ queue .append ((nx , ny ))
144+
145+ return outside
146+
147+ def fill_interior_as_green (self , grid : list [list [int ]], outside : list [list [bool ]]) -> None :
148+ height = len (grid )
149+ width = len (grid [0 ])
150+
151+ for y in range (height ):
152+ for x in range (width ):
153+ if grid [y ][x ] == 0 and not outside [y ][x ]:
154+ grid [y ][x ] = 2
155+
156+ def rectangle_all_non_zero (
157+ self ,
158+ grid : list [list [int ]],
159+ x_left : int ,
160+ x_right : int ,
161+ y_top : int ,
162+ y_bottom : int ,
163+ ) -> bool :
164+ for y in range (y_top , y_bottom + 1 ):
165+ row = grid [y ]
166+ for x in range (x_left , x_right + 1 ):
167+ if row [x ] == 0 :
168+ return False
169+
170+ return True
171+
33172 def part1 (self , data : list [str ]) -> int :
34173 r"""Find largest rectangle area using two red tiles as opposite corners.
35174
@@ -45,41 +184,24 @@ def part1(self, data: list[str]) -> int:
45184 -------
46185 int: Largest rectangle area using two red tiles as opposite corners
47186 """
48- tiles = [ tuple ( map ( int , line . split ( "," ))) for line in data ]
187+ tiles = self . parse_data ( data )
49188
50- if len ( tiles ) < 2 :
189+ if tiles . N < 2 :
51190 return 0
52191
53- # Coordinate compression (space distortion)
54- xs = sorted ({x for x , _ in tiles })
55- ys = sorted ({y for _ , y in tiles })
56-
57- x_to_idx = {x : i for i , x in enumerate (xs )}
58- y_to_idx = {y : i for i , y in enumerate (ys )}
59-
60- compressed_tiles : list [tuple [int , int ]] = [(x_to_idx [x ], y_to_idx [y ]) for x , y in tiles ]
61-
62192 max_area = 0
63- n = len ( compressed_tiles )
193+ N = tiles . N # noqa: N806
64194
65- for i in range (n ):
66- x1_idx , y1_idx = compressed_tiles [i ]
67- for j in range (i + 1 , n ):
68- x2_idx , y2_idx = compressed_tiles [j ]
69-
70- # Opposite corners must differ in both x and y to form area
195+ for i in range (N ):
196+ x1_idx , y1_idx = tiles .coords [i ]
197+ for j in range (i + 1 , N ):
198+ x2_idx , y2_idx = tiles .coords [j ]
71199 if x1_idx == x2_idx or y1_idx == y2_idx :
72200 continue
73201
74- # Get original coordinates
75- x1 = xs [x1_idx ]
76- y1 = ys [y1_idx ]
77- x2 = xs [x2_idx ]
78- y2 = ys [y2_idx ]
79-
80- width = abs (x2 - x1 ) + 1
81- height = abs (y2 - y1 ) + 1
82- area = width * height
202+ x1 , y1 = tiles .xs [x1_idx ], tiles .ys [y1_idx ]
203+ x2 , y2 = tiles .xs [x2_idx ], tiles .ys [y2_idx ]
204+ area = self .calculate_area (x1 , y1 , x2 , y2 )
83205
84206 if area > max_area :
85207 max_area = area
@@ -105,111 +227,38 @@ def part2(self, data: list[str]) -> int:
105227 int: Largest rectangle area that uses red tiles as opposite corners
106228 and includes only red or green tiles inside
107229 """
108- tiles = [ tuple ( map ( int , line . split ( "," ))) for line in data if line . strip ()]
230+ tiles = self . parse_data ( data )
109231
110- if len ( tiles ) < 2 :
232+ if tiles . N < 2 :
111233 return 0
112234
113- # Coordinate compression
114- xs = sorted ({x for x , _ in tiles })
115- ys = sorted ({y for _ , y in tiles })
116- x_to_idx = {x : i for i , x in enumerate (xs )}
117- y_to_idx = {y : i for i , y in enumerate (ys )}
118- compressed = [(x_to_idx [x ], y_to_idx [y ]) for x , y in tiles ]
235+ grid = self .construct_grid (tiles .height , tiles .width , 0 )
236+ self .mark_red_tiles (grid , tiles .coords )
237+ self .mark_green_boundary (grid , tiles .coords )
119238
120- w , h = len (xs ), len (ys )
121- # 0 = empty, 1 = red, 2 = green
122- grid = [[0 ] * w for _ in range (h )]
239+ outside = self .flood_fill_outside_zeros (grid )
240+ self .fill_interior_as_green (grid , outside )
123241
124- # Mark red tiles
125- for cx , cy in compressed :
126- grid [cy ][cx ] = 1
127-
128- # Draw green boundary segments between consecutive reds (wrap around)
129- n = len (compressed )
130- for i in range (n ):
131- x1 , y1 = compressed [i ]
132- x2 , y2 = compressed [(i + 1 ) % n ]
133- if x1 == x2 :
134- ys_min , ys_max = sorted ((y1 , y2 ))
135- for y in range (ys_min , ys_max + 1 ):
136- if grid [y ][x1 ] == 0 :
137- grid [y ][x1 ] = 2
138- elif y1 == y2 :
139- xs_min , xs_max = sorted ((x1 , x2 ))
140- for x in range (xs_min , xs_max + 1 ):
141- if grid [y1 ][x ] == 0 :
142- grid [y1 ][x ] = 2
143- else :
144- err_msg = "Non axis-aligned segment in input"
145- raise ValueError (err_msg )
146-
147- # Flood-fill outside empty cells
148- outside = [[False ] * w for _ in range (h )]
149- q : deque [tuple [int , int ]] = deque ()
150-
151- # Seed flood fill from outer boundary
152- for x in range (w ):
153- if grid [0 ][x ] == 0 :
154- outside [0 ][x ] = True
155- q .append ((x , 0 ))
156- if grid [h - 1 ][x ] == 0 :
157- outside [h - 1 ][x ] = True
158- q .append ((x , h - 1 ))
159-
160- for y in range (h ):
161- if grid [y ][0 ] == 0 :
162- outside [y ][0 ] = True
163- q .append ((0 , y ))
164- if grid [y ][w - 1 ] == 0 :
165- outside [y ][w - 1 ] = True
166- q .append ((w - 1 , y ))
167-
168- while q :
169- x , y = q .popleft ()
170- for dx , dy in ((1 , 0 ), (- 1 , 0 ), (0 , 1 ), (0 , - 1 )):
171- nx , ny = x + dx , y + dy
172- if 0 <= nx < w and 0 <= ny < h and not outside [ny ][nx ] and grid [ny ][nx ] == 0 :
173- outside [ny ][nx ] = True
174- q .append ((nx , ny ))
175-
176- # Interior empty cells become green
177- for y in range (h ):
178- for x in range (w ):
179- if grid [y ][x ] == 0 and not outside [y ][x ]:
180- grid [y ][x ] = 2
181-
182- # Search for largest valid rectangle (only red/green inside)
183242 max_area = 0
184- n = len ( compressed )
243+ N = tiles . N # noqa: N806
185244
186- for i in range (n ):
187- x1i , y1i = compressed [i ]
188- for j in range (i + 1 , n ):
189- x2i , y2i = compressed [j ]
190- if x1i == x2i or y1i == y2i :
245+ for i in range (N ):
246+ x1_idx , y1_idx = tiles . coords [i ]
247+ for j in range (i + 1 , N ):
248+ x2_idx , y2_idx = tiles . coords [j ]
249+ if x1_idx == x2_idx or y1_idx == y2_idx :
191250 continue
192251
193- xl , xr = sorted ((x1i , x2i ))
194- yt , yb = sorted ((y1i , y2i ))
195-
196- # Check rectangle only contains red/green
197- ok = True
198- for yy in range (yt , yb + 1 ):
199- row = grid [yy ]
200- for xx in range (xl , xr + 1 ):
201- if row [xx ] == 0 :
202- ok = False
203- break
204- if not ok :
205- break
206-
207- if not ok :
252+ x_left , x_right = sorted ((x1_idx , x2_idx ))
253+ y_top , y_bottom = sorted ((y1_idx , y2_idx ))
254+
255+ if not self .rectangle_all_non_zero (grid , x_left , x_right , y_top , y_bottom ):
208256 continue
209257
210- x1 , y1 = xs [x1i ], ys [y1i ]
211- x2 , y2 = xs [x2i ], ys [y2i ]
212- area = (abs (x2 - x1 ) + 1 ) * (abs (y2 - y1 ) + 1 )
258+ x1 , y1 = tiles .xs [x1_idx ], tiles .ys [y1_idx ]
259+ x2 , y2 = tiles .xs [x2_idx ], tiles .ys [y2_idx ]
260+ area = self .calculate_area (x1 , y1 , x2 , y2 )
261+
213262 if area > max_area :
214263 max_area = area
215264
0 commit comments