Critical Connections in a Network is the classic graph-bridge problem. It teaches Tarjan's bridge-finding algorithm: a single DFS that, with two numbers per node, finds every edge whose removal would split the network.
Problem. You are given n servers numbered 0 to n-1 and a list of undirected connections
where connections[i] = [a, b] is a link between servers a and b. A critical connection is an
edge that, if removed, makes some server unreachable from another. Return all critical connections in
any order.
Example: n = 5, connections [[0,1],[1,2],[0,2],[1,3],[3,4]] → answer [[1,3],[3,4]]. The triangle
0-1-2 has redundant paths, but the tail 1-3-4 hangs on by single links.
The slow way first
The brute-force idea: remove each edge one at a time, then run a full traversal to check whether the graph is still connected. With E edges and an O(V + E) traversal each, that is O(E · (V + E)) — far too slow for a large network.
The question to ask: can I detect a bridge in a single pass, without ever removing an edge? A bridge is an edge with no alternate route around it. If I can measure, for each node, whether its part of the graph has a "back door" to an ancestor, I can spot bridges directly.
The idea: discovery times and low-links
Run one DFS. Give each node two numbers:
disc[u]— the discovery time, the order in which DFS first reachedu.low[u]— the low-link, the smallestdiscvalue reachable fromu's subtree, including through one back-edge (an edge to an already-visited ancestor).
When DFS returns from a child v to its parent u, the edge u-v is a bridge exactly when low[v] > disc[u]. That inequality means nothing in v's subtree can reach u or anything above it without using the edge u-v itself — so cutting it disconnects them.
The key insight: a back-edge to an ancestor lowers low. If a subtree has any back-edge that climbs above u, the connecting edge is safe. If it has none, that edge is the only link — a bridge.
Walk through it
Step through the animation. DFS dives 0 → 1 → 2, and node 2 finds a back-edge 2-0 that pulls low[2] down to 0. That makes the whole triangle two-way connected, so none of its edges are bridges. Then DFS explores the tail 1 → 3 → 4. Node 4 is a dead end with no back-edge, so low[4] > disc[3]: edge 3-4 is a bridge. Likewise low[3] > disc[1], so 1-3 is a bridge too.
Pseudocode
build an adjacency list from connections
disc[*] = low[*] = -1 # -1 means "not visited yet"
timer = 0
dfs(u, parent):
disc[u] = low[u] = timer; timer += 1
for each neighbor v of u:
if v is unvisited: # tree edge
dfs(v, u)
low[u] = min(low[u], low[v])
else if v is not parent: # back edge
low[u] = min(low[u], disc[v])
if low[v] > disc[u]: # no back door around u-v
record (u, v) as a bridge
dfs(0, -1)
return all recorded bridgesThe Python solution
def critical_connections(n, connections):
graph = build_adjacency(connections)
disc, low = [-1] * n, [-1] * n
bridges = []
timer = [0]
def dfs(u, parent):
disc[u] = low[u] = timer[0]
timer[0] += 1
for v in graph[u]:
if disc[v] == -1: # tree edge
dfs(v, u)
low[u] = min(low[u], low[v])
elif v != parent: # back edge
low[u] = min(low[u], disc[v])
if low[v] > disc[u]:
bridges.append([u, v])
dfs(0, -1)
return bridgesdiscandlowstart at-1, which doubles as the "not visited yet" marker.timeris a one-element list so the nesteddfscan mutate it (a shared counter).- When
vis unvisited we recurse (a tree edge), then fold the child'slowinto the parent. elif v != parenthandles a back-edge to an ancestor: we lowerlow[u]towarddisc[v]. Skippingparentavoids treating the edge we just came down as a back-edge.low[v] > disc[u]is the bridge test: the child reached nothing at or aboveu, so the edge is critical.
Complexity
| Case | Time | Notes |
|---|---|---|
| Brute force (remove each edge) | O(E · (V + E)) (moderate) | a full traversal per edge |
| Tarjan (this solution) | O(V + E) (moderate) | one DFS over the graph |
O(V + E) (moderate)We visit every node and edge once. The extra space is the adjacency list plus the disc, low, and recursion stack — all linear in the graph size.
When this pattern shows up
Whenever a problem asks about edges or nodes whose removal disconnects a graph — bridges, articulation points, strongly connected components — reach for disc and low-link DFS (Tarjan). The shared move is: track discovery order, then use the lowest reachable ancestor to decide what is critical.
Do not treat the edge back to your parent as a back-edge. The v != parent guard is essential; without
it low[u] would wrongly drop to disc[parent] and you would miss real bridges. (If the graph can have
duplicate edges between the same pair, guard on edge id instead of the parent node.)
Practice
During the DFS, node 2 finds edge 2-0 to the already-visited node 0. What happens to low[2], and does that make edge 1-2 a bridge?
1. What does low[u] represent?
2. When is the tree edge (u, v) a bridge?
3. Why do we skip the parent when looking for back-edges?
4. What is the time complexity of the Tarjan bridge algorithm?