From 7e5e6b10a29d4eae1b926fdb452e5ab537eed9ea Mon Sep 17 00:00:00 2001 From: Nettika Date: Wed, 11 Dec 2024 22:42:59 -0800 Subject: [PATCH] Implement day 12 solver --- puzzles/12.py | 196 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 puzzles/12.py diff --git a/puzzles/12.py b/puzzles/12.py new file mode 100644 index 0000000..4418ca5 --- /dev/null +++ b/puzzles/12.py @@ -0,0 +1,196 @@ +from dataclasses import dataclass +from functools import cached_property +from itertools import count +from textwrap import dedent +from typing import Iterator, NamedTuple, override +from unittest import TestCase + +from more_itertools import first_true, ilen +from multidict import MultiDict + +from puzzles._solver import Solver + + +@dataclass(frozen=True) +class Fence: + inside: tuple[int, int] + outside: tuple[int, int] + + def left(self) -> "Fence": + (xi, yi) = self.inside + (xo, yo) = self.outside + dx, dy = (xo - xi, yo - yi) + return Fence((xi + dy, yi + dx), (xo + dy, yo + dx)) + + def right(self) -> "Fence": + (xi, yi) = self.inside + (xo, yo) = self.outside + dx, dy = (xo - xi, yo - yi) + return Fence((xi - dy, yi - dx), (xo - dy, yo - dx)) + + +@dataclass +class Region: + label: str + plants: set[tuple[int, int]] + + @property + def area(self) -> int: + return len(self.plants) + + @cached_property + def fences(self) -> set[Fence]: + return { + Fence((x, y), (x + dx, y + dy)) + for x, y in self.plants + for dx, dy in ((0, 1), (1, 0), (0, -1), (-1, 0)) + if (x + dx, y + dy) not in self.plants + } + + @property + def perimeter(self) -> int: + return len(self.fences) + + @cached_property + def sides(self) -> set[tuple[int, int]]: + fence_ids: dict[Fence, int] = {} + id_gen = count(0) + + for fence in self.fences: + if fence in fence_ids: + continue + + id = next(id_gen) + fence_ids[fence] = id + + left_fence = fence.left() + while left_fence in self.fences: + fence_ids[left_fence] = id + left_fence = left_fence.left() + + right_fence = fence.right() + while right_fence in self.fences: + fence_ids[right_fence] = id + right_fence = right_fence.right() + + return len(set(fence_ids.values())) + + +class DayTwelveSolver(Solver): + regions: MultiDict[Region] + + @override + def __init__(self, puzzle_input: str): + self.regions = MultiDict() + + for x, row in enumerate(puzzle_input.strip().splitlines()): + for y, plant in enumerate(row): + if plant not in self.regions: + self.regions.add(plant, Region(plant, {(x, y)})) + continue + + similar_regions = self.regions.popall(plant) + merge_top_region = first_true( + similar_regions, + None, + pred=lambda region: (x - 1, y) in region.plants, + ) + merge_left_region = first_true( + similar_regions, + None, + pred=lambda region: (x, y - 1) in region.plants, + ) + + if merge_top_region: + similar_regions.remove(merge_top_region) + merge_top_region.plants.add((x, y)) + if merge_left_region and merge_left_region is not merge_top_region: + similar_regions.remove(merge_left_region) + merge_top_region.plants.update(merge_left_region.plants) + self.regions.add(plant, merge_top_region) + + elif merge_left_region: + similar_regions.remove(merge_left_region) + merge_left_region.plants.add((x, y)) + self.regions.add(plant, merge_left_region) + + else: + self.regions.add(plant, Region(plant, {(x, y)})) + + self.regions.extend( + [(plant, similar_region) for similar_region in similar_regions] + ) + + @override + def solve_p1(self) -> int: + return sum(region.area * region.perimeter for region in self.regions.values()) + + @override + def solve_p2(self) -> int: + return sum(region.area * region.sides for region in self.regions.values()) + + +class TestDayTwelveSolver(TestCase): + def test_solve_p1(self): + solver = DayTwelveSolver( + dedent( + """ + AAAA + BBCD + BBCC + EEEC + """ + ) + ) + self.assertEqual(solver.solve_p1(), 140) + + solver = DayTwelveSolver( + dedent( + """ + RRRRIICCFF + RRRRIICCCF + VVRRRCCFFF + VVRCCCJFFF + VVVVCJJCFE + VVIVCCJJEE + VVIIICJJEE + MIIIIIJJEE + MIIISIJEEE + MMMISSJEEE + """ + ) + ) + self.assertEqual(solver.solve_p1(), 1930) + + def test_solve_p2(self): + solver = DayTwelveSolver( + dedent( + """ + AAAA + BBCD + BBCC + EEEC + """ + ) + ) + for region in solver.regions.values(): + print(region.label, region.sides) + self.assertEqual(solver.solve_p2(), 80) + + solver = DayTwelveSolver( + dedent( + """ + RRRRIICCFF + RRRRIICCCF + VVRRRCCFFF + VVRCCCJFFF + VVVVCJJCFE + VVIVCCJJEE + VVIIICJJEE + MIIIIIJJEE + MIIISIJEEE + MMMISSJEEE + """ + ) + ) + self.assertEqual(solver.solve_p2(), 1206) diff --git a/pyproject.toml b/pyproject.toml index 91a43bd..31adc03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,4 +2,4 @@ name = "advent-of-code-2024" version = "0.0.0" description = "Advent of Code 2024" -dependencies = ["advent", "more-itertools"] +dependencies = ["advent", "more-itertools", "multidict"]