petgraph/algo/
astar.rs

1use alloc::{collections::BinaryHeap, vec, vec::Vec};
2use core::hash::Hash;
3
4use hashbrown::hash_map::{
5    Entry::{Occupied, Vacant},
6    HashMap,
7};
8
9use crate::algo::Measure;
10use crate::scored::MinScored;
11use crate::visit::{EdgeRef, GraphBase, IntoEdges, Visitable};
12
13/// \[Generic\] A* shortest path algorithm.
14///
15/// Computes the shortest path from `start` to `finish`, including the total path cost.
16///
17/// `finish` is implicitly given via the `is_goal` callback, which should return `true` if the
18/// given node is the finish node.
19///
20/// The function `edge_cost` should return the cost for a particular edge. Edge costs must be
21/// non-negative.
22///
23/// The function `estimate_cost` should return the estimated cost to the finish for a particular
24/// node. For the algorithm to find the actual shortest path, it should be admissible, meaning that
25/// it should never overestimate the actual cost to get to the nearest goal node. Estimate costs
26/// must also be non-negative.
27///
28/// The graph should be `Visitable` and implement `IntoEdges`.
29///
30/// # Example
31/// ```
32/// use petgraph::Graph;
33/// use petgraph::algo::astar;
34///
35/// let mut g = Graph::new();
36/// let a = g.add_node((0., 0.));
37/// let b = g.add_node((2., 0.));
38/// let c = g.add_node((1., 1.));
39/// let d = g.add_node((0., 2.));
40/// let e = g.add_node((3., 3.));
41/// let f = g.add_node((4., 2.));
42/// g.extend_with_edges(&[
43///     (a, b, 2),
44///     (a, d, 4),
45///     (b, c, 1),
46///     (b, f, 7),
47///     (c, e, 5),
48///     (e, f, 1),
49///     (d, e, 1),
50/// ]);
51///
52/// // Graph represented with the weight of each edge
53/// // Edges with '*' are part of the optimal path.
54/// //
55/// //     2       1
56/// // a ----- b ----- c
57/// // | 4*    | 7     |
58/// // d       f       | 5
59/// // | 1*    | 1*    |
60/// // \------ e ------/
61///
62/// let path = astar(&g, a, |finish| finish == f, |e| *e.weight(), |_| 0);
63/// assert_eq!(path, Some((6, vec![a, d, e, f])));
64/// ```
65///
66/// Returns the total cost + the path of subsequent `NodeId` from start to finish, if one was
67/// found.
68pub fn astar<G, F, H, K, IsGoal>(
69    graph: G,
70    start: G::NodeId,
71    mut is_goal: IsGoal,
72    mut edge_cost: F,
73    mut estimate_cost: H,
74) -> Option<(K, Vec<G::NodeId>)>
75where
76    G: IntoEdges + Visitable,
77    IsGoal: FnMut(G::NodeId) -> bool,
78    G::NodeId: Eq + Hash,
79    F: FnMut(G::EdgeRef) -> K,
80    H: FnMut(G::NodeId) -> K,
81    K: Measure + Copy,
82{
83    let mut visit_next = BinaryHeap::new();
84    let mut scores = HashMap::new(); // g-values, cost to reach the node
85    let mut estimate_scores = HashMap::new(); // f-values, cost to reach + estimate cost to goal
86    let mut path_tracker = PathTracker::<G>::new();
87
88    let zero_score = K::default();
89    scores.insert(start, zero_score);
90    visit_next.push(MinScored(estimate_cost(start), start));
91
92    while let Some(MinScored(estimate_score, node)) = visit_next.pop() {
93        if is_goal(node) {
94            let path = path_tracker.reconstruct_path_to(node);
95            let cost = scores[&node];
96            return Some((cost, path));
97        }
98
99        // This lookup can be unwrapped without fear of panic since the node was necessarily scored
100        // before adding it to `visit_next`.
101        let node_score = scores[&node];
102
103        match estimate_scores.entry(node) {
104            Occupied(mut entry) => {
105                // If the node has already been visited with an equal or lower score than now, then
106                // we do not need to re-visit it.
107                if *entry.get() <= estimate_score {
108                    continue;
109                }
110                entry.insert(estimate_score);
111            }
112            Vacant(entry) => {
113                entry.insert(estimate_score);
114            }
115        }
116
117        for edge in graph.edges(node) {
118            let next = edge.target();
119            let next_score = node_score + edge_cost(edge);
120
121            match scores.entry(next) {
122                Occupied(mut entry) => {
123                    // No need to add neighbors that we have already reached through a shorter path
124                    // than now.
125                    if *entry.get() <= next_score {
126                        continue;
127                    }
128                    entry.insert(next_score);
129                }
130                Vacant(entry) => {
131                    entry.insert(next_score);
132                }
133            }
134
135            path_tracker.set_predecessor(next, node);
136            let next_estimate_score = next_score + estimate_cost(next);
137            visit_next.push(MinScored(next_estimate_score, next));
138        }
139    }
140
141    None
142}
143
144struct PathTracker<G>
145where
146    G: GraphBase,
147    G::NodeId: Eq + Hash,
148{
149    came_from: HashMap<G::NodeId, G::NodeId>,
150}
151
152impl<G> PathTracker<G>
153where
154    G: GraphBase,
155    G::NodeId: Eq + Hash,
156{
157    fn new() -> PathTracker<G> {
158        PathTracker {
159            came_from: HashMap::new(),
160        }
161    }
162
163    fn set_predecessor(&mut self, node: G::NodeId, previous: G::NodeId) {
164        self.came_from.insert(node, previous);
165    }
166
167    fn reconstruct_path_to(&self, last: G::NodeId) -> Vec<G::NodeId> {
168        let mut path = vec![last];
169
170        let mut current = last;
171        while let Some(&previous) = self.came_from.get(&current) {
172            path.push(previous);
173            current = previous;
174        }
175
176        path.reverse();
177
178        path
179    }
180}