17.10 Critical Connections In A Network (Tarjan Bridges)
Source:
src/main/kotlin/graph/articulation_point/CriticalConnectionsInANetwork.ktPattern: Tarjan’s bridge-finding DFS · Core page
The Problem
Given n servers and undirected connections, return the critical connections — edges whose removal disconnects the network (bridges).
- Constraints: $2 \le n \le 10^5$; the graph is connected.
Examples
Input: n = 4, connections = [[0,1],[1,2],[2,0],[1,3]]
Output: [[1,3]] (the only edge whose removal splits the network)
Intuition — an edge is a bridge iff no back edge from its subtree reaches above it
The classic Tarjan bridge test: during a DFS, give each node a discovery label. Define low[u] = the lowest label reachable from u’s subtree via tree edges plus at most one back edge. Then:
Edge
(u, v)(tree edge,uthe parent) is a bridge ⟺low[v] > label[u]— the child’s subtree has no way back aboveu.
dfs(node, parent):
label[node] = low[node] = depth++
for neighbor in graph[node]:
if neighbor == parent: continue
if not visited[neighbor]:
dfs(neighbor, node)
if label[node] < low[neighbor]: result += (node, neighbor) # bridge!
low[node] = min(low[node], low[neighbor]) # subtree can reach up
else:
low[node] = min(low[node], label[neighbor]) # back edge: reach up
Why is low[v] > label[u] exactly “no way around”? If v’s subtree can reach any node at or above u (a back edge to u or an ancestor), then low[v] <= label[u] — the edge (u,v) is bypassable. Only when the subtree is fully confined below u (low[v] > label[u]) does (u,v) become the sole connection — a bridge. This is the 6.7 Tarjan idea (the same low-link bookkeeping) applied to bridges instead of SCCs.
Why label[neighbor] (not low[neighbor]) on the back edge? low may be polluted by cycles already merged; the discovery label is the honest “how far up does this edge reach.” Using low[neighbor] would understate bridges — the classic Tarjan gotcha.
Approach 1 — Remove each edge, BFS connectivity (O(E·V))
Delete an edge and check if the graph stays connected: correct, quadratic-ish on dense graphs.
Approach 2 — Tarjan’s single DFS (the repo’s version, optimal)
class CriticalConnectionsInANetwork {
private lateinit var G: Array<MutableList<Int>>
private lateinit var result: MutableList<List<Int>>
private var depth = 0
private lateinit var label: IntArray // discovery time
private lateinit var low: IntArray // lowest label reachable from the subtree
private lateinit var visited: BooleanArray
/**
* @param n number of servers
* @param connections undirected edges
* @return all bridges (critical connections)
*/
fun criticalConnections(n: Int, connections: List<List<Int>>): List<List<Int>> {
G = Array(n) { mutableListOf() }
result = mutableListOf()
depth = 0
label = IntArray(n)
low = IntArray(n)
visited = BooleanArray(n)
for (edge in connections) {
G[edge[0]].add(edge[1])
G[edge[1]].add(edge[0])
}
dfs(0, -1)
return result
}
private fun dfs(node: Int, parent: Int) {
visited[node] = true
label[node] = depth
low[node] = depth
depth++
for (neighbour in G[node]) {
if (neighbour == parent) continue
if (!visited[neighbour]) {
dfs(neighbour, node)
// Subtree cannot reach above node: (node, neighbour) is a bridge
if (label[node] < low[neighbour]) {
result.add(listOf(node, neighbour))
}
low[node] = minOf(low[node], low[neighbour]) // absorb subtree reach
} else {
low[node] = minOf(low[node], label[neighbour]) // back edge reach
}
}
}
}
import java.util.*;
public class CriticalConnectionsInANetwork {
private List<List<Integer>> graph;
private List<List<Integer>> result;
private int[] label, low;
private boolean[] visited;
private int depth;
/**
* @param n number of servers
* @param connections undirected edges
* @return all bridges (critical connections)
*/
public List<List<Integer>> criticalConnections(int n, List<List<Integer>> connections) {
graph = new ArrayList<>();
for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
for (List<Integer> e : connections) {
graph.get(e.get(0)).add(e.get(1));
graph.get(e.get(1)).add(e.get(0));
}
result = new ArrayList<>();
label = new int[n]; low = new int[n];
visited = new boolean[n];
depth = 0;
dfs(0, -1);
return result;
}
private void dfs(int node, int parent) {
visited[node] = true;
label[node] = low[node] = depth++;
for (int next : graph.get(node)) {
if (next == parent) continue;
if (!visited[next]) {
dfs(next, node);
if (label[node] < low[next]) { // bridge!
result.add(List.of(node, next));
}
low[node] = Math.min(low[node], low[next]);
} else {
low[node] = Math.min(low[node], label[next]); // back edge
}
}
}
}
#include <algorithm>
#include <vector>
class CriticalConnectionsInANetwork {
std::vector<std::vector<int>> graph;
std::vector<std::vector<int>> result;
std::vector<int> label, low;
std::vector<bool> visited;
int depth = 0;
void dfs(int node, int parent) {
visited[node] = true;
label[node] = low[node] = depth++;
for (int next : graph[node]) {
if (next == parent) continue;
if (!visited[next]) {
dfs(next, node);
if (label[node] < low[next]) { // bridge!
result.push_back({node, next});
}
low[node] = std::min(low[node], low[next]);
} else {
low[node] = std::min(low[node], label[next]); // back edge
}
}
}
public:
/**
* @param n number of servers
* @param connections undirected edges
* @return all bridges (critical connections)
*/
std::vector<std::vector<int>> criticalConnections(int n,
std::vector<std::vector<int>>& connections) {
graph.assign(n, {});
for (auto& e : connections) {
graph[e[0]].push_back(e[1]);
graph[e[1]].push_back(e[0]);
}
label.assign(n, 0); low.assign(n, 0);
visited.assign(n, false);
dfs(0, -1);
return result;
}
};
def critical_connections(n: int, connections: list[list[int]]) -> list[list[int]]:
"""
@param n: number of servers
@param connections: undirected edges
@return: all bridges (critical connections)
"""
graph = [[] for _ in range(n)]
for u, v in connections:
graph[u].append(v)
graph[v].append(u)
label = [0] * n
low = [0] * n
visited = [False] * n
result = []
depth = 0
def dfs(node: int, parent: int) -> None:
nonlocal depth
visited[node] = True
label[node] = low[node] = depth
depth += 1
for nxt in graph[node]:
if nxt == parent:
continue
if not visited[nxt]:
dfs(nxt, node)
if label[node] < low[nxt]: # subtree can't reach above: bridge
result.append([node, nxt])
low[node] = min(low[node], low[nxt])
else:
low[node] = min(low[node], label[nxt]) # back edge reach
dfs(0, -1)
return result
#![allow(unused)]
fn main() {
impl Solution {
/// @param n number of servers
/// @param connections undirected edges
/// @return all bridges (critical connections)
pub fn critical_connections(n: i32, connections: Vec<Vec<i32>>) -> Vec<Vec<i32>> {
let n = n as usize;
let mut graph = vec![Vec::new(); n];
for c in &connections {
graph[c[0] as usize].push(c[1] as usize);
graph[c[1] as usize].push(c[0] as usize);
}
let (mut label, mut low) = (vec![0usize; n], vec![0usize; n]);
let mut visited = vec![false; n];
let mut depth = 0usize;
let mut result: Vec<Vec<i32>> = Vec::new();
fn dfs(node: usize, parent: usize, graph: &Vec<Vec<usize>>, label: &mut Vec<usize>,
low: &mut Vec<usize>, visited: &mut Vec<bool>, depth: &mut usize,
result: &mut Vec<Vec<i32>>) {
visited[node] = true;
label[node] = *depth;
low[node] = *depth;
*depth += 1;
for &nxt in &graph[node] {
if nxt == parent { continue; }
if !visited[nxt] {
dfs(nxt, node, graph, label, low, visited, depth, result);
if label[node] < low[nxt] { // subtree can't reach above: bridge
result.push(vec![node as i32, nxt as i32]);
}
low[node] = low[node].min(low[nxt]);
} else {
low[node] = low[node].min(label[nxt]); // back edge reach
}
}
}
dfs(0, usize::MAX, &graph, &mut label, &mut low, &mut visited, &mut depth, &mut result);
result
}
}
}
5. CriticalConnectionsInANetworkShortCode.kt — Tarjan bridges, compressed
17.10 documents the full class; this file compresses the bridge DFS into the smallest faithful form:
// sketch of the short-code shape (CriticalConnectionsInANetworkShortCode.kt)
// label/low arrays + one dfs() that emits a bridge when label[node] < low[neighbor]
// — the same algorithm as 17.10, with the class scaffolding stripped to the essentials
What’s cool: it proves the algorithm has ~15 essential lines. When an interviewer asks “can you write it tighter?” — this file is the answer: no result-list as a field, no explicit depth class member, the recursion carries everything.
Dry run
Input: n = 4, connections = [[0,1],[1,2],[2,0],[1,3]].
dfs(0): label[0]=low[0]=0. neighbors: 1.
dfs(1): label[1]=low[1]=1. neighbors: 0 (parent, skip), 2, 3.
dfs(2): label[2]=low[2]=2. neighbors: 1 (parent, skip), 0.
0 is visited (back edge) -> low[2] = min(2, label[0]=0) = 0.
back in dfs(1): low[1] = min(1, low[2]=0) = 0. label[1]=1 < low[2]=0? NO -> not a bridge.
dfs(3): label[3]=low[3]=3. neighbor 1 (parent, skip).
back in dfs(1): label[1]=1 < low[3]=3? YES -> bridge [1,3]. low[1]=min(0,3)=0.
back in dfs(0): label[0]=0 < low[1]=0? NO -> not a bridge.
Output: [[1,3]] ✓
The low propagation is the whole story: node 2’s back edge to 0 drags low[2] down to 0, and that 0 flows up through 1 — so the cycle edges (0-1, 1-2, 2-0) all fail the label < low test. Only node 3’s subtree is confined (low[3] = 3 > label[1] = 1), marking 1-3 as the single point of failure.
Complexity
Time. One DFS pass:
$$ T(V, E) = O(V + E) $$
Space. Label/low/visited arrays + recursion:
$$ S(V) = O(V) $$
Variants & follow-ups
- Articulation Points (
graph/articulation_point/FindArticulationPoints.kt) — the samelow-link machinery for vertices instead of edges (root rule + child rule differ slightly). - Strongly Connected Components (6.7) — Tarjan’s other use of
low-links, for directed graphs. - Interview follow-up: “Why
label[neighbor]on a back edge instead oflow[neighbor]?”low[neighbor]may already include cycles merged from other branches — it overstates how far up the back edge reaches, understating bridges. The discoverylabelis the honest “this edge connects to node X at depth label[X]”; using it keeps the bridge test exact.