diff --git a/src/main/java/com/thealgorithms/graph/Bridges.java b/src/main/java/com/thealgorithms/graph/Bridges.java new file mode 100644 index 000000000000..6f64403691db --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/Bridges.java @@ -0,0 +1,112 @@ +package com.thealgorithms.graph; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Finds bridges in an undirected graph using depth-first search. + * + *

A bridge is an edge whose removal increases the number of connected + * components in the graph. + * + *

Vertices are numbered from 0 to {@code vertexCount - 1}. + * + *

Time complexity: O(V + E), where V is the number of vertices + * and E is the number of edges. + * + *

Reference: https://en.wikipedia.org/wiki/Bridge_(graph_theory) + */ +public final class Bridges { + + private Bridges() { + // Utility class; do not instantiate. + } + + /** + * Returns all bridges in an undirected graph. + * + * @param vertexCount number of vertices + * @param edges array of edges, with each edge represented as {u, v} + * @return list of bridges, with each bridge represented as {u, v} + * @throws IllegalArgumentException if the vertex count is negative, + * or an edge is null, malformed, + * or references an invalid vertex + */ + public static List findBridges(int vertexCount, int[][] edges) { + if (vertexCount < 0) { + throw new IllegalArgumentException("Vertex count cannot be negative."); + } + + if (edges == null) { + throw new IllegalArgumentException("Edges cannot be null."); + } + + List> adjacencyList = new ArrayList<>(vertexCount); + for (int i = 0; i < vertexCount; i++) { + adjacencyList.add(new ArrayList<>()); + } + + for (int edgeId = 0; edgeId < edges.length; edgeId++) { + int[] edge = edges[edgeId]; + if (edge == null || edge.length != 2) { + throw new IllegalArgumentException("Each edge must contain exactly two vertices."); + } + + int u = edge[0]; + int v = edge[1]; + if (u < 0 || u >= vertexCount || v < 0 || v >= vertexCount) { + throw new IllegalArgumentException("Edge contains an invalid vertex."); + } + + adjacencyList.get(u).add(new int[] {v, edgeId}); + adjacencyList.get(v).add(new int[] {u, edgeId}); + } + + int[] discoveryTime = new int[vertexCount]; + int[] low = new int[vertexCount]; + Arrays.fill(discoveryTime, -1); + + List bridges = new ArrayList<>(); + int[] timer = {0}; + + for (int vertex = 0; vertex < vertexCount; vertex++) { + if (discoveryTime[vertex] == -1) { + dfs(vertex, -1, adjacencyList, discoveryTime, low, timer, bridges); + } + } + + bridges.sort((first, second) -> { + int comparison = Integer.compare(first[0], second[0]); + return comparison != 0 ? comparison : Integer.compare(first[1], second[1]); + }); + + return bridges; + } + + private static void dfs(int vertex, int parentEdgeId, List> adjacencyList, int[] discoveryTime, int[] low, int[] timer, List bridges) { + discoveryTime[vertex] = timer[0]; + low[vertex] = timer[0]++; + + for (int[] adjacent : adjacencyList.get(vertex)) { + int neighbor = adjacent[0]; + int edgeId = adjacent[1]; + + if (edgeId == parentEdgeId) { + continue; + } + + if (discoveryTime[neighbor] != -1) { + low[vertex] = Math.min(low[vertex], discoveryTime[neighbor]); + } else { + dfs(neighbor, edgeId, adjacencyList, discoveryTime, low, timer, bridges); + + low[vertex] = Math.min(low[vertex], low[neighbor]); + + if (low[neighbor] > discoveryTime[vertex]) { + bridges.add(new int[] {Math.min(vertex, neighbor), Math.max(vertex, neighbor)}); + } + } + } + } +} diff --git a/src/test/java/com/thealgorithms/graph/BridgesTest.java b/src/test/java/com/thealgorithms/graph/BridgesTest.java new file mode 100644 index 000000000000..7a3774c9aee2 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/BridgesTest.java @@ -0,0 +1,166 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class BridgesTest { + + @Test + void testTriangleHasNoBridges() { + int[][] edges = {{0, 1}, {1, 2}, {2, 0}}; + + List result = Bridges.findBridges(3, edges); + + assertEquals(0, result.size()); + } + + @Test + void testChainHasTwoBridges() { + int[][] edges = {{0, 1}, {1, 2}}; + + List result = Bridges.findBridges(3, edges); + + assertEquals(2, result.size()); + assertArrayEquals(new int[] {0, 1}, result.get(0)); + assertArrayEquals(new int[] {1, 2}, result.get(1)); + } + + @Test + void testDisconnectedGraph() { + int[][] edges = {{0, 1}, {2, 3}, {3, 4}}; + + List result = Bridges.findBridges(5, edges); + + assertEquals(3, result.size()); + assertArrayEquals(new int[] {0, 1}, result.get(0)); + assertArrayEquals(new int[] {2, 3}, result.get(1)); + assertArrayEquals(new int[] {3, 4}, result.get(2)); + } + + @Test + void testEmptyGraph() { + List result = Bridges.findBridges(4, new int[0][]); + + assertEquals(0, result.size()); + } + + @Test + void testZeroVertices() { + List result = Bridges.findBridges(0, new int[0][]); + + assertEquals(0, result.size()); + } + + @Test + void testParallelEdgesAreNotBridges() { + int[][] edges = {{0, 1}, {0, 1}}; + + List result = Bridges.findBridges(2, edges); + + assertEquals(0, result.size()); + } + + @Test + void testSelfLoopIsNotBridge() { + int[][] edges = {{0, 0}}; + + List result = Bridges.findBridges(1, edges); + + assertEquals(0, result.size()); + } + + @Test + void testSingleEdgeIsBridge() { + int[][] edges = {{0, 1}}; + + List result = Bridges.findBridges(2, edges); + + assertEquals(1, result.size()); + assertArrayEquals(new int[] {0, 1}, result.get(0)); + } + + @Test + void testCycleWithTail() { + int[][] edges = {{0, 1}, {1, 2}, {2, 0}, {2, 3}}; + + List result = Bridges.findBridges(4, edges); + + assertEquals(1, result.size()); + assertArrayEquals(new int[] {2, 3}, result.get(0)); + } + + @Test + void testNegativeVertexCountThrowsException() { + assertThrows(IllegalArgumentException.class, () -> Bridges.findBridges(-1, new int[0][])); + } + + @Test + void testNullEdgesThrowsException() { + assertThrows(IllegalArgumentException.class, () -> Bridges.findBridges(2, null)); + } + + @Test + void testMalformedEdgeThrowsException() { + int[][] edges = {{0}}; + + assertThrows(IllegalArgumentException.class, () -> Bridges.findBridges(2, edges)); + } + + @Test + void testEdgeWithTooManyVerticesThrowsException() { + int[][] edges = {{0, 1, 2}}; + + assertThrows(IllegalArgumentException.class, () -> Bridges.findBridges(3, edges)); + } + + @Test + void testNullEdgeThrowsException() { + int[][] edges = {null}; + + assertThrows(IllegalArgumentException.class, () -> Bridges.findBridges(2, edges)); + } + + @Test + void testNegativeFirstVertexThrowsException() { + int[][] edges = {{-1, 1}}; + + assertThrows(IllegalArgumentException.class, () -> Bridges.findBridges(2, edges)); + } + + @Test + void testNegativeSecondVertexThrowsException() { + int[][] edges = {{0, -1}}; + + assertThrows(IllegalArgumentException.class, () -> Bridges.findBridges(2, edges)); + } + + @Test + void testFirstVertexOutOfRangeThrowsException() { + int[][] edges = {{2, 1}}; + + assertThrows(IllegalArgumentException.class, () -> Bridges.findBridges(2, edges)); + } + + @Test + void testSecondVertexOutOfRangeThrowsException() { + int[][] edges = {{0, 2}}; + + assertThrows(IllegalArgumentException.class, () -> Bridges.findBridges(2, edges)); + } + + @Test + void testBridgesAreSortedByBothEndpoints() { + int[][] edges = {{2, 3}, {0, 2}, {0, 1}}; + + List result = Bridges.findBridges(4, edges); + + assertEquals(3, result.size()); + assertArrayEquals(new int[] {0, 1}, result.get(0)); + assertArrayEquals(new int[] {0, 2}, result.get(1)); + assertArrayEquals(new int[] {2, 3}, result.get(2)); + } +}