Implement day 14 solver

This commit is contained in:
2024-12-13 22:18:56 -08:00
parent 4cbd1c9aee
commit 5ee410d26d
3 changed files with 135 additions and 1 deletions

View File

@@ -2,6 +2,7 @@ import re
from textwrap import dedent from textwrap import dedent
from typing import Iterator, NamedTuple, override from typing import Iterator, NamedTuple, override
from unittest import TestCase from unittest import TestCase
from puzzles._solver import Solver from puzzles._solver import Solver

133
puzzles/14.py Normal file
View File

@@ -0,0 +1,133 @@
import re
from collections import Counter
from itertools import count
from math import prod
from textwrap import dedent
from typing import NamedTuple, override
from unittest import TestCase
from more_itertools import filter_map, first_true, iterate
from puzzles._solver import Solver
class Robot(NamedTuple):
x: int
y: int
dx: int
dy: int
robot_spec_pattern = re.compile(r"p=(-?\d+),(-?\d+) v=(-?\d+),(-?\d+)")
class DayFourteenSolver(Solver):
robots: list[Robot]
width: int
height: int
@override
def __init__(self, puzzle_input: str, width=101, height=103):
self.robots = [
Robot(*map(int, match.groups()))
for match in robot_spec_pattern.finditer(puzzle_input)
]
self.width = width
self.height = height
@override
def solve_p1(self) -> int:
quadrant_counts = Counter(
filter_map(
lambda r: self.get_quadrant(self.move_robot(r, 100)),
self.robots,
)
)
return prod(quadrant_counts.values())
@override
def solve_p2(self) -> int:
state = iterate(
lambda robots: [self.move_robot(robot, 1) for robot in robots],
self.robots,
)
return first_true(
count(),
pred=lambda i: self.total_overlap(next(state)) == 0,
)
def move_robot(self, robot: Robot, seconds: int) -> Robot:
return Robot(
(robot.x + robot.dx * seconds) % self.width,
(robot.y + robot.dy * seconds) % self.height,
robot.dx,
robot.dy,
)
def get_quadrant(self, robot: Robot) -> int | None:
if robot.x > self.width // 2 and robot.y < self.height // 2:
return 1
if robot.x < self.width // 2 and robot.y < self.height // 2:
return 2
if robot.x < self.width // 2 and robot.y > self.height // 2:
return 3
if robot.x > self.width // 2 and robot.y > self.height // 2:
return 4
return None
def total_overlap(self, robots: list[Robot]) -> int:
locations = Counter((robot.x, robot.y) for robot in robots)
return sum(count for count in locations.values() if count > 1)
class TestDayFourteenSolver(TestCase):
def test(self):
solver = DayFourteenSolver(
dedent(
"""
p=0,4 v=3,-3
p=6,3 v=-1,-3
p=10,3 v=-1,2
p=2,0 v=2,-1
p=0,0 v=1,3
p=3,0 v=-2,-2
p=7,6 v=-1,-3
p=3,0 v=-1,-2
p=9,3 v=2,3
p=7,3 v=-1,2
p=2,4 v=2,-3
p=9,5 v=-3,-3
"""
),
width=11,
height=7,
)
self.assertTupleEqual(
solver.robots[0],
Robot(0, 4, 3, -3),
)
self.assertTupleEqual(
solver.move_robot(solver.robots[0], 1),
Robot(3, 1, 3, -3),
)
self.assertEqual(solver.get_quadrant(Robot(0, 0, 0, 0)), 2)
self.assertEqual(solver.get_quadrant(Robot(5, 0, 0, 0)), None)
self.assertEqual(
solver.total_overlap(
[
Robot(0, 0, 0, 0),
Robot(0, 0, 0, 0),
]
),
2,
)
self.assertEqual(
solver.total_overlap(
[
Robot(0, 0, 0, 0),
Robot(1, 0, 0, 0),
]
),
0,
)
self.assertEqual(solver.solve_p1(), 12)

View File

@@ -1,5 +1,5 @@
from itertools import count, pairwise
import math import math
from itertools import count, pairwise
from typing import Iterable, NamedTuple, override from typing import Iterable, NamedTuple, override
from unittest import TestCase from unittest import TestCase