From 0716dde8b12166d26fb22eb3606a87d5d9ef80a0 Mon Sep 17 00:00:00 2001 From: Keenan Tims Date: Mon, 9 Dec 2024 21:59:39 -0800 Subject: [PATCH] day10: complete solution --- 10/Cargo.lock | 30 ++++++++++ 10/Cargo.toml | 8 +++ 10/src/main.rs | 153 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 10/Cargo.lock create mode 100644 10/Cargo.toml create mode 100644 10/src/main.rs diff --git a/10/Cargo.lock b/10/Cargo.lock new file mode 100644 index 0000000..6df79a2 --- /dev/null +++ b/10/Cargo.lock @@ -0,0 +1,30 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "day10" +version = "0.1.0" +dependencies = [ + "grid", + "itertools", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "grid" +version = "0.1.0" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] diff --git a/10/Cargo.toml b/10/Cargo.toml new file mode 100644 index 0000000..8b9b86f --- /dev/null +++ b/10/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "day10" +version = "0.1.0" +edition = "2021" + +[dependencies] +grid = { version = "0.1.0", path = "../libs/grid" } +itertools = "0.13.0" diff --git a/10/src/main.rs b/10/src/main.rs new file mode 100644 index 0000000..aa36c0b --- /dev/null +++ b/10/src/main.rs @@ -0,0 +1,153 @@ +use std::fs::File; +use std::io::{BufRead, BufReader, Lines}; +use std::time::{Duration, Instant}; + +use grid::Grid; +use itertools::Itertools; + +// 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)); +} + +struct TrailMap { + map: Grid, +} + +impl From> for TrailMap { + fn from(input: Lines) -> Self { + Self { map: input.into() } + } +} + +impl TrailMap { + fn trailheads(&self) -> Vec<(i64, i64)> { + self.map + .data + .iter() + .enumerate() + .filter(|(_, v)| **v == b'0') + .map(|(i, _v)| self.map.coord(i as i64).unwrap()) + .collect_vec() + } + fn count_reachable_from(&self, pos: (i64, i64), needle: u8, visited: &mut Grid) -> u64 { + visited.set(pos.0, pos.1, true); + let our_val = self.map.get(pos.0, pos.1).unwrap(); + if our_val == needle { + return 1; + } + // adjacents that are +1 + let valid_moves = [(-1, 0), (1, 0), (0, -1), (0, 1)] // left, right, up, down + .iter() + .map(|(x_ofs, y_ofs)| (pos.0 + x_ofs, pos.1 + y_ofs)) // get target position + .filter(|(x, y)| visited.get(*x, *y) == Some(false)) // skip already visited targets + .map(|(x, y)| ((x, y), self.map.get(x, y))) // get value at that position + .filter(|(_, val)| *val == Some(our_val + 1)) // only interested if it's our value + 1 + .map(|(pos, _)| pos) // discard the value + .collect_vec(); // need to collect since the next map will also require access to `visited` + + valid_moves + .iter() + .map(|pos| self.count_reachable_from(*pos, needle, visited)) + .sum() + } + + fn paths_to(&self, pos: (i64, i64), needle: u8, mut path: Vec<(i64, i64)>) -> Vec> { + path.push(pos); + let our_val = self.map.get(pos.0, pos.1).unwrap(); + if our_val == needle { + return vec![path]; + } + let valid_moves = [(-1, 0), (1, 0), (0, -1), (0, 1)] // left, right, up, down + .iter() + .map(|(x_ofs, y_ofs)| (pos.0 + x_ofs, pos.1 + y_ofs)) // get target position + .filter(|pos| !path.contains(pos)) // skip already visited targets + .map(|(x, y)| ((x, y), self.map.get(x, y))) // get value at that position + .filter(|(_, val)| *val == Some(our_val + 1)) // only interested if it's our value + 1 + .map(|(pos, _)| pos) // discard the value + .collect_vec(); // need to collect since the next map will also require access to `visited` + + valid_moves + .iter() + .flat_map(|mov| self.paths_to(*mov, needle, path.clone())) + .collect_vec() + } +} + +// PROBLEM 1 solution + +fn problem1(input: Lines) -> u64 { + let map: TrailMap = input.into(); + + map.trailheads() + .iter() + .map(|pos| { + let mut visited = Grid::with_shape(map.map.width(), map.map.height(), false); + map.count_reachable_from(*pos, b'9', &mut visited) + }) + .sum() +} + +// PROBLEM 2 solution +fn problem2(input: Lines) -> u64 { + let map: TrailMap = input.into(); + + map.trailheads() + .iter() + .flat_map(|pos| map.paths_to(*pos, b'9', Vec::new())) + .count() as u64 +} + +#[cfg(test)] +mod tests { + use crate::*; + use std::io::Cursor; + + const EXAMPLE: &str = &"89010123 +78121874 +87430965 +96549874 +45678903 +32019012 +01329801 +10456732"; + + #[test] + fn problem1_example() { + let c = Cursor::new(EXAMPLE); + assert_eq!(problem1(c.lines()), 36); + } + + #[test] + fn problem2_example() { + let c = Cursor::new(EXAMPLE); + assert_eq!(problem2(c.lines()), 81); + } +}