org.jgrapht.alg.cycle.HawickJamesSimpleCycles Maven / Gradle / Ivy
/*
* (C) Copyright 2014-2021, by Luiz Kill 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.*;
import java.util.*;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;
/**
* Find all simple cycles of a directed graph using the algorithm described by Hawick and James.
*
*
* See:
* K. A. Hawick, H. A. James. Enumerating Circuits and Loops in Graphs with Self-Arcs and
* Multiple-Arcs. Computational Science Technical Note CSTN-013, 2008
*
* @param the vertex type.
* @param the edge type.
*
* @author Luiz Kill
*/
public class HawickJamesSimpleCycles
implements
DirectedSimpleCycles
{
private Graph graph;
private int nVertices = 0;
private long nCycles = 0;
private List> cycles = null;
// The main state of the algorithm
private Integer start = 0;
private List[] aK = null;
private List[] b = null;
private boolean[] blocked = null;
private ArrayDeque stack = null;
// Indexing the vertices
private V[] iToV = null;
private Map vToI = null;
private int pathLimit = 0;
private boolean hasLimit = false;
private Runnable operation;
/**
* Create a simple cycle finder with an unspecified graph.
*/
public HawickJamesSimpleCycles()
{
}
/**
* Create a simple cycle finder for the specified graph.
*
* @param graph the DirectedGraph in which to find cycles.
*
* @throws IllegalArgumentException if the graph argument is
* null
.
*/
public HawickJamesSimpleCycles(Graph graph)
throws IllegalArgumentException
{
this.graph = GraphTests.requireDirected(graph, "Graph must be directed");
}
@SuppressWarnings("unchecked")
private void initState()
{
nCycles = 0;
nVertices = graph.vertexSet().size();
blocked = new boolean[nVertices];
stack = new ArrayDeque<>(nVertices);
b = new ArrayList[nVertices];
for (int i = 0; i < nVertices; i++) {
b[i] = new ArrayList<>();
}
iToV = (V[]) graph.vertexSet().toArray();
vToI = new HashMap<>();
for (int i = 0; i < iToV.length; i++) {
vToI.put(iToV[i], i);
}
aK = buildAdjacencyList();
stack.clear();
}
@SuppressWarnings("unchecked")
private List[] buildAdjacencyList()
{
@SuppressWarnings("rawtypes") List[] listAk = new ArrayList[nVertices];
for (int j = 0; j < nVertices; j++) {
V v = iToV[j];
List s = Graphs.successorListOf(graph, v);
listAk[j] = new ArrayList(s.size());
for (V value : s) {
listAk[j].add(vToI.get(value));
}
}
return listAk;
}
private void clearState()
{
aK = null;
nVertices = 0;
blocked = null;
stack = null;
iToV = null;
vToI = null;
b = null;
operation = () -> {
};
}
private boolean circuit(Integer v, int steps)
{
boolean f = false;
stack.push(v);
blocked[v] = true;
for (Integer w : aK[v]) {
if (w < start) {
continue;
}
if (Objects.equals(w, start)) {
operation.run();
f = true;
} else if (!blocked[w]) {
if (limitReached(steps) || circuit(w, steps + 1)) {
f = true;
}
}
}
if (f) {
unblock(v);
} else {
for (Integer w : aK[v]) {
if (w < start) {
continue;
}
if (!b[w].contains(v)) {
b[w].add(v);
}
}
}
stack.pop();
return f;
}
private void unblock(Integer u)
{
blocked[u] = false;
for (int wPos = 0; wPos < b[u].size(); wPos++) {
Integer w = b[u].get(wPos);
int sizeBeforeRemove = b[u].size();
b[u].removeAll(singletonList(w));
wPos -= (sizeBeforeRemove - b[u].size());
if (blocked[w]) {
unblock(w);
}
}
}
/**
* Get the graph
*
* @return graph
*/
public Graph getGraph()
{
return graph;
}
/**
* Set the graph
*
* @param graph graph
*/
public void setGraph(Graph graph)
{
this.graph = GraphTests.requireDirected(graph, "Graph must be directed");
}
/**
* {@inheritDoc}
*/
@Override
public List> findSimpleCycles()
throws IllegalArgumentException
{
if (graph == null) {
throw new IllegalArgumentException("Null graph.");
}
initState();
cycles = new ArrayList<>();
operation = () -> {
List cycle = stack.stream().map(v -> iToV[v]).collect(toList());
Collections.reverse(cycle);
cycles.add(cycle);
};
analyzeCircuits();
List> result = cycles;
clearState();
return result;
}
/**
* Print to the standard output all simple cycles without building a list to keep them, thus
* avoiding high memory consumption when investigating large and much connected graphs.
*/
public void printSimpleCycles()
{
if (graph == null) {
throw new IllegalArgumentException("Null graph.");
}
initState();
operation = () -> {
stack.stream().map(i -> iToV[i].toString() + " ").forEach(System.out::print);
System.out.println();
};
analyzeCircuits();
clearState();
}
/**
* Count the number of simple cycles. It can count up to Long.MAX cycles in a graph.
*
* @return the number of simple cycles
*/
public long countSimpleCycles()
{
if (graph == null) {
throw new IllegalArgumentException("Null graph.");
}
initState();
nCycles = 0;
operation = () -> nCycles++;
analyzeCircuits();
clearState();
return nCycles;
}
private void analyzeCircuits()
{
for (int i = 0; i < nVertices; i++) {
for (int j = 0; j < nVertices; j++) {
blocked[j] = false;
b[j].clear();
}
start = vToI.get(iToV[i]);
circuit(start, 0);
}
}
/**
* Limits the maximum number of edges in a cycle.
*
* @param pathLimit maximum paths.
*/
public void setPathLimit(int pathLimit)
{
this.pathLimit = pathLimit - 1;
this.hasLimit = true;
}
/**
* This is the default behaviour of the algorithm. It will keep looking as long as there are
* paths available.
*/
public void clearPathLimit()
{
this.hasLimit = false;
}
private boolean limitReached(int steps)
{
return hasLimit && steps >= pathLimit;
}
}