org.jgrapht.alg.cycle.HowardMinimumMeanCycle Maven / Gradle / Ivy
/*
* (C) Copyright 2020-2023, by Semen Chudakov 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.cycle;
import org.jgrapht.Graph;
import org.jgrapht.GraphPath;
import org.jgrapht.Graphs;
import org.jgrapht.alg.connectivity.GabowStrongConnectivityInspector;
import org.jgrapht.alg.interfaces.MinimumCycleMeanAlgorithm;
import org.jgrapht.alg.interfaces.StrongConnectivityAlgorithm;
import org.jgrapht.alg.util.ToleranceDoubleComparator;
import org.jgrapht.graph.GraphWalk;
import org.jgrapht.util.CollectionUtil;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Implementation of Howard`s algorithm for finding minimum cycle mean in a graph.
*
*
* The algorithm is described in the article: Ali Dasdan, Sandy S. Irani, and Rajesh K. Gupta. 1999.
* Efficient algorithms for optimum cycle mean and optimum cost to time ratio problems. In
* Proceedings of the 36th annual ACM/IEEE Design Automation Conference (DAC ’99). Association for
* Computing Machinery, New York, NY, USA, 37–42. DOI:https://doi.org/10.1145/309847.309862
*
*
* Firstly, the graph is divided into strongly connected components. The minimum cycle mean is then
* computed as the globally minimum cycle mean over all components. In the process the necessary
* information is recorded to be able to reconstruct the cycle with minimum mean.
*
*
* The computations are divided into iterations. In each iteration the algorithm tries to update
* current minimum cycle mean value. There is a possibility to limit the total number of iteration
* via a constructor parameter.
*
* @param graph vertex type
* @param graph edge type
* @author Semen Chudakov
*/
public class HowardMinimumMeanCycle
implements MinimumCycleMeanAlgorithm
{
/**
* The underlying graph.
*/
private final Graph graph;
/**
* Algorithm for computing strongly connected components in the {@code graph}.
*/
private final StrongConnectivityAlgorithm strongConnectivityAlgorithm;
/**
* Maximum number of iterations performed during the computation. If not provided via
* constructor the value if defaulted to {@link Integer#MAX_VALUE}.
*/
private final int maximumIterations;
/**
* Used to compare floating point numbers.
*/
private final Comparator comparator;
/**
* Determines if a cycle is found on current iteration.
*/
private boolean isCurrentCycleFound;
/**
* Total weight of a cycle found on current iteration.
*/
private double currentCycleWeight;
/**
* Length of a cycle found on current iteration.
*/
private int currentCycleLength;
/**
* Vertex which is used to reconstruct the cycle found on current iteration.
*/
private V currentCycleVertex;
/**
* For each vertex contains an edge, which together for the policy graph on current iteration.
*/
private Map policyGraph;
/**
* For each vertex indicates, if it has been reached by a search during computing vertices
* distance in the policy graph.
*/
private Map reachedVertices;
/**
* For each vertex stores its level which is used to find a cycle in the policy graph.
*/
private Map vertexLevel;
/**
* For each vertex stores its distance in the policy graph.
*/
private Map vertexDistance;
/**
* Constructs an instance of the algorithm for the given {@code graph}.
*
* @param graph graph
*/
public HowardMinimumMeanCycle(Graph graph)
{
this(graph, Integer.MAX_VALUE);
}
/**
* Constructs an instance of the algorithm for the given {@code graph} and
* {@code maximumIterations}.
*
* @param graph graph
* @param maximumIterations maximum number of iterations
*/
public HowardMinimumMeanCycle(Graph graph, int maximumIterations)
{
this(graph, maximumIterations, new GabowStrongConnectivityInspector<>(graph), 1e-9);
}
/**
* Constructs an instance of the algorithm for the given {@code graph},
* {@code maximumIterations}, {@code strongConnectivityAlgorithm} and {@code toleranceEpsilon}.
*
* @param graph graph
* @param maximumIterations maximum number of iterations
* @param strongConnectivityAlgorithm algorithm to compute strongly connected components
* @param toleranceEpsilon tolerance to compare floating point numbers
*/
public HowardMinimumMeanCycle(
Graph graph, int maximumIterations,
StrongConnectivityAlgorithm strongConnectivityAlgorithm, double toleranceEpsilon)
{
this.graph = Objects.requireNonNull(graph, "graph should not be null!");
this.strongConnectivityAlgorithm = Objects.requireNonNull(
strongConnectivityAlgorithm, "strongConnectivityAlgorithm should not be null!");
if (maximumIterations < 0) {
throw new IllegalArgumentException("maximumIterations should be non-negative");
}
this.maximumIterations = maximumIterations;
this.comparator = new ToleranceDoubleComparator(toleranceEpsilon);
this.policyGraph = CollectionUtil.newHashMapWithExpectedSize(graph.vertexSet().size());
this.reachedVertices = CollectionUtil.newHashMapWithExpectedSize(graph.vertexSet().size());
this.vertexLevel = CollectionUtil.newHashMapWithExpectedSize(graph.vertexSet().size());
this.vertexDistance = CollectionUtil.newHashMapWithExpectedSize(graph.vertexSet().size());
}
/**
* {@inheritDoc}
*/
@Override
public double getCycleMean()
{
GraphPath cycle = getCycle();
if (cycle == null) {
return Double.POSITIVE_INFINITY;
}
return cycle.getWeight() / cycle.getLength();
}
/**
* {@inheritDoc}
*/
@Override
public GraphPath getCycle()
{
// best cycle information
boolean isBestCycleFound = false;
double bestCycleWeight = 0.0;
int bestCycleLength = 1;
V bestCycleVertex = null;
// search for best cycle over strongly connected components separately
int numberOfIterations = 0;
for (Graph component : strongConnectivityAlgorithm.getStronglyConnectedComponents()) {
// special case: connected component is empty
// or contains one vertex with no incoming edges
boolean skip = component.vertexSet().size() == 0;
skip |= component.vertexSet().size() == 1
&& component.incomingEdgesOf(component.vertexSet().iterator().next()).size() == 0;
if (skip) {
continue;
}
constructPolicyGraph(component);
// try to improve currently best cycle
boolean improved = true;
while (numberOfIterations < maximumIterations && improved) {
constructCycle(component);
improved = computeVertexDistance(component);
++numberOfIterations;
}
// update best cycle information if necessary
if (isCurrentCycleFound && (!isBestCycleFound
|| currentCycleWeight * bestCycleLength < bestCycleWeight * currentCycleLength))
{
isBestCycleFound = true;
bestCycleWeight = currentCycleWeight;
bestCycleLength = currentCycleLength;
bestCycleVertex = currentCycleVertex;
}
// iterations limit reached
if (numberOfIterations == maximumIterations) {
break;
}
}
if (isBestCycleFound) {
return buildPath(bestCycleVertex, bestCycleLength, bestCycleWeight);
}
// no cycle found in the graph
return null;
}
/**
* Computes policy graph for {@code component} and stores result in {@code policyGraph} and
* {@code vertexDistance}. For every vertex in the policy graph an edge with the minimum weight
* is retained in the policy graph.
*
* @param component connected component
*/
private void constructPolicyGraph(Graph component)
{
for (V v : component.vertexSet()) {
vertexDistance.put(v, Double.POSITIVE_INFINITY);
}
for (V u : component.vertexSet()) {
for (E e : component.incomingEdgesOf(u)) {
V v = Graphs.getOppositeVertex(component, e, u);
double eWeight = component.getEdgeWeight(e);
if (eWeight < vertexDistance.get(v)) {
vertexDistance.put(v, eWeight);
policyGraph.put(v, e);
}
}
}
}
/**
* Finds cycle in the {@code policyGraph} and computes computes its mean. The found cycle is
* identified by a vertex {@code currentCycleVertex}. The cycle returned by this method does not
* necessarily has the smalles mean over all cycles in the policy graph.
*
*
* To find cycles this methods assigns a level to each vertex. Initially every vertex has a
* level equal to $-1$ which means that the vertex has not been visited. During the computations
* this method starts DFS from every not visited vertex and assigns a unique positive level $l$
* to every traversed vertex. If DFS comes across a vertex with level $l$ this indicates that a
* cycle has been detected.
*
* @param component connected component
*/
private void constructCycle(Graph component)
{
for (V v : component.vertexSet()) {
vertexLevel.put(v, -1);
}
isCurrentCycleFound = false;
int currentCycleLevel = 0;
double currentWeight;
int currentSize;
for (V u : component.vertexSet()) {
if (vertexLevel.get(u) >= 0) { // vertex is already belongs to a cycle
continue;
}
// run DFS
while (vertexLevel.get(u) < 0) {
vertexLevel.put(u, currentCycleLevel);
u = Graphs.getOppositeVertex(component, policyGraph.get(u), u);
}
// check if a cycle has been found
if (vertexLevel.get(u) == currentCycleLevel) {
currentWeight = component.getEdgeWeight(policyGraph.get(u));
currentSize = 1;
// compute weight and length of the found cycle
V v = Graphs.getOppositeVertex(component, policyGraph.get(u), u);
while (!v.equals(u)) {
currentWeight += component.getEdgeWeight(policyGraph.get(v));
++currentSize;
v = Graphs.getOppositeVertex(component, policyGraph.get(v), v);
}
// update minimum mean value
if (!isCurrentCycleFound
|| (currentWeight * currentCycleLength < currentCycleWeight * currentSize))
{
isCurrentCycleFound = true;
currentCycleWeight = currentWeight;
currentCycleLength = currentSize;
currentCycleVertex = u;
}
}
++currentCycleLevel;
}
}
/**
* This method runs the reverted BFS starting from {@code currentCycleVertex} to update data in
* {@code policyGraph} and {@code vertexDistance}. This step is needed to identify if current
* value of minimum mean is optimal for the {@code graph}. This method also uses
* {@code comparator} to find out if update value of minium mean is sufficiently smaller than
* the previous one.
*
* @param component connected component
* @return if the currently best mean has been improved
*/
private boolean computeVertexDistance(Graph component)
{
// BFS queue
Deque queue = new ArrayDeque<>();
for (V v : component.vertexSet()) {
reachedVertices.put(v, false);
}
queue.addLast(currentCycleVertex);
reachedVertices.put(currentCycleVertex, true);
double currentMean = currentCycleWeight / currentCycleLength;
// run reversed BFS
while (!queue.isEmpty()) {
V u = queue.removeFirst();
for (E e : component.incomingEdgesOf(u)) {
V v = Graphs.getOppositeVertex(component, e, u);
if (policyGraph.get(v).equals(e) && !reachedVertices.get(v)) {
reachedVertices.put(v, true);
double updatedDistance =
vertexDistance.get(u) + component.getEdgeWeight(e) - currentMean;
vertexDistance.put(v, updatedDistance);
queue.addLast(v);
}
}
}
// identify if the current value of minimum mean
// is optimal for the graph
boolean improved = false;
for (V u : component.vertexSet()) {
for (E e : component.incomingEdgesOf(u)) {
V v = Graphs.getOppositeVertex(component, e, u);
double oldDistance = vertexDistance.get(v);
double updatedDistance =
vertexDistance.get(u) + component.getEdgeWeight(e) - currentMean;
if (oldDistance > updatedDistance) {
// check if the value of minimum mean
// has been sufficiently improved
if (comparator.compare(oldDistance, updatedDistance) > 0) {
improved = true;
}
vertexDistance.put(v, updatedDistance);
policyGraph.put(v, e);
}
}
}
return improved;
}
/**
* Constructs cycle with minimum mean using information in {@code policyGraph}.
*
* @param bestCycleVertex cycle vertex
* @param bestCycleLength cycle length
* @param bestCycleWeight cycle weight
* @return constructed minimum mean cycle
*/
private GraphPath buildPath(
V bestCycleVertex, int bestCycleLength, double bestCycleWeight)
{
List pathEdges = new ArrayList<>(bestCycleLength);
List pathVertices = new ArrayList<>(bestCycleLength + 1);
V v = bestCycleVertex;
pathVertices.add(bestCycleVertex);
do {
E e = policyGraph.get(v);
v = Graphs.getOppositeVertex(graph, e, v);
pathEdges.add(e);
pathVertices.add(v);
} while (!v.equals(bestCycleVertex));
return new GraphWalk<>(
graph, bestCycleVertex, bestCycleVertex, pathVertices, pathEdges, bestCycleWeight);
}
}