Day 10
This commit is contained in:
parent
5f58d7b95b
commit
638522da69
4 changed files with 126 additions and 0 deletions
7
2024/day10/Cargo.lock
generated
Normal file
7
2024/day10/Cargo.lock
generated
Normal file
|
@ -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"
|
6
2024/day10/Cargo.toml
Normal file
6
2024/day10/Cargo.toml
Normal file
|
@ -0,0 +1,6 @@
|
|||
[package]
|
||||
name = "day10"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
33
2024/day10/README.md
Normal file
33
2024/day10/README.md
Normal file
|
@ -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.
|
80
2024/day10/src/main.rs
Normal file
80
2024/day10/src/main.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
type HeightMap = Vec<Vec<i8>>;
|
||||
|
||||
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<Vec<i8>> = 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();
|
||||
}
|
Loading…
Add table
Reference in a new issue