diff --git a/aoc2024/src/day12/python/solution.py b/aoc2024/src/day12/python/solution.py index 90a1b73..e574c32 100644 --- a/aoc2024/src/day12/python/solution.py +++ b/aoc2024/src/day12/python/solution.py @@ -17,17 +17,27 @@ 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 @@ -35,10 +45,23 @@ def _dfs_helper(pos: Pos, flower: str, garden_map: Sequence[str], 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. @@ -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 diff --git a/aoc2024/test/day12/python/example4.txt b/aoc2024/test/day12/python/example4.txt new file mode 100644 index 0000000..26ada03 --- /dev/null +++ b/aoc2024/test/day12/python/example4.txt @@ -0,0 +1,5 @@ +EEEEE +EXXXX +EEEEE +EXXXX +EEEEE \ No newline at end of file diff --git a/aoc2024/test/day12/python/example5.txt b/aoc2024/test/day12/python/example5.txt new file mode 100644 index 0000000..c7541ac --- /dev/null +++ b/aoc2024/test/day12/python/example5.txt @@ -0,0 +1,6 @@ +AAAAAA +AAABBA +AAABBA +ABBAAA +ABBAAA +AAAAAA \ No newline at end of file diff --git a/aoc2024/test/day12/python/test_solution.py b/aoc2024/test/day12/python/test_solution.py index 0b5d9aa..4eedab2 100644 --- a/aoc2024/test/day12/python/test_solution.py +++ b/aoc2024/test/day12/python/test_solution.py @@ -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()