Finding all the paths between two vertices with weight limit in a directed graph
Asked Answered
T

1

14

I'm trying to find all the paths between two vertices that have weight less than N in a directed weighted graph that may have loops but not self-loops. So far, I could do it only by using AllDirectedPaths and then filter out the paths that have weight bigger than N:

SimpleDirectedWeightedGraph<String, DefaultWeightedEdge> g = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);
AllDirectedPaths<String, DefaultWeightedEdge> allPaths = new AllDirectedPaths<>(g);
int maxWeight = 30;
List<GraphPath<String, DefaultWeightedEdge>> pathsLimitedByWeight = allPaths.getAllPaths(startVertex, endVertex, false, maxWeight / minGraphLatency)
    .filter(graphPath -> graphPath.getWeight() < maxWeight)
    .collect(Collectors.toList());

This approach is obviously suboptimal because for larger graphs it is slow. To limit the output and make it faster, I supply maxPathLength to the AllDirectedPaths#getAllPaths, which I set to the max weight a path can have divided by a minimal weight an edge in the graph has since in my case edge weight is an integer and is always greater than 1.

I considered using KShortestSimplePaths with a custom PathValidator, but it supports only simple paths, i.e, doesn't allow loops.

What other option, if any, do I have within jgrapht that can be used to solve finding all paths without having to traverse the graph myself.

Thomajan answered 14/2, 2021 at 23:34 Comment(3)
This question is being discusses on meta.Deafanddumb
I think one thing to consider is that no matter how you do this if you actually want to get a list of all of the paths, then it will be exponential time. Consider nodes s_i, u_i and d_i. For each s_i we have (s_i, u_i) and (s_i, d_i) in E as well as (u_(i), s_(i+1)) and (d_(i), s_(i+1)). If you draw out this graph, it should be fairly clear to see that there are exponentially many paths so an efficient algorithm is not possible.Salami
@Hive7, I understand that an efficient algorithm is not possible, and that the time to get all the paths will be exponential. In the documentation of AllDirectedPaths, which implements retrieval of a list of all the paths between two vertices it says that "if all paths are considered (i.e., there is no limit on the max path length), it may be very slow due to potentially huge output", and I realize that since non simple paths are allowed, it will be even slower with even bigger output. What I'm looking for is an API in jgrapht similar to AllDirectedPaths#getAllPaths(Graph, s, t, maxPathWeight)Thomajan
S
3

There is no algorithm that allows you to efficiently query all non-simple paths between a pair of vertices. There can be exponentially many paths. Imagine a graph with the following edges: (s,u),(u,v),(v,u),(u,t), where all edges have length 1. Now find all non-simple paths from s to t, with a weight limit of N. You would get the following paths:

  • s,u,t
  • s,u,v,u,t
  • s,u,v,u,v,u,t
  • s,u,v,u,v,u,v,u,t
  • ....

You could continue cycling [u,v,u] until you finally hit the weight limit. If this is really what you want, I would recommend implementing a simple labeling algorithm. A label encodes a partial path. A label keeps a reference to its preceding label, a reference to the node the label is associated with, as well as a cost equal to the total cost of the partial path represented by the label. Start the algorithm by creating a label for the source node s with cost 0 and add it to a queue of open labels. During every iteration of the algorithm, poll a label from the open queue until the queue is exhausted. For a polled label L associated with node i and having cost c, expand the label: for each neighbor j of node i, create a new label L' that points back to label L and set its cost equal to c plus edge weight d_ij. If the cost of the new label L' exceeds the available budget, discard the label. Else, if j is the target node, we found a new path, so store the label such that we can recover the path later. Else, add L' to the queue of open labels. A simple implementation of this algorithm can be found below.

Notes:

  1. The above labeling algorithm will only work when either the graph is relatively small, N is low, or the edge weights are high, since the number of possible paths from s to t can grow very fast.
  2. The performance of the above algorithm can be slightly improved by including a admissible heuristic to compute the least amount of budget required to complete a path from a given node to the terminal. This would allow you to prune some labels.
  3. All edge weights are required to be larger than 0.
import org.jgrapht.*;
import org.jgrapht.graph.*;

import java.util.*;

public class NonSimplePaths<V,E> {

    public List<GraphPath<V, E>> computeNoneSimplePaths(Graph<V,E> graph, V source, V target, double budget){
        GraphTests.requireDirected(graph); //Require input graph to be directed
        if(source.equals(target))
            return Collections.emptyList();

        Label start = new Label(null, source, 0);
        Queue<Label> openQueue = new LinkedList<>(); //List of open labels
        List<Label> targetLabels = new LinkedList<>(); //Labels associated with target node
        openQueue.add(start);

        while(!openQueue.isEmpty()){
            Label openLabel = openQueue.poll();
            for(E e : graph.outgoingEdgesOf(openLabel.node)){
                double weight = graph.getEdgeWeight(e);
                V neighbor = Graphs.getOppositeVertex(graph, e, openLabel.node);

                //Check whether extension is possible
                if(openLabel.cost + weight <= budget){
                    Label label = new Label(openLabel, neighbor, openLabel.cost + weight); //Create new label
                    if(neighbor.equals(target)) //Found a new path from source to target
                        targetLabels.add(label);
                    else //Must continue extending the path until a complete path is found
                        openQueue.add(label);
                }
            }
        }

        //Every label in the targetLabels list corresponds to a unique path. Recreate paths by backtracking labels
        List<GraphPath<V,E>> paths = new ArrayList<>(targetLabels.size());
        for(Label label : targetLabels){ //Iterate over every path
            List<V> path = new LinkedList<>();
            double pathWeight = label.cost;
            do{
                path.add(label.node);
                label=label.pred;
            }while(label != null);
            Collections.reverse(path); //By backtracking the labels, we recoved the path in reverse order
            paths.add(new GraphWalk<>(graph, path, pathWeight));
        }

       return paths;
   }

    private final class Label{
        private final Label pred;
        private final V node;
        private final double cost;

        private Label(Label pred, V node, double cost) {
            this.pred = pred;
            this.node = node;
            this.cost = cost;
        }
    }

    public static void main(String[] args){
        Graph<String,DefaultWeightedEdge> graph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);
        Graphs.addAllVertices(graph, Arrays.asList("s","u","v","t"));
        graph.addEdge("s","u");
        graph.addEdge("u","t");
        graph.addEdge("u","v");
        graph.addEdge("v","u");
        graph.edgeSet().forEach(e -> graph.setEdgeWeight(e,1.0)); //Set weight of all edges to 1

        NonSimplePaths<String,DefaultWeightedEdge> nonSimplePaths = new NonSimplePaths<>();
        List<GraphPath<String,DefaultWeightedEdge>> paths = nonSimplePaths.computeNoneSimplePaths(graph, "s", "t", 10);
        for(GraphPath<String,DefaultWeightedEdge> path : paths)
            System.out.println(path+" cost: "+path.getWeight());
    }
}

Output of the above sample code:

[s, u, t] cost: 2.0
[s, u, v, u, t] cost: 4.0
[s, u, v, u, v, u, t] cost: 6.0
[s, u, v, u, v, u, v, u, t] cost: 8.0
[s, u, v, u, v, u, v, u, v, u, t] cost: 10.0

Improving the performance of the above implementation, e.g. by adding an admissible heuristic, I leave as an exercise to the OP.

Submit answered 16/2, 2021 at 18:46 Comment(2)
Thanks for your answer. I do realize that there is no algorithm for efficiently querying all non-simple paths between a pair of vertices. My question is not about existence of such an algorithm but more about implementation (of inefficient algorithm) in particular library (jgrapht), which already offers something similar AllDirectedPaths, which returns all the paths between two vertices potentially limited by max path length. So I had in mind implementation that allows to limit by weight instead of length.Thomajan
@Thomajan What you asked cannot be done with the existing libraries in JGraphT. I've updated my answer by adding the complete algorithm implemented in Java using JGraphT as the backing graph library.Submit

© 2022 - 2024 — McMap. All rights reserved.