From 62fac164a842ef48fc36a45bc03d1af273d6d492 Mon Sep 17 00:00:00 2001 From: Burnus Date: Tue, 9 May 2023 17:56:16 +0200 Subject: [PATCH] Added Solution for 2021 day 23 --- 2021/day23_amphipod/Cargo.toml | 8 + 2021/day23_amphipod/challenge.txt | 375 ++++++++++++++++++++++ 2021/day23_amphipod/src/lib.rs | 277 ++++++++++++++++ 2021/day23_amphipod/tests/challenge_input | 5 + 2021/day23_amphipod/tests/sample_input | 5 + 5 files changed, 670 insertions(+) create mode 100644 2021/day23_amphipod/Cargo.toml create mode 100644 2021/day23_amphipod/challenge.txt create mode 100644 2021/day23_amphipod/src/lib.rs create mode 100644 2021/day23_amphipod/tests/challenge_input create mode 100644 2021/day23_amphipod/tests/sample_input diff --git a/2021/day23_amphipod/Cargo.toml b/2021/day23_amphipod/Cargo.toml new file mode 100644 index 0000000..61de6f0 --- /dev/null +++ b/2021/day23_amphipod/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "day23_amphipod" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/2021/day23_amphipod/challenge.txt b/2021/day23_amphipod/challenge.txt new file mode 100644 index 0000000..65b7bed --- /dev/null +++ b/2021/day23_amphipod/challenge.txt @@ -0,0 +1,375 @@ +A group of [amphipods](https://en.wikipedia.org/wiki/Amphipoda) notice your fancy submarine and flag you down. "With such an impressive shell," one amphipod says, "surely you can help us with a question that has stumped our best scientists." + +They go on to explain that a group of timid, stubborn amphipods live in a nearby burrow. Four types of amphipods live there: *Amber* (`A`), *Bronze* (`B`), *Copper* (`C`), and *Desert* (`D`). They live in a burrow that consists of a *hallway* and four *side rooms*. The side rooms are initially full of amphipods, and the hallway is initially empty. + +They give you a *diagram of the situation* (your puzzle input), including locations of each amphipod (`A`, `B`, `C`, or `D`, each of which is occupying an otherwise open space), walls (`#`), and open space (`.`). + +For example: + +``` +############# +#...........# +###B#C#B#D### + #A#D#C#A# + ######### + +``` + +The amphipods would like a method to organize every amphipod into side rooms so that each side room contains one type of amphipod and the types are sorted `A`-`D` going left to right, like this: + +``` +############# +#...........# +###A#B#C#D### + #A#B#C#D# + ######### + +``` + +Amphipods can move up, down, left, or right so long as they are moving into an unoccupied open space. Each type of amphipod requires a different amount of *energy* to move one step: Amber amphipods require `1` energy per step, Bronze amphipods require `10` energy, Copper amphipods require `100`, and Desert ones require `1000`. The amphipods would like you to find a way to organize the amphipods that requires the *least total energy*. + +However, because they are timid and stubborn, the amphipods have some extra rules: + +* Amphipods will never *stop on the space immediately outside any room*. They can move into that space so long as they immediately continue moving. (Specifically, this refers to the four open spaces in the hallway that are directly above an amphipod starting position.) +* Amphipods will never *move from the hallway into a room* unless that room is their destination room *and* that room contains no amphipods which do not also have that room as their own destination. If an amphipod's starting room is not its destination room, it can stay in that room until it leaves the room. (For example, an Amber amphipod will not move from the hallway into the right three rooms, and will only move into the leftmost room if that room is empty or if it only contains other Amber amphipods.) +* Once an amphipod stops moving in the hallway, *it will stay in that spot until it can move into a room*. (That is, once any amphipod starts moving, any other amphipods currently in the hallway are locked in place and will not move again until they can move fully into a room.) + +In the above example, the amphipods can be organized using a minimum of `*12521*` energy. One way to do this is shown below. + +Starting configuration: + +``` +############# +#...........# +###B#C#B#D### + #A#D#C#A# + ######### + +``` + +One Bronze amphipod moves into the hallway, taking 4 steps and using `40` energy: + +``` +############# +#...B.......# +###B#C#.#D### + #A#D#C#A# + ######### + +``` + +The only Copper amphipod not in its side room moves there, taking 4 steps and using `400` energy: + +``` +############# +#...B.......# +###B#.#C#D### + #A#D#C#A# + ######### + +``` + +A Desert amphipod moves out of the way, taking 3 steps and using `3000` energy, and then the Bronze amphipod takes its place, taking 3 steps and using `30` energy: + +``` +############# +#.....D.....# +###B#.#C#D### + #A#B#C#A# + ######### + +``` + +The leftmost Bronze amphipod moves to its room using `40` energy: + +``` +############# +#.....D.....# +###.#B#C#D### + #A#B#C#A# + ######### + +``` + +Both amphipods in the rightmost room move into the hallway, using `2003` energy in total: + +``` +############# +#.....D.D.A.# +###.#B#C#.### + #A#B#C#.# + ######### + +``` + +Both Desert amphipods move into the rightmost room using `7000` energy: + +``` +############# +#.........A.# +###.#B#C#D### + #A#B#C#D# + ######### + +``` + +Finally, the last Amber amphipod moves into its room, using `8` energy: + +``` +############# +#...........# +###A#B#C#D### + #A#B#C#D# + ######### + +``` + +*What is the least energy required to organize the amphipods?* + +Your puzzle answer was `10526`. + +\--- Part Two --- +---------- + +As you prepare to give the amphipods your solution, you notice that the diagram they handed you was actually folded up. As you unfold it, you discover an extra part of the diagram. + +Between the first and second lines of text that contain amphipod starting positions, insert the following lines: + +``` + #D#C#B#A# + #D#B#A#C# + +``` + +So, the above example now becomes: + +``` +############# +#...........# +###B#C#B#D### + #D#C#B#A# + #D#B#A#C# + #A#D#C#A# + ######### + +``` + +The amphipods still want to be organized into rooms similar to before: + +``` +############# +#...........# +###A#B#C#D### + #A#B#C#D# + #A#B#C#D# + #A#B#C#D# + ######### + +``` + +In this updated example, the least energy required to organize these amphipods is `*44169*`: + +``` +############# +#...........# +###B#C#B#D### + #D#C#B#A# + #D#B#A#C# + #A#D#C#A# + ######### + +############# +#..........D# +###B#C#B#.### + #D#C#B#A# + #D#B#A#C# + #A#D#C#A# + ######### + +############# +#A.........D# +###B#C#B#.### + #D#C#B#.# + #D#B#A#C# + #A#D#C#A# + ######### + +############# +#A........BD# +###B#C#.#.### + #D#C#B#.# + #D#B#A#C# + #A#D#C#A# + ######### + +############# +#A......B.BD# +###B#C#.#.### + #D#C#.#.# + #D#B#A#C# + #A#D#C#A# + ######### + +############# +#AA.....B.BD# +###B#C#.#.### + #D#C#.#.# + #D#B#.#C# + #A#D#C#A# + ######### + +############# +#AA.....B.BD# +###B#.#.#.### + #D#C#.#.# + #D#B#C#C# + #A#D#C#A# + ######### + +############# +#AA.....B.BD# +###B#.#.#.### + #D#.#C#.# + #D#B#C#C# + #A#D#C#A# + ######### + +############# +#AA...B.B.BD# +###B#.#.#.### + #D#.#C#.# + #D#.#C#C# + #A#D#C#A# + ######### + +############# +#AA.D.B.B.BD# +###B#.#.#.### + #D#.#C#.# + #D#.#C#C# + #A#.#C#A# + ######### + +############# +#AA.D...B.BD# +###B#.#.#.### + #D#.#C#.# + #D#.#C#C# + #A#B#C#A# + ######### + +############# +#AA.D.....BD# +###B#.#.#.### + #D#.#C#.# + #D#B#C#C# + #A#B#C#A# + ######### + +############# +#AA.D......D# +###B#.#.#.### + #D#B#C#.# + #D#B#C#C# + #A#B#C#A# + ######### + +############# +#AA.D......D# +###B#.#C#.### + #D#B#C#.# + #D#B#C#.# + #A#B#C#A# + ######### + +############# +#AA.D.....AD# +###B#.#C#.### + #D#B#C#.# + #D#B#C#.# + #A#B#C#.# + ######### + +############# +#AA.......AD# +###B#.#C#.### + #D#B#C#.# + #D#B#C#.# + #A#B#C#D# + ######### + +############# +#AA.......AD# +###.#B#C#.### + #D#B#C#.# + #D#B#C#.# + #A#B#C#D# + ######### + +############# +#AA.......AD# +###.#B#C#.### + #.#B#C#.# + #D#B#C#D# + #A#B#C#D# + ######### + +############# +#AA.D.....AD# +###.#B#C#.### + #.#B#C#.# + #.#B#C#D# + #A#B#C#D# + ######### + +############# +#A..D.....AD# +###.#B#C#.### + #.#B#C#.# + #A#B#C#D# + #A#B#C#D# + ######### + +############# +#...D.....AD# +###.#B#C#.### + #A#B#C#.# + #A#B#C#D# + #A#B#C#D# + ######### + +############# +#.........AD# +###.#B#C#.### + #A#B#C#D# + #A#B#C#D# + #A#B#C#D# + ######### + +############# +#..........D# +###A#B#C#.### + #A#B#C#D# + #A#B#C#D# + #A#B#C#D# + ######### + +############# +#...........# +###A#B#C#D### + #A#B#C#D# + #A#B#C#D# + #A#B#C#D# + ######### + +``` + +Using the initial configuration from the full diagram, *what is the least energy required to organize the amphipods?* + +Your puzzle answer was `41284`. + +Both parts of this puzzle are complete! They provide two gold stars: \*\* + +At this point, you should [return to your Advent calendar](/2021) and try another puzzle. + +If you still want to see it, you can [get your puzzle input](23/input). \ No newline at end of file diff --git a/2021/day23_amphipod/src/lib.rs b/2021/day23_amphipod/src/lib.rs new file mode 100644 index 0000000..ba3585c --- /dev/null +++ b/2021/day23_amphipod/src/lib.rs @@ -0,0 +1,277 @@ +use core::fmt::Display; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Debug, PartialEq, Eq)] +pub enum ParseError { + InputMalformed(String), + InvalidChar(char), +} + +impl Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InputMalformed(v) => write!(f, "Input is malformed: {v}"), + Self::InvalidChar(c) => write!(f, "Tried to construct Amphipod from invalid character {c}"), + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum Type { + Amber, + Bronze, + Copper, + Desert, +} + +impl Type { + fn energy_modifier(&self) -> usize { + match self { + Self::Amber => 1, + Self::Bronze => 10, + Self::Copper => 100, + Self::Desert => 1000, + } + } + + fn target_x(&self) -> usize { + match self { + Self::Amber => 2, + Self::Bronze => 4, + Self::Copper => 6, + Self::Desert => 8, + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum Space { + Free, + Occupied(Amphipod), + Entrance, +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum State { + Initial, + Hallway, + Stopped, +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct Amphipod { + amphipod_type: Type, + amphipod_state: State, +} + +impl TryFrom for Amphipod { + type Error = ParseError; + + fn try_from(value: char) -> Result { + let amphipod_type = match value { + 'A' => Ok(Type::Amber), + 'B' => Ok(Type::Bronze), + 'C' => Ok(Type::Copper), + 'D' => Ok(Type::Desert), + _ => Err(Self::Error::InvalidChar(value)), + }?; + let amphipod_state = State::Initial; + + Ok(Self { amphipod_type, amphipod_state, }) + } +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] +struct Burrow { + energy_spent: usize, + spaces: BTreeMap<(usize, usize), Space>, +} + +impl TryFrom<&str> for Burrow { + type Error = ParseError; + + fn try_from(value: &str) -> Result { + let lines: Vec<_> = value.lines().collect(); + if lines.len() != 5 || lines.iter().enumerate().any(|(idx, line)| match (idx, line.len()) { + (s, 13) if s<3 => false, + (e, 11) if e>2 => false, + _ => true, + }) { + return Err(Self::Error::InputMalformed(value.to_string())); + } + let mut spaces = BTreeMap::new(); + (0..11).for_each(|x| { + let space = match x { + 2 | 4 | 6 | 8 => Space::Entrance, + _ => Space::Free, + }; + spaces.insert((x, 0), space); + }); + for y in 1..=2 { + for x in (2..=8).step_by(2) { + let amphipod = Amphipod::try_from(lines[1+y].chars().nth(1+x).unwrap())?; + spaces.insert((x, y), Space::Occupied(amphipod)); + } + } + + for ((x, _y), s) in spaces.iter_mut().filter(|((_x, y), _s)| *y == 2) { + if let Space::Occupied(curr) = s { + if *x == curr.amphipod_type.target_x() { + curr.amphipod_state = State::Stopped; + } + } + } + + Ok(Self { energy_spent: 0, spaces, }) + } +} + +impl Burrow { + fn possible_moves(&self, ((x, y), s): (&(usize, usize), &Space)) -> Vec { + if let Space::Occupied(amphipod) = s { + let mut res = Vec::new(); + + // If possible, move into target. + if *y < 2 || (1..*y).all(|row| self.spaces.get(&(*x, row)) == Some(&Space::Free)) { + let target_x = amphipod.amphipod_type.target_x(); + if !self.spaces.iter().any(|((col, y), s)| + // Are there any amphipods that still need to leave this chamber? + (*col == target_x && match s { + Space::Occupied(a) => a.amphipod_state == State::Initial, + _ => false, + }) || + // Are any of the hallway spaces between current x and target_x occupied? + ((*x.min(&target_x)+1..*x.max(&target_x)).contains(col) && *y == 0 && matches!(s, Space::Occupied(_)) )) { + // The new y is the max y of free spaces in this chamber + let new_y = self.spaces.iter().filter(|(&(x, _y), &s)| x == target_x && s == Space::Free ).map(|((_x, y), _s)| *y).max().unwrap(); + + // Move the amphipod into the chamber and set its status to Stopped + let mut spaces = self.spaces.clone(); + let s = Space::Occupied(Amphipod { amphipod_type: amphipod.amphipod_type, amphipod_state: State::Stopped }); + spaces.insert((target_x, new_y), s); + spaces.insert((*x, *y), Space::Free); + + // return the new state early since there can't possibly any better move + // for this amphipod. + return vec![Self { energy_spent: self.energy_spent + amphipod.amphipod_type.energy_modifier()*(*y+new_y+target_x.abs_diff(*x)), spaces }]; + } + + // Otherwise, try moveing into corridor. This is only allowed if we haven't moved + // yet. + if amphipod.amphipod_state == State::Initial { + // First look right + for dx in 1.. { + // skip spaces right outside the chambers and stop if we hit a wall or + // another amphipod + match self.spaces.get(&(*x+dx, 0)) { + Some(Space::Free) => (), + Some(Space::Entrance) => continue, + _ => break, + } + // Move the amphipod into the new space and set its status to Hallway + let mut spaces = self.spaces.clone(); + let s = Space::Occupied(Amphipod { amphipod_type: amphipod.amphipod_type, amphipod_state: State::Hallway }); + spaces.insert((x+dx, 0), s); + spaces.insert((*x, *y), Space::Free); + let next = Self { energy_spent: self.energy_spent + amphipod.amphipod_type.energy_modifier()*(y+dx), spaces }; + res.push(next); + } + // Look left -- same as right, but make sure x doesn't become negative + for dx in 1..=*x { + match self.spaces.get(&(*x-dx, 0)) { + Some(Space::Free) => (), + Some(Space::Entrance) => continue, + _ => break, + } + let mut spaces = self.spaces.clone(); + let s = Space::Occupied(Amphipod { amphipod_type: amphipod.amphipod_type, amphipod_state: State::Hallway }); + spaces.insert((x-dx, 0), s); + spaces.insert((*x, *y), Space::Free); + let next = Self { energy_spent: self.energy_spent + amphipod.amphipod_type.energy_modifier()*(y+dx), spaces }; + res.push(next); + } + } + } + // Return whatever we have collected. May be nothing if all paths are blocked. + res + } else { + Vec::new() + } + } + + // Dykstra's Algorithm + fn organize(&mut self) -> usize { + let mut open_set = BTreeSet::from([self.clone()]); + + // Pick the cheapest move available + while let Some(current) = open_set.pop_first() { + // If we are done, return the energy spent so far + let to_move: Vec<_> = current.spaces.iter().filter(|(_coords, s)| match s { + Space::Occupied(a) => a.amphipod_state != State::Stopped, + _ => false, + }).collect(); + if to_move.is_empty() { + return current.energy_spent; + } else { + // Otherwise find out where each amphipod can go from here + for a in to_move { + for next in current.possible_moves(a) { + open_set.insert(next); + } + } + } + } + // return 0 if there was no solution + 0 + } + + fn expand(&self) -> Self { + let middle = BTreeMap::from([ + ((2, 2), Space::Occupied(Amphipod::try_from('D').unwrap())), + ((2, 3), Space::Occupied(Amphipod::try_from('D').unwrap())), + ((4, 2), Space::Occupied(Amphipod::try_from('C').unwrap())), + ((4, 3), Space::Occupied(Amphipod::try_from('B').unwrap())), + ((6, 2), Space::Occupied(Amphipod::try_from('B').unwrap())), + ((6, 3), Space::Occupied(Amphipod::try_from('A').unwrap())), + ((8, 2), Space::Occupied(Amphipod::try_from('A').unwrap())), + ((8, 3), Space::Occupied(Amphipod::try_from('C').unwrap())), + ]); + let mut spaces = self.spaces.iter() + .map(|(&(x, y), &s)| ((x, y*y), s)) + .collect::>(); + spaces.extend(middle); + + Self { energy_spent: self.energy_spent, spaces, } + } +} + +pub fn run(input: &str) -> Result<(usize, usize), ParseError> { + let mut burrow_1 = Burrow::try_from(input)?; + let mut burrow_2 = burrow_1.expand(); + + let first = burrow_1.organize(); + let second = burrow_2.organize(); + Ok((first, second)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::read_to_string; + + fn read_file(name: &str) -> String { + read_to_string(name).expect(&format!("Unable to read file: {name}")[..]) + } + + #[test] + fn test_sample() { + let sample_input = read_file("tests/sample_input"); + assert_eq!(run(&sample_input), Ok((12521, 44169))); + } + + #[test] + fn test_challenge() { + let challenge_input = read_file("tests/challenge_input"); + assert_eq!(run(&challenge_input), Ok((10526, 41284))); + } +} diff --git a/2021/day23_amphipod/tests/challenge_input b/2021/day23_amphipod/tests/challenge_input new file mode 100644 index 0000000..2ade41b --- /dev/null +++ b/2021/day23_amphipod/tests/challenge_input @@ -0,0 +1,5 @@ +############# +#...........# +###C#A#D#D### + #B#A#B#C# + ######### diff --git a/2021/day23_amphipod/tests/sample_input b/2021/day23_amphipod/tests/sample_input new file mode 100644 index 0000000..6a7120d --- /dev/null +++ b/2021/day23_amphipod/tests/sample_input @@ -0,0 +1,5 @@ +############# +#...........# +###B#C#B#D### + #A#D#C#A# + #########