diff --git a/day11/Cargo.lock b/day11/Cargo.lock new file mode 100644 index 0000000..73122b2 --- /dev/null +++ b/day11/Cargo.lock @@ -0,0 +1,25 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "day11" +version = "0.1.0" +dependencies = [ + "itertools", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] diff --git a/day11/Cargo.toml b/day11/Cargo.toml new file mode 100644 index 0000000..4bd62d4 --- /dev/null +++ b/day11/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "day11" +version = "0.1.0" +edition = "2024" + +[dependencies] +itertools = "*" diff --git a/day11/src/main.rs b/day11/src/main.rs new file mode 100644 index 0000000..2f0b54c --- /dev/null +++ b/day11/src/main.rs @@ -0,0 +1,192 @@ +use std::collections::{BTreeSet, HashSet, VecDeque}; + +use itertools::Itertools; + +fn main() { + part_1(); // 33 + part_2(); +} + +fn part_2() { + let state = State::generate_start_state_2(); + let result = bfs(state); + println!("{result}"); +} + +fn part_1() { + let state = State::generate_start_state_1(); + let result = bfs(state); + println!("{result}"); +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +struct State { + floors: [BTreeSet; 4], + elevator: usize, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +enum Item { + Generator(String), + Chip(String), +} + +impl State { + fn is_valid(&self) -> bool { + self.floors.iter().all(floor_is_valid) + } + + fn is_goal_state(&self) -> bool { + self.elevator == 3 // fourth floor, off by one + && self.floors[0..=2].iter().all(|floor| floor.is_empty()) + } + + fn move_candidates(&self) -> &BTreeSet { + &self.floors[self.elevator] + } + + fn possible_moves(&self) -> impl Iterator> { + let candidates = self.move_candidates(); + + candidates + .iter() + .combinations(1) + .chain(candidates.iter().combinations(2)) + } + + fn possible_move_to_floors(&self) -> Vec { + match self.elevator { + 0 => vec![1], + 3 => vec![2], + floor => vec![floor - 1, floor + 1], // floor can never be > 3 + } + } + + fn apply_move(&self, movers: &[&Item], destination: usize) -> State { + let mut result = self.clone(); + + for mover in movers { + result.floors[result.elevator].remove(*mover); + } + + result.elevator = destination; + + for mover in movers { + result.floors[result.elevator].insert((*mover).clone()); + } + + result + } + + fn neighbors(&self) -> impl Iterator { + self.possible_moves() + .cartesian_product(self.possible_move_to_floors()) + .map(|(movers, destination)| self.apply_move(&movers, destination)) + .filter(|state| state.is_valid()) + } + + #[cfg(debug_assertions)] + fn generate_start_state_1() -> Self { + let mut result = Self { + floors: [const { BTreeSet::new() }; 4], + elevator: 0, + }; + + result.floors[0].insert(Item::Chip("hydrogen".to_string())); + result.floors[0].insert(Item::Chip("lithium".to_string())); + + result.floors[1].insert(Item::Generator("hydrogen".to_string())); + + result.floors[2].insert(Item::Generator("lithium".to_string())); + + result + } + + #[cfg(not(debug_assertions))] + fn generate_start_state_1() -> Self { + let mut result = Self { + floors: [const { BTreeSet::new() }; 4], + elevator: 0, + }; + + result.floors[0].insert(Item::Generator("promethium".to_string())); + result.floors[0].insert(Item::Chip("promethium".to_string())); + + result.floors[1].insert(Item::Generator("cobalt".to_string())); + result.floors[1].insert(Item::Generator("curium".to_string())); + result.floors[1].insert(Item::Generator("ruthenium".to_string())); + result.floors[1].insert(Item::Generator("plutonium".to_string())); + + result.floors[2].insert(Item::Chip("cobalt".to_string())); + result.floors[2].insert(Item::Chip("curium".to_string())); + result.floors[2].insert(Item::Chip("ruthenium".to_string())); + result.floors[2].insert(Item::Chip("plutonium".to_string())); + + result + } + + fn generate_start_state_2() -> State { + let mut result = Self { + floors: [const { BTreeSet::new() }; 4], + elevator: 0, + }; + + result.floors[0].insert(Item::Generator("promethium".to_string())); + result.floors[0].insert(Item::Chip("promethium".to_string())); + result.floors[0].insert(Item::Generator("elerium".to_string())); + result.floors[0].insert(Item::Chip("elerium".to_string())); + result.floors[0].insert(Item::Generator("dilithium".to_string())); + result.floors[0].insert(Item::Chip("dilithium".to_string())); + + result.floors[1].insert(Item::Generator("cobalt".to_string())); + result.floors[1].insert(Item::Generator("curium".to_string())); + result.floors[1].insert(Item::Generator("ruthenium".to_string())); + result.floors[1].insert(Item::Generator("plutonium".to_string())); + + result.floors[2].insert(Item::Chip("cobalt".to_string())); + result.floors[2].insert(Item::Chip("curium".to_string())); + result.floors[2].insert(Item::Chip("ruthenium".to_string())); + result.floors[2].insert(Item::Chip("plutonium".to_string())); + + result + } +} + +fn floor_is_valid(floor: &BTreeSet) -> bool { + let mut generators = HashSet::new(); + let mut chips = HashSet::new(); + + for item in floor { + match item { + Item::Generator(name) => generators.insert(name.as_str()), + Item::Chip(name) => chips.insert(name.as_str()), + }; + } + + chips + .into_iter() + .all(|chip| generators.contains(chip) || generators.is_empty()) +} + +fn bfs(start: State) -> usize { + let mut queue = VecDeque::new(); + + let mut visited = HashSet::new(); + visited.insert(start.clone()); + + queue.push_back((start, 0_usize)); + + while let Some((current_state, distance)) = queue.pop_front() { + if current_state.is_goal_state() { + return distance; + } + + for neighbor in current_state.neighbors() { + if !visited.contains(&neighbor) { + visited.insert(neighbor.clone()); + queue.push_back((neighbor, distance + 1)); + } + } + } + panic!("goal state unreachable"); +}