1use utils::array::ArrayVec;
2use utils::hash::FastMap;
3use utils::prelude::*;
4
5#[derive(Clone, Debug)]
7pub struct Day07 {
8 rules: Vec<Contents>,
9 target: usize,
10}
11
12const MAX_INSIDE: usize = 4;
13type Contents = ArrayVec<(u16, u8), MAX_INSIDE>;
14
15#[derive(Clone, Copy, Debug)]
16enum State<T> {
17 Unvisited,
18 CurrentlyVisiting,
19 Visited(T),
20}
21
22impl Day07 {
23 pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
24 let word = parser::take_while1(u8::is_ascii_lowercase);
25 let color = word
26 .with_suffix(b' ')
27 .then(word)
28 .with_consumed()
29 .map(|(_, consumed)| consumed);
30 let rule = color
31 .with_suffix(" bags contain ")
32 .then(
33 parser::parse_tree!(
34 ("1 ".commit(), c @ color, " bag") => (1, c),
35 (count @ parser::nonzero_u8(), b' ', c @ color, " bags") => (count.get(), c),
36 )
37 .repeat_arrayvec::<MAX_INSIDE, _>(", ", 1)
38 .or("no other bags".map(|_| ArrayVec::new())),
39 )
40 .with_suffix(b'.')
41 .with_eol();
42
43 let mut bag_ids = FastMap::with_capacity(1024);
44 let mut rules = Vec::with_capacity(1024);
45 let mut intern = |name, rules: &mut Vec<Option<Contents>>| {
46 *bag_ids.entry(name).or_insert_with(|| {
47 let id = rules.len() as u16;
48 rules.push(None);
49 id
50 })
51 };
52 for item in rule.parse_iterator(input) {
53 let (outer, contains) = item?;
54 let outer_id = intern(outer, &mut rules);
55 if rules[usize::from(outer_id)].is_some() {
56 return Err(InputError::new(input, outer, "duplicate bag rule"));
57 }
58
59 let mut children = Contents::new();
60 for &(count, inner) in &contains {
61 let inner_id = intern(inner, &mut rules);
62 if inner_id == outer_id {
63 return Err(InputError::new(input, inner, "bag cannot contain itself"));
64 }
65 if children.iter().any(|&(id, _)| id == inner_id) {
66 return Err(InputError::new(input, inner, "duplicate bag"));
67 }
68 children.push((inner_id, count)).unwrap();
69 }
70
71 rules[usize::from(outer_id)] = Some(children);
72 }
73
74 let Some(rules) = rules.into_iter().collect() else {
75 return Err(InputError::new(input, 0, "missing bag rule"));
76 };
77 let Some(&target) = bag_ids.get(b"shiny gold".as_slice()) else {
78 return Err(InputError::new(input, 0, "no shiny gold bag"));
79 };
80
81 Ok(Self {
82 rules,
83 target: usize::from(target),
84 })
85 }
86
87 #[must_use]
88 pub fn part1(&self) -> usize {
89 fn contains_target(
90 target: usize,
91 rules: &[Contents],
92 bag: usize,
93 states: &mut [State<bool>],
94 ) -> bool {
95 match states[bag] {
96 State::CurrentlyVisiting => panic!("no solution found: rules contain a cycle"),
97 State::Visited(b) => return b,
98 State::Unvisited => states[bag] = State::CurrentlyVisiting,
99 }
100
101 let mut result = false;
102 for &(child, _) in &rules[bag] {
103 let child = usize::from(child);
104 result |= child == target || contains_target(target, rules, child, states);
105 }
106
107 states[bag] = State::Visited(result);
108 result
109 }
110
111 let mut states = vec![State::Unvisited; self.rules.len()];
112 (0..self.rules.len())
113 .filter(|&bag| contains_target(self.target, &self.rules, bag, &mut states))
114 .count()
115 }
116
117 #[must_use]
118 pub fn part2(&self) -> u64 {
119 fn total_bags(rules: &[Contents], bag: usize, states: &mut [State<u64>]) -> u64 {
120 match states[bag] {
121 State::CurrentlyVisiting => panic!("no solution found: rules contain a cycle"),
122 State::Visited(b) => return b,
123 State::Unvisited => states[bag] = State::CurrentlyVisiting,
124 }
125
126 let mut result = 0u64;
127 for &(child, count) in &rules[bag] {
128 result += count as u64 * (1 + total_bags(rules, usize::from(child), states));
129 }
130
131 states[bag] = State::Visited(result);
132 result
133 }
134
135 let mut states = vec![State::Unvisited; self.rules.len()];
136 total_bags(&self.rules, self.target, &mut states)
137 }
138}
139
140examples!(Day07 -> (usize, u64) [
141 {file: "day07_example0.txt", part1: 4, part2: 32},
142 {file: "day07_example1.txt", part2: 126},
143]);