diff --git a/advent/days/day19/__init__.py b/advent/days/day19/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/advent/days/day19/data/example01.txt b/advent/days/day19/data/example01.txt new file mode 100644 index 0000000..5212cde --- /dev/null +++ b/advent/days/day19/data/example01.txt @@ -0,0 +1,2 @@ +Blueprint 1: Each ore robot costs 4 ore. Each clay robot costs 2 ore. Each obsidian robot costs 3 ore and 14 clay. Each geode robot costs 2 ore and 7 obsidian. +Blueprint 2: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 8 clay. Each geode robot costs 3 ore and 12 obsidian. \ No newline at end of file diff --git a/advent/days/day19/data/input.txt b/advent/days/day19/data/input.txt new file mode 100644 index 0000000..a0e82c1 --- /dev/null +++ b/advent/days/day19/data/input.txt @@ -0,0 +1,30 @@ +Blueprint 1: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 17 clay. Each geode robot costs 4 ore and 20 obsidian. +Blueprint 2: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 17 clay. Each geode robot costs 3 ore and 8 obsidian. +Blueprint 3: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 7 clay. Each geode robot costs 4 ore and 13 obsidian. +Blueprint 4: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 10 clay. Each geode robot costs 3 ore and 14 obsidian. +Blueprint 5: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 17 clay. Each geode robot costs 3 ore and 16 obsidian. +Blueprint 6: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 16 clay. Each geode robot costs 2 ore and 15 obsidian. +Blueprint 7: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 15 clay. Each geode robot costs 2 ore and 15 obsidian. +Blueprint 8: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 19 clay. Each geode robot costs 2 ore and 18 obsidian. +Blueprint 9: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 7 clay. Each geode robot costs 2 ore and 19 obsidian. +Blueprint 10: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 6 clay. Each geode robot costs 3 ore and 16 obsidian. +Blueprint 11: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 8 clay. Each geode robot costs 3 ore and 19 obsidian. +Blueprint 12: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 19 clay. Each geode robot costs 2 ore and 12 obsidian. +Blueprint 13: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 14 clay. Each geode robot costs 4 ore and 17 obsidian. +Blueprint 14: Each ore robot costs 2 ore. Each clay robot costs 2 ore. Each obsidian robot costs 2 ore and 20 clay. Each geode robot costs 2 ore and 14 obsidian. +Blueprint 15: Each ore robot costs 2 ore. Each clay robot costs 2 ore. Each obsidian robot costs 2 ore and 10 clay. Each geode robot costs 2 ore and 11 obsidian. +Blueprint 16: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 13 clay. Each geode robot costs 3 ore and 11 obsidian. +Blueprint 17: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 19 clay. Each geode robot costs 3 ore and 10 obsidian. +Blueprint 18: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 20 clay. Each geode robot costs 2 ore and 17 obsidian. +Blueprint 19: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 11 clay. Each geode robot costs 4 ore and 12 obsidian. +Blueprint 20: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 7 clay. Each geode robot costs 3 ore and 10 obsidian. +Blueprint 21: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 13 clay. Each geode robot costs 3 ore and 7 obsidian. +Blueprint 22: Each ore robot costs 2 ore. Each clay robot costs 2 ore. Each obsidian robot costs 2 ore and 15 clay. Each geode robot costs 2 ore and 7 obsidian. +Blueprint 23: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 20 clay. Each geode robot costs 3 ore and 18 obsidian. +Blueprint 24: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 18 clay. Each geode robot costs 4 ore and 8 obsidian. +Blueprint 25: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 14 clay. Each geode robot costs 4 ore and 15 obsidian. +Blueprint 26: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 20 clay. Each geode robot costs 3 ore and 9 obsidian. +Blueprint 27: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 5 clay. Each geode robot costs 3 ore and 7 obsidian. +Blueprint 28: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 11 clay. Each geode robot costs 2 ore and 8 obsidian. +Blueprint 29: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 12 clay. Each geode robot costs 3 ore and 15 obsidian. +Blueprint 30: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 10 clay. Each geode robot costs 3 ore and 10 obsidian. diff --git a/advent/days/day19/solution.py b/advent/days/day19/solution.py new file mode 100644 index 0000000..469a54d --- /dev/null +++ b/advent/days/day19/solution.py @@ -0,0 +1,140 @@ +from __future__ import annotations +from dataclasses import dataclass +from enum import IntEnum +from itertools import islice +from math import prod +from queue import PriorityQueue + +from typing import Iterator, Self +from advent.parser.parser import P + +day_num = 19 + + +def part1(lines: Iterator[str]) -> int: + return sum(blueprint.number * blueprint.run(24) + for blueprint in (Blueprint.parse(line) for line in lines)) + + +def part2(lines: Iterator[str]) -> int: + return prod(Blueprint.parse(line).run(32) for line in islice(lines, 3)) + + +number_parser = P.second(P.string("Blueprint "), P.unsigned()) +ore_parser = P.second(P.string(": Each ore robot costs "), P.unsigned()) +clay_parser = P.second(P.string(" ore. Each clay robot costs "), P.unsigned()) +tuple_parser = P.seq(P.unsigned(), P.second(P.string(" ore and "), P.unsigned())) +obsidian_parser = P.second(P.string(" ore. Each obsidian robot costs "), tuple_parser) +geode_parser = P.second(P.string(" clay. Each geode robot costs "), tuple_parser) +blueprint_parser = P.map5(number_parser, ore_parser, clay_parser, obsidian_parser, geode_parser, + lambda number, ore, clay, obsidian, geode: + Blueprint.create(number, ore, clay, obsidian, geode)) + + +class Element(IntEnum): + Geode = 0 + Obsidian = 1 + Clay = 2 + Ore = 3 + + +Elements = tuple[int, int, int, int] + + +def gt(first: Elements, second: Elements) -> bool: + return all(f >= s for f, s in zip(first, second)) + + +def add(first: Elements, second: Elements) -> Elements: + return tuple(v1 + v2 for v1, v2 in zip(first, second)) + + +def sub(first: Elements, second: Elements) -> Elements: + return Elements(tuple(v1 - v2 for v1, v2 in zip(first, second))) + + +def inc_tuple(elements: Elements, pos: Element) -> Elements: + return tuple(v + 1 if num == pos else v for num, v in enumerate(elements)) + + +@dataclass(slots=True) +class Path: + time: int + material: Elements + robots: Elements + blueprint: Blueprint + path: list[Element | None] + + @classmethod + def start(cls, blueprint: Blueprint) -> Path: + return Path(0, (0, 0, 0, 0), (0, 0, 0, 1), blueprint, []) + + def _check(self, element: Element) -> Path | None: + if gt(self.material, self.blueprint.requirements[element]): + return Path(self.time + 1, + add(sub(self.material, self.blueprint.requirements[element]), self.robots), + inc_tuple(self.robots, element), self.blueprint, self.path + [element]) + else: + return None + + def find_next(self) -> Iterator[Path]: + if (path := self._check(Element.Geode)) is not None: + yield path + if self.blueprint.max_requirement(Element.Obsidian) >= self.material[Element.Obsidian]: + if (path := self._check(Element.Obsidian)) is not None: + yield path + if self.blueprint.max_requirement(Element.Clay) >= self.material[Element.Clay]: + if (path := self._check(Element.Clay)) is not None: + yield path + if self.blueprint.max_requirement(Element.Ore) >= self.material[Element.Ore]: + if (path := self._check(Element.Ore)) is not None: + yield path + yield Path(self.time + 1, add(self.material, self.robots), self.robots, self.blueprint, + self.path + [None]) + + def __lt__(self, other: Path) -> bool: + if self.time != other.time: + return self.time < other.time + return self.material > other.material + + +@dataclass(slots=True) +class Blueprint: + number: int + requirements: tuple[Elements, Elements, Elements, Elements] + + @classmethod + def create(cls, number: int, ore: int, clay: int, + obsidian: tuple[int, int], geode: tuple[int, int]) -> Self: + requirements = ( + (0, geode[1], 0, geode[0]), + (0, 0, obsidian[1], obsidian[0]), + (0, 0, 0, clay), + (0, 0, 0, ore), + ) + return Blueprint(number, requirements) + + @classmethod + def parse(cls, line: str) -> Self: + return blueprint_parser.parse(line).get() + + def max_requirement(self, element: Element) -> int: + return round(max(requirement[element] for requirement in self.requirements) * 1.2) + + def run(self, rounds: int) -> int: + queue: PriorityQueue[Path] = PriorityQueue() + queue.put(Path.start(self)) + seen: dict[tuple[Elements, int], Elements] = {} + while not queue.empty(): + current = queue.get() + if ((current.robots, current.time) in seen + and gt(seen[(current.robots, current.time)], current.material)): + continue + seen[(current.robots, current.time)] = current.material + + if current.time == rounds: + return current.material[Element.Geode] + for next in current.find_next(): + queue.put(next) + + raise Exception("No optimum found") diff --git a/advent/days/day19/test_solution.py b/advent/days/day19/test_solution.py new file mode 100644 index 0000000..553913a --- /dev/null +++ b/advent/days/day19/test_solution.py @@ -0,0 +1,41 @@ +from advent.common import input + +from .solution import Blueprint, day_num, part1, part2 + + +def test_part1(): + lines = input.read_lines(day_num, 'example01.txt') + expected = 33 + result = part1(lines) + assert result == expected + + +def test_part2(): + lines = input.read_lines(day_num, 'example01.txt') + expected = None + result = part2(lines) + assert result == expected + + +def test_parse(): + lines = input.read_lines(day_num, 'example01.txt') + blueprint = Blueprint.parse(next(lines)) + expected = Blueprint(1, ((0, 0, 0, 4), (0, 0, 0, 2), (0, 0, 14, 3), (0, 7, 0, 2))) + assert blueprint == expected + + +def test_blueprint1(): + lines = input.read_lines(day_num, 'example01.txt') + blueprint = Blueprint.parse(next(lines)) + expected = 9 + result = blueprint.run(24) + assert result == expected + + +def test_blueprint2(): + lines = input.read_lines(day_num, 'example01.txt') + next(lines) + blueprint = Blueprint.parse(next(lines)) + expected = 12 + result = blueprint.run(24) + assert result == expected