pathfinding/directed/astar.rs
1//! Compute a shortest path (or all shorted paths) using the [A* search
2//! algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm).
3
4use indexmap::map::Entry::{Occupied, Vacant};
5use num_traits::Zero;
6use std::cmp::Ordering;
7use std::collections::{BinaryHeap, HashSet};
8use std::hash::Hash;
9use std::iter::FusedIterator;
10
11use super::reverse_path;
12use crate::FxIndexMap;
13
14/// Compute a shortest path using the [A* search
15/// algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm).
16///
17/// The shortest path starting from `start` up to a node for which `success` returns `true` is
18/// computed and returned along with its total cost, in a `Some`. If no path can be found, `None`
19/// is returned instead.
20///
21/// - `start` is the starting node.
22/// - `successors` returns a list of successors for a given node, along with the cost for moving
23/// from the node to the successor. This cost must be non-negative.
24/// - `heuristic` returns an approximation of the cost from a given node to the goal. The
25/// approximation must not be greater than the real cost, or a wrong shortest path may be returned.
26/// - `success` checks whether the goal has been reached. It is not a node as some problems require
27/// a dynamic solution instead of a fixed node.
28///
29/// A node will never be included twice in the path as determined by the `Eq` relationship.
30///
31/// The returned path comprises both the start and end node.
32///
33/// # Example
34///
35/// We will search the shortest path on a chess board to go from (1, 1) to (4, 6) doing only knight
36/// moves.
37///
38/// The first version uses an explicit type `Pos` on which the required traits are derived.
39///
40/// ```
41/// use pathfinding::prelude::astar;
42///
43/// #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
44/// struct Pos(i32, i32);
45///
46/// impl Pos {
47/// fn distance(&self, other: &Pos) -> u32 {
48/// (self.0.abs_diff(other.0) + self.1.abs_diff(other.1)) as u32
49/// }
50///
51/// fn successors(&self) -> Vec<(Pos, u32)> {
52/// let &Pos(x, y) = self;
53/// vec![Pos(x+1,y+2), Pos(x+1,y-2), Pos(x-1,y+2), Pos(x-1,y-2),
54/// Pos(x+2,y+1), Pos(x+2,y-1), Pos(x-2,y+1), Pos(x-2,y-1)]
55/// .into_iter().map(|p| (p, 1)).collect()
56/// }
57/// }
58///
59/// static GOAL: Pos = Pos(4, 6);
60/// let result = astar(&Pos(1, 1), |p| p.successors(), |p| p.distance(&GOAL) / 3,
61/// |p| *p == GOAL);
62/// assert_eq!(result.expect("no path found").1, 4);
63/// ```
64///
65/// The second version does not declare a `Pos` type, makes use of more closures,
66/// and is thus shorter.
67///
68/// ```
69/// use pathfinding::prelude::astar;
70///
71/// static GOAL: (i32, i32) = (4, 6);
72/// let result = astar(&(1, 1),
73/// |&(x, y)| vec![(x+1,y+2), (x+1,y-2), (x-1,y+2), (x-1,y-2),
74/// (x+2,y+1), (x+2,y-1), (x-2,y+1), (x-2,y-1)]
75/// .into_iter().map(|p| (p, 1)),
76/// |&(x, y)| (GOAL.0.abs_diff(x) + GOAL.1.abs_diff(y)) / 3,
77/// |&p| p == GOAL);
78/// assert_eq!(result.expect("no path found").1, 4);
79/// ```
80#[allow(clippy::missing_panics_doc)]
81pub fn astar<N, C, FN, IN, FH, FS>(
82 start: &N,
83 mut successors: FN,
84 mut heuristic: FH,
85 mut success: FS,
86) -> Option<(Vec<N>, C)>
87where
88 N: Eq + Hash + Clone,
89 C: Zero + Ord + Copy,
90 FN: FnMut(&N) -> IN,
91 IN: IntoIterator<Item = (N, C)>,
92 FH: FnMut(&N) -> C,
93 FS: FnMut(&N) -> bool,
94{
95 let mut to_see = BinaryHeap::new();
96 to_see.push(SmallestCostHolder {
97 estimated_cost: Zero::zero(),
98 cost: Zero::zero(),
99 index: 0,
100 });
101 let mut parents: FxIndexMap<N, (usize, C)> = FxIndexMap::default();
102 parents.insert(start.clone(), (usize::MAX, Zero::zero()));
103 while let Some(SmallestCostHolder { cost, index, .. }) = to_see.pop() {
104 let successors = {
105 let (node, &(_, c)) = parents.get_index(index).unwrap(); // Cannot fail
106 if success(node) {
107 let path = reverse_path(&parents, |&(p, _)| p, index);
108 return Some((path, cost));
109 }
110 // We may have inserted a node several time into the binary heap if we found
111 // a better way to access it. Ensure that we are currently dealing with the
112 // best path and discard the others.
113 if cost > c {
114 continue;
115 }
116 successors(node)
117 };
118 for (successor, move_cost) in successors {
119 let new_cost = cost + move_cost;
120 let h; // heuristic(&successor)
121 let n; // index for successor
122 match parents.entry(successor) {
123 Vacant(e) => {
124 h = heuristic(e.key());
125 n = e.index();
126 e.insert((index, new_cost));
127 }
128 Occupied(mut e) => {
129 if e.get().1 > new_cost {
130 h = heuristic(e.key());
131 n = e.index();
132 e.insert((index, new_cost));
133 } else {
134 continue;
135 }
136 }
137 }
138
139 to_see.push(SmallestCostHolder {
140 estimated_cost: new_cost + h,
141 cost: new_cost,
142 index: n,
143 });
144 }
145 }
146 None
147}
148
149/// Compute all shortest paths using the [A* search
150/// algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm).
151///
152/// Whereas `astar` (non-deterministic-ally) returns a single shortest
153/// path, `astar_bag` returns all shortest paths (in a
154/// non-deterministic order).
155///
156/// The shortest paths starting from `start` up to a node for which `success` returns `true` are
157/// computed and returned in an iterator along with the cost (which, by definition, is the same for
158/// each shortest path), wrapped in a `Some`. If no paths are found, `None` is returned.
159///
160/// - `start` is the starting node.
161/// - `successors` returns a list of successors for a given node, along with the cost for moving
162/// from the node to the successor.
163/// - `heuristic` returns an approximation of the cost from a given node to the goal. The
164/// approximation must not be greater than the real cost, or a wrong shortest path may be returned.
165/// - `success` checks whether the goal has been reached. It is not a node as some problems require
166/// a dynamic solution instead of a fixed node.
167///
168/// A node will never be included twice in the path as determined by the `Eq` relationship.
169///
170/// Each path comprises both the start and an end node. Note that while every path shares the same
171/// start node, different paths may have different end nodes.
172#[allow(clippy::missing_panics_doc)]
173pub fn astar_bag<N, C, FN, IN, FH, FS>(
174 start: &N,
175 mut successors: FN,
176 mut heuristic: FH,
177 mut success: FS,
178) -> Option<(AstarSolution<N>, C)>
179where
180 N: Eq + Hash + Clone,
181 C: Zero + Ord + Copy,
182 FN: FnMut(&N) -> IN,
183 IN: IntoIterator<Item = (N, C)>,
184 FH: FnMut(&N) -> C,
185 FS: FnMut(&N) -> bool,
186{
187 let mut to_see = BinaryHeap::new();
188 let mut min_cost = None;
189 let mut sinks = HashSet::new();
190 to_see.push(SmallestCostHolder {
191 estimated_cost: Zero::zero(),
192 cost: Zero::zero(),
193 index: 0,
194 });
195 let mut parents: FxIndexMap<N, (HashSet<usize>, C)> = FxIndexMap::default();
196 parents.insert(start.clone(), (HashSet::new(), Zero::zero()));
197 while let Some(SmallestCostHolder {
198 cost,
199 index,
200 estimated_cost,
201 ..
202 }) = to_see.pop()
203 {
204 if matches!(min_cost, Some(min_cost) if estimated_cost > min_cost) {
205 break;
206 }
207 let successors = {
208 let (node, &(_, c)) = parents.get_index(index).unwrap(); // Cannot fail
209 if success(node) {
210 min_cost = Some(cost);
211 sinks.insert(index);
212 }
213 // We may have inserted a node several time into the binary heap if we found
214 // a better way to access it. Ensure that we are currently dealing with the
215 // best path and discard the others.
216 if cost > c {
217 continue;
218 }
219 successors(node)
220 };
221 for (successor, move_cost) in successors {
222 let new_cost = cost + move_cost;
223 let h; // heuristic(&successor)
224 let n; // index for successor
225 match parents.entry(successor) {
226 Vacant(e) => {
227 h = heuristic(e.key());
228 n = e.index();
229 let mut p = HashSet::new();
230 p.insert(index);
231 e.insert((p, new_cost));
232 }
233 Occupied(mut e) => {
234 if e.get().1 > new_cost {
235 h = heuristic(e.key());
236 n = e.index();
237 let s = e.get_mut();
238 s.0.clear();
239 s.0.insert(index);
240 s.1 = new_cost;
241 } else {
242 if e.get().1 == new_cost {
243 // New parent with an identical cost, this is not
244 // considered as an insertion.
245 e.get_mut().0.insert(index);
246 }
247 continue;
248 }
249 }
250 }
251
252 to_see.push(SmallestCostHolder {
253 estimated_cost: new_cost + h,
254 cost: new_cost,
255 index: n,
256 });
257 }
258 }
259
260 min_cost.map(|cost| {
261 let parents = parents
262 .into_iter()
263 .map(|(k, (ps, _))| (k, ps.into_iter().collect()))
264 .collect();
265 (
266 AstarSolution {
267 sinks: sinks.into_iter().collect(),
268 parents,
269 current: vec![],
270 terminated: false,
271 },
272 cost,
273 )
274 })
275}
276
277/// Compute all shortest paths using the [A* search
278/// algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm).
279///
280/// Whereas `astar` (non-deterministic-ally) returns a single shortest
281/// path, `astar_bag` returns all shortest paths (in a
282/// non-deterministic order).
283///
284/// This is a utility function which collects the results of the `astar_bag` function into a
285/// vector. Most of the time, it is more appropriate to use `astar_bag` directly.
286///
287/// ### Warning
288///
289/// The number of results with the same value might be very large in some graphs. Use with caution.
290pub fn astar_bag_collect<N, C, FN, IN, FH, FS>(
291 start: &N,
292 successors: FN,
293 heuristic: FH,
294 success: FS,
295) -> Option<(Vec<Vec<N>>, C)>
296where
297 N: Eq + Hash + Clone,
298 C: Zero + Ord + Copy,
299 FN: FnMut(&N) -> IN,
300 IN: IntoIterator<Item = (N, C)>,
301 FH: FnMut(&N) -> C,
302 FS: FnMut(&N) -> bool,
303{
304 astar_bag(start, successors, heuristic, success)
305 .map(|(solutions, cost)| (solutions.collect(), cost))
306}
307
308/// This structure is used to implement Rust's max-heap as a min-heap
309/// version for A*. The smallest `estimated_cost` (which is the sum of
310/// the `cost` and the heuristic) is preferred. For the same
311/// `estimated_cost`, the highest `cost` will be favored, as it may
312/// indicate that the goal is nearer, thereby requiring fewer
313/// exploration steps.
314struct SmallestCostHolder<K> {
315 estimated_cost: K,
316 cost: K,
317 index: usize,
318}
319
320impl<K: PartialEq> PartialEq for SmallestCostHolder<K> {
321 fn eq(&self, other: &Self) -> bool {
322 self.estimated_cost.eq(&other.estimated_cost) && self.cost.eq(&other.cost)
323 }
324}
325
326impl<K: PartialEq> Eq for SmallestCostHolder<K> {}
327
328impl<K: Ord> PartialOrd for SmallestCostHolder<K> {
329 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
330 Some(self.cmp(other))
331 }
332}
333
334impl<K: Ord> Ord for SmallestCostHolder<K> {
335 fn cmp(&self, other: &Self) -> Ordering {
336 match other.estimated_cost.cmp(&self.estimated_cost) {
337 Ordering::Equal => self.cost.cmp(&other.cost),
338 s => s,
339 }
340 }
341}
342
343/// Iterator structure created by the `astar_bag` function.
344#[derive(Clone)]
345pub struct AstarSolution<N> {
346 sinks: Vec<usize>,
347 parents: Vec<(N, Vec<usize>)>,
348 current: Vec<Vec<usize>>,
349 terminated: bool,
350}
351
352impl<N: Clone + Eq + Hash> AstarSolution<N> {
353 fn complete(&mut self) {
354 loop {
355 let ps = match self.current.last() {
356 None => self.sinks.clone(),
357 Some(last) => self.parents(*last.last().unwrap()).clone(),
358 };
359 if ps.is_empty() {
360 break;
361 }
362 self.current.push(ps);
363 }
364 }
365
366 fn next_vec(&mut self) {
367 while self.current.last().map(Vec::len) == Some(1) {
368 self.current.pop();
369 }
370 self.current.last_mut().map(Vec::pop);
371 }
372
373 fn node(&self, i: usize) -> &N {
374 &self.parents[i].0
375 }
376
377 fn parents(&self, i: usize) -> &Vec<usize> {
378 &self.parents[i].1
379 }
380}
381
382impl<N: Clone + Eq + Hash> Iterator for AstarSolution<N> {
383 type Item = Vec<N>;
384
385 fn next(&mut self) -> Option<Self::Item> {
386 if self.terminated {
387 return None;
388 }
389 self.complete();
390 let path = self
391 .current
392 .iter()
393 .rev()
394 .map(|v| v.last().copied().unwrap())
395 .map(|i| self.node(i).clone())
396 .collect::<Vec<_>>();
397 self.next_vec();
398 self.terminated = self.current.is_empty();
399 Some(path)
400 }
401}
402
403impl<N: Clone + Eq + Hash> FusedIterator for AstarSolution<N> {}