Skip to content

Commit f374924

Browse files
committed
Day 12 solution
1 parent 75dcc8c commit f374924

File tree

1 file changed

+172
-0
lines changed

1 file changed

+172
-0
lines changed

solutions/day12.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
from typing import Callable, List, Set
2+
3+
from aoc.models.base import SolutionBase
4+
5+
6+
class Solution(SolutionBase):
7+
"""Solution for Advent of Code 2024 - Day 12: Garden Groups.
8+
9+
This class solves a puzzle involving analyzing groups of plants in a garden grid.
10+
Each group (region) of identical plants must be fenced, with the cost depending on
11+
either the perimeter (part 1) or the number of distinct sides (part 2) multiplied
12+
by the area of the region.
13+
14+
Input format:
15+
List of strings representing a grid where each character represents a different
16+
type of plant. Adjacent identical characters form regions that need to be fenced.
17+
18+
This class inherits from `SolutionBase` and provides methods to identify connected
19+
regions, calculate their perimeters or distinct sides, and determine total fencing costs.
20+
"""
21+
22+
def find_region(
23+
self, grid: List[List[str]], start_x: int, start_y: int, char: str, visited: set
24+
) -> Set[str]:
25+
"""Find all connected cells containing the same character using depth-first search.
26+
27+
Args:
28+
grid: 2D list representing the garden layout
29+
start_x: Starting x-coordinate to explore from
30+
start_y: Starting y-coordinate to explore from
31+
char: Character type to match for region
32+
visited: Set of already visited coordinates
33+
34+
Returns:
35+
Set of (y, x) coordinates that form the connected region
36+
"""
37+
if (
38+
not (0 <= start_x < len(grid[0]) and 0 <= start_y < len(grid))
39+
or grid[start_y][start_x] != char
40+
or (start_y, start_x) in visited
41+
):
42+
return set()
43+
44+
region = {(start_y, start_x)}
45+
visited.add((start_y, start_x))
46+
47+
for dy, dx in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
48+
region.update(self.find_region(grid, start_x + dx, start_y + dy, char, visited))
49+
50+
return region
51+
52+
def calculate_perimeter(self, grid: List[List[str]], region: Set[str]) -> int:
53+
"""Calculate the total perimeter of a region.
54+
55+
Counts each cell edge that either borders the grid boundary or
56+
neighbors a different plant type.
57+
58+
Args:
59+
grid: 2D list representing the garden layout
60+
region: Set of (y, x) coordinates in the region
61+
62+
Returns:
63+
Total length of the perimeter
64+
"""
65+
perimeter = 0
66+
for y, x in region:
67+
for dy, dx in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
68+
new_y, new_x = y + dy, x + dx
69+
if (new_y, new_x) not in region or not (
70+
0 <= new_x < len(grid[0]) and 0 <= new_y < len(grid)
71+
):
72+
perimeter += 1
73+
74+
return perimeter
75+
76+
def in_region(self, y: int, x: int, region: Set[str]) -> bool:
77+
"""Check if given coordinates are part of the region.
78+
79+
Args:
80+
y: Y-coordinate to check
81+
x: X-coordinate to check
82+
region: Set of (y, x) coordinates in the region
83+
84+
Returns:
85+
True if coordinates are in the region, False otherwise
86+
"""
87+
return (y, x) in region
88+
89+
def count_sides(self, grid: List[List[str]], region: Set[str]) -> int:
90+
"""Count unique sides of a region, merging adjacent parallel edges.
91+
92+
A side is a continuous straight line segment that forms part of the region's
93+
boundary, regardless of its length. Multiple adjacent cell edges in the same
94+
direction count as a single side.
95+
96+
Args:
97+
grid: 2D list representing the garden layout
98+
region: Set of (y, x) coordinates in the region
99+
100+
Returns:
101+
Number of distinct sides in the region's boundary
102+
"""
103+
edges = set()
104+
105+
for y, x in sorted(region):
106+
# Check all four directions for potential edges
107+
if not self.in_region(y - 1, x, region):
108+
if not self.in_region(y, x - 1, region) or self.in_region(y - 1, x - 1, region):
109+
edges.add(("H", y, x))
110+
111+
if not self.in_region(y + 1, x, region):
112+
if not self.in_region(y, x - 1, region) or self.in_region(y + 1, x - 1, region):
113+
edges.add(("H", y + 1, x))
114+
115+
if not self.in_region(y, x - 1, region):
116+
if not self.in_region(y - 1, x, region) or self.in_region(y - 1, x - 1, region):
117+
edges.add(("V", x, y))
118+
119+
if not self.in_region(y, x + 1, region):
120+
if not self.in_region(y - 1, x, region) or self.in_region(y - 1, x + 1, region):
121+
edges.add(("V", x + 1, y))
122+
123+
return len(edges)
124+
125+
def solve(self, data: List[str], calc_func: Callable) -> int:
126+
"""Process the garden grid and calculate total fencing cost.
127+
128+
For each unique plant type, identifies all connected regions and calculates
129+
their price based on area multiplied by either perimeter or number of sides.
130+
131+
Args:
132+
data: List of strings representing the garden grid
133+
calc_func: Function to calculate either perimeter or number of sides
134+
135+
Returns:
136+
Total cost of fencing all regions
137+
"""
138+
grid = [list(line) for line in data]
139+
chars = {char for row in grid for char in row}
140+
total_price = 0
141+
visited = set()
142+
143+
for char in chars:
144+
for y in range(len(grid)):
145+
for x in range(len(grid[0])):
146+
if grid[y][x] == char and (y, x) not in visited:
147+
region = self.find_region(grid, x, y, char, visited)
148+
total_price += len(region) * calc_func(grid, region)
149+
150+
return total_price
151+
152+
def part1(self, data: List[str]) -> int:
153+
"""Calculate total fencing cost using perimeter-based pricing.
154+
155+
Args:
156+
data: List of strings representing the garden grid
157+
158+
Returns:
159+
Total cost when each region's price is `area` * `perimeter`
160+
"""
161+
return self.solve(data, self.calculate_perimeter)
162+
163+
def part2(self, data: List[str]) -> int:
164+
"""Calculate total fencing cost using distinct sides-based pricing.
165+
166+
Args:
167+
data: List of strings representing the garden grid
168+
169+
Returns:
170+
Total cost when each region's price is `area` * `number_of_sides`
171+
"""
172+
return self.solve(data, self.count_sides)

0 commit comments

Comments
 (0)