org.jgrapht.alg.connectivity.BiconnectivityInspector Maven / Gradle / Ivy
/*
* (C) Copyright 2017-2023, by Joris Kinable 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.connectivity;
import org.jgrapht.*;
import org.jgrapht.graph.*;
import java.util.*;
/**
* Allows obtaining various connectivity aspects of a graph. The inspected graph is specified
* at construction time and cannot be modified. No restrictions are imposed on the input graph.
* Multigraphs and pseudographs are also supported. The inspector traverses connected components
* (undirected graphs) or weakly connected components (directed graphs). To find strongly connected
* components, use {@link KosarajuStrongConnectivityInspector} instead. This class offers an
* alternative implementation of some of the functionality encountered in
* {@link ConnectivityInspector}. It is likely to perform somewhat slower than
* {@link ConnectivityInspector}, but offers more functionality in return.
*
* The algorithm implemented in this class is Hopcroft and Tarjan's biconnected components
* algorithm, described in: Hopcroft, J. Tarjan, R. Algorithm 447: efficient algorithms for graph
* manipulation, 1973. Communications of the ACM. 16 (6): 372–378. This implementation runs in
* linear time $O(|V|+|E|)$ and is based on a recursive depth-first search. More information about
* this subject be be found in this wikipedia
* article.
*
*
* The inspector methods work in a lazy fashion: no computations are performed unless immediately
* necessary. Computation are done once and results are cached within this class for future need.
* The core of this class is built around a recursive Depth-first search.
*
*
* @param the graph vertex type
* @param the graph edge type
*
* @author Joris Kinable
*/
public class BiconnectivityInspector
{
/**
* Constructs a new BiconnectivityInspector
*
* @param graph the input graph
*/
public BiconnectivityInspector(Graph graph)
{
this.graph = Objects.requireNonNull(graph);
if (graph.getType().isDirected())
this.graph = new AsUndirectedGraph<>(graph);
}
private Graph graph;
private Set> blocks;
private Set cutpoints;
private Set bridges;
/* Set which holds the vertices in the connected component which is being processed */
private Set connectedSet;
/* Set which holds all connected components, expressed in vertex sets */
private Set> connectedSets;
/* Set of connected components */
private Set> connectedComponents;
/* Mapping of vertices to the blocks they are contained in */
private Map>> vertex2blocks;
/* Mapping of vertices to the connected components they are contained in */
private Map> vertex2components;
/* Discovery time of a vertex. */
private int time;
/* Stack which keeps track of edges in biconnected components */
private Deque stack;
/* Map which tracks when a vertex is discovered in the DFS search */
private Map discTime = new HashMap<>();
/**
* Returns the cutpoints
* (articulation points) of the graph. A vertex is a cutpoint if removal of that vertex (and all
* edges incident to that vertex) would increase the number of (weakly) connected components in
* the graph.
*
* @return the cutpoints of the graph
*/
public Set getCutpoints()
{
performLazyInspection();
return this.cutpoints;
}
/**
* Returns the graph's bridges. An edge is a
* bridge if removal of that edge
* would increase the number of (weakly) connected components in the graph. Note that this
* definition remains applicable in case of multigraphs or pseudographs.
*
* @return the graph's bridges
*/
public Set getBridges()
{
performLazyInspection();
return this.bridges;
}
/**
* Returns a set of blocks (biconnected
* components) containing the specified vertex. A block is a maximal biconnected subgraph. Each
* non-cutpoint resides in at most one block. Each cutpoint resides in at least two blocks.
*
* @param vertex vertex in the initial graph.
* @return the blocks containing the given vertex
*/
public Set> getBlocks(V vertex)
{
assert graph.containsVertex(vertex);
if (vertex2blocks == null) {
vertex2blocks = new HashMap<>();
for (V v : graph.vertexSet())
vertex2blocks.put(v, new LinkedHashSet<>());
for (Graph block : this.getBlocks()) {
for (V v : block.vertexSet())
vertex2blocks.get(v).add(block);
}
}
return this.vertex2blocks.get(vertex);
}
/**
* Returns all blocks (biconnected
* components) in the graph. A block is a maximal biconnected subgraph.
*
* @return all blocks (biconnected components) in the graph.
*/
public Set> getBlocks()
{
performLazyInspection();
return this.blocks;
}
/**
* Returns all connected components in the graph. In case the graph is directed, this method
* returns all weakly connected components.
*
* @return all connected components in the graph if the graph is undirected, or all weakly
* connected components if the graph is directed.
*/
public Set> getConnectedComponents()
{
if (connectedComponents == null) {
performLazyInspection();
connectedComponents = new LinkedHashSet<>();
for (Set vertexComponent : connectedSets)
connectedComponents.add(new AsSubgraph<>(this.graph, vertexComponent));
}
return connectedComponents;
}
/**
* Returns the connected component containing the given vertex. If the underlying graph is
* directed, this method returns a weakly connected component.
*
* @param vertex vertex
* @return the connected component containing the given vertex, or a weakly connected component
* if the underlying graph is directed.
*/
public Graph getConnectedComponent(V vertex)
{
assert this.graph.containsVertex(vertex);
if (vertex2components == null) {
vertex2components = new HashMap<>();
for (Graph component : this.getConnectedComponents())
for (V v : component.vertexSet())
vertex2components.put(v, component);
}
return vertex2components.get(vertex);
}
/**
* Tests if the inspected graph is biconnected. A biconnected graph is a connected graph on two
* or more vertices having no cutpoints.
*
* @return true if the graph is biconnected, false otherwise
*/
public boolean isBiconnected()
{
performLazyInspection();
return graph.vertexSet().size() >= 2 && blocks.size() == 1;
}
/**
* Test if the inspected graph is connected. A graph is connected when, while ignoring edge
* directionality, there exists a path between every pair of vertices. In a connected graph,
* there are no unreachable vertices. When the inspected graph is a directed graph, this
* method returns true if and only if the inspected graph is weakly connected. An empty
* graph is not considered connected.
*
* @return true
if and only if inspected graph is connected.
*/
public boolean isConnected()
{
performLazyInspection();
return connectedSets.size() == 1;
}
private void init()
{
blocks = new LinkedHashSet<>();
cutpoints = new LinkedHashSet<>();
bridges = new LinkedHashSet<>();
connectedSets = new LinkedHashSet<>();
stack = new ArrayDeque<>(graph.edgeSet().size());
for (V v : graph.vertexSet())
discTime.put(v, -1);
}
private void performLazyInspection()
{
if (blocks == null) {
init();
// Iterate over all connected components
for (V v : graph.vertexSet()) {
if (discTime.get(v) == -1) {
connectedSet = new HashSet<>();
dfs(v, null);
// Stack can be non-empty when dfs finishes, for instance if the graph has no
// cutpoints.
// Construct the final component from the remaining edges.
if (!stack.isEmpty())
buildBlock(0);
connectedSets.add(connectedSet);
}
}
if (this.graph.getType().isAllowingMultipleEdges()) {
// check parallel edges: an edge is not a bridge when there are multiple edges
// between the same pair of vertices
for (Iterator it = bridges.iterator(); it.hasNext();) {
E edge = it.next();
int nrParallelEdges = graph
.getAllEdges(graph.getEdgeSource(edge), graph.getEdgeTarget(edge)).size();
if (nrParallelEdges > 1)
it.remove();
}
}
}
}
/**
* Each time a cutpoint is discovered, this method computes the biconnected component
*
* @param discTimeCutpoint discovery time of cutpoint
*/
private void buildBlock(int discTimeCutpoint)
{
Set vertexComponent = new HashSet<>();
while (!stack.isEmpty()) {
E edge = stack.peek();
V source = graph.getEdgeSource(edge);
V target = graph.getEdgeTarget(edge);
if (discTime.get(source) < discTimeCutpoint && discTime.get(target) < discTimeCutpoint)
break;
stack.pop();
vertexComponent.add(source);
vertexComponent.add(target);
}
blocks.add(new AsSubgraph<>(this.graph, vertexComponent));
}
/**
* Performs a depth-first search, starting from vertex v
*
* @param v vertex
* @param parent parent of v
* @return lowpoint of v
*/
private int dfs(V v, V parent)
{
int lowV = ++this.time;
discTime.put(v, time);
connectedSet.add(v);
int children = 0;
for (E edge : this.graph.edgesOf(v)) {
V nv = Graphs.getOppositeVertex(this.graph, edge, v);
if (discTime.get(nv) == -1) { // Node hasn't been discovered yet
children++;
this.stack.push(edge);
int lowNV = dfs(nv, v);
lowV = Math.min(lowNV, lowV);
if (lowNV > discTime.get(v))
bridges.add(edge);
// 1. nonroot vertex v is a cutpoint iff there is a child y of v such that
// lowpoint(y) >= depth(v)
// 2. root vertex v is a cutpoint if it has more than 1 child
if ((parent != null && lowNV >= discTime.get(v))
|| (parent == null && children > 1))
{
this.cutpoints.add(v); // v is a cutpoint
buildBlock(discTime.get(nv)); // construct biconnected component
}
} else if ((discTime.get(nv) < discTime.get(v)) && !nv.equals(parent)) { // found
// backedge
this.stack.push(edge);
lowV = Math.min(discTime.get(nv), lowV);
}
}
return lowV;
}
}