use std::{cmp::Ordering, collections::BinaryHeap};
use crate::{
grid::Grid,
search::{BudgetWatch, GridSearchError, SearchOutcome, SearchRequest, SearchStats},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OptimalPathCount {
pub cost: usize,
pub paths: u64,
}
pub type OptimalPathCountResult =
Result<SearchOutcome<OptimalPathCount, SearchStats>, GridSearchError>;
#[derive(Debug, Default, Clone, Copy)]
pub struct ShortestPathCount;
impl ShortestPathCount {
pub const NAME: &str = "count-optimal-paths";
pub fn count(&self, grid: &Grid, request: SearchRequest) -> OptimalPathCountResult {
crate::search::validate_request(grid, request)?;
let Some(start_index) = grid.index_of(request.start) else {
return Ok(SearchOutcome::no_path(SearchStats { visited_nodes: 0 }));
};
let Some(goal_index) = grid.index_of(request.goal) else {
return Ok(SearchOutcome::no_path(SearchStats { visited_nodes: 0 }));
};
if request.start == request.goal {
return Ok(SearchOutcome::found(
OptimalPathCount { cost: 0, paths: 1 },
SearchStats { visited_nodes: 1 },
));
}
let watch = BudgetWatch::start(request.budget);
let mut frontier = BinaryHeap::from([FrontierEntry {
cost_so_far: 0,
index: start_index,
}]);
let mut best_costs: Vec<Option<usize>> = vec![None; grid.cell_count()];
let mut path_counts: Vec<u64> = vec![0; grid.cell_count()];
let mut visited_nodes = 0usize;
best_costs[start_index] = Some(0);
path_counts[start_index] = 1;
while let Some(entry) = frontier.pop() {
if best_costs[entry.index] != Some(entry.cost_so_far) {
continue;
}
visited_nodes += 1;
if let Err(reason) = watch.check(visited_nodes) {
return Err(crate::search::budget_error(reason));
}
let current = grid.point_from_index(entry.index);
for neighbor in grid.neighbors4(current) {
let neighbor_index = grid
.index_of(neighbor)
.expect("walkable neighbors must exist inside the grid");
let edge_cost = grid
.traversal_cost(neighbor)
.expect("walkable neighbors must have a traversal cost");
let Some(next_cost) = entry.cost_so_far.checked_add(edge_cost) else {
continue;
};
match best_costs[neighbor_index] {
Some(best) if next_cost > best => {}
Some(best) if next_cost == best => {
path_counts[neighbor_index] =
path_counts[neighbor_index].saturating_add(path_counts[entry.index]);
}
_ => {
best_costs[neighbor_index] = Some(next_cost);
path_counts[neighbor_index] = path_counts[entry.index];
frontier.push(FrontierEntry {
cost_so_far: next_cost,
index: neighbor_index,
});
}
}
}
}
let Some(goal_cost) = best_costs[goal_index] else {
return Ok(SearchOutcome::no_path(SearchStats { visited_nodes }));
};
Ok(SearchOutcome::found(
OptimalPathCount {
cost: goal_cost,
paths: path_counts[goal_index],
},
SearchStats { visited_nodes },
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct FrontierEntry {
cost_so_far: usize,
index: usize,
}
impl Ord for FrontierEntry {
fn cmp(&self, other: &Self) -> Ordering {
other
.cost_so_far
.cmp(&self.cost_so_far)
.then_with(|| other.index.cmp(&self.index))
}
}
impl PartialOrd for FrontierEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[cfg(test)]
mod tests {
use crate::{
algorithms::shortest_path_count::{OptimalPathCount, ShortestPathCount},
grid::{Cell, Grid},
point::Point,
search::SearchRequest,
};
#[test]
fn open_rectangle_counts_lattice_paths() {
let grid = Grid::new(3, 3).expect("grid dimensions are valid");
let outcome = ShortestPathCount
.count(
&grid,
SearchRequest::new(Point::new(0, 0), Point::new(2, 2)),
)
.expect("endpoints are walkable");
assert_eq!(
outcome.path().copied(),
Some(OptimalPathCount { cost: 4, paths: 6 })
);
}
#[test]
fn corridor_counts_one_path() {
let mut grid = Grid::new(5, 5).expect("grid dimensions are valid");
for y in 0..5 {
if y != 2 {
grid.set_cell(Point::new(2, y), Cell::Blocked)
.expect("valid grid edit");
}
}
let outcome = ShortestPathCount
.count(
&grid,
SearchRequest::new(Point::new(0, 2), Point::new(4, 2)),
)
.expect("endpoints are walkable");
assert_eq!(outcome.path().map(|count| count.paths), Some(1));
}
#[test]
fn reports_when_no_path_exists() {
let mut grid = Grid::new(3, 3).expect("grid dimensions are valid");
for x in 0..3 {
grid.set_cell(Point::new(x, 1), Cell::Blocked)
.expect("valid grid edit");
}
let outcome = ShortestPathCount
.count(
&grid,
SearchRequest::new(Point::new(0, 0), Point::new(2, 2)),
)
.expect("endpoints are walkable");
assert!(!outcome.is_found());
}
}