diff --git a/src/bin/16.rs b/src/bin/16.rs new file mode 100644 index 0000000..12c3de8 --- /dev/null +++ b/src/bin/16.rs @@ -0,0 +1,303 @@ +use aoc::*; +use glam::IVec2; +use std::collections::{HashMap, HashSet}; +use std::{fmt, iter}; + +const INPUT: &str = include_str!("../../input/16"); + +fn main() { + assert_example!(part1, "16-test", 7036); + println!("Part 1: {}", part1(INPUT)); + assert_example!(part2, "16-test", 45); + // TODO Make part 2 terminate + println!("Part 2: {}", part2(INPUT)); +} + +fn part1(input: &str) -> usize { + let mut maze = Maze::parse(input); + maze.solve(); + maze.best_score() +} + +fn part2(input: &str) -> usize { + let mut maze = Maze::parse(input); + maze.solve(); + maze.good_seats() +} + +#[derive(Clone, Debug)] +struct Tile { + pos: IVec2, + visited: bool, + score: usize, + direction: Direction, +} + +impl Tile { + fn new(pos: IVec2) -> Self { + Self { + pos, + visited: false, + score: usize::MAX, + direction: Direction::East, + } + } +} + +struct Maze { + start: IVec2, + end: IVec2, + walls: HashSet, + tiles: HashMap, +} + +impl Maze { + fn solve(&mut self) { + loop { + let next = self + .tiles + .values() + .filter(|t| !t.visited) + .min_by_key(|t| t.score); + let Some(next) = next.cloned() else { + break; + }; + + self.tiles.insert( + next.pos, + Tile { + visited: true, + ..next + }, + ); + + for dir in Direction::ALL { + let neighbor = next.pos + dir.vec(); + let Some(neighbor) = self.tiles.get_mut(&neighbor) else { + continue; + }; + if neighbor.visited { + continue; + } + + let new_score = if dir == next.direction { + next.score + 1 + } else { + next.score + 1001 + }; + + if new_score < neighbor.score { + neighbor.score = new_score; + neighbor.direction = dir; + } + } + } + + // println!("==="); + // let size = self.tiles.iter().fold(IVec2::ZERO, |a, b| a.max(*b.0)); + // for y in 1..=size.y { + // for x in 1..=size.x { + // let pos = IVec2::new(x, y); + // let tile = self.tiles.get(&pos).map(|t| t.score).unwrap_or(0); + // print!("{tile:>6}"); + // } + // println!(); + // } + } + + fn best_score(&self) -> usize { + self.tiles.get(&self.end).unwrap().score + } + + fn good_seats(&self) -> usize { + self.good_seats_rec( + self.best_score(), + 0, + HashSet::new(), + Direction::East, + self.start, + ) + .len() + } + + fn good_seats_rec( + &self, + target_score: usize, + score: usize, + mut visited: HashSet, + dir: Direction, + pos: IVec2, + ) -> HashSet { + if score > target_score { + return HashSet::new(); + } + + if !self.tiles.contains_key(&pos) { + return HashSet::new(); + } + + if visited.contains(&pos) { + return HashSet::new(); + } + visited.insert(pos); + + if score == target_score && pos == self.end { + return visited; + } + + let mut result = HashSet::new(); + { + result.extend(self.good_seats_rec( + target_score, + score + 1, + visited.clone(), + dir, + pos + dir.vec(), + )); + } + { + let new_dir = dir.rotate_cw(); + result.extend(self.good_seats_rec( + target_score, + score + 1001, + visited.clone(), + new_dir, + pos + new_dir.vec(), + )); + } + { + let new_dir = dir.rotate_ccw(); + result.extend(self.good_seats_rec( + target_score, + score + 1001, + visited.clone(), + new_dir, + pos + new_dir.vec(), + )); + } + + result + } + + fn parse(input: &str) -> Self { + let mut start = IVec2::ZERO; + let mut end = IVec2::ZERO; + let mut tiles = HashMap::new(); + let mut walls = HashSet::new(); + + let input = input + .lines() + .enumerate() + .flat_map(|(y, line)| line.chars().enumerate().zip(iter::repeat(y))) + .map(|((x, c), y)| (IVec2::new(x as i32, y as i32), c)); + for (pos, c) in input { + match c { + 'S' => { + start = pos; + tiles.insert( + pos, + Tile { + pos, + visited: false, + score: 0, + direction: Direction::East, + }, + ); + } + 'E' => { + end = pos; + tiles.insert(pos, Tile::new(pos)); + } + '#' => { + walls.insert(pos); + } + '.' => { + tiles.insert(pos, Tile::new(pos)); + } + other => panic!("unknown tile: '{other}'"), + } + } + + Self { + start, + end, + walls, + tiles, + } + } +} + +impl fmt::Display for Maze { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let size = self.walls.iter().fold(IVec2::ZERO, |a, b| a.max(*b)); + for y in 0..=size.y { + for x in 0..=size.x { + let pos = IVec2::new(x, y); + if self.walls.contains(&pos) { + write!(f, "█")?; + } else if let Some(t) = self.tiles.get(&pos) { + write!(f, "{}", t.direction)?; + } else { + write!(f, "?")?; + } + } + writeln!(f)?; + } + Ok(()) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum Direction { + North, + East, + South, + West, +} + +impl Direction { + const ALL: [Direction; 4] = [ + Direction::North, + Direction::East, + Direction::South, + Direction::West, + ]; + + fn rotate_cw(self) -> Self { + match self { + Self::North => Self::East, + Self::East => Self::South, + Self::South => Self::West, + Self::West => Self::North, + } + } + + fn rotate_ccw(self) -> Self { + match self { + Self::North => Self::West, + Self::East => Self::North, + Self::South => Self::East, + Self::West => Self::South, + } + } + + fn vec(self) -> IVec2 { + match self { + Self::North => IVec2::new(0, -1), + Self::East => IVec2::new(1, 0), + Self::South => IVec2::new(0, 1), + Self::West => IVec2::new(-1, 0), + } + } +} + +impl fmt::Display for Direction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::North => write!(f, "^"), + Self::East => write!(f, ">"), + Self::South => write!(f, "v"), + Self::West => write!(f, "<"), + } + } +}