Advent of Code 2024-Day 12: Garden Groups
from advent import parse, grid_to_hash
grid = grid_to_hash(parse(12, list))
dirs = (n, e, s, w) = (-1j, 1, 1j, -1)
def dfs(pos, plant, seen, fences, area=0):
seen.add(pos)
for dir in dirs:
next_pos = pos + dir
if grid.get(next_pos, None) == plant:
if next_pos not in seen:
area, fences = dfs(next_pos, plant, seen, fences, area)
else:
fences.append((pos, dir))
return (area + 1, fences)
def sides(fences):
sides = 0
horizontal = sorted([ (pos, dir) for pos, dir in fences if dir in (n, s) ],
key=lambda f: (str(f[1]), f[0].imag, f[0].real))
vertical = sorted([ (pos, dir) for pos, dir in fences if dir in (e, w) ],
key=lambda f: (str(f[1]), f[0].real, f[0].imag))
prev_d, prev_x, prev_y = None, None, None
for pos, dir in horizontal:
x, y = pos.real, pos.imag
if dir != prev_d or y != prev_y or x != prev_x + 1:
sides += 1
prev_d, prev_x, prev_y = dir, x, y
prev_d, prev_x, prev_y = None, None, None
for pos, dir in vertical:
x, y = pos.real, pos.imag
if dir != prev_d or x != prev_x or y != prev_y + 1:
sides += 1
prev_d, prev_x, prev_y = dir, x, y
return sides
def solve(part):
seen = set()
return sum(area * part(fences) for area, fences in
[ dfs(pos, grid[pos], seen, []) for pos in grid.keys() if pos not in seen ])
# ---------------------------------------------------------------------------------------------
assert solve(len) == 1437300
assert solve(sides) == 849332
Parsing
We're given a grid of letters, and I'd like a dict with keys = position and values = letters:
Solution
The two parts for today's puzzle are identical with the exception of the method of "scoring" the fence segments. Our first function is a depth first search to compute an entire garden plot. For each position in the plot, we look at all the (north, east, south, west) neighbors. If the neighbor does not have the same plant, we add a fence segment to a list. If it does have the same plant, we recurse if we haven't seen the neighbor before:
def dfs(pos, plant, seen, fences, area=0):
seen.add(pos)
for dir in dirs:
next_pos = pos + dir
if grid.get(next_pos, None) == plant:
if next_pos not in seen:
area, fences = dfs(next_pos, plant, seen, fences, area)
else:
fences.append((pos, dir))
return (area + 1, fences)
The next function is a bit trickier. We need to compute the "sides" which are defined by contiguous sections of fences. To do this, we collect the fence segments into horizontal and vertical lists. Then, we sort each list by type (top, bottom, left or right), then x, y (or vice versa depending on horizontal or vertical). We process each list looking for changes that signify we have a new side:
def sides(fences):
sides = 0
horizontal = sorted([ (pos, dir) for pos, dir in fences if dir in (n, s) ],
key=lambda f: (str(f[1]), f[0].imag, f[0].real))
vertical = sorted([ (pos, dir) for pos, dir in fences if dir in (e, w) ],
key=lambda f: (str(f[1]), f[0].real, f[0].imag))
prev_d, prev_x, prev_y = None, None, None
for pos, dir in horizontal:
x, y = pos.real, pos.imag
if dir != prev_d or y != prev_y or x != prev_x + 1:
sides += 1
prev_d, prev_x, prev_y = dir, x, y
prev_d, prev_x, prev_y = None, None, None
for pos, dir in vertical:
x, y = pos.real, pos.imag
if dir != prev_d or x != prev_x or y != prev_y + 1:
sides += 1
prev_d, prev_x, prev_y = dir, x, y
return sides
Lastly, we have the solve() function. For every unseen position in the grid, call the dfs() function which
returns a 2-tuple of area and fences. We parameterize solve() with the scoring function unique to each
part. For Part 1, we just count the fence segments. For Part 2, we compute the sides from the fence segments:
def solve(part):
seen = set()
return sum(area * part(fences) for area, fences in
[ dfs(pos, grid[pos], seen, []) for pos in grid.keys() if pos not in seen ])
solve(len)
solve(sides)
New Python Concepts
dict.get(key, default)
Conclusion
I feel my sides() function is clunky, and I expect there is a more elegant way to do this, but
it works, and is reasonably understandable :) Although my methodology is clunky, Python made
implementing it as painless as possible.