org.jgrapht.alg.shortestpath.AStarShortestPath Maven / Gradle / Ivy
/*
* (C) Copyright 2015-2023, by Joris Kinable, Jon Robison, Thomas Breitbart and Contributors.
*
* JGraphT : a free Java graph-theory library
*
* See the CONTRIBUTORS.md file distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the
* GNU Lesser General Public License v2.1 or later
* which is available at
* http://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html.
*
* SPDX-License-Identifier: EPL-2.0 OR LGPL-2.1-or-later
*/
package org.jgrapht.alg.shortestpath;
import org.jgrapht.*;
import org.jgrapht.alg.interfaces.*;
import org.jgrapht.alg.util.*;
import org.jgrapht.graph.*;
import org.jheaps.*;
import org.jheaps.tree.*;
import java.util.*;
import java.util.function.*;
/**
* A* shortest path.
*
* An implementation of A* shortest path
* algorithm. This class works for directed and undirected graphs, as well as multi-graphs and
* mixed-graphs. The graph can also change between invocations of the
* {@link #getPath(Object, Object)} method; no new instance of this class has to be created. The
* heuristic is implemented using a PairingHeap data structure by default to maintain the set of
* open nodes. However, there still exist several approaches in literature to improve the
* performance of this heuristic which one could consider to implement. Custom heap implementation
* can be specified during the construction time. Another issue to take into consideration is the
* following: given two candidate nodes, $i$, $j$ to expand, where $f(i)=f(j)$, $g(i)$ > $g(j)$,
* $h(i)$ < $g(j)$, $f(i)=g(i)+h(i)$, $g(i)$ is the actual distance from the source node to $i$,
* $h(i)$ is the estimated distance from $i$ to the target node. Usually a depth-first search is
* desired, so ideally we would expand node $i$ first. Using the PairingHeap, this is not
* necessarily the case though. This could be improved in a later version.
*
*
* Note: This implementation works with both consistent and inconsistent admissible heuristics. For
* details on consistency, refer to the description of the method
* {@link AStarAdmissibleHeuristic#isConsistent(Graph)}. However, this class is not optimized
* for inconsistent heuristics. Several opportunities to improve both worst case and average runtime
* complexities for A* with inconsistent heuristics described in literature can be used to improve
* this implementation!
*
* @param the graph vertex type
* @param the graph edge type
* @author Joris Kinable
* @author Jon Robison
* @author Thomas Breitbart
*/
public class AStarShortestPath
extends BaseShortestPathAlgorithm
{
// Supplier of the preferable heap implementation
protected final Supplier> heapSupplier;
// List of open nodes
protected AddressableHeap openList;
protected Map> vertexToHeapNodeMap;
// List of closed nodes
protected Set closedList;
// Mapping of nodes to their g-scores (g(x)).
protected Map gScoreMap;
// Predecessor map: mapping of a node to an edge that leads to its
// predecessor on its shortest path towards the targetVertex
protected Map cameFrom;
// Reference to the admissible heuristic
protected AStarAdmissibleHeuristic admissibleHeuristic;
// Counter which keeps track of the number of expanded nodes
protected int numberOfExpandedNodes;
// Comparator for comparing doubles with tolerance
protected Comparator comparator;
/**
* Create a new instance of the A* shortest path algorithm.
*
* @param graph the input graph
* @param admissibleHeuristic admissible heuristic which estimates the distance from a node to
* the target node. The heuristic must never overestimate the distance.
*/
public AStarShortestPath(Graph graph, AStarAdmissibleHeuristic admissibleHeuristic)
{
this(graph, admissibleHeuristic, PairingHeap::new);
}
/**
* Create a new instance of the A* shortest path algorithm.
*
* @param graph the input graph
* @param admissibleHeuristic admissible heuristic which estimates the distance from a node to
* the target node. The heuristic must never overestimate the distance.
* @param heapSupplier supplier of the preferable heap implementation
*/
public AStarShortestPath(
Graph graph, AStarAdmissibleHeuristic admissibleHeuristic,
Supplier> heapSupplier)
{
super(graph);
this.admissibleHeuristic =
Objects.requireNonNull(admissibleHeuristic, "Heuristic function cannot be null!");
this.comparator = new ToleranceDoubleComparator();
this.heapSupplier = Objects.requireNonNull(heapSupplier, "Heap supplier cannot be null!");
}
/**
* Initializes the data structures.
*
* @param admissibleHeuristic admissible heuristic
*/
private void initialize(AStarAdmissibleHeuristic admissibleHeuristic)
{
this.admissibleHeuristic = admissibleHeuristic;
openList = heapSupplier.get();
vertexToHeapNodeMap = new HashMap<>();
closedList = new HashSet<>();
gScoreMap = new HashMap<>();
cameFrom = new HashMap<>();
numberOfExpandedNodes = 0;
}
/**
* Calculates (and returns) the shortest path from the sourceVertex to the targetVertex. Note:
* each time you invoke this method, the path gets recomputed.
*
* @param sourceVertex source vertex
* @param targetVertex target vertex
* @return the shortest path from sourceVertex to targetVertex
*/
@Override
public GraphPath getPath(V sourceVertex, V targetVertex)
{
if (!graph.containsVertex(sourceVertex) || !graph.containsVertex(targetVertex)) {
throw new IllegalArgumentException(
"Source or target vertex not contained in the graph!");
}
if (sourceVertex.equals(targetVertex)) {
return createEmptyPath(sourceVertex, targetVertex);
}
this.initialize(admissibleHeuristic);
gScoreMap.put(sourceVertex, 0.0);
AddressableHeap.Handle heapNode = openList.insert(0.0, sourceVertex);
vertexToHeapNodeMap.put(sourceVertex, heapNode);
do {
AddressableHeap.Handle currentNode = openList.deleteMin();
// Check whether we reached the target vertex
if (currentNode.getValue().equals(targetVertex)) {
// Build the path
return this.buildGraphPath(sourceVertex, targetVertex, currentNode.getKey());
}
// We haven't reached the target vertex yet; expand the node
expandNode(currentNode, targetVertex);
closedList.add(currentNode.getValue());
} while (!openList.isEmpty());
// No path exists from sourceVertex to TargetVertex
return createEmptyPath(sourceVertex, targetVertex);
}
/**
* Returns how many nodes have been expanded in the A* search procedure in its last invocation.
* A node is expanded if it is removed from the open list.
*
* @return number of expanded nodes
*/
public int getNumberOfExpandedNodes()
{
return numberOfExpandedNodes;
}
private void expandNode(AddressableHeap.Handle currentNode, V endVertex)
{
numberOfExpandedNodes++;
Set outgoingEdges = graph.outgoingEdgesOf(currentNode.getValue());
for (E edge : outgoingEdges) {
V successor = Graphs.getOppositeVertex(graph, edge, currentNode.getValue());
if (successor.equals(currentNode.getValue())) { // Ignore self-loop
continue;
}
double gScore = gScoreMap.get(currentNode.getValue());
double tentativeGScore = gScore + graph.getEdgeWeight(edge);
double fScore =
tentativeGScore + admissibleHeuristic.getCostEstimate(successor, endVertex);
if (vertexToHeapNodeMap.containsKey(successor)) { // We re-encountered a vertex. It's
// either in the open or closed list.
if (tentativeGScore >= gScoreMap.get(successor)) // Ignore path since it is
// non-improving
continue;
cameFrom.put(successor, edge);
gScoreMap.put(successor, tentativeGScore);
if (closedList.contains(successor)) { // it's in the closed list. Move node back to
// open list, since we discovered a shorter
// path to this node
closedList.remove(successor);
openList.insert(fScore, vertexToHeapNodeMap.get(successor).getValue());
} else { // It's in the open list
vertexToHeapNodeMap.get(successor).decreaseKey(fScore);
}
} else { // We've encountered a new vertex.
cameFrom.put(successor, edge);
gScoreMap.put(successor, tentativeGScore);
AddressableHeap.Handle heapNode = openList.insert(fScore, successor);
vertexToHeapNodeMap.put(successor, heapNode);
}
}
}
/**
* Builds the graph path
*
* @param startVertex starting vertex of the path
* @param targetVertex ending vertex of the path
* @param pathLength length of the path
* @return the shortest path from startVertex to endVertex
*/
private GraphPath buildGraphPath(V startVertex, V targetVertex, double pathLength)
{
List edgeList = new ArrayList<>();
List vertexList = new ArrayList<>();
vertexList.add(targetVertex);
V v = targetVertex;
while (!v.equals(startVertex)) {
edgeList.add(cameFrom.get(v));
v = Graphs.getOppositeVertex(graph, cameFrom.get(v), v);
vertexList.add(v);
}
Collections.reverse(edgeList);
Collections.reverse(vertexList);
return new GraphWalk<>(graph, startVertex, targetVertex, vertexList, edgeList, pathLength);
}
}