diff --git a/src-test/org/graphstream/algorithm/test/TestHopcroftTarjanBiconnectedComponents.java b/src-test/org/graphstream/algorithm/test/TestHopcroftTarjanBiconnectedComponents.java new file mode 100644 index 00000000..21000a39 --- /dev/null +++ b/src-test/org/graphstream/algorithm/test/TestHopcroftTarjanBiconnectedComponents.java @@ -0,0 +1,139 @@ +package org.graphstream.algorithm.test; + +import org.graphstream.algorithm.HopcroftTarjanBiconnectedComponents; +import org.graphstream.graph.Graph; +import org.graphstream.graph.implementations.DefaultGraph; +import org.graphstream.stream.file.FileSourceDGS; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; + +public class TestHopcroftTarjanBiconnectedComponents { + + @Test + public void testBasic() throws Exception { + Graph g = new DefaultGraph("g"); + load(g, "data/bcc-basic.dgs", true); + + HopcroftTarjanBiconnectedComponents bcc = new HopcroftTarjanBiconnectedComponents(g, g.getNode("C")); + + bcc.compute(); + + check(bcc, createBCC("A", "B", "C"), createBCC("D", "E", "F"), createBCC("C", "D")); + } + + @Test + public void testSingleDFSStrand() { + Graph g = new DefaultGraph("g"); + load(g, "data/bcc-basic.dgs", true); + + HopcroftTarjanBiconnectedComponents bcc = new HopcroftTarjanBiconnectedComponents(g); + + bcc.compute(); + + check(bcc, createBCC("A", "B", "C"), createBCC("D", "E", "F"), createBCC("C", "D")); + } + + @Test + public void testSingleNode() { + Graph g = new DefaultGraph("g"); + + g.addNode("A"); + + HopcroftTarjanBiconnectedComponents bcc = new HopcroftTarjanBiconnectedComponents(g); + + bcc.compute(); + + check(bcc, createBCC("A")); + } + + @Test + public void testNodePair() { + Graph g = new DefaultGraph("g"); + + g.addNode("A"); + g.addNode("B"); + g.addEdge("AB","A","B"); + + HopcroftTarjanBiconnectedComponents bcc = new HopcroftTarjanBiconnectedComponents(g); + + bcc.compute(); + + check(bcc, createBCC("A","B")); + } + + @Test + public void testNormal() { + Graph g = new DefaultGraph("g"); + load(g, "data/bcc-normal.dgs", true); + + HopcroftTarjanBiconnectedComponents bcc = new HopcroftTarjanBiconnectedComponents(g, g.getNode("H")); + + bcc.compute(); + + check(bcc, createBCC("A", "B", "C", "D"), createBCC("D", "E"), createBCC("E", "F"), createBCC("F", "G"), createBCC("G", "H", "I", "J", "K", "L"), createBCC("G", "M"), createBCC("L", "N")); + } + + @Test + public void testNormalSubtreeWithoutCutVertex() { + Graph g = new DefaultGraph("g"); + load(g, "data/bcc-normal-subtree-wo-cut-vertex.dgs", true); + + HopcroftTarjanBiconnectedComponents bcc = new HopcroftTarjanBiconnectedComponents(g, g.getNode("K")); + + bcc.compute(); + + check(bcc, createBCC("A", "B", "C", "F"), createBCC("F", "G"), createBCC("G", "H", "I", "J", "K", "L"), createBCC("G", "M"), createBCC("L", "N")); + } + + static FileSourceDGS load(Graph g, String dgsPath, boolean all) { + FileSourceDGS dgs = new FileSourceDGS(); + InputStream in = TestHopcroftTarjanBiconnectedComponents.class.getResourceAsStream(dgsPath); + + dgs.addSink(g); + + if (all) { + try { + dgs.readAll(in); + } catch (IOException e) { + Assert.fail(e.getMessage()); + } + } else { + try { + dgs.begin(in); + } catch (IOException e) { + Assert.fail(e.getMessage()); + } + } + + return dgs; + } + + static String[] createBCC(String... nodes) { + return nodes; + } + + static void check(HopcroftTarjanBiconnectedComponents algo, String[]... bccs) { + Assert.assertEquals(bccs.length, algo.getBiconnectedComponentsCount()); + + for (int i = 0; i < bccs.length; i++) { + for (int j = 1; j < bccs[i].length; j++) { + ArrayList cc1 = algo.getBiconnectedComponentsOf(bccs[i][0]); + ArrayList cc2 = algo.getBiconnectedComponentsOf(bccs[i][j]); + + Assert.assertFalse(Collections.disjoint(cc1, cc2)); + } + + for (int j = i + 1; j < bccs.length; j++) { + ArrayList cc1 = algo.getBiconnectedComponentsOf(bccs[i][0]); + ArrayList cc2 = algo.getBiconnectedComponentsOf(bccs[j][0]); + + Assert.assertTrue(Collections.disjoint(cc1, cc2) || (cc1.size() > 1 || cc2.size() > 1)); + } + } + } +} diff --git a/src-test/org/graphstream/algorithm/test/data/bcc-basic.dgs b/src-test/org/graphstream/algorithm/test/data/bcc-basic.dgs new file mode 100644 index 00000000..213830ff --- /dev/null +++ b/src-test/org/graphstream/algorithm/test/data/bcc-basic.dgs @@ -0,0 +1,19 @@ +DGS004 +null 0 0 + +an A +an B +an C + +ae AB A B +ae AC A C +ae BC B C + +an D +an E +an F + +ae CD C D +ae DE D E +ae DF D F +ae EF E F diff --git a/src-test/org/graphstream/algorithm/test/data/bcc-normal-subtree-wo-cut-vertex.dgs b/src-test/org/graphstream/algorithm/test/data/bcc-normal-subtree-wo-cut-vertex.dgs new file mode 100644 index 00000000..00220a29 --- /dev/null +++ b/src-test/org/graphstream/algorithm/test/data/bcc-normal-subtree-wo-cut-vertex.dgs @@ -0,0 +1,38 @@ +DGS004 +null 0 0 + +an A +an B +an C + +an F + +an G +an H +an I +an J +an K +an L + +an M + +an N + +ae AB A B +ae AC A C +ae BF B F +ae CF C F + +ae FG F G + +ae GH G H +ae HI H I +ae HJ H J +ae IJ I J +ae JK J K +ae KL K L +ae LG L G + +ae GM G M + +ae LN L N diff --git a/src-test/org/graphstream/algorithm/test/data/bcc-normal.dgs b/src-test/org/graphstream/algorithm/test/data/bcc-normal.dgs new file mode 100644 index 00000000..f83d392a --- /dev/null +++ b/src-test/org/graphstream/algorithm/test/data/bcc-normal.dgs @@ -0,0 +1,44 @@ +DGS004 +null 0 0 + +an A +an B +an C +an D + +an E +an F + +an G +an H +an I +an J +an K +an L + +an M + +an N + +ae AB A B +ae AC A C +ae BD B D +ae CD C D + +ae DE D E + +ae EF E F + +ae FG F G + +ae GH G H +ae HI H I +ae HJ H J +ae IJ I J +ae JK J K +ae KL K L +ae LG L G + +ae GM G M + +ae LN L N diff --git a/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java b/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java new file mode 100644 index 00000000..22638958 --- /dev/null +++ b/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java @@ -0,0 +1,586 @@ +/* + * This file is part of GraphStream . + * + * GraphStream is a library whose purpose is to handle static or dynamic + * graph, create them from scratch, file or any source and display them. + * + * This program is free software distributed under the terms of two licenses, the + * CeCILL-C license that fits European law, and the GNU Lesser General Public + * License. You can use, modify and/ or redistribute the software under the terms + * of the CeCILL-C license as circulated by CEA, CNRS and INRIA at the following + * URL or under the terms of the GNU LGPL as published by + * the Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C and LGPL licenses and that you accept their terms. + * + * + * @since 2009-02-19 + * + * @author Guilhelm Savin + * @author Yoann Pigné + * @author Antoine Dutot + * @author Guillaume-Jean Herbiet + * @author Stefan Balev + * @author Hicham Brahimi + */ +package org.graphstream.algorithm; + +import org.graphstream.algorithm.util.Result; +import org.graphstream.graph.Edge; +import org.graphstream.graph.Graph; +import org.graphstream.graph.Node; +import org.graphstream.graph.Structure; + +import java.util.*; +import java.util.stream.Stream; + +/** + * Compute the number of biconnected components of a graph + * according to the algorithm by Hopcroft and Tarjan (See https://doi.org/10.1145%2F362248.362272) + * using a depth-first approach + * + *

An overview can be found on Wikipedia

+ * + * The algorithm first calculates via a depth first search approach so called articulation points or cut vertices that, when removed, split the graph into separate pieces. + * Once the articulation points are known, from each articulation points' neighbor that has a higher lowpoint than the articulation points depth, a biconnected is formed from the articulation point, the neighbor and the subtree from that neighbor. + * For the root node this computation is also done, but if it is not an articulation point a biconnected component simply is the tree from the root node until other biconnected components are reached. + * + *

+ * This algorithm computes the biconnected components for a given graph. Biconnected + * components are the set of its maximal biconnected subgraphs, + * for which every one contained node can be removed + * without splitting the subgraph further. When two nodes belong to the + * same biconnected component there exist at least two paths (without considering the + * direction of the edges) between them. The algorithm does not + * consider the direction of the edges. + *

+ *

+ * + *

Usage

+ * + *

+ * To start using the algorithm, you first need an instance of + * {@link Graph}, then you only have to instantiate the + * algorithm class. You can also specify a starting {@link Node} for the algorithm, otherwise the first node will be chosen. You can specify a reference to the graph in the + * constructor or you set it with the {@link #init(Graph)} method. + *

+ * + *

+ * The computation of the algorithm starts only when the graph is specified with + * the {@link #init(Graph)} method or with the appropriated constructor. In case + * of a static graph, you may call the {@link #compute()} method. In case of a + * dynamic graph, the algorithm will compute itself automatically when an event + * (node or edge added or removed) occurs. + *

+ * + *

+ * You may ask the algorithm for the number of biconnected components at + * any moment with a call to the {@link #getBiconnectedComponentsCount()} method. + *

+ * + * + *

Additional features

+ * + * + *

Giant component

+ *

+ * The {@link #getGiantComponent()} method gives you a list of nodes belonging + * to the biggest biconnected component of the graph. + *

+ * + *

+ * Note that setting the cut attribute will trigger a new computation of the + * algorithm. + *

+ * + * @author Max Kißgen + * @complexity For the articulation points, let n be the number of nodes, then + * the time complexity is 0(n). + * @since May 05 2022 + */ +public class HopcroftTarjanBiconnectedComponents implements Algorithm { + protected HashSet components; + protected HashMap> componentsMap; + protected Graph graph; + + /** + * Optional attribute to set on each node of a given component. This + * attribute will have for value an index different for each component. + */ + protected String countAttribute; + + /** + * Flag used to tell if the {@link #compute()} method has already been + * called. + */ + protected boolean started; + + /** + * Used to get components index. + */ + protected int currentComponentId; + + + /** + * Map of node depths from Node index to Node depth + */ + protected HashMap nodeDepths; + + /** + * Map of node lowpoints from Node index to Node lowpoints + */ + protected HashMap nodeLowpoints; + + /** + * Map of node depths from Node index to Node parent index + */ + protected HashMap nodeParents; + + /** + * Map of node depths from Node index to Boolean + */ + protected HashMap nodeArticulationPoints; + + /** + * Node to start computation from + */ + protected Node root; + + /** + * Build a new biconnected component algorithm + */ + public HopcroftTarjanBiconnectedComponents() { + nodeDepths = new HashMap(); + nodeLowpoints = new HashMap(); + nodeParents = new HashMap(); + nodeArticulationPoints = new HashMap(); + components = new HashSet(); + componentsMap = new HashMap>(); + this.started = false; + } + + /** + * Build a new biconnected component algorithm + * + * @param graph the graph to perform computation on + */ + public HopcroftTarjanBiconnectedComponents(Graph graph) { + nodeDepths = new HashMap(); + nodeLowpoints = new HashMap(); + nodeParents = new HashMap(); + nodeArticulationPoints = new HashMap(); + components = new HashSet(); + componentsMap = new HashMap>(); + this.started = false; + + init(graph); + } + + public HopcroftTarjanBiconnectedComponents(Graph graph, Node node) { + root = node; + nodeDepths = new HashMap(); + nodeLowpoints = new HashMap(); + nodeParents = new HashMap(); + nodeArticulationPoints = new HashMap(); + components = new HashSet(); + componentsMap = new HashMap>(); + this.started = false; + + init(graph); + } + + /* + * (non-Javadoc) + * + * @see + * org.graphstream.algorithm.Algorithm#init(org.graphstream.graph.Graph) + */ + @Override + public void init(Graph graph) { + this.graph = graph; + } + + /* + * (non-Javadoc) + * + * @see org.graphstream.algorithm.Algorithm#compute() + */ + @Override + public void compute() { + started = true; + + if (graph.getNodeCount() != 0) { + + + if (root == null) { + root = graph.getNode(0); + } + calculateArticulationPoints(root, 0); + calculateBiconnectedComponents(root); + } + } + + /** + * Recursively compute the articulation points starting at `from`. + * + * @param from The node we start from + * @param depth the nodes' depth in the DFS tree + */ + protected void calculateArticulationPoints(Node from, Integer depth) { + + nodeDepths.put(from.getIndex(), depth); + nodeLowpoints.put(from.getIndex(), depth); + boolean isArticulationPoint = false; + int childCount = 0; + + Iterator neighborIt = from.neighborNodes().iterator(); + while (neighborIt.hasNext()) { + Node neighbor = neighborIt.next(); + if (nodeDepths.get(neighbor.getIndex()) == null) { + nodeParents.put(neighbor.getIndex(), from.getIndex()); + calculateArticulationPoints(neighbor, depth + 1); + childCount++; + + if (nodeLowpoints.get(neighbor.getIndex()) >= nodeDepths.get(from.getIndex())) { + isArticulationPoint = true; + } + nodeLowpoints.put(from.getIndex(), Math.min(nodeLowpoints.get(from.getIndex()), nodeLowpoints.get(neighbor.getIndex()))); + } else if (nodeParents.get(from.getIndex()) == null || nodeParents.get(from.getIndex()) != neighbor.getIndex()) { + nodeLowpoints.put(from.getIndex(), Math.min(nodeLowpoints.get(from.getIndex()), nodeDepths.get(neighbor.getIndex()))); + } + } + + if ((nodeParents.get(from.getIndex()) == null && childCount > 1) || (nodeParents.get(from.getIndex()) != null && isArticulationPoint)) { + nodeArticulationPoints.put(from.getIndex(), true); + } + } + + /** + * Using the articulation points, compute the biconnected components iteratively + * + *

+ * We use here the {@link BiconnectedComponent#registerNode(Node)} method + * which will update the {@link #componentsMap} and the size of the + * biconnected component + * @param root The root node of the DFS that computed the articulation points + */ + protected void calculateBiconnectedComponents(Node root) { + HashMap visitedNormalNodes = new HashMap(); // All special children are nodes adjacent to an articulation point that have a smaller lowpoint than the articulation points depth + + for (Integer currentNodeIndex : nodeArticulationPoints.keySet()) { + Node currentNode = graph.getNode(currentNodeIndex); + HashMap visitedArticulationPoints = new HashMap(); // Articulation points can (and have to) be in multiple biconnected components, therefore only allow one shared component for every pair of them + + Iterator lowpointNeighbors = currentNode.neighborNodes() + .filter(n -> (visitedNormalNodes.get(n.getIndex()) == null) && nodeLowpoints.get(n.getIndex()) >= nodeDepths.get(currentNode.getIndex())) + .iterator(); // get all neighbors with larger lowpoints than current articulation points' depth + while (lowpointNeighbors.hasNext()) { + Node neighbor = lowpointNeighbors.next(); + + BiconnectedComponent bcc = createBiconnectedComponent(visitedNormalNodes, visitedArticulationPoints, currentNode, neighbor); + + if (bcc.size != 0) { // Only add component if component didn't contain only previously visited articulation points + components.add(bcc); + } + } + } + + //Do last iteration for root node + if (nodeArticulationPoints.get(root.getIndex()) == null) { + HashMap visitedArticulationPoints = new HashMap(); + + BiconnectedComponent bcc = createBiconnectedComponent(visitedNormalNodes, visitedArticulationPoints, root, root); + + components.add(bcc); + } + } + + /** + * Creates a biconnected component by first adding one initial node and from a node starting point adding all other nodes in the tree, each branch ending when either articulation points or already visited nodes are hit. + * @param visitedNormalNodes A Hashmap of non-articulation point nodes Indexes (Integer) and Booleans whether they are visited or not (To save memory non visited wont even have an entry) + * @param visitedArticulationPoints A Hashmap of articulation point nodes Indexes (Integer) and Booleans whether they are visited or not (To save memory non visited wont even have an entry) + * @param first The first node of the biconnected component + * @param startingSearchFrom The node to start the visiting other nodes from. Can be the same as first + * @return the completed biconnected Component. + */ + protected BiconnectedComponent createBiconnectedComponent(HashMap visitedNormalNodes, HashMap visitedArticulationPoints, Node first, Node startingSearchFrom) { + BiconnectedComponent bcc = new BiconnectedComponent(); + bcc.registerNode(first); + + LinkedList open = new LinkedList(); + open.add(startingSearchFrom); + while (!open.isEmpty()) { + Node n = open.poll(); + if (nodeArticulationPoints.get(n.getIndex()) == null) { // If we dont have an articulation point here we can add further neighbors + visitedNormalNodes.put(n.getIndex(), true); + open.addAll(Arrays.asList(n.neighborNodes().filter(child -> (child != first + && visitedNormalNodes.get(child.getIndex()) == null + && visitedArticulationPoints.get(child.getIndex()) == null)).toArray(Node[]::new)) + ); // Add all still unvisited neighbors and articulation points that are unvisited for the "first" node + } + if (visitedArticulationPoints.get(n.getIndex()) == null) { + bcc.registerNode(n); + if (nodeArticulationPoints.get(n.getIndex()) != null) { + visitedArticulationPoints.put(n.getIndex(), true); + } + } + } + + return bcc; + } + + /** + * Get the biconnected component that contains the biggest number of nodes. + * + * @return the biggest BCC. + */ + public BiconnectedComponent getGiantComponent() { + checkStarted(); + + BiconnectedComponent maxBCC = null; + + maxBCC = components.stream() + .max((bcc1, bcc2) -> Integer.compare(bcc1.size, bcc2.size)) + .get(); + + return maxBCC; + } + + /** + * Get the found biconnected components + * + * @return the number of biconnected components in this graph. + */ + public ArrayList getBiconnectedComponents() { + checkStarted(); + + return new ArrayList(this.components); + } + + /** + * Ask the algorithm for the number of biconnected components. + * + * @return the number of biconnected components in this graph. + */ + public int getBiconnectedComponentsCount() { + checkStarted(); + + return components.size(); + } + + @Result + public String defaultResult() { + return getBiconnectedComponentsCount() + " biconnected component(s) in this graph"; + } + + /** + * Ask the algorithm for the number of biconnected components whose size is + * equal to or greater than the specified threshold. + * + * @param sizeThreshold Minimum size for the biconnected component to be considered + * @return the number of biconnected components, bigger than the given size + * threshold, in this graph. + */ + public int getBiconnectedComponentsCount(int sizeThreshold) { + return getBiconnectedComponentsCount(sizeThreshold, 0); + } + + /** + * Ask the algorithm for the number of biconnected components whose size is + * equal to or greater than the specified threshold and lesser than the + * specified ceiling. + * + * @param sizeThreshold Minimum size for the biconnected component to be considered + * @param sizeCeiling Maximum size for the biconnected component to be considered (use + * 0 or lower values to ignore the ceiling) + * @return the number of biconnected components, bigger than the given size + * threshold, and smaller than the given size ceiling, in this + * graph. + */ + public int getBiconnectedComponentsCount(int sizeThreshold, int sizeCeiling) { + checkStarted(); + + // + // Simplest case : threshold is lesser than or equal to 1 and + // no ceiling is specified, we return all the counted components + // + if (sizeThreshold <= 1 && sizeCeiling <= 0) { + return components.size(); + } else { + int count = 0; + + count = (int) components.stream() + .filter(bcc -> (bcc.size >= sizeThreshold && (sizeCeiling <= 0 || bcc.size < sizeCeiling))) + .count(); + + return count; + } + } + + /** + * Return the biconnected component where a node belonged. The validity of the + * result ends if any new computation is done. So you will have to call this + * method again to be sure you are manipulating the good component. + * + * @param n a node + * @return the biconnected component containing `n` + */ + public ArrayList getBiconnectedComponentsOf(Node n) { + return n == null ? null : componentsMap.get(n); + } + + /** + * Same as {@link #getBiconnectedComponentsOf(Node)} but using the node id. + * + * @param nodeId a node id + * @return the biconnected component containing the node `nodeId` + */ + public ArrayList getBiconnectedComponentsOf(String nodeId) { + return getBiconnectedComponentsOf(graph.getNode(nodeId)); + } + + /** + * Same as {@link #getBiconnectedComponentsOf(Node)} but using the node index. + * + * @param nodeIndex a node index + * @return the biconnected component containing the node `nodeIndex` + */ + public ArrayList getBiconnectedComponentOf(int nodeIndex) { + return getBiconnectedComponentsOf(graph.getNode(nodeIndex)); + } + + + protected void checkStarted() { + if (!started && graph != null) { + compute(); + } + } + + /** + * A representation of a biconnected component. These objects are used to + * store informations about components and to allow to iterate over all + * nodes of a same component. + *

+ * You can retrieve these objects using the + * {@link HopcroftTarjanBiconnectedComponents#getBiconnectedComponentsOf(Node)} methods of the + * algorithm. + */ + public class BiconnectedComponent implements Structure { + /** + * The unique id of this component. + *

+ * The uniqueness of the id is local to an instance of the + * {@link HopcroftTarjanBiconnectedComponents} algorithm. + */ + public final int id = currentComponentId++; + + int size; + + BiconnectedComponent() { + this.size = 0; + + } + + void registerNode(Node n) { + componentsMap.computeIfAbsent(n, k -> new ArrayList()); + componentsMap.get(n).add(this); + + if (countAttribute != null) { + n.setAttribute(countAttribute, id); + } + + size++; + } + + void unregisterNode(Node n) { + size--; + + if (size == 0) { + components.remove(this); + } + } + + /** + * Return an stream over the nodes of this component. + * + * @return an stream over the nodes of this component + */ + + public Stream nodes() { + return graph.nodes().filter(n -> componentsMap.get(n).contains(BiconnectedComponent.this)); + } + + + /** + * Get a set containing all the nodes of this component. + *

+ * A new set is built for each call to this method, so handle with care. + * + * @return a new set of nodes belonging to this component + */ + public Set getNodeSet() { + HashSet nodes = new HashSet(); + + nodes().forEach(n -> nodes.add(n)); + + return nodes; + } + + /** + * Return an stream over the edge of this component. + *

+ * An edge is in the component if the two ends of this edges are in the + * component and the edge does not have the cut attribute. Note that, + * using cut attribute, some edges can be in none of the components. + * + * @return an stream over the edges of this component + */ + public Stream edges() { + return graph.edges().filter(e -> { + return (componentsMap.get(e.getNode0()).contains(BiconnectedComponent.this)) + && (componentsMap.get(e.getNode1()).contains(BiconnectedComponent.this)); + }); + } + + /** + * Test if this component contains a given node. + * + * @param n a node + * @return true if the node is in this component + */ + public boolean contains(Node n) { + return componentsMap.get(n).contains(this); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("BiconnectedComponent#%d", id); + } + + @Override + public int getNodeCount() { + return (int) nodes().count(); + } + + @Override + public int getEdgeCount() { + return (int) edges().count(); + } + } +}