use itertools::Itertools; use std::cmp::{Ord, Ordering, PartialOrd}; use std::collections::BinaryHeap; use std::collections::HashSet; use std::collections::VecDeque; use std::rc::Rc; #[allow(dead_code)] pub fn run() { let input: Map = Map(std::fs::read_to_string("input/day18.txt") .unwrap() .lines() .map(|line| line.chars().collect_vec()) .collect_vec()); let t1 = task(input.clone()); println!("Task 1: best bound to get all keys is {}", t1); //let t2 = task(input.clone().split_robot()); //println!("Task 2: best bound to get all keys is {}", t2); } fn task(map: Map) -> usize { let mut all_keys = map .0 .iter() .flatten() .filter(|c| c.is_alphabetic() && c.is_lowercase()) .collect_vec(); all_keys.sort(); let all_keys: String = all_keys.into_iter().collect(); let map = Rc::from(map); let mut visited: HashSet = HashSet::new(); let mut open: BinaryHeap = BinaryHeap::new(); open.push(State::new(map.clone())); while let Some(state) = open.pop() { let summary = StateSummary::from(&state); if visited.contains(&summary) { // there could come no better solution continue; } if summary.1 == all_keys { return state.steps_taken; } state .get_available_options() .into_iter() .for_each(|s| open.push(s)); visited.insert(summary); } panic!("if we reach this point no path can be found"); } #[derive(Eq, PartialEq, Hash, Debug)] struct StateSummary(Vec, String); impl StateSummary { fn from(state: &State) -> Self { let mut s = state.opened.iter().collect_vec(); s.sort(); Self( state.currents.clone(), s.into_iter() .map(|c| { let mut x = *c; x.make_ascii_lowercase(); x }) .collect(), ) } } #[derive(Eq, PartialEq)] struct StateSummaryDist(Pos, HashSet, usize); #[derive(PartialEq, Eq, Clone)] struct Map(Vec>); #[allow(dead_code)] impl Map { fn coordinate_of(&self, symbol: char) -> Vec { self.0 .iter() .enumerate() .fold(Vec::new(), |vec, (y, line)| { line.iter() .enumerate() .filter(|(_x, c)| **c == symbol) .fold(vec, |mut vec, (x, _c)| { vec.push(Pos(x, y)); vec }) }) } /** * return: (char: found key, usize1: index of moved robot, * usize2: distance that robot moved, Pos: new position of robot[index]), * usize3: number of empty points on path to last_door_opened */ fn reachable_keys( &self, start_points: Vec, open_doors: &HashSet, last_door_opened: Option, ) -> Vec<(char, usize, usize, Pos, usize)> { let all_keys = 'a'..='z'; let bfs = |start: Pos, open_doors: &HashSet| { // key, distance, new_pos, number of steps of distance until last_open door is met let mut result: Vec<(char, usize, Pos, usize)> = vec![]; let mut open: VecDeque<(Pos, usize, usize)> = VecDeque::new(); //position, distance form start, distance until last door opened is met open.push_back((start, 0, 0)); let mut visited: HashSet = HashSet::new(); while let Some((current, walked, ldod)) = open.pop_front() { if visited.contains(¤t) { continue; } let field = self.0[current.1][current.0]; let field_is_key = all_keys.contains(&field); // if can move over current type: push neighbors to open if field == '.' || field == '@' || open_doors.contains(&field) || field_is_key { let mut ldod = 0; if let Some(ldo) = last_door_opened { if ldo == field { ldod = walked; } }; current .neighbors() .iter() .for_each(|n| open.push_back((*n, walked + 1, ldod))); } // if it is a key: push it to result if field_is_key { result.push((field, walked, current, ldod)); } // anyways: push to visited visited.insert(current); } result }; start_points .iter() .enumerate() .flat_map(|(robot_i, robot_pos)| { bfs(*robot_pos, open_doors) .into_iter() .map(|(key, dist, final_pos, ldod)| (key, robot_i, dist, final_pos, ldod)) .collect_vec() }) .collect_vec() } fn split_robot(mut self) -> Self { let Pos(x, y) = self.coordinate_of('@')[0]; self.0[x][y] = '#'; self.0[x - 1][y - 1] = '@'; self.0[x - 1][y] = '#'; self.0[x - 1][y + 1] = '@'; self.0[x + 1][y] = '#'; self.0[x + 1][y + 1] = '@'; self.0[x][y - 1] = '#'; self.0[x + 1][y - 1] = '@'; self.0[x][y + 1] = '#'; self } } #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] struct Pos(usize, usize); impl Pos { fn neighbors(&self) -> [Pos; 4] { [ Pos(self.0 - 1, self.1), Pos(self.0 + 1, self.1), Pos(self.0, self.1 - 1), Pos(self.0, self.1 + 1), ] } } #[derive(Eq, PartialEq)] struct State { currents: Vec, underrun: Vec, // the number of steps a robot is behind the leading robots steps opened: HashSet, // capital letter map: Rc, steps_taken: usize, last_door_opened: Option, } impl State { fn new(map: Rc) -> Self { let currents = map.coordinate_of('@'); let no_robots = currents.len(); Self { currents: currents, underrun: std::iter::repeat(0).take(no_robots).collect_vec(), opened: HashSet::new(), map: map, steps_taken: 0, last_door_opened: None, } } fn get_available_options(&self) -> Vec { // find all keys that are not yet collected + their distance + position let next_keys = self .map .reachable_keys(self.currents.clone(), &self.opened, self.last_door_opened) .into_iter() .filter(|(key, _, _, _, _)| { let mut c = *key; c.make_ascii_uppercase(); !self.opened.contains(&c) }) .collect_vec(); // create new state with one open door added + steps increased + current position updated next_keys .into_iter() .map(|(key, robot_i, distance, current, ldod)| { self.advance(key, robot_i, distance, current, ldod) }) .collect_vec() } fn advance( &self, key_added: char, robot_index: usize, additional_steps: usize, new_pos: Pos, ldod: usize, ) -> Self { let mut open_doors = self.opened.clone(); let mut door = key_added; door.make_ascii_uppercase(); open_doors.insert(door); let mut positions = self.currents.clone(); positions[robot_index] = new_pos; let underrun = self .underrun .iter() .enumerate() .map(|(i, u)| { if i != robot_index { *u + additional_steps } else { 0 } }) .collect(); let usable_underrun = if self.underrun[robot_index] >= ldod { ldod } else { self.underrun[robot_index] }; let steps_diff = additional_steps - usable_underrun; Self { currents: positions, underrun: underrun, opened: open_doors, map: self.map.clone(), steps_taken: self.steps_taken + steps_diff as usize, last_door_opened: Some(key_added), } } } // The priority queue depends on `Ord`. // Explicitly implement the trait so the queue becomes a min-heap // instead of a max-heap. impl Ord for State { fn cmp(&self, other: &State) -> Ordering { // Notice that the we flip the ordering on costs. // In case of a tie we compare positions - this step is necessary // to make implementations of `PartialEq` and `Ord` consistent. other.steps_taken.cmp(&self.steps_taken) } } // `PartialOrd` needs to be implemented as well. impl PartialOrd for State { fn partial_cmp(&self, other: &State) -> Option { Some(self.cmp(other)) } }