From 3886006a72a1a72ec34680248363031d20949348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Sun, 15 Dec 2024 20:14:12 +0000 Subject: [PATCH] 2024: day06 (#32) --- 2024/day06/Cargo.toml | 32 +++++ 2024/day06/benches/benchmarks.rs | 18 +++ 2024/day06/src/common.rs | 206 +++++++++++++++++++++++++++++++ 2024/day06/src/lib.rs | 112 +++++++++++++++++ 2024/day06/src/main.rs | 22 ++++ Cargo.toml | 3 +- common/src/types.rs | 19 ++- solution-runner/Cargo.toml | 1 + solution-runner/src/main.rs | 1 + 9 files changed, 412 insertions(+), 2 deletions(-) create mode 100644 2024/day06/Cargo.toml create mode 100644 2024/day06/benches/benchmarks.rs create mode 100644 2024/day06/src/common.rs create mode 100644 2024/day06/src/lib.rs create mode 100644 2024/day06/src/main.rs diff --git a/2024/day06/Cargo.toml b/2024/day06/Cargo.toml new file mode 100644 index 0000000..2362997 --- /dev/null +++ b/2024/day06/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "day06_2024" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day06_2024" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } +winnow = { workspace = true } +rayon = { workspace = true } + +[dev-dependencies] +criterion = { workspace = true } + +[[bench]] +name = "benchmarks" +harness = false + +[lints] +workspace = true \ No newline at end of file diff --git a/2024/day06/benches/benchmarks.rs b/2024/day06/benches/benchmarks.rs new file mode 100644 index 0000000..8bdde53 --- /dev/null +++ b/2024/day06/benches/benchmarks.rs @@ -0,0 +1,18 @@ +// Copyright 2023 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::define_aoc_benchmark; +use day06_2024::Day06; + +define_aoc_benchmark!("inputs/2024/day06", Day06); diff --git a/2024/day06/src/common.rs b/2024/day06/src/common.rs new file mode 100644 index 0000000..97b30c4 --- /dev/null +++ b/2024/day06/src/common.rs @@ -0,0 +1,206 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use anyhow::bail; +use aoc_common::types::Position; +use std::collections::HashSet; +use std::str::FromStr; +use winnow::ascii::line_ending; +use winnow::combinator::{alt, repeat, separated}; +use winnow::token::literal; +use winnow::{PResult, Parser}; + +#[derive(Debug, Copy, Clone)] +enum MapFeature { + Empty, + Obstruction, + UpFacingGuard, +} + +#[derive(Debug, Default, Copy, Clone, Hash, PartialEq, Eq)] +pub enum GuardDirection { + #[default] + Up, + Right, + Down, + Left, +} + +#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub struct Guard { + position: Position, + direction: GuardDirection, +} + +impl Guard { + fn rotate(&mut self) { + self.direction = match self.direction { + GuardDirection::Up => GuardDirection::Right, + GuardDirection::Right => GuardDirection::Down, + GuardDirection::Down => GuardDirection::Left, + GuardDirection::Left => GuardDirection::Up, + } + } + + fn move_to(&mut self, new_position: Position) { + self.position = new_position; + } + + fn in_front(&self) -> Position { + let (dx, dy) = match self.direction { + GuardDirection::Up => (0isize, -1isize), + GuardDirection::Right => (1, 0), + GuardDirection::Down => (0, 1), + GuardDirection::Left => (-1, 0), + }; + self.position + (dx, dy) + } +} + +fn map_feature_parser(input: &mut &str) -> PResult { + alt(( + literal('.').value(MapFeature::Empty), + literal('#').value(MapFeature::Obstruction), + literal('^').value(MapFeature::UpFacingGuard), + )) + .parse_next(input) +} + +fn row_parser(input: &mut &str) -> PResult> { + repeat(1.., map_feature_parser).parse_next(input) +} + +#[derive(Debug, Clone)] +struct RawMap { + rows: Vec>, +} + +impl FromStr for RawMap { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let rows = separated(1.., row_parser, line_ending) + .parse(s.trim()) + .map_err(|err| anyhow::format_err!("{err}"))?; + + Ok(RawMap { rows }) + } +} + +impl TryFrom for Map { + type Error = anyhow::Error; + + fn try_from(value: RawMap) -> Result { + let mut obstacles = HashSet::new(); + let mut guard = Guard::default(); + + let height = value.rows.len(); + let width = value.rows[0].len(); + for (y, row) in value.rows.into_iter().enumerate() { + for (x, feature) in row.into_iter().enumerate() { + let pos = Position::from((x, y)); + match feature { + MapFeature::Empty => {} + MapFeature::Obstruction => { + obstacles.insert(pos); + } + MapFeature::UpFacingGuard => { + guard.position = pos; + } + } + } + } + + if guard.position.is_origin() { + bail!("failed to find initial guard position") + } + + Ok(Map { + guard, + obstacles, + dimensions: Rect { width, height }, + }) + } +} + +impl FromStr for Map { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + RawMap::from_str(s)?.try_into() + } +} + +#[derive(Clone, Copy, Debug)] +pub struct Rect { + pub width: usize, + pub height: usize, +} + +#[derive(Clone, Debug)] +pub struct Map { + guard: Guard, + obstacles: HashSet, + pub dimensions: Rect, +} + +impl Map { + pub fn is_outside_map(&self, pos: Position) -> bool { + pos.x < 0 + || pos.y < 0 + || pos.x >= self.dimensions.width as isize + || pos.y >= self.dimensions.height as isize + } + + pub fn has_obstacle(&self, position: Position) -> bool { + self.obstacles.contains(&position) + } + + pub fn move_guard(&mut self) -> Position { + loop { + let front = self.guard.in_front(); + if self.has_obstacle(front) { + self.guard.rotate() + } else { + self.guard.move_to(front); + return front; + } + } + } + + pub fn guard_position(&self) -> Position { + self.guard.position + } + + pub fn test_loop(mut self) -> bool { + let mut visited = HashSet::new(); + visited.insert(self.guard); + + loop { + let next_guard_position = self.move_guard(); + if !visited.insert(self.guard) { + return true; + } + if self.is_outside_map(next_guard_position) { + return false; + } + } + } + + pub fn new_with_obstacle(&self, obstacle: Position) -> Self { + let mut tester = self.clone(); + tester.obstacles.insert(obstacle); + tester + } +} diff --git a/2024/day06/src/lib.rs b/2024/day06/src/lib.rs new file mode 100644 index 0000000..6cfeef6 --- /dev/null +++ b/2024/day06/src/lib.rs @@ -0,0 +1,112 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::common::Map; +use aoc_common::parsing::FromStrParser; +use aoc_solution::Aoc; +use rayon::prelude::*; +use std::collections::HashSet; + +mod common; + +#[derive(Aoc)] +#[aoc(input = Map)] +#[aoc(parser = FromStrParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day06; + +pub fn part1(mut input: Map) -> usize { + let mut visited = HashSet::new(); + visited.insert(input.guard_position()); + loop { + let next_guard_position = input.move_guard(); + + if input.is_outside_map(next_guard_position) { + return visited.len(); + } + + visited.insert(next_guard_position); + } +} + +pub fn part2(mut input: Map) -> usize { + let base_loop_tester = input.clone(); + + // 1. get all possible spots visited by the guard + let mut visited = HashSet::new(); + loop { + let next_guard_position = input.move_guard(); + + if input.is_outside_map(next_guard_position) { + break; + } + visited.insert(next_guard_position); + } + + // 2. for each of them, try to put an obstacle on the way to see if it would result in a loop + // single-threaded: + // let mut loops = 0; + // for possible_obstacle in visited { + // if base_loop_tester + // .new_with_obstacle(possible_obstacle) + // .test_loop() + // { + // loops += 1 + // } + // } + + visited + .par_iter() + .map(|pos| { + if base_loop_tester.new_with_obstacle(*pos).test_loop() { + 1 + } else { + 0 + } + }) + .sum() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_input() -> Map { + r#"....#..... +.........# +.......... +..#....... +.......#.. +.......... +.#..^..... +........#. +#......... +......#..."# + .parse() + .unwrap() + } + + #[test] + fn part1_sample_input() { + let expected = 41; + assert_eq!(expected, part1(sample_input())) + } + + #[test] + fn part2_sample_input() { + let expected = 6; + assert_eq!(expected, part2(sample_input())) + } +} diff --git a/2024/day06/src/main.rs b/2024/day06/src/main.rs new file mode 100644 index 0000000..27ea9c8 --- /dev/null +++ b/2024/day06/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::helpers::root_path; +use aoc_solution::AocSolutionSolver; +use day06_2024::Day06; + +#[cfg(not(tarpaulin_include))] +fn main() { + Day06::try_solve_from_file(root_path("inputs/2024/day06")) +} diff --git a/Cargo.toml b/Cargo.toml index 44edd96..78a7cb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +83,8 @@ members = [ "2024/day02", "2024/day03", "2024/day04", - "2024/day05" + "2024/day05", + "2024/day06" ] [workspace.package] diff --git a/common/src/types.rs b/common/src/types.rs index de99a36..a0830b4 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -15,7 +15,7 @@ use crate::constants::{EMPTY_PIXEL, FILLED_PIXEL}; use anyhow::bail; use std::fmt::{Display, Formatter}; -use std::ops::Add; +use std::ops::{Add, AddAssign}; #[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] pub enum Pixel { @@ -78,6 +78,12 @@ pub struct Position { pub y: isize, } +impl Display for Position { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {})", self.x, self.y) + } +} + impl Add<(isize, isize)> for Position { type Output = Position; @@ -89,6 +95,13 @@ impl Add<(isize, isize)> for Position { } } +impl AddAssign<(isize, isize)> for Position { + fn add_assign(&mut self, (dx, dy): (isize, isize)) { + self.x += dx; + self.y += dy; + } +} + impl From<(usize, usize)> for Position { fn from((x, y): (usize, usize)) -> Self { Position { @@ -105,6 +118,10 @@ impl From for (isize, isize) { } impl Position { + pub const fn is_origin(&self) -> bool { + self.x == 0 && self.y == 0 + } + pub const fn next_horizontal(&self) -> Position { Position { x: self.x + 1, diff --git a/solution-runner/Cargo.toml b/solution-runner/Cargo.toml index 2fa6f3a..1bdd0e3 100644 --- a/solution-runner/Cargo.toml +++ b/solution-runner/Cargo.toml @@ -103,3 +103,4 @@ day02_2024 = { path = "../2024/day02" } day03_2024 = { path = "../2024/day03" } day04_2024 = { path = "../2024/day04" } day05_2024 = { path = "../2024/day05" } +day06_2024 = { path = "../2024/day06" } diff --git a/solution-runner/src/main.rs b/solution-runner/src/main.rs index 294998e..b508366 100644 --- a/solution-runner/src/main.rs +++ b/solution-runner/src/main.rs @@ -125,6 +125,7 @@ fn main() { define_solution!(args, 2024, 3, "inputs/2024/day03", day03_2024::Day03); define_solution!(args, 2024, 4, "inputs/2024/day04", day04_2024::Day04); define_solution!(args, 2024, 5, "inputs/2024/day05", day05_2024::Day05); + define_solution!(args, 2024, 6, "inputs/2024/day06", day06_2024::Day06); // AUTOGENERATED SOLUTIONS END println!("no solution found for year {}, day {}", args.year, args.day);