Advent of Code 2022 Day 23
Day 23: Unstable Diffusion
https://adventofcode.com/2022/day/23
The grove that was supposed to be full of star fruit is barren.
The elves want to plant new seedlings. The trees need space, so the elves should spread out.
Today’s input is a 2D map of locations the elves are.
- A
#
represents an elf - A
.
represents an empty space
The map is surrounded by an infinite amount of empty spaces off the sides.
An example input looks like this:
....#....###.##...#.#.#...###.###..##.#.##.#..#..
The map is printed so north is up. (so if in elf moves north, it moves up)
Parsing
The Coord
struct is like Gandalf’s headbobbing, it just keeps going!
#[derive(PartialEq, Eq, Hash, Clone, Copy)]struct Coord {row: i32,col: i32,}
I’m only keeping the positions an elf is in memory.
Every elf has a Coord
, and the result of parsing the input as a set of them.
The Coord
struct uses numbers that can go negative.
Because the elves can move off the map in the input to the top or to the left.
fn parse(input: &str) -> HashSet<Coord> {let mut elves = HashSet::new();for (row, line) in input.lines().enumerate() {for (col, c) in line.chars().enumerate() {if c == '#' {elves.insert(Coord {row: row as i32,col: col as i32,});}}}elves}
Part 1
The elves use a time-consuming method to spread out.
After 10 full rounds of this method, the elves like to check if they’re on the right track. Draw a box from the top-left elf to the bottom-right one.
The question asks how many empty spaces are in the box.
The process consists of three phases per round:
- The consideration phase
- The movement phase
- The swappy-checky phase (I couldn’t come up with a name, ok?)
The consideration phase
Each Elf considers their 8 adjactent neighbours.
A quick sidebar I like to call “we’re making a data type”.
It’s the Direction
enum!
enum Direction {North,NorthEast,East,SouthEast,South,SouthWest,West,NorthWest,}
If no other Elves are in one of those eight positions, the Elf does not do anything during this round.
Otherwise, the Elf looks in each of four directions in the following order and proposes moving one step in the first valid direction:
- If there is no Elf in the N, NE, or NW adjacent positions, the Elf proposes moving north one step.
- If there is no Elf in the S, SE, or SW adjacent positions, the Elf proposes moving south one step.
- If there is no Elf in the W, NW, or SW adjacent positions, the Elf proposes moving west one step.
- If there is no Elf in the E, NE, or SE adjacent positions, the Elf proposes moving east one step.
The movement phase
All at once, each Elf moves to their proposed destination tile if they were the only Elf to propose moving to that position. If two or more Elves proposed moving to the same position, none of those Elves move.
The swappy-checky phase
This is the part that changes the order of those 4 checks in the consideration phase.
The first round starts by checking the cardinal directions as they appear in the text.
First North
, then South
, followed by West
, and finally East
.
Here, the first direction in that list moves to the back.
So the second round would start by checking South
, then West
, followed by East
, and finally North
.
Helpers
A few helpers for the Coord
struct that let us take one step in a given Direction
.
That’s also handy to find a Coord
’s 8 neighbours.
impl Coord {fn neighbours(&self) -> [Self; 8] {use Direction::*;let n = self.add_dir(&North);let ne = self.add_dir(&NorthEast);let e = self.add_dir(&East);let se = self.add_dir(&SouthEast);let s = self.add_dir(&South);let sw = self.add_dir(&SouthWest);let w = self.add_dir(&West);let nw = self.add_dir(&NorthWest);[n, ne, e, se, s, sw, w, nw]}fn add_dir(&self, dir: &Direction) -> Self {use Direction::*;match dir {North => Coord {row: self.row - 1,col: self.col,},NorthEast => Coord {row: self.row - 1,col: self.col + 1,},East => Coord {row: self.row,col: self.col + 1,},SouthEast => Coord {row: self.row + 1,col: self.col + 1,},South => Coord {row: self.row + 1,col: self.col,},SouthWest => Coord {row: self.row + 1,col: self.col - 1,},West => Coord {row: self.row,col: self.col - 1,},NorthWest => Coord {row: self.row - 1,col: self.col - 1,},}}}
And a helper on Direction
that, given a list of 8 neighbour occupation booleans says if the corresponding check succeeds.
The check from the consideration phase.
There’s 1 check for North
, 1 for South
, 1 for West
, and 1 for East
.
impl Direction {fn check(&self, neighbours: &[bool; 8]) -> bool {// neighbours in form [n, ne, e, se, s, sw, w, nw]let [n, ne, e, se, s, sw, w, nw] = neighbours;// in these 4 conditions the question says OR, but it means ANDmatch &self {// If there is no Elf in the N, NE, or NW adjacent positions, the Elf proposes moving north one step.Direction::North => !n && !ne && !nw,// If there is no Elf in the S, SE, or SW adjacent positions, the Elf proposes moving south one step.Direction::South => !s && !se && !sw,// If there is no Elf in the W, NW, or SW adjacent positions, the Elf proposes moving west one step.Direction::West => !w && !nw && !sw,// If there is no Elf in the E, NE, or SE adjacent positions, the Elf proposes moving east one step.Direction::East => !e && !ne && !se,// question doesn't specify, but I assume the elf doesn't propose anything_ => false,}}}
As a bonus, a function to print out the current map.
- It gets the min and max value for the
row
coordinate of every elf. - It gets the min and max value for the
col
coordinate of every elf.
It then loops through the rows and cols. And prints something depending if an elf was found at that combination of row and col.
#
if an elf is there.
if the space is open
Don’t forget to print a newline after every row, or the output will be one big lne.
fn print(elves: &HashSet<Coord>) {let (minmax_row, minmax_col) = elves.iter().fold(((i32::MAX, i32::MIN), (i32::MAX, i32::MIN)),|(minmax_row, minmax_col), Coord { row, col }| {((minmax_row.0.min(*row), minmax_row.1.max(*row)),(minmax_col.0.min(*col), minmax_col.1.max(*col)),)},);for row in minmax_row.0..=minmax_row.1 {for col in minmax_col.0..=minmax_col.1 {if elves.contains(&Coord { row, col }) {print!("#")} else {print!(".")}}println!();}}
Skeleton code time! Scratch that, with these helpers, implementation time!
Final code
pub fn part_1(input: &str) -> usize {let mut elves = parse(input);let mut checks = [Direction::North,Direction::South,Direction::West,Direction::East,];for round in 1.. {// key: proposed coordinate, val: list of old coordinates that proposed going therelet mut proposals: HashMap<Coord, Vec<Coord>> = HashMap::new();// consideration phasefor elf in &elves {let neighbours = elf.neighbours();// If no other Elves are in one of those eight positions, the Elf does not do anything during this round.if neighbours.iter().all(|coord| !elves.contains(coord)) {continue;}let neighbours = neighbours.iter().map(|neighbour| elves.contains(neighbour)).collect::<Vec<_>>().try_into().unwrap();let proposed_dir = checks.iter().find(|dir| dir.check(&neighbours));if let Some(dir) = proposed_dir {let proposal = elf.add_dir(dir);proposals.entry(proposal).or_default().push(*elf);}}// movement phasefor (new_coord, old_coords) in proposals {if old_coords.len() == 1 {elves.remove(&old_coords[0]);elves.insert(new_coord);}}// after round// println!("== End of Round {} ==", round);// print(&elves);// println!();checks.rotate_left(1);if round == 10 {let (minmax_row, minmax_col) = elves.iter().fold(((i32::MAX, i32::MIN), (i32::MAX, i32::MIN)),|(minmax_row, minmax_col), Coord { row, col }| {((minmax_row.0.min(*row), minmax_row.1.max(*row)),(minmax_col.0.min(*col), minmax_col.1.max(*col)),)},);return (minmax_row.0..=minmax_row.1).cartesian_product(minmax_col.0..=minmax_col.1).filter(|(row, col)| {!elves.contains(&Coord {row: *row,col: *col,})}).count();}}usize::MAX}
Part 2
Eventually, the elves come to a stop.
The question asks for the round number where no elf moves.
A moved
boolean that starts at false
, and flips to true
every time an elf moves.
If it’s still false
at the end of a round, done!
Final code
pub fn part_2(input: &str) -> i32 {let mut elves = parse(input);let mut checks = [Direction::North,Direction::South,Direction::West,Direction::East,];for round in 1.. {let mut moved = false;// key: proposed coordinate, val: list of old coordinates that proposed going therelet mut proposals: HashMap<Coord, Vec<Coord>> = HashMap::new();// consideration phasefor elf in &elves {let neighbours = elf.neighbours();// If no other Elves are in one of those eight positions, the Elf does not do anything during this round.if neighbours.iter().all(|coord| !elves.contains(coord)) {continue;}let neighbours = neighbours.iter().map(|neighbour| elves.contains(neighbour)).collect::<Vec<_>>().try_into().unwrap();let proposed_dir = checks.iter().find(|dir| dir.check(&neighbours));if let Some(dir) = proposed_dir {let proposal = elf.add_dir(dir);proposals.entry(proposal).or_default().push(*elf);}}// movement phasefor (new_coord, old_coords) in proposals {if old_coords.len() == 1 {moved = true;elves.remove(&old_coords[0]);elves.insert(new_coord);}}// after roundif !moved {return round;}checks.rotate_left(1);}i32::MAX}
Final code
1use std::collections::{HashMap, HashSet};23use itertools::Itertools;45#[derive(PartialEq, Eq, Hash, Clone, Copy)]6struct Coord {7 row: i32,8 col: i32,9}1011enum Direction {12 North,13 NorthEast,14 East,15 SouthEast,16 South,17 SouthWest,18 West,19 NorthWest,20}2122impl Direction {23 fn check(&self, neighbours: &[bool; 8]) -> bool {24 // neighbours in form [n, ne, e, se, s, sw, w, nw]25 let [n, ne, e, se, s, sw, w, nw] = neighbours;26 // in these 4 conditions the question says OR, but it means AND, right?27 match &self {28 // If there is no Elf in the N, NE, or NW adjacent positions, the Elf proposes moving north one step.29 Direction::North => !n && !ne && !nw,30 // If there is no Elf in the S, SE, or SW adjacent positions, the Elf proposes moving south one step.31 Direction::South => !s && !se && !sw,32 // If there is no Elf in the W, NW, or SW adjacent positions, the Elf proposes moving west one step.33 Direction::West => !w && !nw && !sw,34 // If there is no Elf in the E, NE, or SE adjacent positions, the Elf proposes moving east one step.35 Direction::East => !e && !ne && !se,36 // question doesn't specify, but I assume the elf doesn't propose anything37 _ => false,38 }39 }40}4142impl Coord {43 fn neighbours(&self) -> [Self; 8] {44 use Direction::*;45 let n = self.add_dir(&North);46 let ne = self.add_dir(&NorthEast);47 let e = self.add_dir(&East);48 let se = self.add_dir(&SouthEast);49 let s = self.add_dir(&South);50 let sw = self.add_dir(&SouthWest);51 let w = self.add_dir(&West);52 let nw = self.add_dir(&NorthWest);53 [n, ne, e, se, s, sw, w, nw]54 }5556 fn add_dir(&self, dir: &Direction) -> Self {57 use Direction::*;58 match dir {59 North => Coord {60 row: self.row - 1,61 col: self.col,62 },63 NorthEast => Coord {64 row: self.row - 1,65 col: self.col + 1,66 },67 East => Coord {68 row: self.row,69 col: self.col + 1,70 },71 SouthEast => Coord {72 row: self.row + 1,73 col: self.col + 1,74 },75 South => Coord {76 row: self.row + 1,77 col: self.col,78 },79 SouthWest => Coord {80 row: self.row + 1,81 col: self.col - 1,82 },83 West => Coord {84 row: self.row,85 col: self.col - 1,86 },87 NorthWest => Coord {88 row: self.row - 1,89 col: self.col - 1,90 },91 }92 }93}9495fn parse(input: &str) -> HashSet<Coord> {96 let mut elves = HashSet::new();97 for (row, line) in input.lines().enumerate() {98 for (col, c) in line.chars().enumerate() {99 if c == '#' {100 elves.insert(Coord {101 row: row as i32,102 col: col as i32,103 });104 }105 }106 }107 elves108}109110#[allow(unused)]111fn print(elves: &HashSet<Coord>) {112 let (minmax_row, minmax_col) = elves.iter().fold(113 ((i32::MAX, i32::MIN), (i32::MAX, i32::MIN)),114 |(minmax_row, minmax_col), Coord { row, col }| {115 (116 (minmax_row.0.min(*row), minmax_row.1.max(*row)),117 (minmax_col.0.min(*col), minmax_col.1.max(*col)),118 )119 },120 );121 for row in minmax_row.0..=minmax_row.1 {122 for col in minmax_col.0..=minmax_col.1 {123 if elves.contains(&Coord { row, col }) {124 print!("#")125 } else {126 print!(".")127 }128 }129 println!();130 }131}132133pub fn part_1(input: &str) -> usize {134 let mut elves = parse(input);135 let mut checks = [136 Direction::North,137 Direction::South,138 Direction::West,139 Direction::East,140 ];141142 for round in 1.. {143 // key: proposed coordinate, val: list of old coordinates that proposed going there144 let mut proposals: HashMap<Coord, Vec<Coord>> = HashMap::new();145146 // consideration phase147 for elf in &elves {148 let neighbours = elf.neighbours();149 // If no other Elves are in one of those eight positions, the Elf does not do anything during this round.150 if neighbours.iter().all(|coord| !elves.contains(coord)) {151 continue;152 }153 let neighbours = neighbours154 .iter()155 .map(|neighbour| elves.contains(neighbour))156 .collect::<Vec<_>>()157 .try_into()158 .unwrap();159160 let proposed_dir = checks.iter().find(|dir| dir.check(&neighbours));161 if let Some(dir) = proposed_dir {162 let proposal = elf.add_dir(dir);163 proposals.entry(proposal).or_default().push(*elf);164 }165 }166167 // movement phase168 for (new_coord, old_coords) in proposals {169 if old_coords.len() == 1 {170 elves.remove(&old_coords[0]);171 elves.insert(new_coord);172 }173 }174175 // after round176 // println!("== End of Round {} ==", round);177 // print(&elves);178 // println!();179180 checks.rotate_left(1);181 if round == 10 {182 let (minmax_row, minmax_col) = elves.iter().fold(183 ((i32::MAX, i32::MIN), (i32::MAX, i32::MIN)),184 |(minmax_row, minmax_col), Coord { row, col }| {185 (186 (minmax_row.0.min(*row), minmax_row.1.max(*row)),187 (minmax_col.0.min(*col), minmax_col.1.max(*col)),188 )189 },190 );191 return (minmax_row.0..=minmax_row.1)192 .cartesian_product(minmax_col.0..=minmax_col.1)193 .filter(|(row, col)| {194 !elves.contains(&Coord {195 row: *row,196 col: *col,197 })198 })199 .count();200 }201 }202203 usize::MAX204}205206pub fn part_2(input: &str) -> i32 {207 let mut elves = parse(input);208 let mut checks = [209 Direction::North,210 Direction::South,211 Direction::West,212 Direction::East,213 ];214215 for round in 1.. {216 let mut moved = false;217 // key: proposed coordinate, val: list of old coordinates that proposed going there218 let mut proposals: HashMap<Coord, Vec<Coord>> = HashMap::new();219220 // consideration phase221 for elf in &elves {222 let neighbours = elf.neighbours();223 // If no other Elves are in one of those eight positions, the Elf does not do anything during this round.224 if neighbours.iter().all(|coord| !elves.contains(coord)) {225 continue;226 }227 let neighbours = neighbours228 .iter()229 .map(|neighbour| elves.contains(neighbour))230 .collect::<Vec<_>>()231 .try_into()232 .unwrap();233234 let proposed_dir = checks.iter().find(|dir| dir.check(&neighbours));235 if let Some(dir) = proposed_dir {236 let proposal = elf.add_dir(dir);237 proposals.entry(proposal).or_default().push(*elf);238 }239 }240241 // movement phase242 for (new_coord, old_coords) in proposals {243 if old_coords.len() == 1 {244 moved = true;245 elves.remove(&old_coords[0]);246 elves.insert(new_coord);247 }248 }249250 // after round251 if !moved {252 return round;253 }254 checks.rotate_left(1);255 }256257 i32::MAX258}
Series navigation for: Advent of Code 2022