diff --git a/Cargo.lock b/Cargo.lock index 00645ad..4d3f299 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,12 +22,15 @@ name = "advent_of_code" version = "0.9.4" dependencies = [ "dhat", + "either", "enum-ordinalize", "itertools", "num-integer", "pico-args", "rayon", "smallvec", + "thiserror", + "tinyvec", ] [[package]] @@ -416,12 +419,38 @@ dependencies = [ "libc", ] +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thousands" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" + [[package]] name = "unicode-ident" version = "1.0.12" diff --git a/Cargo.toml b/Cargo.toml index 7396973..7b01842 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,9 @@ smallvec = "1.11.2" itertools = "0.12.0" rayon = "1.8.0" num-integer = "0.1" +thiserror = "1.0.50" +either = "1.9.0" +tinyvec = "1.6.0" [profile.dhat] inherits = "release" diff --git a/README.md b/README.md index 2cf4496..23de99c 100644 --- a/README.md +++ b/README.md @@ -25,17 +25,18 @@ Solutions for [Advent of Code](https://adventofcode.com/) in [Rust](https://www. | Day | Part 1 | Part 2 | | :---: | :---: | :---: | -| [Day 1](./src/bin/01.rs) | `33.3µs` | `35.8µs` | -| [Day 2](./src/bin/02.rs) | `42.5µs` | `40.5µs` | -| [Day 3](./src/bin/03.rs) | `82.7µs` | `97.8µs` | -| [Day 4](./src/bin/04.rs) | `48.2µs` | `49.5µs` | -| [Day 5](./src/bin/05.rs) | `20.4µs` | `24.4µs` | -| [Day 6](./src/bin/06.rs) | `203.0ns` | `102.0ns` | -| [Day 7](./src/bin/07.rs) | `93.9µs` | `94.0µs` | -| [Day 8](./src/bin/08.rs) | `73.0µs` | `160.4µs` | -| [Day 9](./src/bin/09.rs) | `58.0µs` | `57.5µs` | - -**Total: 1.01ms** +| [Day 1](./src/bin/01.rs) | `34.6µs` | `37.9µs` | +| [Day 2](./src/bin/02.rs) | `41.4µs` | `40.7µs` | +| [Day 3](./src/bin/03.rs) | `82.2µs` | `99.7µs` | +| [Day 4](./src/bin/04.rs) | `49.6µs` | `50.0µs` | +| [Day 5](./src/bin/05.rs) | `20.7µs` | `24.2µs` | +| [Day 6](./src/bin/06.rs) | `217.0ns` | `102.0ns` | +| [Day 7](./src/bin/07.rs) | `94.6µs` | `92.1µs` | +| [Day 8](./src/bin/08.rs) | `73.0µs` | `160.5µs` | +| [Day 9](./src/bin/09.rs) | `57.2µs` | `57.5µs` | +| [Day 10](./src/bin/10.rs) | `394.6µs` | `764.1µs` | + +**Total: 2.17ms** --- diff --git a/data/examples/10-2.txt b/data/examples/10-2.txt new file mode 100644 index 0000000..6933a28 --- /dev/null +++ b/data/examples/10-2.txt @@ -0,0 +1,9 @@ +........... +.S-------7. +.|F-----7|. +.||.....||. +.||.....||. +.|L-7.F-J|. +.|..|.|..|. +.L--J.L--J. +........... \ No newline at end of file diff --git a/data/examples/10-3.txt b/data/examples/10-3.txt new file mode 100644 index 0000000..c9f3dd0 --- /dev/null +++ b/data/examples/10-3.txt @@ -0,0 +1,9 @@ +.......... +.S------7. +.|F----7|. +.||....||. +.||....||. +.|L-7F-J|. +.|..||..|. +.L--JL--J. +.......... \ No newline at end of file diff --git a/data/examples/10-4.txt b/data/examples/10-4.txt new file mode 100644 index 0000000..2e5dcbb --- /dev/null +++ b/data/examples/10-4.txt @@ -0,0 +1,10 @@ +.F----7F7F7F7F-7.... +.|F--7||||||||FJ.... +.||.FJ||||||||L7.... +FJL7L7LJLJ||LJ.L-7.. +L--J.L7...LJS7F-7L7. +....F-J..F7FJ|L7L7L7 +....L7.F7||L7|.L7L7| +.....|FJLJ|FJ|F7|.LJ +....FJL-7.||.||||... +....L---J.LJ.LJLJ... \ No newline at end of file diff --git a/data/examples/10-5.txt b/data/examples/10-5.txt new file mode 100644 index 0000000..fbc0300 --- /dev/null +++ b/data/examples/10-5.txt @@ -0,0 +1,10 @@ +FF7FSF7F7F7F7F7F---7 +L|LJ||||||||||||F--J +FL-7LJLJ||||||LJL-77 +F--JF--7||LJLJ7F7FJ- +L---JF-JLJ.||-FJLJJ7 +|F|F-JF---7F7-L7L|7| +|FFJF7L7F-JF7|JL---7 +7-L-JL7||F7|L7F-7F7| +L.L7LFJ|||||FJL7||LJ +L7JLJL-JLJLJL--JLJ.L \ No newline at end of file diff --git a/data/examples/10.txt b/data/examples/10.txt new file mode 100644 index 0000000..73b3d66 --- /dev/null +++ b/data/examples/10.txt @@ -0,0 +1,5 @@ +..... +.S-7. +.|.|. +.L-J. +..... \ No newline at end of file diff --git a/src/bin/10.rs b/src/bin/10.rs new file mode 100644 index 0000000..2bac00b --- /dev/null +++ b/src/bin/10.rs @@ -0,0 +1,515 @@ +use std::{collections::HashSet, str::FromStr}; + +use either::Either; +use tinyvec::ArrayVec; + +advent_of_code::solution!(10); + +pub fn part_one(input: &str) -> Option { + let field = input.parse::().expect("valid input"); + field + .find_definite_loop_position() + .map(|(_, distance)| distance) +} + +pub fn part_two(input: &str) -> Option { + let mut field = input.parse::().expect("valid input"); + + // Figure out where the loop is, and construct a new binary map of loop/not loop. + let (loop_position, _) = field.find_definite_loop_position()?; + let mut is_in_loop = vec![false; field.tiles.len()]; + is_in_loop[field.index(loop_position)?] = true; + is_in_loop[field.index(field.starting_position)?] = true; + let mut cur = ArrayVec::<[(Position, Position); 2]>::new(); + cur.extend( + field + .pipe_neighbors(loop_position) + .map(|n| (n, loop_position)), + ); + if cur.len() != 2 { + return None; + } + while !cur.is_empty() { + let mut next = ArrayVec::<[(Position, Position); 2]>::new(); + for (pos, from_pos) in cur + .iter() + .flat_map(|&(pos, last_pos)| field.next(pos, last_pos)) + { + let Some(index) = field.index(from_pos) else { + continue; + }; + if !is_in_loop[index] { + is_in_loop[index] = true; + if pos != field.starting_position { + next.push((pos, from_pos)); + } + } + } + cur = next; + } + + // Figure out the missing tile for the start position to make things easier later on. + let starting_tile = Tile::pipes().find(|&pipe| { + pipe.neighbors(field.starting_position).all(|pos| { + let is_in_loop = field + .index(pos) + .map(|index| is_in_loop[index]) + .unwrap_or(false); + let is_connected = field + .pipe_neighbors(pos) + .any(|p| p == field.starting_position); + is_in_loop && is_connected + }) + })?; + if let Some(cell) = field.get_mut(field.starting_position) { + *cell = starting_tile; + } else { + return None; + } + + // Create a helper that will let us determine the size of a connected region bounded + // by "loop pipes". + let mut closed = HashSet::new(); + let mut flood_fill = |position: Position| -> u32 { + if closed.contains(&position) { + return 0; + } + let mut open = vec![position]; + closed.insert(position); + let mut tile_count = 0; + while let Some(cur) = open.pop() { + tile_count += 1; + for dir in Direction::cardinal() { + let next = cur.go(dir); + if let Some(index) = field.index(next) { + if !is_in_loop[index] && !closed.contains(&next) { + open.push(next); + closed.insert(next); + } + } + } + } + tile_count + }; + + // Find a spot on the loop where the inside/outside direction is known. + // We can start from the north on the column where the definite loop position is, + // and go south until we hit a loop tile. Then we know the direction north from + // that tile is "outside" and south is "inside". + // From there, we will walk clockwise direction around the loop, so we always know the + // outside direction. + let row = (0..field.rows).find(|&row| { + field + .index(Position { + row: row as isize, + col: loop_position.col, + }) + .filter(|&index| is_in_loop[index]) + .is_some() + })?; + let start = Position { + row: row as isize, + col: loop_position.col, + }; + let mut cur = start; + let mut outside_dir = match field.get(start) { + // Due to how the algorithm below works, we need to pretend + // we were coming from the south, so the outside is "west", + // in order to get properly rotated to north when we move. + Some(Tile::SouthEastPipe) => Direction::West, + _ => Direction::North, + }; + let mut inside_count = 0; + loop { + for inside_pos in field.inside_neighbors(cur, outside_dir).filter(|&pos| { + field + .index(pos) + .filter(|&index| is_in_loop[index]) + .is_none() + }) { + inside_count += flood_fill(inside_pos); + } + let (next_dir, next_outside) = match (field.get(cur)?, outside_dir) { + (Tile::HorizontalPipe, Direction::North) => (Direction::East, Direction::North), + (Tile::HorizontalPipe, Direction::South) => (Direction::West, Direction::South), + (Tile::VerticalPipe, Direction::East) => (Direction::South, Direction::East), + (Tile::VerticalPipe, Direction::West) => (Direction::North, Direction::West), + (Tile::NorthEastPipe, Direction::South) => (Direction::North, Direction::West), + (Tile::NorthEastPipe, _) => (Direction::East, Direction::North), + (Tile::NorthWestPipe, Direction::East) => (Direction::West, Direction::South), + (Tile::NorthWestPipe, _) => (Direction::North, Direction::West), + (Tile::SouthEastPipe, Direction::West) => (Direction::East, Direction::North), + (Tile::SouthEastPipe, _) => (Direction::South, Direction::East), + (Tile::SouthWestPipe, Direction::North) => (Direction::South, Direction::East), + (Tile::SouthWestPipe, _) => (Direction::West, Direction::South), + _ => unreachable!(), + }; + let next_pos = cur.go(next_dir); + if next_pos == start { + break; + } + cur = next_pos; + outside_dir = next_outside; + } + + Some(inside_count) +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum Tile { + VerticalPipe, + HorizontalPipe, + NorthEastPipe, + NorthWestPipe, + SouthWestPipe, + SouthEastPipe, + Ground, + StartingPosition, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum Direction { + North, + South, + East, + West, + None, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)] +struct Position { + row: isize, + col: isize, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +struct Field { + tiles: Vec, + rows: usize, + cols: usize, + starting_position: Position, +} + +impl Field { + pub fn get(&self, position: Position) -> Option { + self.tiles.get(self.index(position)?).copied() + } + + pub fn get_mut(&mut self, position: Position) -> Option<&mut Tile> { + let index = self.index(position)?; + self.tiles.get_mut(index) + } + + pub fn index(&self, position: Position) -> Option { + if position.row < 0 + || position.col < 0 + || position.row >= self.rows as isize + || position.col >= self.cols as isize + { + return None; + } + let index = self.cols as isize * position.row + position.col; + Some(index as usize) + } + + pub fn next( + &self, + pos: Position, + last_pos: Position, + ) -> impl Iterator { + let mut next = ArrayVec::<[(Position, Position); 4]>::new(); + use Tile::*; + match (self.get(pos), pos.direction_from(last_pos)) { + (Some(StartingPosition), _) => { + for dir in Direction::cardinal() { + next.push((pos.go(dir), pos)); + } + } + (Some(HorizontalPipe), Direction::East) => next.push((pos.go(Direction::West), pos)), + (Some(HorizontalPipe), Direction::West) => next.push((pos.go(Direction::East), pos)), + (Some(VerticalPipe), Direction::North) => next.push((pos.go(Direction::South), pos)), + (Some(VerticalPipe), Direction::South) => next.push((pos.go(Direction::North), pos)), + (Some(NorthEastPipe), Direction::North) => next.push((pos.go(Direction::East), pos)), + (Some(NorthEastPipe), Direction::East) => next.push((pos.go(Direction::North), pos)), + (Some(NorthWestPipe), Direction::North) => next.push((pos.go(Direction::West), pos)), + (Some(NorthWestPipe), Direction::West) => next.push((pos.go(Direction::North), pos)), + (Some(SouthWestPipe), Direction::South) => next.push((pos.go(Direction::West), pos)), + (Some(SouthWestPipe), Direction::West) => next.push((pos.go(Direction::South), pos)), + (Some(SouthEastPipe), Direction::South) => next.push((pos.go(Direction::East), pos)), + (Some(SouthEastPipe), Direction::East) => next.push((pos.go(Direction::South), pos)), + _ => {} + } + next.into_iter() + } + + pub fn find_definite_loop_position(&self) -> Option<(Position, u32)> { + let mut distance = 0; + let mut cur = ArrayVec::<[(Position, Position); 4]>::new(); + let mut definite_loop_position = None; + cur.push((self.starting_position, self.starting_position)); + while definite_loop_position.is_none() && !cur.is_empty() { + let mut next = ArrayVec::<[(Position, Position); 4]>::new(); + for (pos, from_pos) in cur + .iter() + .flat_map(|&(pos, last_pos)| self.next(pos, last_pos)) + { + if next.iter().any(|&(p, _)| pos == p) { + definite_loop_position = Some(pos); + break; + } + next.push((pos, from_pos)); + } + cur = next; + distance += 1; + } + let Some(pos) = definite_loop_position else { + return None; + }; + Some((pos, distance)) + } + + pub fn pipe_neighbors(&self, position: Position) -> impl Iterator { + let tile = self.get(position).unwrap_or(Tile::Ground); + tile.neighbors(position) + } + + pub fn inside_neighbors( + &self, + position: Position, + outside_dir: Direction, + ) -> impl Iterator { + let mut ret = ArrayVec::<[Position; 2]>::new(); + match (self.get(position), outside_dir) { + (Some(Tile::HorizontalPipe), _) => ret.push(position.go(outside_dir.rev())), + (Some(Tile::VerticalPipe), _) => ret.push(position.go(outside_dir.rev())), + (Some(Tile::SouthWestPipe), Direction::North) => {} // Nothing inside here + (Some(Tile::SouthWestPipe), _) => { + ret.push(position.go(Direction::North)); + ret.push(position.go(Direction::East)); + } + (Some(Tile::SouthEastPipe), Direction::West) => {} // Nothing inside here + (Some(Tile::SouthEastPipe), _) => { + ret.push(position.go(Direction::North)); + ret.push(position.go(Direction::West)); + } + (Some(Tile::NorthWestPipe), Direction::East) => {} // Nothing inside here + (Some(Tile::NorthWestPipe), _) => { + ret.push(position.go(Direction::South)); + ret.push(position.go(Direction::East)); + } + (Some(Tile::NorthEastPipe), Direction::South) => {} // Nothing inside here + (Some(Tile::NorthEastPipe), _) => { + ret.push(position.go(Direction::South)); + ret.push(position.go(Direction::West)); + } + _ => {} + } + ret.into_iter() + } +} + +impl Position { + pub fn direction_from(self, from: Self) -> Direction { + match ( + (self.row - from.row).signum(), + (self.col - from.col).signum(), + ) { + (0, 0) => Direction::None, + (0, -1) => Direction::East, + (0, 1) => Direction::West, + (-1, 0) => Direction::South, + (1, 0) => Direction::North, + _ => unreachable!(), + } + } + + pub fn go(self, direction: Direction) -> Self { + match direction { + Direction::East => Self { + row: self.row, + col: self.col + 1, + }, + Direction::West => Self { + row: self.row, + col: self.col - 1, + }, + Direction::North => Self { + row: self.row - 1, + col: self.col, + }, + Direction::South => Self { + row: self.row + 1, + col: self.col, + }, + Direction::None => self, + } + } +} + +impl Direction { + pub fn cardinal() -> impl Iterator { + [Self::North, Self::South, Self::East, Self::West].into_iter() + } + + pub fn rev(self) -> Self { + match self { + Self::North => Self::South, + Self::South => Self::North, + Self::East => Self::West, + Self::West => Self::East, + _ => self, + } + } +} + +impl Tile { + pub fn pipes() -> impl Iterator { + [ + Self::HorizontalPipe, + Self::VerticalPipe, + Self::NorthEastPipe, + Self::NorthWestPipe, + Self::SouthEastPipe, + Self::SouthWestPipe, + ] + .into_iter() + } + + pub fn neighbors(self, position: Position) -> impl Iterator { + let mut ret = ArrayVec::<[Position; 2]>::new(); + match self { + Tile::HorizontalPipe => { + ret.extend([position.go(Direction::West), position.go(Direction::East)]) + } + Tile::VerticalPipe => { + ret.extend([position.go(Direction::North), position.go(Direction::South)]) + } + Tile::NorthEastPipe => { + ret.extend([position.go(Direction::North), position.go(Direction::East)]) + } + Tile::NorthWestPipe => { + ret.extend([position.go(Direction::North), position.go(Direction::West)]) + } + Tile::SouthEastPipe => { + ret.extend([position.go(Direction::South), position.go(Direction::East)]) + } + Tile::SouthWestPipe => { + ret.extend([position.go(Direction::South), position.go(Direction::West)]) + } + _ => {} + } + ret.into_iter() + } +} + +#[derive(thiserror::Error, Debug)] +enum ParseFieldError { + #[error("{0} is not a valid tile character")] + InvalidTileCharacter(char), + + #[error("input had no tiles")] + EmptyInput, + + #[error("rows do not have equal column counts")] + InconsistentColumnCount, + + #[error("no starting position was found")] + NoStartingPosition, + + #[error("multiple starting positions were found")] + MultipleStartingPositions, +} + +impl FromStr for Field { + type Err = ParseFieldError; + + fn from_str(s: &str) -> Result { + let mut lines = s.lines().peekable(); + let mut rows = 0; + let cols = lines.peek().ok_or(ParseFieldError::EmptyInput)?.len(); + let mut starting_position = None; + let tiles = lines + .flat_map(|line| { + rows += 1; + if line.len() != cols { + return Either::Left(std::iter::once(Err( + ParseFieldError::InconsistentColumnCount, + ))); + } + Either::Right(line.chars().map(Ok)) + }) + .enumerate() + .map(|(index, ch)| { + let tile: Tile = ch?.try_into()?; + if tile == Tile::StartingPosition { + if starting_position.is_some() { + return Err(ParseFieldError::MultipleStartingPositions); + } + starting_position = Some(index); + } + Ok(tile) + }) + .collect::>()?; + Ok(Self { + tiles, + rows, + cols, + starting_position: starting_position + .ok_or(ParseFieldError::NoStartingPosition) + .map(|index| Position { + row: (index / cols) as isize, + col: (index % cols) as isize, + })?, + }) + } +} + +impl TryFrom for Tile { + type Error = ParseFieldError; + + fn try_from(value: char) -> Result { + use Tile::*; + Ok(match value { + '|' => VerticalPipe, + '-' => HorizontalPipe, + 'L' => NorthEastPipe, + 'J' => NorthWestPipe, + '7' => SouthWestPipe, + 'F' => SouthEastPipe, + '.' => Ground, + 'S' => StartingPosition, + ch => return Err(ParseFieldError::InvalidTileCharacter(ch)), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_part_one() { + let result = part_one(&advent_of_code::template::read_file("examples", DAY)); + assert_eq!(result, Some(4)); + } + + #[test] + fn test_part_two() { + let result = part_two(&advent_of_code::template::read_file_part( + "examples", DAY, 2, + )); + assert_eq!(result, Some(4)); + let result = part_two(&advent_of_code::template::read_file_part( + "examples", DAY, 3, + )); + assert_eq!(result, Some(4)); + let result = part_two(&advent_of_code::template::read_file_part( + "examples", DAY, 4, + )); + assert_eq!(result, Some(8)); + let result = part_two(&advent_of_code::template::read_file_part( + "examples", DAY, 5, + )); + assert_eq!(result, Some(10)); + } +}