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> {}