diff --git a/6/Cargo.lock b/6/Cargo.lock new file mode 100644 index 0000000..f1f33b1 --- /dev/null +++ b/6/Cargo.lock @@ -0,0 +1,14 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "day6" +version = "0.1.0" +dependencies = [ + "grid", +] + +[[package]] +name = "grid" +version = "0.1.0" diff --git a/6/Cargo.toml b/6/Cargo.toml new file mode 100644 index 0000000..f4e63ad --- /dev/null +++ b/6/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "day6" +version = "0.1.0" +edition = "2021" + +[dependencies] +grid = { version = "0.1.0", path = "../libs/grid" } diff --git a/6/src/main.rs b/6/src/main.rs new file mode 100644 index 0000000..fcbc3c0 --- /dev/null +++ b/6/src/main.rs @@ -0,0 +1,227 @@ +use std::collections::{HashMap, HashSet}; +use std::fmt::Display; +use std::fs::File; +use std::io::{BufRead, BufReader, Lines}; +use std::time::{Duration, Instant}; + +// BOILERPLATE +type InputIter = Lines>; + +pub fn get_input() -> InputIter { + let f = File::open("input").unwrap(); + let br = BufReader::new(f); + br.lines() +} + +fn duration_format(duration: Duration) -> String { + match duration.as_secs_f64() { + x if x > 1.0 => format!("{:.3}s", x), + x if x > 0.010 => format!("{:.3}ms", x * 1e3), + x => format!("{:.3}us", x * 1e6), + } +} + +fn main() { + let input = get_input(); + let start = Instant::now(); + let ans1 = problem1(input); + let duration1 = start.elapsed(); + println!("Problem 1 solution: {} [{}]", ans1, duration_format(duration1)); + + let input = get_input(); + let start = Instant::now(); + let ans2 = problem2(input); + let duration2 = start.elapsed(); + println!("Problem 2 solution: {} [{}]", ans2, duration_format(duration2)); + println!("Total duration: {}", duration_format(duration1 + duration2)); +} + +#[repr(u8)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +enum FacingDirection { + Up = b'^', + Down = b'v', + Left = b'<', + Right = b'>', +} + +impl FacingDirection { + fn next(&self) -> FacingDirection { + match self { + FacingDirection::Up => FacingDirection::Right, + FacingDirection::Down => FacingDirection::Left, + FacingDirection::Left => FacingDirection::Up, + FacingDirection::Right => FacingDirection::Down, + } + } + fn pos_ofs(&self, pos: (i64, i64)) -> (i64, i64) { + match self { + FacingDirection::Up => (pos.0 + 0, pos.1 + -1), + FacingDirection::Down => (pos.0 + 0, pos.1 + 1), + FacingDirection::Left => (pos.0 + -1, pos.1 + 0), + FacingDirection::Right => (pos.0 + 1, pos.1 + 0), + } + } +} + +enum StepOutcome { + LeftMap, + LoopFound, + Continue, +} + +enum RunOutcome { + LeftMap, + LoopFound, + Stuck, +} + +#[derive(Clone)] +struct Map { + grid: grid::Grid, + visited_from: HashMap<(i64, i64), HashSet>, + guard_facing: FacingDirection, + guard_pos: (i64, i64), +} + +impl From> for Map { + fn from(input: Lines) -> Self { + let grid = grid::Grid::from(input); + let visited_from = HashMap::new(); + let guard_pos = grid.find(b'^').expect("Guard not found"); + let guard_facing = FacingDirection::Up; + Self { + grid, + guard_pos, + guard_facing, + visited_from, + } + } +} + +impl Map { + fn look(&self, dir: &FacingDirection) -> Option { + match dir { + FacingDirection::Up => self.grid.get(self.guard_pos.0, self.guard_pos.1 - 1), + FacingDirection::Down => self.grid.get(self.guard_pos.0, self.guard_pos.1 + 1), + FacingDirection::Left => self.grid.get(self.guard_pos.0 - 1, self.guard_pos.1), + FacingDirection::Right => self.grid.get(self.guard_pos.0 + 1, self.guard_pos.1), + } + } + /// Move one step in the facing direction, return if we are still inside the bounds + fn step_guard(&mut self) -> StepOutcome { + let new_pos = self.guard_facing.pos_ofs(self.guard_pos); + if self.visited_from.contains_key(&new_pos) && self.visited_from[&new_pos].contains(&self.guard_facing) { + return StepOutcome::LoopFound; + } + if self.grid.set(new_pos.0, new_pos.1, b'X') { + self.visited_from + .entry(new_pos) + .or_insert(HashSet::new()) + .insert(self.guard_facing); + self.guard_pos = new_pos; + StepOutcome::Continue + } else { + StepOutcome::LeftMap + } + } + fn run_guard(&mut self) -> RunOutcome { + // if the guard is surrounded by obstacles, bail out + if self + .grid + .get(self.guard_pos.0 - 1, self.guard_pos.1) + .is_some_and(|v| v == b'#') + && self + .grid + .get(self.guard_pos.0 + 1, self.guard_pos.1) + .is_some_and(|v| v == b'#') + && self + .grid + .get(self.guard_pos.0, self.guard_pos.1 - 1) + .is_some_and(|v| v == b'#') + && self + .grid + .get(self.guard_pos.0, self.guard_pos.1 + 1) + .is_some_and(|v| v == b'#') + { + return RunOutcome::Stuck; + } + while let Some(val) = self.look(&self.guard_facing) { + match val { + b'#' => { + // obstacle, turn right + self.guard_facing = self.guard_facing.next(); + } + _ => match self.step_guard() { + StepOutcome::LeftMap => return RunOutcome::LeftMap, + StepOutcome::LoopFound => return RunOutcome::LoopFound, + StepOutcome::Continue => {} + }, + + } + } + return RunOutcome::LeftMap; + } +} + +// PROBLEM 1 solution + +fn problem1(input: Lines) -> u64 { + let mut map = Map::from(input); + eprintln!("problem 1"); + map.run_guard(); + + (map.grid.count(b'X') + map.grid.count(b'-') + map.grid.count(b'|') + map.grid.count(b'^')) as u64 +} + +// PROBLEM 2 solution +fn problem2(input: Lines) -> u64 { + let input_map = Map::from(input); + let mut loop_count = 0u64; + for y in 0..input_map.grid.height() { + for x in 0..input_map.grid.width() { + eprintln!("Replacing ({}, {}) with obstacle", x, y); + match input_map.grid.get(x as i64, y as i64) { + Some(b'.') => { + let mut test_map = input_map.clone(); + test_map.grid.set(x as i64, y as i64, b'#'); + match test_map.run_guard() { + RunOutcome::LoopFound => loop_count += 1, + _ => {} + } + } + _ => continue, + } + } + } + loop_count +} + +#[cfg(test)] +mod tests { + use crate::*; + use std::io::Cursor; + + const EXAMPLE: &str = &"....#..... +.........# +.......... +..#....... +.......#.. +.......... +.#..^..... +........#. +#......... +......#..."; + + #[test] + fn problem1_example() { + let c = Cursor::new(EXAMPLE); + assert_eq!(problem1(c.lines()), 41); + } + + #[test] + fn problem2_example() { + let c = Cursor::new(EXAMPLE); + assert_eq!(problem2(c.lines()), 6); + } +} diff --git a/libs/grid/Cargo.lock b/libs/grid/Cargo.lock new file mode 100644 index 0000000..1c1a197 --- /dev/null +++ b/libs/grid/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "grid" +version = "0.1.0" diff --git a/libs/grid/Cargo.toml b/libs/grid/Cargo.toml new file mode 100644 index 0000000..d55031c --- /dev/null +++ b/libs/grid/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "grid" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/libs/grid/src/lib.rs b/libs/grid/src/lib.rs new file mode 100644 index 0000000..66880a1 --- /dev/null +++ b/libs/grid/src/lib.rs @@ -0,0 +1,188 @@ +use std::{ + fmt::{self, Debug, Display, Formatter, Write}, + io::{BufRead, Cursor, Lines}, +}; + +#[derive(Clone)] +pub struct Grid { + data: Vec, + width: i64, +} + +impl Grid { + pub fn new(width: i64) -> Self { + Self { + data: Vec::new(), + width, + } + } + pub fn width(&self) -> usize { + return self.width as usize; + } + pub fn height(&self) -> usize { + return self.data.len() / self.width(); + } + fn pos(&self, x: i64, y: i64) -> i64 { + y * self.width + x + } + fn coord(&self, pos: i64) -> Option<(i64, i64)> { + if pos < 0 || pos >= self.data.len() as i64 { + None + } else { + Some((pos % self.width, pos / self.width)) + } + } + fn valid_pos(&self, x: i64, y: i64) -> Option { + if x < 0 || x >= self.width { + return None; + } + if y < 0 || y >= self.data.len() as i64 / self.width { + return None; + } + let pos = self.pos(x, y); + if pos < 0 || pos as usize >= self.data.len() { + return None; + } + self.pos(x, y).try_into().ok() + } + pub fn get(&self, x: i64, y: i64) -> Option { + match self.valid_pos(x, y) { + Some(pos) => Some(self.data[pos]), + None => None, + } + } + pub fn set(&mut self, x: i64, y: i64, val: T) -> bool { + match self.valid_pos(x, y) { + Some(pos) => { + self.data[pos] = val; + true + } + None => false, + } + } + pub fn row(&self, y: i64) -> Option<&[T]> { + if y < self.height() as i64 { + Some(&self.data[self.pos(0, y) as usize..self.pos(self.width, y) as usize]) + } else { + None + } + } + + pub fn find(&self, haystack: T) -> Option<(i64, i64)> { + self.coord( + self.data + .iter() + .enumerate() + .find_map(|(pos, val)| if *val == haystack { Some(pos as i64) } else { None }) + .unwrap_or(-1), + ) + } + pub fn count(&self, haystack: T) -> usize { + self.data.iter().filter(|item| **item == haystack).count() + } + + pub fn forward_slice(&self, x: i64, y: i64, len: i64) -> Option<&[T]> { + let pos = (self.valid_pos(x, y), self.valid_pos(x + len - 1, y)); + match pos { + (Some(pos1), Some(pos2)) => Some(&self.data[pos1..pos2 + 1]), + _ => None, + } + } + + // fn window_compare_impl(&self, needle: &[T]) -> Vec<(i64, i64)> { + // if (self.width as usize) < needle.len() { + // return Vec::new(); + // } + // let mut res = Vec::new(); + // for y in 0..self.height() as i64 { + // let mut windows_tmp = self.row(y).unwrap().windows(needle.len()); + // let windows = if REV { + // windows_tmp.rev() + // } else { + // windows_tmp + // }; + + // res.extend( + // windows + // .enumerate() + // .filter_map(|(x, w)| if w == needle { Some((x as i64, y)) } else { None }), + // ); + // } + // res + // } +} + +impl From> for Grid { + fn from(input: Lines) -> Grid { + let mut data = Vec::new(); + let mut width = 0; + for line in input.map(|i| i.unwrap()) { + if width == 0 { + width = line.len() as i64 + } else if line.len() as i64 != width { + panic!("Grids must have fixed length rows") + } + data.extend_from_slice(line.as_bytes()); + } + Grid { data, width } + } +} + +impl Display for Grid { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + for y in 0..self.height() { + for x in 0..self.width() { + self.get(x as i64, y as i64).fmt(f); + } + } + f.write_char('\n') + } +} + +#[cfg(test)] +mod tests { + use super::*; + + static TEST_VECTOR: &str = &"ABCD +EFGH +IJKL +FBCG"; + + fn unchecked_load() -> Grid { + Grid::from(Cursor::new(TEST_VECTOR).lines()) + } + + #[test] + fn from_string() { + let grid = unchecked_load(); + assert_eq!(grid.data, "ABCDEFGHIJKLFBCG".as_bytes()); + } + + #[test] + fn indexing() { + let grid = unchecked_load(); + assert_eq!(grid.get(0, 0), Some(b'A')); + assert_eq!(grid.get(3, 3), Some(b'G')); + assert_eq!(grid.get(-1, 0), None); + assert_eq!(grid.get(0, -1), None); + assert_eq!(grid.get(5, 0), None); + assert_eq!(grid.get(0, 5), None); + } + + #[test] + fn forward_slice() { + let grid = unchecked_load(); + assert_eq!(grid.forward_slice(0, 0, 2), Some(b"AB".as_slice())); + assert_eq!(grid.forward_slice(2, 0, 2), Some(b"CD".as_slice())); + assert_eq!(grid.forward_slice(2, 0, 3), None); + assert_eq!(grid.forward_slice(0, 2, 4), Some(b"IJKL".as_slice())); + } + + #[test] + fn window_compare() { + let grid = unchecked_load(); + assert_eq!(grid.window_compare(b"IJKL"), &[(0, 2)]); + assert_eq!(grid.window_compare(b"BC"), &[(1, 0), (1, 3)]); + assert_eq!(grid.window_compare(b"LF").len(), 0); + } +}