From 210047f56b285764be00d89adace9bfbbee31f66 Mon Sep 17 00:00:00 2001 From: Max Kissgen Date: Mon, 16 May 2022 15:07:50 +0200 Subject: [PATCH 1/5] Add algorithm from hopcroft and tarjan to compute biconnected components --- .../HopcroftTarjanBiconnectedComponents.java | 570 ++++++++++++++++++ 1 file changed, 570 insertions(+) create mode 100644 src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java diff --git a/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java b/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java new file mode 100644 index 0000000..8cd98ab --- /dev/null +++ b/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java @@ -0,0 +1,570 @@ +/* + * 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

+ * + *

+ * 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 connected 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). For the re-optimization steps, let k be + * the number of nodes concerned by the changes (k <= n), the + * complexity is O(k). + * @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 + */ + 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 = new BiconnectedComponent(); + bcc.registerNode(currentNode); + + LinkedList open = new LinkedList(); + open.add(neighbor); + 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 != currentNode + && visitedNormalNodes.get(child.getIndex()) == null + && visitedArticulationPoints.get(n.getIndex()) == null)).toArray(Node[]::new)) + ); // Add all still unvisited neighbors and articulation points that are unvisited for this current articulation point + } + if (visitedArticulationPoints.get(n.getIndex()) == null) { + bcc.registerNode(n); + if (nodeArticulationPoints.get(n.getIndex()) != null) { + visitedArticulationPoints.put(n.getIndex(), true); + } + } + } + + 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 //TODO: Check + if (nodeArticulationPoints.get(root.getIndex()) == null) { + BiconnectedComponent bcc = new BiconnectedComponent(); + components.add(bcc); + + LinkedList open = new LinkedList(); + + open.add(root); + while (!open.isEmpty()) { + Node n = open.poll(); + if (nodeArticulationPoints.get(n.getIndex()) == null) { // If we dont have an articulation point here + visitedNormalNodes.put(n.getIndex(), true); + open.addAll( + Arrays.asList( + n.neighborNodes().filter(child -> (visitedNormalNodes.get(child.getIndex()) == null)).toArray(Node[]::new) + ) + ); //TODO: Currently multiple traversals of articulation points are possible here, not so nice + } + bcc.registerNode(n); + } + } + } + + /** + * Get the connected component that contains the biggest number of nodes. + * + * @return the biggest CC. + */ + public BiconnectedComponent getGiantComponent() { + checkStarted(); + + BiconnectedComponent maxCC = null; + + maxCC = components.stream() + .max((bcc1, bcc2) -> Integer.compare(bcc1.size, bcc2.size)) + .get(); + + return maxCC; + } + + /** + * Ask the algorithm for the number of biconnected components. + * + * @return the number of connected 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 connected components whose size is + * equal to or greater than the specified threshold. + * + * @param sizeThreshold Minimum size for the connected component to be considered + * @return the number of connected 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 connected components whose size is + * equal to or greater than the specified threshold and lesser than the + * specified ceiling. + * + * @param sizeThreshold Minimum size for the connected component to be considered + * @param sizeCeiling Maximum size for the connected component to be considered (use + * 0 or lower values to ignore the ceiling) + * @return the number of connected 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 connected 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 connected 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 connected 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 connected 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(); + } + } +} From fc7639300156f824994e3377302c9ea7d78b2e8d Mon Sep 17 00:00:00 2001 From: Max Kissgen Date: Mon, 16 May 2022 15:08:22 +0200 Subject: [PATCH 2/5] Add testcases for the biconnected component algorithm --- ...stHopcroftTarjanBiconnectedComponents.java | 139 ++++++++++++++++++ .../algorithm/test/data/bcc-basic.dgs | 19 +++ .../data/bcc-normal-subtree-wo-cut-vertex.dgs | 38 +++++ .../algorithm/test/data/bcc-normal.dgs | 44 ++++++ 4 files changed, 240 insertions(+) create mode 100644 src-test/org/graphstream/algorithm/test/TestHopcroftTarjanBiconnectedComponents.java create mode 100644 src-test/org/graphstream/algorithm/test/data/bcc-basic.dgs create mode 100644 src-test/org/graphstream/algorithm/test/data/bcc-normal-subtree-wo-cut-vertex.dgs create mode 100644 src-test/org/graphstream/algorithm/test/data/bcc-normal.dgs 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 0000000..21000a3 --- /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 0000000..213830f --- /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 0000000..00220a2 --- /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 0000000..f83d392 --- /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 From 6a1cdddfa5d2da16a3fe56555b7ab68f83ff14de Mon Sep 17 00:00:00 2001 From: Max Kissgen Date: Tue, 17 May 2022 11:46:02 +0200 Subject: [PATCH 3/5] Add extra helper method for bcc creation and add algorithm description --- .../HopcroftTarjanBiconnectedComponents.java | 117 ++++++++++-------- 1 file changed, 62 insertions(+), 55 deletions(-) diff --git a/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java b/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java index 8cd98ab..957af02 100644 --- a/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java +++ b/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java @@ -52,6 +52,10 @@ * "https://en.wikipedia.org/wiki/Biconnected_component" * >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, @@ -92,7 +96,7 @@ *

Giant component

*

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

* *

@@ -219,6 +223,8 @@ public void compute() { started = true; if (graph.getNodeCount() != 0) { + + if (root == null) { root = graph.getNode(0); } @@ -269,7 +275,7 @@ protected void calculateArticulationPoints(Node from, Integer depth) { * We use here the {@link BiconnectedComponent#registerNode(Node)} method * which will update the {@link #componentsMap} and the size of the * biconnected component - * @param root + * @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 @@ -284,27 +290,7 @@ protected void calculateBiconnectedComponents(Node root) { while (lowpointNeighbors.hasNext()) { Node neighbor = lowpointNeighbors.next(); - BiconnectedComponent bcc = new BiconnectedComponent(); - bcc.registerNode(currentNode); - - LinkedList open = new LinkedList(); - open.add(neighbor); - 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 != currentNode - && visitedNormalNodes.get(child.getIndex()) == null - && visitedArticulationPoints.get(n.getIndex()) == null)).toArray(Node[]::new)) - ); // Add all still unvisited neighbors and articulation points that are unvisited for this current articulation point - } - if (visitedArticulationPoints.get(n.getIndex()) == null) { - bcc.registerNode(n); - if (nodeArticulationPoints.get(n.getIndex()) != null) { - visitedArticulationPoints.put(n.getIndex(), true); - } - } - } + 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); @@ -312,50 +298,71 @@ protected void calculateBiconnectedComponents(Node root) { } } - //Do last iteration for root node //TODO: Check + //Do last iteration for root node if (nodeArticulationPoints.get(root.getIndex()) == null) { - BiconnectedComponent bcc = new BiconnectedComponent(); + HashMap visitedArticulationPoints = new HashMap(); + + BiconnectedComponent bcc = createBiconnectedComponent(visitedNormalNodes, visitedArticulationPoints, root, root); + components.add(bcc); + } + } - LinkedList open = new LinkedList(); - - open.add(root); - while (!open.isEmpty()) { - Node n = open.poll(); - if (nodeArticulationPoints.get(n.getIndex()) == null) { // If we dont have an articulation point here - visitedNormalNodes.put(n.getIndex(), true); - open.addAll( - Arrays.asList( - n.neighborNodes().filter(child -> (visitedNormalNodes.get(child.getIndex()) == null)).toArray(Node[]::new) - ) - ); //TODO: Currently multiple traversals of articulation points are possible here, not so nice - } + /** + * 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 connected component that contains the biggest number of nodes. + * Get the biconnected component that contains the biggest number of nodes. * - * @return the biggest CC. + * @return the biggest BCC. */ public BiconnectedComponent getGiantComponent() { checkStarted(); - BiconnectedComponent maxCC = null; + BiconnectedComponent maxBCC = null; - maxCC = components.stream() + maxBCC = components.stream() .max((bcc1, bcc2) -> Integer.compare(bcc1.size, bcc2.size)) .get(); - return maxCC; + return maxBCC; } /** * Ask the algorithm for the number of biconnected components. * - * @return the number of connected components in this graph. + * @return the number of biconnected components in this graph. */ public int getBiconnectedComponentsCount() { checkStarted(); @@ -369,11 +376,11 @@ public String defaultResult() { } /** - * Ask the algorithm for the number of connected components whose size is + * 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 connected component to be considered - * @return the number of connected components, bigger than the given size + * @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) { @@ -381,14 +388,14 @@ public int getBiconnectedComponentsCount(int sizeThreshold) { } /** - * Ask the algorithm for the number of connected components whose size is + * 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 connected component to be considered - * @param sizeCeiling Maximum size for the connected component to be considered (use + * @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 connected components, bigger than the given size + * @return the number of biconnected components, bigger than the given size * threshold, and smaller than the given size ceiling, in this * graph. */ @@ -413,12 +420,12 @@ public int getBiconnectedComponentsCount(int sizeThreshold, int sizeCeiling) { } /** - * Return the connected component where a node belonged. The validity of the + * 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 connected component containing `n` + * @return the biconnected component containing `n` */ public ArrayList getBiconnectedComponentsOf(Node n) { return n == null ? null : componentsMap.get(n); @@ -428,7 +435,7 @@ public ArrayList getBiconnectedComponentsOf(Node n) { * Same as {@link #getBiconnectedComponentsOf(Node)} but using the node id. * * @param nodeId a node id - * @return the connected component containing the node `nodeId` + * @return the biconnected component containing the node `nodeId` */ public ArrayList getBiconnectedComponentsOf(String nodeId) { return getBiconnectedComponentsOf(graph.getNode(nodeId)); @@ -438,7 +445,7 @@ public ArrayList getBiconnectedComponentsOf(String nodeId) * Same as {@link #getBiconnectedComponentsOf(Node)} but using the node index. * * @param nodeIndex a node index - * @return the connected component containing the node `nodeIndex` + * @return the biconnected component containing the node `nodeIndex` */ public ArrayList getBiconnectedComponentOf(int nodeIndex) { return getBiconnectedComponentsOf(graph.getNode(nodeIndex)); From 73aec21cd2a77a06ada2ee55dc4600d7a37fc0fd Mon Sep 17 00:00:00 2001 From: Max Kissgen Date: Tue, 17 May 2022 11:57:44 +0200 Subject: [PATCH 4/5] Remove leftover part of complexity explanation for the bcc algorithm --- .../algorithm/HopcroftTarjanBiconnectedComponents.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java b/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java index 957af02..d732d7d 100644 --- a/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java +++ b/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java @@ -106,9 +106,7 @@ * * @author Max Kißgen * @complexity For the articulation points, let n be the number of nodes, then - * the time complexity is 0(n). For the re-optimization steps, let k be - * the number of nodes concerned by the changes (k <= n), the - * complexity is O(k). + * the time complexity is 0(n). * @since May 05 2022 */ public class HopcroftTarjanBiconnectedComponents implements Algorithm { From 507aef7a5b3ae2f35434983bb921d6cac277ab39 Mon Sep 17 00:00:00 2001 From: Max Kissgen Date: Tue, 12 Jul 2022 10:27:32 +0200 Subject: [PATCH 5/5] Add missing getter for Biconnected coponents in the hopcroft tarjan algorithm --- .../HopcroftTarjanBiconnectedComponents.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java b/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java index d732d7d..2263895 100644 --- a/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java +++ b/src/org/graphstream/algorithm/HopcroftTarjanBiconnectedComponents.java @@ -357,6 +357,17 @@ public BiconnectedComponent getGiantComponent() { 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. *