diff --git a/2019/aoc2019/day14.py b/2019/aoc2019/day14.py new file mode 100644 index 0000000..f641ccd --- /dev/null +++ b/2019/aoc2019/day14.py @@ -0,0 +1,71 @@ +import math +from collections import defaultdict +from typing import TextIO, Tuple + +from networkx import DiGraph, topological_sort + + +def read_pair(item: str) -> Tuple[str, int]: + amount, element = item.split(' ') + + return element, int(amount) + + +def read_recipes(data: TextIO) -> DiGraph: + graph = DiGraph() + + for line in data: + requisites, production = line.strip().split(' => ') + + produced, produced_amount = read_pair(production) + graph.add_node(produced, weight=produced_amount) + + for requisite in requisites.split(', '): + required, required_amount = read_pair(requisite) + graph.add_edge(produced, required, weight=required_amount) + + return graph + + +def ore_required(graph: DiGraph, fuel_required: int) -> int: + requirements = defaultdict(int) + requirements['FUEL'] = fuel_required + + for element in topological_sort(graph): + if element not in requirements: + continue + + if element == 'ORE': + break + + element_produced = graph.nodes[element]['weight'] + productions_required = math.ceil(requirements[element] / element_produced) + + for _, elem_required, amount_required in graph.edges(element, data='weight'): + requirements[elem_required] += amount_required * productions_required + + return requirements['ORE'] + + +def part1(data: TextIO) -> int: + return ore_required(read_recipes(data), 1) + + +def part2(data: TextIO) -> int: + ore_available = 1000000000000 + graph = read_recipes(data) + + min_possible = 1 # lower bound of ORE / ore_required(graph, 1) exists but is slower + max_possible = ore_available + + while min_possible != max_possible: + check = min_possible + (max_possible - min_possible + 1) // 2 + + required = ore_required(graph, check) + + if required <= ore_available: + min_possible = check + else: + max_possible = check - 1 + + return min_possible diff --git a/2019/tests/test_day14.py b/2019/tests/test_day14.py new file mode 100644 index 0000000..41e9a5a --- /dev/null +++ b/2019/tests/test_day14.py @@ -0,0 +1,80 @@ +from io import StringIO +from textwrap import dedent + +import pytest + +from aoc2019.day14 import part1, part2 + +SAMPLES = [ + dedent("""\ + 9 ORE => 2 A + 8 ORE => 3 B + 7 ORE => 5 C + 3 A, 4 B => 1 AB + 5 B, 7 C => 1 BC + 4 C, 1 A => 1 CA + 2 AB, 3 BC, 4 CA => 1 FUEL + """), + dedent("""\ + 157 ORE => 5 NZVS + 165 ORE => 6 DCFZ + 44 XJWVT, 5 KHKGT, 1 QDVJ, 29 NZVS, 9 GPVTF, 48 HKGWZ => 1 FUEL + 12 HKGWZ, 1 GPVTF, 8 PSHF => 9 QDVJ + 179 ORE => 7 PSHF + 177 ORE => 5 HKGWZ + 7 DCFZ, 7 PSHF => 2 XJWVT + 165 ORE => 2 GPVTF + 3 DCFZ, 7 NZVS, 5 HKGWZ, 10 PSHF => 8 KHKGT + """), + dedent("""\ + 2 VPVL, 7 FWMGM, 2 CXFTF, 11 MNCFX => 1 STKFG + 17 NVRVD, 3 JNWZP => 8 VPVL + 53 STKFG, 6 MNCFX, 46 VJHF, 81 HVMC, 68 CXFTF, 25 GNMV => 1 FUEL + 22 VJHF, 37 MNCFX => 5 FWMGM + 139 ORE => 4 NVRVD + 144 ORE => 7 JNWZP + 5 MNCFX, 7 RFSQX, 2 FWMGM, 2 VPVL, 19 CXFTF => 3 HVMC + 5 VJHF, 7 MNCFX, 9 VPVL, 37 CXFTF => 6 GNMV + 145 ORE => 6 MNCFX + 1 NVRVD => 8 CXFTF + 1 VJHF, 6 MNCFX => 4 RFSQX + 176 ORE => 6 VJHF + """), + dedent("""\ + 171 ORE => 8 CNZTR + 7 ZLQW, 3 BMBT, 9 XCVML, 26 XMNCP, 1 WPTQ, 2 MZWV, 1 RJRHP => 4 PLWSL + 114 ORE => 4 BHXH + 14 VRPVC => 6 BMBT + 6 BHXH, 18 KTJDG, 12 WPTQ, 7 PLWSL, 31 FHTLT, 37 ZDVW => 1 FUEL + 6 WPTQ, 2 BMBT, 8 ZLQW, 18 KTJDG, 1 XMNCP, 6 MZWV, 1 RJRHP => 6 FHTLT + 15 XDBXC, 2 LTCX, 1 VRPVC => 6 ZLQW + 13 WPTQ, 10 LTCX, 3 RJRHP, 14 XMNCP, 2 MZWV, 1 ZLQW => 1 ZDVW + 5 BMBT => 4 WPTQ + 189 ORE => 9 KTJDG + 1 MZWV, 17 XDBXC, 3 XCVML => 2 XMNCP + 12 VRPVC, 27 CNZTR => 2 XDBXC + 15 KTJDG, 12 BHXH => 5 XCVML + 3 BHXH, 2 VRPVC => 7 MZWV + 121 ORE => 7 VRPVC + 7 XCVML => 6 RJRHP + 5 BHXH, 4 VRPVC => 5 LTCX + """), +] + + +@pytest.mark.parametrize("sample,correct", zip(SAMPLES, [165, 13312, 180697, 2210736])) +def test_part1(sample: str, correct: int): + data = StringIO(sample) + + result = part1(data) + + assert result == correct + + +@pytest.mark.parametrize("sample,correct", zip(SAMPLES[1:], [82892753, 5586022, 460664])) +def test_part2(sample: str, correct: int): + data = StringIO(sample) + + result = part2(data) + + assert result == correct