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(¤t) {
172 path.push(previous);
173 current = previous;
174 }
175
176 path.reverse();
177
178 path
179 }
180}