diff --git a/src/main/java/com/thealgorithms/datastructures/graphs/ArticulationPointsAndBridges.java b/src/main/java/com/thealgorithms/datastructures/graphs/ArticulationPointsAndBridges.java new file mode 100644 index 000000000000..b1eb718ea6c0 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/graphs/ArticulationPointsAndBridges.java @@ -0,0 +1,268 @@ +package com.thealgorithms.datastructures.graphs; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Java program that implements an algorithm to find Articulation Points (Cut Vertices) and Bridges (Cut Edges) + * in an undirected graph using Depth-First Search (DFS). + * + *

+ * Articulation Point (Cut Vertex): A vertex in a graph whose removal increases the number of connected components. + * In other words, removing an articulation point disconnects the graph or increases the number of separate subgraphs. + * + *

+ * Bridge (Cut Edge): An edge in a graph whose removal increases the number of connected components. + * Bridges are critical connections in the graph. + * + *

Algorithm Overview:

+ * + * + *

+ * Time Complexity: O(V + E), where V is the number of vertices and E is the number of edges. + *
+ * Space Complexity: O(V) for storing discovery times, low-link values, and visited status. + * + *

+ * Example of an undirected graph: + *

+ *       0 -------- 1 -------- 2
+ *       |          |
+ *       |          |
+ *       3 -------- 4
+ * 
+ * + *

+ * For the above graph: + *

+ * + *

Applications:

+ * + * + * @see Biconnected Component on Wikipedia + * @see Bridge Finding Algorithm + */ +public final class ArticulationPointsAndBridges { + + // Represents an edge in the graph + public static class Edge { + private final int from; + private final int to; + + public Edge(int from, int to) { + this.from = from; + this.to = to; + } + + public int getFrom() { + return from; + } + + public int getTo() { + return to; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Edge edge = (Edge) o; + return (from == edge.from && to == edge.to) || (from == edge.to && to == edge.from); + } + + @Override + public int hashCode() { + // Ensure edges (u,v) and (v,u) have the same hash code + return Math.min(from, to) * 31 + Math.max(from, to); + } + + @Override + public String toString() { + return "(" + from + ", " + to + ")"; + } + } + + // Result class to hold both articulation points and bridges + public static class Result { + private final Set articulationPoints; + private final Set bridges; + + public Result(Set articulationPoints, Set bridges) { + this.articulationPoints = articulationPoints; + this.bridges = bridges; + } + + public Set getArticulationPoints() { + return articulationPoints; + } + + public Set getBridges() { + return bridges; + } + + @Override + public String toString() { + return "Articulation Points: " + articulationPoints + ", Bridges: " + bridges; + } + } + + private ArticulationPointsAndBridges() { + } + + // Timer for tracking discovery time + private static int time; + + /** + * Finds articulation points and bridges in an undirected graph. + * + * @param vertices the number of vertices in the graph + * @param graph the adjacency list representation of the graph + * @return a Result object containing sets of articulation points and bridges + * @throws IllegalArgumentException if vertices is negative or if graph is null + */ + public static Result findArticulationPointsAndBridges(int vertices, List> graph) { + if (vertices < 0) { + throw new IllegalArgumentException("Number of vertices cannot be negative"); + } + if (graph == null) { + throw new IllegalArgumentException("Graph cannot be null"); + } + + // Initialize data structures + int[] discoveryTime = new int[vertices]; + int[] lowLink = new int[vertices]; + boolean[] visited = new boolean[vertices]; + int[] parent = new int[vertices]; + + Set articulationPoints = new HashSet<>(); + Set bridges = new HashSet<>(); + + // Initialize arrays + for (int i = 0; i < vertices; i++) { + discoveryTime[i] = -1; + lowLink[i] = -1; + parent[i] = -1; + } + + time = 0; + + // Perform DFS from each unvisited vertex (handles disconnected graphs) + for (int i = 0; i < vertices; i++) { + if (!visited[i]) { + dfs(i, visited, discoveryTime, lowLink, parent, articulationPoints, bridges, graph); + } + } + + return new Result(articulationPoints, bridges); + } + + /** + * Depth-First Search utility function to find articulation points and bridges. + * + * @param u the current vertex being visited + * @param visited array tracking visited vertices + * @param discoveryTime array storing discovery time of each vertex + * @param lowLink array storing low-link values + * @param parent array storing parent of each vertex in DFS tree + * @param articulationPoints set to store articulation points + * @param bridges set to store bridges + * @param graph the adjacency list representation of the graph + */ + private static void dfs(int u, boolean[] visited, int[] discoveryTime, int[] lowLink, int[] parent, Set articulationPoints, Set bridges, List> graph) { + // Count children in DFS tree (used for root articulation point check) + int children = 0; + + // Mark the current vertex as visited + visited[u] = true; + + // Set discovery time and low-link value + discoveryTime[u] = time; + lowLink[u] = time; + time++; + + // Explore all adjacent vertices + for (Integer v : graph.get(u)) { + // If v is not visited, make it a child of u in DFS tree and recur + if (!visited[v]) { + children++; + parent[v] = u; + dfs(v, visited, discoveryTime, lowLink, parent, articulationPoints, bridges, graph); + + // Check if the subtree rooted at v has a connection back to an ancestor of u + lowLink[u] = Math.min(lowLink[u], lowLink[v]); + + // Articulation point check for non-root vertex + // If u is not root and low-link value of v is greater than or equal to discovery time of u + if (parent[u] != -1 && lowLink[v] >= discoveryTime[u]) { + articulationPoints.add(u); + } + + // Bridge check + // If low-link value of v is greater than discovery time of u, then (u,v) is a bridge + if (lowLink[v] > discoveryTime[u]) { + bridges.add(new Edge(u, v)); + } + } else if (v != parent[u]) { + // Update low-link value of u for back edge (v is visited and not parent of u) + lowLink[u] = Math.min(lowLink[u], discoveryTime[v]); + } + } + + // Articulation point check for root vertex + // If u is root of DFS tree and has more than one child, it's an articulation point + if (parent[u] == -1 && children > 1) { + articulationPoints.add(u); + } + } + + /** + * Convenience method to find only articulation points. + * + * @param vertices the number of vertices in the graph + * @param graph the adjacency list representation of the graph + * @return a set of articulation points + * @throws IllegalArgumentException if vertices is negative or if graph is null + */ + public static Set findArticulationPoints(int vertices, List> graph) { + return findArticulationPointsAndBridges(vertices, graph).getArticulationPoints(); + } + + /** + * Convenience method to find only bridges. + * + * @param vertices the number of vertices in the graph + * @param graph the adjacency list representation of the graph + * @return a set of bridges + * @throws IllegalArgumentException if vertices is negative or if graph is null + */ + public static Set findBridges(int vertices, List> graph) { + return findArticulationPointsAndBridges(vertices, graph).getBridges(); + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/graphs/ArticulationPointsAndBridgesTest.java b/src/test/java/com/thealgorithms/datastructures/graphs/ArticulationPointsAndBridgesTest.java new file mode 100644 index 000000000000..6d4b108f4fe0 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/graphs/ArticulationPointsAndBridgesTest.java @@ -0,0 +1,373 @@ +package com.thealgorithms.datastructures.graphs; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.thealgorithms.datastructures.graphs.ArticulationPointsAndBridges.Edge; +import com.thealgorithms.datastructures.graphs.ArticulationPointsAndBridges.Result; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; + +public class ArticulationPointsAndBridgesTest { + + /** + * Helper method to create an undirected graph adjacency list. + */ + private List> createGraph(int vertices) { + List> graph = new ArrayList<>(); + for (int i = 0; i < vertices; i++) { + graph.add(new ArrayList<>()); + } + return graph; + } + + /** + * Helper method to add an undirected edge to the graph. + */ + private void addEdge(List> graph, int u, int v) { + graph.get(u).add(v); + graph.get(v).add(u); + } + + @Test + public void testSimpleGraphWithOneArticulationPoint() { + /* + * Graph structure: + * 0 -------- 1 -------- 2 + * | | + * | | + * 3 -------- 4 + * + * Articulation Point: 1 + * Bridges: (0,1), (1,2), (1,4) + */ + int vertices = 5; + List> graph = createGraph(vertices); + addEdge(graph, 0, 1); + addEdge(graph, 0, 3); + addEdge(graph, 1, 2); + addEdge(graph, 1, 4); + addEdge(graph, 3, 4); + + Result result = ArticulationPointsAndBridges.findArticulationPointsAndBridges(vertices, graph); + + // Verify articulation points + Set articulationPoints = result.getArticulationPoints(); + assertEquals(1, articulationPoints.size()); + assertTrue(articulationPoints.contains(1)); + + // Verify bridges + Set bridges = result.getBridges(); + assertEquals(2, bridges.size()); + assertTrue(bridges.contains(new Edge(1, 2))); + assertTrue(bridges.contains(new Edge(0, 1))); + } + + @Test + public void testGraphWithMultipleArticulationPoints() { + /* + * Graph structure: + * 0 -------- 1 -------- 2 -------- 3 + * + * Articulation Points: 1, 2 + * Bridges: (0,1), (1,2), (2,3) + */ + int vertices = 4; + List> graph = createGraph(vertices); + addEdge(graph, 0, 1); + addEdge(graph, 1, 2); + addEdge(graph, 2, 3); + + Result result = ArticulationPointsAndBridges.findArticulationPointsAndBridges(vertices, graph); + + // Verify articulation points + Set articulationPoints = result.getArticulationPoints(); + assertEquals(2, articulationPoints.size()); + assertTrue(articulationPoints.contains(1)); + assertTrue(articulationPoints.contains(2)); + + // Verify bridges + Set bridges = result.getBridges(); + assertEquals(3, bridges.size()); + assertTrue(bridges.contains(new Edge(0, 1))); + assertTrue(bridges.contains(new Edge(1, 2))); + assertTrue(bridges.contains(new Edge(2, 3))); + } + + @Test + public void testGraphWithNoArticulationPoints() { + /* + * Graph structure (triangle): + * 0 + * / \ + * 1---2 + * + * Articulation Points: None (it's a cycle, removing any vertex still leaves graph connected) + * Bridges: None + */ + int vertices = 3; + List> graph = createGraph(vertices); + addEdge(graph, 0, 1); + addEdge(graph, 1, 2); + addEdge(graph, 2, 0); + + Result result = ArticulationPointsAndBridges.findArticulationPointsAndBridges(vertices, graph); + + // Verify no articulation points + Set articulationPoints = result.getArticulationPoints(); + assertEquals(0, articulationPoints.size()); + + // Verify no bridges + Set bridges = result.getBridges(); + assertEquals(0, bridges.size()); + } + + @Test + public void testComplexGraphWithArticulationPointsAndBridges() { + /* + * Graph structure: + * 0 -------- 1 -------- 2 + * | / | | + * | / | | + * | / | | + * | / | | + * | / | | + * 3 ---------4 5 + * | | + * | | + * 6----------7 + * + * Articulation Points: 1, 2, 4, 6 + * Bridges: (1,2), (2,5), (4,6), (5,7) + */ + int vertices = 8; + List> graph = createGraph(vertices); + addEdge(graph, 0, 1); + addEdge(graph, 0, 3); + addEdge(graph, 1, 2); + addEdge(graph, 1, 3); + addEdge(graph, 1, 4); + addEdge(graph, 3, 4); + addEdge(graph, 2, 5); + addEdge(graph, 4, 6); + addEdge(graph, 5, 7); + addEdge(graph, 6, 7); + + Result result = ArticulationPointsAndBridges.findArticulationPointsAndBridges(vertices, graph); + + // Verify articulation points + Set articulationPoints = result.getArticulationPoints(); + assertEquals(4, articulationPoints.size()); + assertTrue(articulationPoints.contains(1)); + assertTrue(articulationPoints.contains(2)); + assertTrue(articulationPoints.contains(4)); + assertTrue(articulationPoints.contains(6)); + + // Verify bridges + Set bridges = result.getBridges(); + assertEquals(3, bridges.size()); + assertTrue(bridges.contains(new Edge(1, 2))); + assertTrue(bridges.contains(new Edge(2, 5))); + assertTrue(bridges.contains(new Edge(4, 6))); + } + + @Test + public void testDisconnectedGraph() { + /* + * Graph structure: + * 0 -------- 1 2 -------- 3 + * + * Articulation Points: None (each component is too small) + * Bridges: (0,1), (2,3) + */ + int vertices = 4; + List> graph = createGraph(vertices); + addEdge(graph, 0, 1); + addEdge(graph, 2, 3); + + Result result = ArticulationPointsAndBridges.findArticulationPointsAndBridges(vertices, graph); + + // Verify no articulation points (each component has only 2 vertices) + Set articulationPoints = result.getArticulationPoints(); + assertEquals(0, articulationPoints.size()); + + // Verify bridges + Set bridges = result.getBridges(); + assertEquals(2, bridges.size()); + assertTrue(bridges.contains(new Edge(0, 1))); + assertTrue(bridges.contains(new Edge(2, 3))); + } + + @Test + public void testStarGraph() { + /* + * Graph structure (star with center at 0): + * 1 + * | + * 2---0---3 + * | + * 4 + * + * Articulation Point: 0 (center) + * Bridges: (0,1), (0,2), (0,3), (0,4) + */ + int vertices = 5; + List> graph = createGraph(vertices); + addEdge(graph, 0, 1); + addEdge(graph, 0, 2); + addEdge(graph, 0, 3); + addEdge(graph, 0, 4); + + Result result = ArticulationPointsAndBridges.findArticulationPointsAndBridges(vertices, graph); + + // Verify articulation point + Set articulationPoints = result.getArticulationPoints(); + assertEquals(1, articulationPoints.size()); + assertTrue(articulationPoints.contains(0)); + + // Verify bridges (all edges are bridges in a star graph) + Set bridges = result.getBridges(); + assertEquals(4, bridges.size()); + assertTrue(bridges.contains(new Edge(0, 1))); + assertTrue(bridges.contains(new Edge(0, 2))); + assertTrue(bridges.contains(new Edge(0, 3))); + assertTrue(bridges.contains(new Edge(0, 4))); + } + + @Test + public void testSingleVertexGraph() { + /* + * Graph with single vertex (no edges) + * Articulation Points: None + * Bridges: None + */ + int vertices = 1; + List> graph = createGraph(vertices); + + Result result = ArticulationPointsAndBridges.findArticulationPointsAndBridges(vertices, graph); + + // Verify no articulation points + Set articulationPoints = result.getArticulationPoints(); + assertEquals(0, articulationPoints.size()); + + // Verify no bridges + Set bridges = result.getBridges(); + assertEquals(0, bridges.size()); + } + + @Test + public void testEmptyGraph() { + /* + * Empty graph (no vertices) + * Articulation Points: None + * Bridges: None + */ + int vertices = 0; + List> graph = createGraph(vertices); + + Result result = ArticulationPointsAndBridges.findArticulationPointsAndBridges(vertices, graph); + + // Verify no articulation points + Set articulationPoints = result.getArticulationPoints(); + assertEquals(0, articulationPoints.size()); + + // Verify no bridges + Set bridges = result.getBridges(); + assertEquals(0, bridges.size()); + } + + @Test + public void testBiconnectedGraph() { + /* + * Graph structure (square with diagonal): + * 0 -------- 1 + * | \ | + * | \ | + * | \ | + * | \ | + * 3 -------- 2 + * + * Articulation Points: None (biconnected - no single point of failure) + * Bridges: None + */ + int vertices = 4; + List> graph = createGraph(vertices); + addEdge(graph, 0, 1); + addEdge(graph, 1, 2); + addEdge(graph, 2, 3); + addEdge(graph, 3, 0); + addEdge(graph, 0, 2); // Diagonal + + Result result = ArticulationPointsAndBridges.findArticulationPointsAndBridges(vertices, graph); + + // Verify no articulation points + Set articulationPoints = result.getArticulationPoints(); + assertEquals(0, articulationPoints.size()); + + // Verify no bridges + Set bridges = result.getBridges(); + assertEquals(0, bridges.size()); + } + + @Test + public void testConvenienceMethodFindArticulationPoints() { + int vertices = 5; + List> graph = createGraph(vertices); + addEdge(graph, 0, 1); + addEdge(graph, 1, 2); + addEdge(graph, 2, 3); + addEdge(graph, 3, 4); + + Set articulationPoints = ArticulationPointsAndBridges.findArticulationPoints(vertices, graph); + + assertEquals(3, articulationPoints.size()); + assertTrue(articulationPoints.contains(1)); + assertTrue(articulationPoints.contains(2)); + assertTrue(articulationPoints.contains(3)); + } + + @Test + public void testConvenienceMethodFindBridges() { + int vertices = 3; + List> graph = createGraph(vertices); + addEdge(graph, 0, 1); + addEdge(graph, 1, 2); + + Set bridges = ArticulationPointsAndBridges.findBridges(vertices, graph); + + assertEquals(2, bridges.size()); + assertTrue(bridges.contains(new Edge(0, 1))); + assertTrue(bridges.contains(new Edge(1, 2))); + } + + @Test + public void testNegativeVerticesThrowsException() { + assertThrows(IllegalArgumentException.class, () -> { + ArticulationPointsAndBridges.findArticulationPointsAndBridges(-1, new ArrayList<>()); + }); + } + + @Test + public void testNullGraphThrowsException() { + assertThrows(IllegalArgumentException.class, () -> { + ArticulationPointsAndBridges.findArticulationPointsAndBridges(5, null); + }); + } + + @Test + public void testEdgeEquality() { + Edge edge1 = new Edge(1, 2); + Edge edge2 = new Edge(2, 1); + Edge edge3 = new Edge(1, 3); + + // Test symmetry: (1,2) should equal (2,1) + assertEquals(edge1, edge2); + assertEquals(edge1.hashCode(), edge2.hashCode()); + + // Test inequality + assertTrue(!edge1.equals(edge3)); + } +}