From 638522da6944ab7ab4adf5c5c7e250b0722a1cda Mon Sep 17 00:00:00 2001 From: Markus Brueckner Date: Tue, 10 Dec 2024 21:14:15 +0100 Subject: [PATCH] Day 10 --- 2024/day10/Cargo.lock | 7 ++++ 2024/day10/Cargo.toml | 6 ++++ 2024/day10/README.md | 33 +++++++++++++++++ 2024/day10/src/main.rs | 80 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+) create mode 100644 2024/day10/Cargo.lock create mode 100644 2024/day10/Cargo.toml create mode 100644 2024/day10/README.md create mode 100644 2024/day10/src/main.rs diff --git a/2024/day10/Cargo.lock b/2024/day10/Cargo.lock new file mode 100644 index 0000000..5f8ea61 --- /dev/null +++ b/2024/day10/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "day10" +version = "0.1.0" diff --git a/2024/day10/Cargo.toml b/2024/day10/Cargo.toml new file mode 100644 index 0000000..3c916ef --- /dev/null +++ b/2024/day10/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "day10" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/2024/day10/README.md b/2024/day10/README.md new file mode 100644 index 0000000..ff384bc --- /dev/null +++ b/2024/day10/README.md @@ -0,0 +1,33 @@ +# Solution Day 10 + +Hillclimbing in software. Today is all about recursion. In order to find a way from a trailhead (a `0`) to +a peak (a `9`), we can just recursively look for the next step up until we either leave the map, cannot go +further (because the jump is too big or too small) or we've reached a peak. + +If we cannot go further or leave the map, we've obviously not reached a peak, so the number of found peaks +is 0 in that step. If we found a peak, we can count it. The recursion then just checks in all 4 directions +from the current point and sums up the reachable peaks. + +_Note:_ Technically checking in all 4 directions isn't necessary, since we just came from one of those directions. +However, in oder to simplify the algorithm, we can just check in all 4 and rely on the fact, that the recursion +will definitely stop in the direction we're coming from, because this will be downhill, which we don't allow. + +## Task 1 + +Easy enough. Just execute the algorithm above and remove the peaks when you reach one (hence the copy of the +heightmap). Removing the peak guarantees, that we don't count it more than once, if there are multiple +ways to reach it (guess what I did wrong at first). Disadvantage: we need a fresh copy of the map for each time +we start the wayfinding process. + +Alternatively we could remember the positions of the peaks we already reached and check against that list when +we found them. Slightly more complex implementation (although with a `HashSet<(i32, i32)>` probably easy enough), +but more efficient because we won't need to clone the map again and again. + +## Task 2 + +I feel like this is actually the simpler tasks of the two. The only change is, that we _don't_ mark the peaks +we've already visited, because this time we specifically _want_ to find all possible ways. Technically we could +technically get around the need to clone the height map, the signature of the `find_ways()` function requires +a mutable reference (even though we're not actually mutating the map in this use case), so the borrow checker +yells at us, if we borrow the map immutable (through the `.iter()` call) and mutable at the same time, so we just +copy it unnecessarily. The problem size does allow for that. \ No newline at end of file diff --git a/2024/day10/src/main.rs b/2024/day10/src/main.rs new file mode 100644 index 0000000..95b6f57 --- /dev/null +++ b/2024/day10/src/main.rs @@ -0,0 +1,80 @@ +type HeightMap = Vec>; + +fn load_input() -> HeightMap { + let data = std::fs::read_to_string("input.txt").expect("Could not read file"); + data.lines() + .map(|line| { + line.chars() + .map(|c| c.to_digit(10).expect("Could not parse digit") as i8) + .collect() + }) + .collect() +} + +// find the next steps from the given point in the map. returns the number of reachable peaks (i.e. 9s) +fn find_way( + height_map: &mut HeightMap, + x: i32, + y: i32, + previous_height: i8, + all_paths: bool, +) -> usize { + if x < 0 || x >= height_map[0].len() as i32 || y < 0 || y >= height_map.len() as i32 { + return 0; + } + let current_height = height_map[y as usize][x as usize]; + if current_height != previous_height + 1 { + return 0; + } + if current_height == 9 { + if !all_paths { + height_map[y as usize][x as usize] = -1; // Mark the peak as already reached as to not count it twice + } + return 1; + } + // check in all 4 directions. While this technically means, that we will check back the way we came, this massively simplifies the algorithm and + // will lead to the same result, because the way we came will always be lower, than the current field, causing the recursion to stop in the next step + find_way(height_map, x - 1, y, current_height, all_paths) + + find_way(height_map, x, y - 1, current_height, all_paths) + + find_way(height_map, x + 1, y, current_height, all_paths) + + find_way(height_map, x, y + 1, current_height, all_paths) +} + +fn task1() { + let height_map = load_input(); + let score = height_map.iter().enumerate().fold(0, |sum, (y, line)| { + sum + line.iter().enumerate().fold(0, |line_sum, (x, &height)| { + if height == 0 { + let mut copy = height_map.clone(); + let ways = find_way(&mut copy, x as i32, y as i32, -1, false); + line_sum + ways + } else { + line_sum + } + }) + }); + + println!("Task 1: score: {score}"); +} + +fn task2() { + let mut height_map = load_input(); + let score = height_map.iter().enumerate().fold(0, |sum, (y, line)| { + sum + line.iter().enumerate().fold(0, |line_sum, (x, &height)| { + if height == 0 { + let mut copy: Vec> = height_map.clone(); + let ways = find_way(&mut copy, x as i32, y as i32, -1, true); + line_sum + ways + } else { + line_sum + } + }) + }); + + println!("Task 2: rating: {score}"); +} + +fn main() { + task1(); + task2(); +}