Implement day 12 solver

This commit is contained in:
2024-12-11 22:42:59 -08:00
parent 978ecb0b5e
commit 92b9f5ca01
2 changed files with 197 additions and 1 deletions

196
puzzles/12.py Normal file
View File

@@ -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)

View File

@@ -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"]