org.jgrapht.alg.shortestpath.AllDirectedPaths Maven / Gradle / Ivy
The newest version!
/*
* (C) Copyright 2015-2023, by Vera-Licona Research Group 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.graph.*;
import java.util.*;
/**
* A Dijkstra-like algorithm to find all paths between two sets of nodes in a directed graph, with
* options to search only simple paths and to limit the path length.
*
* @param the graph vertex type
* @param the graph edge type
*
* @author Andrew Gainer-Dewar, Google LLC
*/
public class AllDirectedPaths
{
private final Graph graph;
/**
* Provides validation for the paths which will be computed. If the validator is {@code null},
* this means that all paths are valid.
*/
private final PathValidator pathValidator;
/**
* Create a new instance.
*
* @param graph the input graph
* @throws IllegalArgumentException if the graph is not directed
*/
public AllDirectedPaths(Graph graph)
{
this(graph, null);
}
/**
* Create a new instance with given {@code pathValidator}.
*
* If non-{@code null}, the {@code pathValidator} will be used while searching for paths,
* validating the addition of any edge to a partial path. Zero-length paths will therefore not
* be subject to {@code pathValidator}; length-1 paths will.
*
* @param graph the input graph
* @param pathValidator validator for computed paths; may be null
* @throws IllegalArgumentException if the graph is not directed
*/
public AllDirectedPaths(Graph graph, PathValidator pathValidator)
{
this.graph = GraphTests.requireDirected(graph);
this.pathValidator = pathValidator;
}
/**
* Calculate (and return) all paths from the source vertex to the target vertex.
*
* @param sourceVertex the source vertex
* @param targetVertex the target vertex
* @param simplePathsOnly if true, only search simple (non-self-intersecting) paths
* @param maxPathLength maximum number of edges to allow in a path (if null, all paths are
* considered, which may be very slow due to potentially huge output)
* @return all paths from the source vertex to the target vertex
*/
public List> getAllPaths(
V sourceVertex, V targetVertex, boolean simplePathsOnly, Integer maxPathLength)
{
return getAllPaths(
Collections.singleton(sourceVertex), Collections.singleton(targetVertex),
simplePathsOnly, maxPathLength);
}
/**
* Calculate (and return) all paths from the source vertices to the target vertices.
*
* @param sourceVertices the source vertices
* @param targetVertices the target vertices
* @param simplePathsOnly if true, only search simple (non-self-intersecting) paths
* @param maxPathLength maximum number of edges to allow in a path (if null, all paths are
* considered, which may be very slow due to potentially huge output)
*
* @return list of all paths from the sources to the targets containing no more than
* maxPathLength edges
*/
public List> getAllPaths(
Set sourceVertices, Set targetVertices, boolean simplePathsOnly,
Integer maxPathLength)
{
if ((maxPathLength != null) && (maxPathLength < 0)) {
throw new IllegalArgumentException("maxPathLength must be non-negative if defined");
}
if (!simplePathsOnly && (maxPathLength == null)) {
throw new IllegalArgumentException(
"If search is not restricted to simple paths, a maximum path length must be set to avoid infinite cycles");
}
if ((sourceVertices.isEmpty()) || (targetVertices.isEmpty())) {
return Collections.emptyList();
}
// Decorate the edges with the minimum path lengths through them
Map edgeMinDistancesFromTargets =
edgeMinDistancesBackwards(targetVertices, maxPathLength);
// Generate all the paths
return generatePaths(
sourceVertices, targetVertices, simplePathsOnly, maxPathLength,
edgeMinDistancesFromTargets);
}
/**
* Compute the minimum number of edges in a path to the targets through each edge, so long as it
* is not greater than a bound.
*
* @param targetVertices the target vertices
* @param maxPathLength maximum number of edges to allow in a path (if null, all edges will be
* considered, which may be expensive)
*
* @return the minimum number of edges in a path from each edge to the targets, encoded in a Map
*/
private Map edgeMinDistancesBackwards(Set targetVertices, Integer maxPathLength)
{
/*
* We walk backwards through the network from the target vertices, marking edges and
* vertices with their minimum distances as we go.
*/
Map edgeMinDistances = new HashMap<>();
Map vertexMinDistances = new HashMap<>();
Queue verticesToProcess = new ArrayDeque<>();
// Input sanity checking
if (maxPathLength != null) {
if (maxPathLength < 0) {
throw new IllegalArgumentException("maxPathLength must be non-negative if defined");
}
if (maxPathLength == 0) {
return edgeMinDistances;
}
}
// Bootstrap the process with the target vertices
for (V target : targetVertices) {
vertexMinDistances.put(target, 0);
verticesToProcess.add(target);
}
// Work through the node queue. When it's empty, we're done!
for (V vertex; (vertex = verticesToProcess.poll()) != null;) {
assert vertexMinDistances.containsKey(vertex);
Integer childDistance = vertexMinDistances.get(vertex) + 1;
// Check whether the incoming edges of this node are correctly
// decorated
for (E edge : graph.incomingEdgesOf(vertex)) {
// Mark the edge if needed
if (!edgeMinDistances.containsKey(edge)
|| (edgeMinDistances.get(edge) > childDistance))
{
edgeMinDistances.put(edge, childDistance);
}
// Mark the edge's source vertex if needed
V edgeSource = graph.getEdgeSource(edge);
if (!vertexMinDistances.containsKey(edgeSource)
|| (vertexMinDistances.get(edgeSource) > childDistance))
{
vertexMinDistances.put(edgeSource, childDistance);
if ((maxPathLength == null) || (childDistance < maxPathLength)) {
verticesToProcess.add(edgeSource);
}
}
}
}
assert verticesToProcess.isEmpty();
return edgeMinDistances;
}
/**
* Generate all paths from the sources to the targets, using pre-computed minimum distances.
*
* @param sourceVertices the source vertices
* @param targetVertices the target vertices
* @param maxPathLength maximum number of edges to allow in a path
* @param simplePathsOnly if true, only search simple (non-self-intersecting) paths (if null,
* all edges will be considered, which may be expensive)
* @param edgeMinDistancesFromTargets the minimum number of edges in a path to a target through
* each edge, as computed by {@code
* edgeMinDistancesBackwards}.
*
* @return a List of all GraphPaths from the sources to the targets satisfying the given
* constraints
*/
private List> generatePaths(
Set sourceVertices, Set targetVertices, boolean simplePathsOnly,
Integer maxPathLength, Map edgeMinDistancesFromTargets)
{
/*
* We walk forwards through the network from the source vertices, exploring all outgoing
* edges whose minimum distances is small enough.
*/
List> completePaths = new ArrayList<>();
Deque> incompletePaths = new LinkedList<>();
// Input sanity checking
if (maxPathLength != null && maxPathLength < 0) {
throw new IllegalArgumentException("maxPathLength must be non-negative if defined");
}
// Bootstrap the search with the source vertices
for (V source : sourceVertices) {
if (targetVertices.contains(source)) {
// pathValidator intentionally not invoked here
completePaths.add(GraphWalk.singletonWalk(graph, source, 0d));
}
if (maxPathLength != null && maxPathLength == 0) {
continue;
}
for (E edge : graph.outgoingEdgesOf(source)) {
assert graph.getEdgeSource(edge).equals(source);
if (pathValidator == null
|| pathValidator.isValidPath(GraphWalk.emptyWalk(graph), edge))
{
if (targetVertices.contains(graph.getEdgeTarget(edge))) {
completePaths.add(makePath(Collections.singletonList(edge)));
}
if (edgeMinDistancesFromTargets.containsKey(edge)
&& (maxPathLength == null || maxPathLength > 1))
{
List path = Collections.singletonList(edge);
incompletePaths.add(path);
}
}
}
}
if (maxPathLength != null && maxPathLength == 0) {
return completePaths;
}
// Walk through the queue of incomplete paths
for (List incompletePath; (incompletePath = incompletePaths.poll()) != null;) {
Integer lengthSoFar = incompletePath.size();
assert (maxPathLength == null) || (lengthSoFar < maxPathLength);
E leafEdge = incompletePath.get(lengthSoFar - 1);
V leafNode = graph.getEdgeTarget(leafEdge);
Set pathVertices = new HashSet<>();
for (E pathEdge : incompletePath) {
pathVertices.add(graph.getEdgeSource(pathEdge));
pathVertices.add(graph.getEdgeTarget(pathEdge));
}
for (E outEdge : graph.outgoingEdgesOf(leafNode)) {
// Proceed if the outgoing edge is marked and the mark
// is sufficiently small
if (edgeMinDistancesFromTargets.containsKey(outEdge)
&& ((maxPathLength == null) || ((edgeMinDistancesFromTargets.get(outEdge)
+ lengthSoFar) <= maxPathLength)))
{
List newPath = new ArrayList<>(incompletePath);
newPath.add(outEdge);
// If requested, make sure this path isn't self-intersecting
if (simplePathsOnly && pathVertices.contains(graph.getEdgeTarget(outEdge))) {
continue;
}
// If requested, validate the path
if (pathValidator != null
&& !pathValidator.isValidPath(makePath(incompletePath), outEdge))
{
continue;
}
// If this path reaches a target, add it to completePaths
if (targetVertices.contains(graph.getEdgeTarget(outEdge))) {
GraphPath completePath = makePath(newPath);
assert sourceVertices.contains(completePath.getStartVertex());
assert targetVertices.contains(completePath.getEndVertex());
assert (maxPathLength == null)
|| (completePath.getLength() <= maxPathLength);
completePaths.add(completePath);
}
// If this path is short enough, consider further
// extensions of it
if ((maxPathLength == null) || (newPath.size() < maxPathLength)) {
incompletePaths.addFirst(newPath); // We use
// incompletePaths in
// FIFO mode to avoid
// memory blowup
}
}
}
}
assert incompletePaths.isEmpty();
return completePaths;
}
/**
* Transform an ordered list of edges into a GraphPath.
*
* The weight of the generated GraphPath is set to the sum of the weights of the edges.
*
* @param edges the edges
*
* @return the corresponding GraphPath
*/
private GraphPath makePath(List edges)
{
V source = graph.getEdgeSource(edges.get(0));
V target = graph.getEdgeTarget(edges.get(edges.size() - 1));
double weight = edges.stream().mapToDouble(edge -> graph.getEdgeWeight(edge)).sum();
return new GraphWalk<>(graph, source, target, edges, weight);
}
}