add day 11

Claude guided me through this extensively, but I wrote all this on my
own in the end. Uses breadth first search (BFS). This is slow, but it
produces the correct answer.
This commit is contained in:
timeshifter
2026-07-01 16:07:52 +02:00
parent b21143a92a
commit bd36a0af12
3 changed files with 224 additions and 0 deletions
+192
View File
@@ -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<Item>; 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<Item> {
&self.floors[self.elevator]
}
fn possible_moves(&self) -> impl Iterator<Item = Vec<&Item>> {
let candidates = self.move_candidates();
candidates
.iter()
.combinations(1)
.chain(candidates.iter().combinations(2))
}
fn possible_move_to_floors(&self) -> Vec<usize> {
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<Item = State> {
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<Item>) -> 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");
}