Skip to content

Commit 8fb222e

Browse files
committed
feat: implement Tarjan's Bridge-Finding Algorithm
Adds a classic graph algorithm to find bridge edges in an undirected graph in O(V + E) time.
1 parent 7ea6636 commit 8fb222e

File tree

2 files changed

+341
-0
lines changed

2 files changed

+341
-0
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package com.thealgorithms.graph;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
/**
7+
* Implementation of Tarjan's Bridge-Finding Algorithm for undirected graphs.
8+
*
9+
* <p>A <b>bridge</b> (also called a cut-edge) is an edge in an undirected graph whose removal
10+
* increases the number of connected components. Bridges represent critical links
11+
* in a network — if any bridge is removed, part of the network becomes unreachable.</p>
12+
*
13+
* <p>The algorithm performs a single Depth-First Search (DFS) traversal, tracking two
14+
* values for each vertex:</p>
15+
* <ul>
16+
* <li><b>discoveryTime</b> — the time step at which the vertex was first visited.</li>
17+
* <li><b>lowLink</b> — the smallest discovery time reachable from the subtree rooted
18+
* at that vertex (via back edges).</li>
19+
* </ul>
20+
*
21+
* <p>An edge (u, v) is a bridge if and only if {@code lowLink[v] > discoveryTime[u]},
22+
* meaning there is no back edge from the subtree of v that can reach u or any ancestor of u.</p>
23+
*
24+
* <p>Time Complexity: O(V + E), where V is the number of vertices and E is the number of edges.</p>
25+
* <p>Space Complexity: O(V + E) for the adjacency list, discovery/low arrays, and recursion stack.</p>
26+
*
27+
* @see <a href="https://en.wikipedia.org/wiki/Bridge_(graph_theory)">Wikipedia: Bridge (graph theory)</a>
28+
*/
29+
public final class TarjanBridges {
30+
31+
private TarjanBridges() {
32+
throw new UnsupportedOperationException("Utility class");
33+
}
34+
35+
/**
36+
* Finds all bridge edges in an undirected graph.
37+
*
38+
* <p>The graph is represented as an adjacency list where each vertex is identified by
39+
* an integer in the range {@code [0, vertexCount)}. For each undirected edge (u, v),
40+
* v must appear in {@code adjacencyList.get(u)} and u must appear in
41+
* {@code adjacencyList.get(v)}.</p>
42+
*
43+
* @param vertexCount the total number of vertices in the graph (must be non-negative)
44+
* @param adjacencyList the adjacency list representation of the graph; must contain
45+
* exactly {@code vertexCount} entries (one per vertex)
46+
* @return a list of bridge edges, where each bridge is represented as an {@code int[]}
47+
* of length 2 with {@code edge[0] < edge[1]}; returns an empty list if no bridges exist
48+
* @throws IllegalArgumentException if {@code vertexCount} is negative, or if
49+
* {@code adjacencyList} is null or its size does not match
50+
* {@code vertexCount}
51+
*/
52+
public static List<int[]> findBridges(int vertexCount, List<List<Integer>> adjacencyList) {
53+
if (vertexCount < 0) {
54+
throw new IllegalArgumentException("vertexCount must be non-negative");
55+
}
56+
if (adjacencyList == null || adjacencyList.size() != vertexCount) {
57+
throw new IllegalArgumentException("adjacencyList size must equal vertexCount");
58+
}
59+
60+
List<int[]> bridges = new ArrayList<>();
61+
62+
if (vertexCount == 0) {
63+
return bridges;
64+
}
65+
66+
BridgeFinder finder = new BridgeFinder(vertexCount, adjacencyList, bridges);
67+
68+
// Run DFS from every unvisited vertex to handle disconnected graphs
69+
for (int i = 0; i < vertexCount; i++) {
70+
if (!finder.visited[i]) {
71+
finder.dfs(i, -1);
72+
}
73+
}
74+
75+
return bridges;
76+
}
77+
78+
private static class BridgeFinder {
79+
private final List<List<Integer>> adjacencyList;
80+
private final List<int[]> bridges;
81+
private final int[] discoveryTime;
82+
private final int[] lowLink;
83+
boolean[] visited;
84+
private int timer;
85+
86+
BridgeFinder(int vertexCount, List<List<Integer>> adjacencyList, List<int[]> bridges) {
87+
this.adjacencyList = adjacencyList;
88+
this.bridges = bridges;
89+
this.discoveryTime = new int[vertexCount];
90+
this.lowLink = new int[vertexCount];
91+
this.visited = new boolean[vertexCount];
92+
this.timer = 0;
93+
}
94+
95+
/**
96+
* Performs DFS from the given vertex, computing discovery times and low-link values,
97+
* and collects any bridge edges found.
98+
*
99+
* @param u the current vertex being explored
100+
* @param parent the parent of u in the DFS tree (-1 if u is a root)
101+
*/
102+
void dfs(int u, int parent) {
103+
visited[u] = true;
104+
discoveryTime[u] = timer;
105+
lowLink[u] = timer;
106+
timer++;
107+
108+
for (int v : adjacencyList.get(u)) {
109+
if (!visited[v]) {
110+
dfs(v, u);
111+
lowLink[u] = Math.min(lowLink[u], lowLink[v]);
112+
113+
if (lowLink[v] > discoveryTime[u]) {
114+
bridges.add(new int[] {Math.min(u, v), Math.max(u, v)});
115+
}
116+
} else if (v != parent) {
117+
lowLink[u] = Math.min(lowLink[u], discoveryTime[v]);
118+
}
119+
}
120+
}
121+
}
122+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package com.thealgorithms.graph;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import java.util.ArrayList;
8+
import java.util.Comparator;
9+
import java.util.List;
10+
import org.junit.jupiter.api.Test;
11+
12+
/**
13+
* Unit tests for {@link TarjanBridges}.
14+
*
15+
* <p>Tests cover a wide range of graph configurations including simple graphs,
16+
* cycles, trees, disconnected components, multigraph-like structures, and
17+
* various edge cases to ensure correct bridge detection.</p>
18+
*/
19+
class TarjanBridgesTest {
20+
21+
/**
22+
* Helper to build a symmetric adjacency list for an undirected graph.
23+
*/
24+
private static List<List<Integer>> buildGraph(int vertexCount, int[][] edges) {
25+
List<List<Integer>> adj = new ArrayList<>();
26+
for (int i = 0; i < vertexCount; i++) {
27+
adj.add(new ArrayList<>());
28+
}
29+
for (int[] edge : edges) {
30+
adj.get(edge[0]).add(edge[1]);
31+
adj.get(edge[1]).add(edge[0]);
32+
}
33+
return adj;
34+
}
35+
36+
/**
37+
* Sorts bridges for deterministic comparison.
38+
*/
39+
private static void sortBridges(List<int[]> bridges) {
40+
bridges.sort(Comparator.comparingInt((int[] a) -> a[0]).thenComparingInt(a -> a[1]));
41+
}
42+
43+
@Test
44+
void testSimpleGraphWithOneBridge() {
45+
// Graph: 0-1-2-3 where 1-2 is the only bridge
46+
// 0---1---2---3
47+
// | |
48+
// +-------+ (via 0-2 would make cycle, but not here)
49+
// Actually: 0-1 in a cycle with 0-1, and 2-3 in a cycle with 2-3
50+
// Let's use: 0--1--2 (linear chain). All edges are bridges.
51+
List<List<Integer>> adj = buildGraph(3, new int[][] {{0, 1}, {1, 2}});
52+
List<int[]> bridges = TarjanBridges.findBridges(3, adj);
53+
sortBridges(bridges);
54+
assertEquals(2, bridges.size());
55+
assertEquals(0, bridges.get(0)[0]);
56+
assertEquals(1, bridges.get(0)[1]);
57+
assertEquals(1, bridges.get(1)[0]);
58+
assertEquals(2, bridges.get(1)[1]);
59+
}
60+
61+
@Test
62+
void testCycleGraphHasNoBridges() {
63+
// Graph: 0-1-2-0 (triangle). No bridges.
64+
List<List<Integer>> adj = buildGraph(3, new int[][] {{0, 1}, {1, 2}, {2, 0}});
65+
List<int[]> bridges = TarjanBridges.findBridges(3, adj);
66+
assertTrue(bridges.isEmpty());
67+
}
68+
69+
@Test
70+
void testTreeGraphAllEdgesAreBridges() {
71+
// Tree: 0
72+
// / \
73+
// 1 2
74+
// / \
75+
// 3 4
76+
List<List<Integer>> adj = buildGraph(5, new int[][] {{0, 1}, {0, 2}, {1, 3}, {1, 4}});
77+
List<int[]> bridges = TarjanBridges.findBridges(5, adj);
78+
assertEquals(4, bridges.size());
79+
}
80+
81+
@Test
82+
void testGraphWithMixedBridgesAndCycles() {
83+
// Graph:
84+
// 0---1
85+
// | |
86+
// 3---2---4---5
87+
// |
88+
// 6
89+
// Cycle: 0-1-2-3-0 (no bridges within)
90+
// Bridges: 2-4, 4-5, 5-6
91+
List<List<Integer>> adj = buildGraph(7, new int[][] {
92+
{0, 1}, {1, 2}, {2, 3}, {3, 0}, {2, 4}, {4, 5}, {5, 6}
93+
});
94+
List<int[]> bridges = TarjanBridges.findBridges(7, adj);
95+
sortBridges(bridges);
96+
assertEquals(3, bridges.size());
97+
assertEquals(2, bridges.get(0)[0]);
98+
assertEquals(4, bridges.get(0)[1]);
99+
assertEquals(4, bridges.get(1)[0]);
100+
assertEquals(5, bridges.get(1)[1]);
101+
assertEquals(5, bridges.get(2)[0]);
102+
assertEquals(6, bridges.get(2)[1]);
103+
}
104+
105+
@Test
106+
void testDisconnectedGraphWithBridges() {
107+
// Component 1: 0-1 (bridge)
108+
// Component 2: 2-3-4-2 (cycle, no bridges)
109+
List<List<Integer>> adj = buildGraph(5, new int[][] {
110+
{0, 1}, {2, 3}, {3, 4}, {4, 2}
111+
});
112+
List<int[]> bridges = TarjanBridges.findBridges(5, adj);
113+
assertEquals(1, bridges.size());
114+
assertEquals(0, bridges.get(0)[0]);
115+
assertEquals(1, bridges.get(0)[1]);
116+
}
117+
118+
@Test
119+
void testSingleVertex() {
120+
List<List<Integer>> adj = buildGraph(1, new int[][] {});
121+
List<int[]> bridges = TarjanBridges.findBridges(1, adj);
122+
assertTrue(bridges.isEmpty());
123+
}
124+
125+
@Test
126+
void testTwoVerticesWithOneEdge() {
127+
List<List<Integer>> adj = buildGraph(2, new int[][] {{0, 1}});
128+
List<int[]> bridges = TarjanBridges.findBridges(2, adj);
129+
assertEquals(1, bridges.size());
130+
assertEquals(0, bridges.get(0)[0]);
131+
assertEquals(1, bridges.get(0)[1]);
132+
}
133+
134+
@Test
135+
void testEmptyGraph() {
136+
List<List<Integer>> adj = buildGraph(0, new int[][] {});
137+
List<int[]> bridges = TarjanBridges.findBridges(0, adj);
138+
assertTrue(bridges.isEmpty());
139+
}
140+
141+
@Test
142+
void testIsolatedVertices() {
143+
// 5 vertices, no edges — all isolated
144+
List<List<Integer>> adj = buildGraph(5, new int[][] {});
145+
List<int[]> bridges = TarjanBridges.findBridges(5, adj);
146+
assertTrue(bridges.isEmpty());
147+
}
148+
149+
@Test
150+
void testLargeCycleNoBridges() {
151+
// Cycle: 0-1-2-3-4-5-6-7-0
152+
int n = 8;
153+
int[][] edges = new int[n][2];
154+
for (int i = 0; i < n; i++) {
155+
edges[i] = new int[] {i, (i + 1) % n};
156+
}
157+
List<List<Integer>> adj = buildGraph(n, edges);
158+
List<int[]> bridges = TarjanBridges.findBridges(n, adj);
159+
assertTrue(bridges.isEmpty());
160+
}
161+
162+
@Test
163+
void testComplexGraphWithMultipleCyclesAndBridges() {
164+
// Two cycles connected by a single bridge edge:
165+
// Cycle A: 0-1-2-0
166+
// Cycle B: 3-4-5-3
167+
// Bridge: 2-3
168+
List<List<Integer>> adj = buildGraph(6, new int[][] {
169+
{0, 1}, {1, 2}, {2, 0}, {3, 4}, {4, 5}, {5, 3}, {2, 3}
170+
});
171+
List<int[]> bridges = TarjanBridges.findBridges(6, adj);
172+
assertEquals(1, bridges.size());
173+
assertEquals(2, bridges.get(0)[0]);
174+
assertEquals(3, bridges.get(0)[1]);
175+
}
176+
177+
@Test
178+
void testNegativeVertexCountThrowsException() {
179+
assertThrows(IllegalArgumentException.class, () -> TarjanBridges.findBridges(-1, new ArrayList<>()));
180+
}
181+
182+
@Test
183+
void testNullAdjacencyListThrowsException() {
184+
assertThrows(IllegalArgumentException.class, () -> TarjanBridges.findBridges(3, null));
185+
}
186+
187+
@Test
188+
void testMismatchedAdjacencyListSizeThrowsException() {
189+
List<List<Integer>> adj = buildGraph(2, new int[][] {{0, 1}});
190+
assertThrows(IllegalArgumentException.class, () -> TarjanBridges.findBridges(5, adj));
191+
}
192+
193+
@Test
194+
void testStarGraphAllEdgesAreBridges() {
195+
// Star graph: center vertex 0 connected to 1, 2, 3, 4
196+
List<List<Integer>> adj = buildGraph(5, new int[][] {
197+
{0, 1}, {0, 2}, {0, 3}, {0, 4}
198+
});
199+
List<int[]> bridges = TarjanBridges.findBridges(5, adj);
200+
assertEquals(4, bridges.size());
201+
}
202+
203+
@Test
204+
void testBridgeBetweenTwoCycles() {
205+
// Two squares connected by one bridge:
206+
// Square 1: 0-1-2-3-0
207+
// Square 2: 4-5-6-7-4
208+
// Bridge: 3-4
209+
List<List<Integer>> adj = buildGraph(8, new int[][] {
210+
{0, 1}, {1, 2}, {2, 3}, {3, 0},
211+
{4, 5}, {5, 6}, {6, 7}, {7, 4},
212+
{3, 4}
213+
});
214+
List<int[]> bridges = TarjanBridges.findBridges(8, adj);
215+
assertEquals(1, bridges.size());
216+
assertEquals(3, bridges.get(0)[0]);
217+
assertEquals(4, bridges.get(0)[1]);
218+
}
219+
}

0 commit comments

Comments
 (0)