Skip to main content

year2019/
day14.rs

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