Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions src/main/java/com/thealgorithms/graph/TarjanBridges.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.thealgorithms.graph;

import java.util.ArrayList;
import java.util.List;

/**
* Implementation of Tarjan's Bridge-Finding Algorithm for undirected graphs.
*
* <p>A <b>bridge</b> (also called a cut-edge) is an edge in an undirected graph whose removal
* increases the number of connected components. Bridges represent critical links
* in a network — if any bridge is removed, part of the network becomes unreachable.</p>
*
* <p>The algorithm performs a single Depth-First Search (DFS) traversal, tracking two
* values for each vertex:</p>
* <ul>
* <li><b>discoveryTime</b> — the time step at which the vertex was first visited.</li>
* <li><b>lowLink</b> — the smallest discovery time reachable from the subtree rooted
* at that vertex (via back edges).</li>
* </ul>
*
* <p>An edge (u, v) is a bridge if and only if {@code lowLink[v] > discoveryTime[u]},
* meaning there is no back edge from the subtree of v that can reach u or any ancestor of u.</p>
*
* <p>Time Complexity: O(V + E), where V is the number of vertices and E is the number of edges.</p>
* <p>Space Complexity: O(V + E) for the adjacency list, discovery/low arrays, and recursion stack.</p>
*
* @see <a href="https://en.wikipedia.org/wiki/Bridge_(graph_theory)">Wikipedia: Bridge (graph theory)</a>
*/
public final class TarjanBridges {

private TarjanBridges() {
throw new UnsupportedOperationException("Utility class");
}

/**
* Finds all bridge edges in an undirected graph.
*
* <p>The graph is represented as an adjacency list where each vertex is identified by
* an integer in the range {@code [0, vertexCount)}. For each undirected edge (u, v),
* v must appear in {@code adjacencyList.get(u)} and u must appear in
* {@code adjacencyList.get(v)}.</p>
*
* @param vertexCount the total number of vertices in the graph (must be non-negative)
* @param adjacencyList the adjacency list representation of the graph; must contain
* exactly {@code vertexCount} entries (one per vertex)
* @return a list of bridge edges, where each bridge is represented as an {@code int[]}
* of length 2 with {@code edge[0] < edge[1]}; returns an empty list if no bridges exist
* @throws IllegalArgumentException if {@code vertexCount} is negative, or if
* {@code adjacencyList} is null or its size does not match
* {@code vertexCount}
*/
public static List<int[]> findBridges(int vertexCount, List<List<Integer>> adjacencyList) {
if (vertexCount < 0) {
throw new IllegalArgumentException("vertexCount must be non-negative");
}
if (adjacencyList == null || adjacencyList.size() != vertexCount) {
throw new IllegalArgumentException("adjacencyList size must equal vertexCount");
}

List<int[]> bridges = new ArrayList<>();

if (vertexCount == 0) {
return bridges;
}

BridgeFinder finder = new BridgeFinder(vertexCount, adjacencyList, bridges);

// Run DFS from every unvisited vertex to handle disconnected graphs
for (int i = 0; i < vertexCount; i++) {
if (!finder.visited[i]) {
finder.dfs(i, -1);
}
}

return bridges;
}

private static class BridgeFinder {
private final List<List<Integer>> adjacencyList;
private final List<int[]> bridges;
private final int[] discoveryTime;
private final int[] lowLink;
boolean[] visited;
private int timer;

BridgeFinder(int vertexCount, List<List<Integer>> adjacencyList, List<int[]> bridges) {
this.adjacencyList = adjacencyList;
this.bridges = bridges;
this.discoveryTime = new int[vertexCount];
this.lowLink = new int[vertexCount];
this.visited = new boolean[vertexCount];
this.timer = 0;
}

/**
* Performs DFS from the given vertex, computing discovery times and low-link values,
* and collects any bridge edges found.
*
* @param u the current vertex being explored
* @param parent the parent of u in the DFS tree (-1 if u is a root)
*/
void dfs(int u, int parent) {
visited[u] = true;
discoveryTime[u] = timer;
lowLink[u] = timer;
timer++;

for (int v : adjacencyList.get(u)) {
if (!visited[v]) {
dfs(v, u);
lowLink[u] = Math.min(lowLink[u], lowLink[v]);

if (lowLink[v] > discoveryTime[u]) {
bridges.add(new int[] {Math.min(u, v), Math.max(u, v)});
}
} else if (v != parent) {
lowLink[u] = Math.min(lowLink[u], discoveryTime[v]);
}
}
}
}
}
207 changes: 207 additions & 0 deletions src/test/java/com/thealgorithms/graph/TarjanBridgesTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package com.thealgorithms.graph;

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 java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import org.junit.jupiter.api.Test;

/**
* Unit tests for {@link TarjanBridges}.
*
* <p>Tests cover a wide range of graph configurations including simple graphs,
* cycles, trees, disconnected components, multigraph-like structures, and
* various edge cases to ensure correct bridge detection.</p>
*/
class TarjanBridgesTest {

/**
* Helper to build a symmetric adjacency list for an undirected graph.
*/
private static List<List<Integer>> buildGraph(int vertexCount, int[][] edges) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < vertexCount; i++) {
adj.add(new ArrayList<>());
}
for (int[] edge : edges) {
adj.get(edge[0]).add(edge[1]);
adj.get(edge[1]).add(edge[0]);
}
return adj;
}

/**
* Sorts bridges for deterministic comparison.
*/
private static void sortBridges(List<int[]> bridges) {
bridges.sort(Comparator.comparingInt((int[] a) -> a[0]).thenComparingInt(a -> a[1]));
}

@Test
void testSimpleGraphWithOneBridge() {
// Graph: 0-1-2-3 where 1-2 is the only bridge
// 0---1---2---3
// | |
// +-------+ (via 0-2 would make cycle, but not here)
// Actually: 0-1 in a cycle with 0-1, and 2-3 in a cycle with 2-3
// Let's use: 0--1--2 (linear chain). All edges are bridges.
List<List<Integer>> adj = buildGraph(3, new int[][] {{0, 1}, {1, 2}});
List<int[]> bridges = TarjanBridges.findBridges(3, adj);
sortBridges(bridges);
assertEquals(2, bridges.size());
assertEquals(0, bridges.get(0)[0]);
assertEquals(1, bridges.get(0)[1]);
assertEquals(1, bridges.get(1)[0]);
assertEquals(2, bridges.get(1)[1]);
}

@Test
void testCycleGraphHasNoBridges() {
// Graph: 0-1-2-0 (triangle). No bridges.
List<List<Integer>> adj = buildGraph(3, new int[][] {{0, 1}, {1, 2}, {2, 0}});
List<int[]> bridges = TarjanBridges.findBridges(3, adj);
assertTrue(bridges.isEmpty());
}

@Test
void testTreeGraphAllEdgesAreBridges() {
// Tree: 0
// / \
// 1 2
// / \
// 3 4
List<List<Integer>> adj = buildGraph(5, new int[][] {{0, 1}, {0, 2}, {1, 3}, {1, 4}});
List<int[]> bridges = TarjanBridges.findBridges(5, adj);
assertEquals(4, bridges.size());
}

@Test
void testGraphWithMixedBridgesAndCycles() {
// Graph:
// 0---1
// | |
// 3---2---4---5
// |
// 6
// Cycle: 0-1-2-3-0 (no bridges within)
// Bridges: 2-4, 4-5, 5-6
List<List<Integer>> adj = buildGraph(7, new int[][] {{0, 1}, {1, 2}, {2, 3}, {3, 0}, {2, 4}, {4, 5}, {5, 6}});
List<int[]> bridges = TarjanBridges.findBridges(7, adj);
sortBridges(bridges);
assertEquals(3, bridges.size());
assertEquals(2, bridges.get(0)[0]);
assertEquals(4, bridges.get(0)[1]);
assertEquals(4, bridges.get(1)[0]);
assertEquals(5, bridges.get(1)[1]);
assertEquals(5, bridges.get(2)[0]);
assertEquals(6, bridges.get(2)[1]);
}

@Test
void testDisconnectedGraphWithBridges() {
// Component 1: 0-1 (bridge)
// Component 2: 2-3-4-2 (cycle, no bridges)
List<List<Integer>> adj = buildGraph(5, new int[][] {{0, 1}, {2, 3}, {3, 4}, {4, 2}});
List<int[]> bridges = TarjanBridges.findBridges(5, adj);
assertEquals(1, bridges.size());
assertEquals(0, bridges.get(0)[0]);
assertEquals(1, bridges.get(0)[1]);
}

@Test
void testSingleVertex() {
List<List<Integer>> adj = buildGraph(1, new int[][] {});
List<int[]> bridges = TarjanBridges.findBridges(1, adj);
assertTrue(bridges.isEmpty());
}

@Test
void testTwoVerticesWithOneEdge() {
List<List<Integer>> adj = buildGraph(2, new int[][] {{0, 1}});
List<int[]> bridges = TarjanBridges.findBridges(2, adj);
assertEquals(1, bridges.size());
assertEquals(0, bridges.get(0)[0]);
assertEquals(1, bridges.get(0)[1]);
}

@Test
void testEmptyGraph() {
List<List<Integer>> adj = buildGraph(0, new int[][] {});
List<int[]> bridges = TarjanBridges.findBridges(0, adj);
assertTrue(bridges.isEmpty());
}

@Test
void testIsolatedVertices() {
// 5 vertices, no edges — all isolated
List<List<Integer>> adj = buildGraph(5, new int[][] {});
List<int[]> bridges = TarjanBridges.findBridges(5, adj);
assertTrue(bridges.isEmpty());
}

@Test
void testLargeCycleNoBridges() {
// Cycle: 0-1-2-3-4-5-6-7-0
int n = 8;
int[][] edges = new int[n][2];
for (int i = 0; i < n; i++) {
edges[i] = new int[] {i, (i + 1) % n};
}
List<List<Integer>> adj = buildGraph(n, edges);
List<int[]> bridges = TarjanBridges.findBridges(n, adj);
assertTrue(bridges.isEmpty());
}

@Test
void testComplexGraphWithMultipleCyclesAndBridges() {
// Two cycles connected by a single bridge edge:
// Cycle A: 0-1-2-0
// Cycle B: 3-4-5-3
// Bridge: 2-3
List<List<Integer>> adj = buildGraph(6, new int[][] {{0, 1}, {1, 2}, {2, 0}, {3, 4}, {4, 5}, {5, 3}, {2, 3}});
List<int[]> bridges = TarjanBridges.findBridges(6, adj);
assertEquals(1, bridges.size());
assertEquals(2, bridges.get(0)[0]);
assertEquals(3, bridges.get(0)[1]);
}

@Test
void testNegativeVertexCountThrowsException() {
assertThrows(IllegalArgumentException.class, () -> TarjanBridges.findBridges(-1, new ArrayList<>()));
}

@Test
void testNullAdjacencyListThrowsException() {
assertThrows(IllegalArgumentException.class, () -> TarjanBridges.findBridges(3, null));
}

@Test
void testMismatchedAdjacencyListSizeThrowsException() {
List<List<Integer>> adj = buildGraph(2, new int[][] {{0, 1}});
assertThrows(IllegalArgumentException.class, () -> TarjanBridges.findBridges(5, adj));
}

@Test
void testStarGraphAllEdgesAreBridges() {
// Star graph: center vertex 0 connected to 1, 2, 3, 4
List<List<Integer>> adj = buildGraph(5, new int[][] {{0, 1}, {0, 2}, {0, 3}, {0, 4}});
List<int[]> bridges = TarjanBridges.findBridges(5, adj);
assertEquals(4, bridges.size());
}

@Test
void testBridgeBetweenTwoCycles() {
// Two squares connected by one bridge:
// Square 1: 0-1-2-3-0
// Square 2: 4-5-6-7-4
// Bridge: 3-4
List<List<Integer>> adj = buildGraph(8, new int[][] {{0, 1}, {1, 2}, {2, 3}, {3, 0}, {4, 5}, {5, 6}, {6, 7}, {7, 4}, {3, 4}});
List<int[]> bridges = TarjanBridges.findBridges(8, adj);
assertEquals(1, bridges.size());
assertEquals(3, bridges.get(0)[0]);
assertEquals(4, bridges.get(0)[1]);
}
}
Loading