Skip to content
112 changes: 112 additions & 0 deletions src/main/java/com/thealgorithms/graph/Bridges.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>A bridge is an edge whose removal increases the number of connected
* components in the graph.
*
* <p>Vertices are numbered from 0 to {@code vertexCount - 1}.
*
* <p>Time complexity: O(V + E), where V is the number of vertices

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The O(V + E) claim does not account for bridges.sort(...) at lines 79–82. With B returned bridges, the implemented method has an upper bound of O(V + E + B log B). Please update the complexity documentation if sorted output is retained, and document the endpoint normalization and lexicographic ordering in the return contract. Alternatively, remove the sorting if ordering is not part of the intended API.

* and E is the number of edges.
*
* <p>Reference: https://en.wikipedia.org/wiki/Bridge_(graph_theory)
*/
public final class Bridges {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This package already contains TarjanBridges, which implements the same DFS/low-link bridge-finding algorithm: https://github.com/TheAlgorithms/Java/blob/96ed2d7970606b51014afd25212c6c86d2af6255/src/main/java/com/thealgorithms/graph/TarjanBridges.java

Please consolidate this contribution into that implementation rather than maintaining a second copy of the algorithm. In particular, preserve the edge-ID handling and add the parallel-edge regression test to TarjanBridgesTest: for two vertices joined by two parallel edges, the existing implementation incorrectly reports one bridge, while this implementation correctly reports none. If an edge-list entry point is useful, it can be added to TarjanBridges while preserving the existing API and sharing the traversal logic. Please update the PR title/description to explain the resulting improvement.


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<int[]> 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<List<int[]>> 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<int[]> 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<List<int[]>> adjacencyList, int[] discoveryTime, int[] low, int[] timer, List<int[]> 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)});
}
}
}
}
}
166 changes: 166 additions & 0 deletions src/test/java/com/thealgorithms/graph/BridgesTest.java
Original file line number Diff line number Diff line change
@@ -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<int[]> result = Bridges.findBridges(3, edges);

assertEquals(0, result.size());
}

@Test
void testChainHasTwoBridges() {
int[][] edges = {{0, 1}, {1, 2}};

List<int[]> 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<int[]> 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<int[]> result = Bridges.findBridges(4, new int[0][]);

assertEquals(0, result.size());
}

@Test
void testZeroVertices() {
List<int[]> result = Bridges.findBridges(0, new int[0][]);

assertEquals(0, result.size());
}

@Test
void testParallelEdgesAreNotBridges() {
int[][] edges = {{0, 1}, {0, 1}};

List<int[]> result = Bridges.findBridges(2, edges);

assertEquals(0, result.size());
}

@Test
void testSelfLoopIsNotBridge() {
int[][] edges = {{0, 0}};

List<int[]> result = Bridges.findBridges(1, edges);

assertEquals(0, result.size());
}

@Test
void testSingleEdgeIsBridge() {
int[][] edges = {{0, 1}};

List<int[]> 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<int[]> 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<int[]> 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));
}
}
Loading