Advent of Code 2022 Day 18
Day 18: Boiling Boulders
https://adventofcode.com/2022/day/18
You exit the vulcano.
A piece of lava (it’s above ground now, magma no more!) flies past you.
Your device quickly scans it.
It’s in low resolution and it approximates the droplet of lava as a bunch of 1x1x1 cubes on a 3D grid.
That’s todays input.
An example input looks like this:
2,2,21,2,23,2,22,1,22,3,22,2,12,2,32,2,42,2,61,2,53,2,52,1,52,3,5
Parsing
That’s right, it’s time for Coord
!
3D ones today!
I want to put all the coordinates in a set, so I derive a few things. Coordinates can get negative, so signed integers as coordinates it is!
use std::collections::HashSet;#[derive(Hash, PartialEq, Eq, Clone, Copy, Default)]struct Coord {x: i16,y: i16,z: i16,}fn parse() -> HashSet<Coord> {let input = std::fs::read_to_string("src/day18.txt").unwrap();input.lines().map(|line| {let mut nums = line.split(",").map(|s| s.parse().unwrap());Coord {x: nums.next().unwrap(),y: nums.next().unwrap(),z: nums.next().unwrap(),}}).collect()}
Part 1
The question asks what the surface area of your scanned lava droplet is.
To get that surface area, count the number of sides of each cube that are not immediately connected to another cube.
For every coordinate in the droplet, I want to count the amount of neighbouring coordinates that are not in the droplet.
In skeleton code:
let cubes = parse();cubes.iter().flat_map(|coord| coord.neighbours())// only keep neighbours that are not a cube.filter(|coord| !cubes.contains(coord)).count()
Helpers
The goal is to define that neighbours
method from the skeleton code.
But first, an enum
to represent the three dimensions!
enum Dimension {X,Y,Z,}
Ok, ok. NOW the neighbours
method.
impl Coord {fn neighbours(&self) -> Vec<Coord> {let mut neighbours = Vec::new();// loop over every dimension in a cubefor dimension in [Dimension::X, Dimension::Y, Dimension::Z] {// add or remove 1 to coordinate in current dimensionfor offset in [-1, 1] {// resulting coordinates are from the coord to a side of a cubelet mut neighbour = self.clone();match dimension {Dimension::X => neighbour.x += offset,Dimension::Y => neighbour.y += offset,Dimension::Z => neighbour.z += offset,}neighbours.push(neighbour);}}neighbours}}
Here’s an alternative that returns an iterator, uses Itertools
, and uses the struct update syntax
fn neighbours(&self) -> impl Iterator<Item = Coord> + '_ {[Dimension::X, Dimension::Y, Dimension::Z].iter().cartesian_product([-1, 1]).map(|(dimension, offset)| match dimension {Dimension::X => Coord {x: self.x + offset,..*self},Dimension::Y => Coord {y: self.y + offset,..*self},Dimension::Z => Coord {z: self.z + offset,..*self},})}
With this helper, the skeleton code above is exactly the final code for part1.
Final code
pub fn part_1() -> usize {let cubes = parse();cubes.iter().flat_map(|coord| coord.neighbours())// only keep neighbours that are not a cube.filter(|coord| !cubes.contains(coord)).count()}
Part 2
The question asks what the surface area of your scanned lava droplet is.
That’s exactly the same question as part1?!
This time, only consider sides that can be reached from the outside and ignore trapped pockets of air.
skeleton code:
let cubes = parse();let exposed = exposed(&cubes);cubes.iter().flat_map(|coord| coord.neighbours())// only keep neighbours that are also exposed.filter(|coord| exposed.contains(coord)).count()
That’s 1 extra method (exposed
), and a slight logic change in the step where we choose what to filter.
The exposed
in that code is the collection of all coordinates that are reachable from outside.
Because we can’t represent literally all coordinates outside, we calculate all the ones in a bounding box the lava droplet would barely fit in.
Helpers
An extra function to return all maxima and minima in the set of scanned coordinates.
fn bounds(cubes: &HashSet<Coord>) -> [Coord; 2] {cubes.iter().fold([Coord::default(), Coord::default()],|[mut mins, mut maxs], cube| {mins.x = mins.x.min(cube.x);mins.y = mins.y.min(cube.y);mins.z = mins.z.min(cube.z);maxs.x = maxs.x.max(cube.x);maxs.y = maxs.y.max(cube.y);maxs.z = maxs.z.max(cube.z);[mins, maxs]},)}
Now the most interesting function: exposed()
.
It runs a floodfill algorithm that starts filling at Coord { x: 0, y: 0, z: 0 }
.
The result is every coordinate (within the bounds plus and minus one!) that is not blocked by the lava droplet.
fn exposed(cubes: &HashSet<Coord>) -> HashSet<Coord> {let bounds = bounds(cubes);let mut exposed = HashSet::new();let start = Coord::default();let mut stack = Vec::new();let mut seen = HashSet::new();stack.push(start);seen.insert(start);while let Some(coord) = stack.pop() {for neighbour in coord.neighbours() {if cubes.contains(&neighbour) || !neighbour.in_bounds(&bounds) {continue;}if seen.insert(neighbour) {stack.push(neighbour);exposed.insert(neighbour);}}}exposed}
in_bounds
is a method on Coord
that returns true if the checked Coord
has coordinates that are at most one more, or one less than the bounds
in any dimension.
fn in_bounds(&self, bounds: &[Self; 2]) -> bool {let [mins, maxs] = bounds;self.x >= mins.x - 1&& self.x <= maxs.x + 1&& self.y >= mins.y - 1&& self.y <= maxs.y + 1&& self.z >= mins.z - 1&& self.z <= maxs.z + 1}
With those helpers, the skeleton code above is exactly the final code for part2.
Final code
pub fn part_2() -> usize {let cubes = parse();let exposed = exposed(&cubes);cubes.iter().flat_map(|coord| coord.neighbours())// only keep neighbours that are also exposed.filter(|coord| exposed.contains(coord)).count()}
Final code
1use std::collections::HashSet;23use itertools::Itertools;45#[derive(Hash, PartialEq, Eq, Clone, Copy, Default)]6struct Coord {7 x: i16,8 y: i16,9 z: i16,10}1112impl Coord {13 fn neighbours(&self) -> impl Iterator<Item = Coord> + '_ {14 [Dimension::X, Dimension::Y, Dimension::Z]15 .iter()16 .cartesian_product([-1, 1])17 .map(|(dimension, offset)| match dimension {18 Dimension::X => Coord {19 x: self.x + offset,20 ..*self21 },22 Dimension::Y => Coord {23 y: self.y + offset,24 ..*self25 },26 Dimension::Z => Coord {27 z: self.z + offset,28 ..*self29 },30 })31 }3233 fn in_bounds(&self, bounds: &[Self; 2]) -> bool {34 let [mins, maxs] = bounds;35 self.x >= mins.x - 136 && self.x <= maxs.x + 137 && self.y >= mins.y - 138 && self.y <= maxs.y + 139 && self.z >= mins.z - 140 && self.z <= maxs.z + 141 }42}4344enum Dimension {45 X,46 Y,47 Z,48}4950fn parse() -> HashSet<Coord> {51 let input = std::fs::read_to_string("src/day18.txt").unwrap();5253 input54 .lines()55 .map(|line| {56 let mut nums = line.split(",").map(|s| s.parse().unwrap());57 Coord {58 x: nums.next().unwrap(),59 y: nums.next().unwrap(),60 z: nums.next().unwrap(),61 }62 })63 .collect()64}6566fn bounds(cubes: &HashSet<Coord>) -> [Coord; 2] {67 cubes.iter().fold(68 [Coord::default(), Coord::default()],69 |[mut mins, mut maxs], cube| {70 mins.x = mins.x.min(cube.x);71 mins.y = mins.y.min(cube.y);72 mins.z = mins.z.min(cube.z);73 maxs.x = maxs.x.max(cube.x);74 maxs.y = maxs.y.max(cube.y);75 maxs.z = maxs.z.max(cube.z);76 [mins, maxs]77 },78 )79}8081fn exposed(cubes: &HashSet<Coord>) -> HashSet<Coord> {82 let bounds = bounds(cubes);83 let mut exposed = HashSet::new();8485 let start = Coord::default();86 let mut stack = Vec::new();87 let mut seen = HashSet::new();8889 stack.push(start);90 seen.insert(start);9192 while let Some(coord) = stack.pop() {93 for neighbour in coord.neighbours() {94 if cubes.contains(&neighbour) || !neighbour.in_bounds(&bounds) {95 continue;96 }97 if seen.insert(neighbour) {98 stack.push(neighbour);99 exposed.insert(neighbour);100 }101 }102 }103104 exposed105}106107pub fn part_1() -> usize {108 let cubes = parse();109110 cubes111 .iter()112 .flat_map(|coord| coord.neighbours())113 // only keep neighbours that are not a cube114 .filter(|coord| !cubes.contains(coord))115 .count()116}117118pub fn part_2() -> usize {119 let cubes = parse();120 let exposed = exposed(&cubes);121122 cubes123 .iter()124 .flat_map(|coord| coord.neighbours())125 // only keep neighbours that are also exposed126 .filter(|coord| exposed.contains(coord))127 .count()128}
Series navigation for: Advent of Code 2022