Skip to content

Commit

Permalink
AoC 2024 day 12 part 2
Browse files Browse the repository at this point in the history
  • Loading branch information
loociano committed Dec 12, 2024
1 parent 697ce2e commit 1437bfb
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 8 deletions.
46 changes: 38 additions & 8 deletions aoc2024/src/day12/python/solution.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,51 @@
type Pos = tuple[int, int] # (y,x)


def _update_perimeter(perimeters: dict[str, list[int]], pos: Pos, direction: tuple[int, int]):
"""Updates perimeter data."""
if direction in ((0, 1), (0, -1)): # Left/right
perimeters[f'{direction}:{pos[1]}'].append(pos[0])
else: # Up/down
perimeters[f'{direction}:{pos[0]}'].append(pos[1])


def _dfs_helper(pos: Pos, flower: str, garden_map: Sequence[str],
visited: set[Pos], area: list[int], perimeter: list[int]):
visited: set[Pos], area: list[int], perimeters: dict[str, list[int]],
direction: tuple[int, int] | None = None):
"""Traverses the garden with DSF, tracking area and perimeter data."""
if (pos[0] < 0 or pos[0] > len(garden_map) - 1
or pos[1] < 0 or pos[1] > len(garden_map[0]) - 1):
# Out of bounds.
perimeter[0] += 1
_update_perimeter(perimeters, pos, direction)
visited.add(pos)
return
if garden_map[pos[0]][pos[1]] != flower:
# Facing another region.
perimeter[0] += 1
_update_perimeter(perimeters, pos, direction)
return
# Same region.
area[0] += 1
visited.add(pos)
for direction in ((-1, 0), (0, 1), (1, 0), (0, -1)):
next_pos = (pos[0] + direction[0], pos[1] + direction[1])
if next_pos not in visited:
_dfs_helper(next_pos, flower, garden_map, visited, area, perimeter)
_dfs_helper(next_pos, flower, garden_map, visited, area, perimeters, direction)


def _calculate_sides(perimeters: dict[str, list[int]]) -> int:
"""Calculates number of sides given perimeter data."""
sides = 0
for coords in perimeters.values():
sides += 1 # Each group of coordinates represents at least one side.
sorted_coords = sorted(coords)
for i in range(1, len(sorted_coords)):
if sorted_coords[i] > sorted_coords[i - 1] + 1:
# Coordinates are disjointed, hence 2 different sides.
sides += 1
return sides


def calculate_fencing_price(garden_map: Sequence[str]) -> int:
def calculate_fencing_price(garden_map: Sequence[str], price_by_side=False) -> int:
"""Calculates the price to fence all regions.
A region price is calculated as its area multiplied by its perimeter."""
visited: dict[str, set[Pos]] = defaultdict(set) # Tracks region positions.
Expand All @@ -48,7 +71,14 @@ def calculate_fencing_price(garden_map: Sequence[str]) -> int:
flower = garden_map[y][x]
pos = (y, x)
if pos not in visited[flower]:
area, perimeter = [0], [0] # Pass by reference
_dfs_helper(pos, garden_map[y][x], garden_map, visited[flower], area, perimeter)
total_price += area[0] * perimeter[0]
area = [0] # Pass by reference
# Track perimeter data. Key by direction at x or y coordinate:
# UP/DOWN at x coordinate -> list of y coordinates
# LEFT/RIGHT at y coordinate -> list of x coordinates
perimeters: dict[str, list[int]] = defaultdict(list)
_dfs_helper(pos, garden_map[y][x], garden_map, visited[flower], area, perimeters)
if price_by_side:
total_price += area[0] * _calculate_sides(perimeters)
else:
total_price += area[0] * sum([len(values) for values in perimeters.values()])
return total_price
5 changes: 5 additions & 0 deletions aoc2024/test/day12/python/example4.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
EEEEE
EXXXX
EEEEE
EXXXX
EEEEE
6 changes: 6 additions & 0 deletions aoc2024/test/day12/python/example5.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
AAAAAA
AAABBA
AAABBA
ABBAAA
ABBAAA
AAAAAA
18 changes: 18 additions & 0 deletions aoc2024/test/day12/python/test_solution.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,24 @@ def test_part1_withLargerExample_calculates(self):
def test_part1_withPuzzleInput_calculates(self):
self.assertEqual(1465112, calculate_fencing_price(self.input))

def test_part2_with4Areas_calculates(self):
self.assertEqual(80, calculate_fencing_price(self.examples[0], price_by_side=True))

def test_part2_withAreaHasHoles_calculates(self):
self.assertEqual(436, calculate_fencing_price(self.examples[1], price_by_side=True))

def test_part2_withEShape_calculates(self):
self.assertEqual(236, calculate_fencing_price(self.examples[3], price_by_side=True))

def test_part2_withAbba_calculates(self):
self.assertEqual(368, calculate_fencing_price(self.examples[4], price_by_side=True))

def test_part2_withLargerExample_calculates(self):
self.assertEqual(1206, calculate_fencing_price(self.examples[2], price_by_side=True))

def test_part2_withPuzzleInput_calculates(self):
self.assertEqual(893790, calculate_fencing_price(self.input, price_by_side=True))


if __name__ == '__main__':
unittest.main()

0 comments on commit 1437bfb

Please sign in to comment.