This document explains a solution for counting trails on a topographical map. The program analyzes input data representing height levels to find paths that ascend strictly by one unit at a time until reaching a specific target height.
The core challenge involves finding paths through a height map where:
This is a classic graph traversal problem that can be solved using recursive depth-first search with some constraints on valid moves.
The solution is organized into three main modules:
main.rs
: Entry point with problem definition and solution executiontopographical_map.rs
: Map representation and utility functionstrailhead.rs
: Path finding and path counting logicLet’s explore each component in detail.
The foundation of our solution is a proper representation of the topographical data.
// topographical_map.rs
pub(crate) struct TopographicalMap(Field<u8>);
impl TopographicalMap {
#[inline]
pub(crate) fn get(&self, loc: Location) -> Option<&u8> {
self.0.get(loc)
}
pub(crate) fn lowests(&self) -> impl Iterator<Item = Location> {
self.0.iter()
.enumerate()
.filter(|&(_,s)| *s == 0)
.map(|(idx,_)| self.0.index_to_cartesian(idx))
}
}
Field<u8>
type which likely handles the grid structureget
method provides safe access to height values at a specific locationlowests
method finds all starting points (locations with height 0) by:
This design creates a clean abstraction that hides the complexity of the underlying data structure.
The core algorithm for path finding is implemented in the TrailHead
struct:
// trailhead.rs
pub(crate) struct TrailHead {
history: Option<HashSet<Location>>
}
impl TrailHead {
pub(crate) fn unique_trails() -> TrailHead {
TrailHead { history: None }
}
pub(crate) fn trail_heads() -> TrailHead {
TrailHead { history: Some(HashSet::new()) }
}
// Path finding and counting method...
}
The history
field is an Option<HashSet<Location>>
, allowing two different trail counting strategies:
Some
: Tracks visited locations to avoid counting them multiple timesNone
: Allows revisiting locations for finding unique pathsThis flexible design enables solving both part 1 and part 2 with minimal code duplication.
The core algorithm uses recursive depth-first search with constraints:
pub(crate) fn count_trails(
&mut self,
map: &TopographicalMap,
loc: Location,
is_found: impl Fn(u8) -> bool + Copy
) -> Option<usize>
{
let &val = map.get(loc)?;
if is_found(val) { return Some(1) }
Some(
[(0,1),(0,-1),(1,0),(-1,0)]
.into_iter()
.filter_map(|dir| loc.move_relative(dir))
.filter_map(|neighbor| {
if self.history.as_ref().is_some_and(|h| h.contains(&neighbor)) { return None };
if map.get(neighbor).is_some_and(|&nv| nv != val+1) { return None }
self.history.as_mut().map(|h| h.insert(neighbor));
self.count_trails(map, neighbor, is_found)
})
.sum::<usize>()
)
}
is_found(val)
)filter_map
to both filter invalid moves and transform resultsis_found
enables flexible termination criteriaThe main.rs
file brings everything together:
// main.rs
fn main() {
let input = std::fs::read_to_string("src/bin/day10/input.txt").unwrap();
let map = input.parse::<TopographicalMap>().unwrap();
let t = Instant::now();
let sum = map.lowests()
.filter_map(|start|
TrailHead::trail_heads().count_trails(&map, start, |d| d == 9)
)
.sum::<usize>();
println!("Part 1: Sum of the scores of all trailheads = {sum} - {:?}", t.elapsed());
assert_eq!(786,sum);
let t = Instant::now();
let sum = map.lowests()
.filter_map(|start|
TrailHead::unique_trails().count_trails(&map, start, |d| d == 9)
)
.sum::<usize>();
println!("Part 2: Sum of the ratings of all unique trailheads = {sum} - {:?}", t.elapsed());
assert_eq!(1722,sum);
}
TopographicalMap
structureTrailHead
that tracks historyThis program demonstrates an elegant recursive approach to path finding on a grid. The use of Rust’s type system and functional programming features creates a solution that is both readable and efficient. The two different modes (with and without history tracking) showcase how a small design difference can change the behavior of the algorithm while maintaining the same core logic.