Skip to main content

year2019/
day14.rs

1use std::num::NonZeroU32;
2use utils::prelude::*;
3use utils::str::TinyStr8;
4
5/// Calculating the maximum output from a process chain.
6#[derive(Clone, Debug)]
7pub struct Day14 {
8    order: Vec<usize>,
9    reactions: Vec<Reaction>,
10}
11
12#[derive(Clone, Debug)]
13struct Reaction {
14    output_amount: NonZeroU32,
15    inputs: Vec<Input>,
16}
17
18#[derive(Clone, Copy, Debug)]
19struct Input {
20    index: usize,
21    amount: NonZeroU32,
22}
23
24const ORE: usize = 0;
25const FUEL: usize = 1;
26const PART2_LIMIT: u64 = 1_000_000_000_000;
27
28impl Day14 {
29    pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
30        let component =
31            parser::nonzero_u32().then(parser::tinystr8(u8::is_ascii_uppercase).with_prefix(b' '));
32        let reaction = component
33            .repeat(", ", 1)
34            .then(component.with_prefix(" => "))
35            .with_consumed()
36            .with_eol();
37
38        let mut names = vec![
39            const { TinyStr8::from_const(b"ORE") },
40            const { TinyStr8::from_const(b"FUEL") },
41        ];
42        let mut reactions = vec![
43            Some(Reaction {
44                output_amount: NonZeroU32::MIN,
45                inputs: Vec::new(),
46            }),
47            None,
48        ];
49
50        for item in reaction.parse_iterator(input) {
51            let ((inputs, (output_amount, output_index)), line) = item?;
52
53            // Using a linear scan over a small Vec instead of a HashMap is ~15% faster.
54            let mut intern = |name| {
55                if let Some(index) = names.iter().position(|&n| n == name) {
56                    return index;
57                }
58                names.push(name);
59                reactions.push(None);
60                reactions.len() - 1
61            };
62
63            let output = intern(output_index);
64            let inputs = inputs
65                .into_iter()
66                .map(|(amount, name)| Input {
67                    index: intern(name),
68                    amount,
69                })
70                .collect();
71
72            if output == ORE {
73                return Err(InputError::new(
74                    input,
75                    line,
76                    "expected ORE to be an input only",
77                ));
78            }
79            if reactions[output].is_some() {
80                return Err(InputError::new(
81                    input,
82                    line,
83                    "duplicate reaction for chemical",
84                ));
85            }
86
87            reactions[output] = Some(Reaction {
88                output_amount,
89                inputs,
90            });
91        }
92
93        let Some(reactions): Option<Vec<Reaction>> = reactions.into_iter().collect() else {
94            return Err(InputError::new(
95                input,
96                0,
97                "expected every chemical to have a reaction",
98            ));
99        };
100
101        let Some(order) = Self::reaction_order(&reactions) else {
102            return Err(InputError::new(input, 0, "expected acyclic reaction graph"));
103        };
104
105        Ok(Self { order, reactions })
106    }
107
108    fn reaction_order(reactions: &[Reaction]) -> Option<Vec<usize>> {
109        #[derive(Clone, Copy, Debug)]
110        enum State {
111            Unvisited,
112            Visiting,
113            Done,
114        }
115
116        fn depth(
117            reactions: &[Reaction],
118            chemical: usize,
119            state: &mut [State],
120            depths: &mut [usize],
121        ) -> Option<usize> {
122            match state[chemical] {
123                State::Done => Some(depths[chemical]),
124                State::Visiting => None,
125                State::Unvisited if chemical == ORE => Some(depths[chemical]),
126                State::Unvisited => {
127                    state[chemical] = State::Visiting;
128
129                    let mut max_input_depth = 0;
130                    for input in &reactions[chemical].inputs {
131                        max_input_depth =
132                            max_input_depth.max(depth(reactions, input.index, state, depths)?);
133                    }
134                    depths[chemical] = max_input_depth + 1;
135
136                    state[chemical] = State::Done;
137                    Some(depths[chemical])
138                }
139            }
140        }
141
142        let mut state = vec![State::Unvisited; reactions.len()];
143        let mut depths = vec![0; reactions.len()];
144        depth(reactions, FUEL, &mut state, &mut depths)?;
145
146        let mut order = (1..reactions.len())
147            .filter(|&i| matches!(state[i], State::Done))
148            .collect::<Vec<_>>();
149        order.sort_unstable_by_key(|&i| depths[i]);
150
151        Some(order)
152    }
153
154    #[must_use]
155    pub fn part1(&self) -> u64 {
156        self.ore_needed(1)
157    }
158
159    #[must_use]
160    pub fn part2(&self) -> u64 {
161        // Using a binary search in a window around PART2_LIMIT instead of from 1 to PART2_LIMIT
162        // reduces ore_needed calls from 40 (log2(PART2_LIMIT)) to ~25
163        let mut lower = PART2_LIMIT / self.ore_needed(1);
164        let mut upper = 2 * lower.max(1);
165
166        while self.ore_needed(upper) <= PART2_LIMIT {
167            lower = upper;
168            upper *= 2;
169        }
170
171        while lower + 1 < upper {
172            let middle = (lower + upper) / 2;
173            if self.ore_needed(middle) <= PART2_LIMIT {
174                lower = middle;
175            } else {
176                upper = middle;
177            }
178        }
179
180        lower
181    }
182
183    #[inline]
184    fn ore_needed(&self, fuel_amount: u64) -> u64 {
185        let mut demand = vec![0u64; self.reactions.len()];
186        demand[FUEL] = fuel_amount;
187
188        for &chemical in self.order.iter().rev() {
189            let needed = demand[chemical];
190            let reaction = &self.reactions[chemical];
191            let batches = needed.div_ceil(u64::from(reaction.output_amount.get()));
192            for input in &reaction.inputs {
193                demand[input.index] += u64::from(input.amount.get()) * batches;
194            }
195        }
196
197        demand[ORE]
198    }
199}
200
201examples!(Day14 -> (u64, u64) [
202    {file: "day14_example0.txt", part1: 31},
203    {file: "day14_example1.txt", part1: 165},
204    {file: "day14_example2.txt", part1: 13312, part2: 82892753},
205    {file: "day14_example3.txt", part1: 180697, part2: 5586022},
206    {file: "day14_example4.txt", part1: 2210736, part2: 460664},
207]);