Solve day 8
This commit is contained in:
@@ -5,7 +5,7 @@ __version__ = "1.0.0"
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from advent_of_code import cubes, gears, scratchcards, trebuchet
|
||||
from advent_of_code import cubes, gears, network, scratchcards, trebuchet
|
||||
|
||||
Solver = Callable[[str], int]
|
||||
|
||||
@@ -15,4 +15,5 @@ solvers: dict[int, tuple[Solver, Solver]] = {
|
||||
2: (cubes.solve_part_1, cubes.solve_part_2),
|
||||
3: (gears.solve_part_1, gears.solve_part_2),
|
||||
4: (scratchcards.solve_part_1, scratchcards.solve_part_2),
|
||||
8: (network.solve_part_1, network.solve_part_2),
|
||||
}
|
||||
|
103
advent_of_code/network.py
Normal file
103
advent_of_code/network.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"Day 8: Haunted Wasteland"
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from functools import cache
|
||||
from math import lcm
|
||||
from typing import ClassVar, NamedTuple
|
||||
|
||||
from frozendict import frozendict
|
||||
|
||||
|
||||
class Instruction(Enum):
|
||||
Left = "L"
|
||||
Right = "R"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Network:
|
||||
instructions: tuple[Instruction, ...]
|
||||
nodes: frozendict[str, tuple[str, str]]
|
||||
|
||||
node_pattern: ClassVar[re.Pattern] = re.compile(
|
||||
r"([0-9A-Z]{3}) = \(([0-9A-Z]{3}), ([0-9A-Z]{3})\)$"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_instructions(instructions_segment: str) -> tuple[Instruction, ...]:
|
||||
try:
|
||||
return tuple(Instruction(symbol) for symbol in instructions_segment)
|
||||
except:
|
||||
raise ValueError("Network instructions are invalid")
|
||||
|
||||
@staticmethod
|
||||
def _parse_node(node_desc) -> tuple[str, tuple[str, str]]:
|
||||
node_match = Network.node_pattern.match(node_desc)
|
||||
if not node_match:
|
||||
raise ValueError("Network node description is invalid")
|
||||
return (
|
||||
node_match.group(1),
|
||||
(
|
||||
node_match.group(2),
|
||||
node_match.group(3),
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, network_desc: str) -> Network:
|
||||
instructions_segment, _, node_desc_segment = network_desc.partition("\n\n")
|
||||
|
||||
instructions = cls._parse_instructions(instructions_segment)
|
||||
nodes = frozendict(
|
||||
cls._parse_node(node_desc)
|
||||
for node_desc in node_desc_segment.split("\n")
|
||||
if node_desc != ""
|
||||
)
|
||||
|
||||
return Network(instructions, nodes)
|
||||
|
||||
@cache
|
||||
def traverse(self, start_node: str) -> str:
|
||||
cursor = start_node
|
||||
for instruction in self.instructions:
|
||||
if cursor not in self.nodes:
|
||||
raise Exception(f"Dead end: node {cursor} not found")
|
||||
left, right = self.nodes[cursor]
|
||||
match instruction:
|
||||
case Instruction.Left:
|
||||
cursor = left
|
||||
case Instruction.Right:
|
||||
cursor = right
|
||||
return cursor
|
||||
|
||||
@cache
|
||||
def distance(self, start_node: str, stop_flag: str) -> int:
|
||||
cursor = start_node
|
||||
cycles = 0
|
||||
while not cursor.endswith(stop_flag):
|
||||
cursor = self.traverse(cursor)
|
||||
cycles += 1
|
||||
return cycles * len(self.instructions)
|
||||
|
||||
@cache
|
||||
def linked_distance(self, start_flag: str, stop_flag: str) -> int:
|
||||
return lcm(
|
||||
*(
|
||||
self.distance(node, stop_flag)
|
||||
for node in self.nodes.keys()
|
||||
if node.endswith(start_flag)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def solve_part_1(input: str) -> int:
|
||||
network = Network.parse(input)
|
||||
return network.distance("AAA", "ZZZ")
|
||||
|
||||
|
||||
def solve_part_2(input: str) -> int:
|
||||
network = Network.parse(input)
|
||||
return network.linked_distance("A", "Z")
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
||||
[project]
|
||||
name = "advent_of_code"
|
||||
version = "0.1.0"
|
||||
dependencies = ["requests"]
|
||||
dependencies = ["requests", "frozendict"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
test = ["pytest", "pytest-cov", "types-requests"]
|
||||
|
99
tests/network_test.py
Normal file
99
tests/network_test.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
from frozendict import frozendict
|
||||
|
||||
from advent_of_code.network import Instruction, Network, solve_part_1, solve_part_2
|
||||
|
||||
mock_network = Network(
|
||||
(
|
||||
Instruction.Left,
|
||||
Instruction.Left,
|
||||
Instruction.Right,
|
||||
),
|
||||
frozendict(
|
||||
{
|
||||
"AAA": ("BBB", "BBB"),
|
||||
"BBB": ("AAA", "ZZZ"),
|
||||
"ZZZ": ("ZZZ", "ZZZ"),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_network_parse():
|
||||
mock_input = dedent(
|
||||
"""\
|
||||
LLR
|
||||
|
||||
AAA = (BBB, BBB)
|
||||
BBB = (AAA, ZZZ)
|
||||
ZZZ = (ZZZ, ZZZ)
|
||||
"""
|
||||
)
|
||||
assert Network.parse(mock_input) == mock_network
|
||||
|
||||
assert Network.parse("LR") == Network((Instruction.Left, Instruction.Right), {})
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
Network.parse("invalid\nAAA = (BBB, CCC)")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
Network.parse("LR\n\ninvalid")
|
||||
|
||||
|
||||
def test_network_traverse():
|
||||
assert mock_network.traverse("AAA") == "BBB"
|
||||
assert mock_network.traverse("BBB") == "ZZZ"
|
||||
|
||||
with pytest.raises(Exception):
|
||||
network = Network(
|
||||
(Instruction.Left, Instruction.Left),
|
||||
frozendict({"AAA": ("BBB", "BBB")}),
|
||||
)
|
||||
network.traverse("AAA")
|
||||
|
||||
|
||||
def test_network_distance():
|
||||
assert mock_network.distance("AAA", "ZZZ") == 6
|
||||
|
||||
|
||||
def test_solve_part_1():
|
||||
assert (
|
||||
solve_part_1(
|
||||
dedent(
|
||||
"""\
|
||||
RL
|
||||
|
||||
AAA = (BBB, AAA)
|
||||
BBB = (CCC, BBB)
|
||||
CCC = (DDD, CCC)
|
||||
DDD = (ZZZ, DDD)
|
||||
ZZZ = (ZZZ, ZZZ)
|
||||
"""
|
||||
)
|
||||
)
|
||||
== 8
|
||||
)
|
||||
|
||||
|
||||
def test_solve_part_2():
|
||||
assert (
|
||||
solve_part_2(
|
||||
dedent(
|
||||
"""\
|
||||
LR
|
||||
|
||||
11A = (11B, XXX)
|
||||
11B = (XXX, 11Z)
|
||||
11Z = (11B, XXX)
|
||||
22A = (22B, XXX)
|
||||
22B = (22C, 22C)
|
||||
22C = (22Z, 22Z)
|
||||
22Z = (22B, 22B)
|
||||
XXX = (XXX, XXX)
|
||||
"""
|
||||
)
|
||||
)
|
||||
== 6
|
||||
)
|
Reference in New Issue
Block a user