Critical Connections in a Network
Problem
There are n servers numbered from 0 to n - 1 connected by undirected server-to-server connections forming a network where connections[i] = [ai, bi] represents a connection between servers ai and bi. Any server can reach other servers directly or indirectly through the network.
A critical connection is a connection that, if removed, will make some servers unable to reach some other server.
Return all critical connections in the network in any order.
Examples
Example 1:
graph LR A[1] --- B[2] --- C[0] C --- A A ---|Critical| 3:::Critical linkStyle 3 stroke:red,stroke-width:4px
Input:
n = 4, connections = [[0,1],[1,2],[2,0],[1,3]]
Output:
[[1,3]]
Explanation: [[3,1]] is also accepted.
Example 2:
Input:
n = 2, connections = [[0,1]]
Output:
[[0,1]]
Solution
Method 1 – Tarjan's Algorithm for Bridges
Intuition
A critical connection (bridge) is an edge whose removal increases the number of connected components in the graph. Tarjan's algorithm allows us to efficiently find all such bridges using DFS and tracking discovery and low-link times.
Approach
-
Build the Graph: Construct an undirected graph from the given
connectionslist, where each server is a node and each connection is an undirected edge. -
DFS Traversal with Discovery and Low-Link Times: Use Depth-First Search (DFS) to traverse the graph. For each node, assign a
discoveryTime(the time when the node is first visited) and alowTime(the earliest discovered node reachable from the current node, including itself and its descendants). These are tracked using arrays likediscandlow(or similar names in code). -
Parent Check in DFS: When iterating over neighbors in DFS, we skip the
parentnode (the node we came from). This is crucial because if we allowed the parent to update thelowTimeof the current node, it would always be less than or equal to the current node'sdiscoveryTime, causing every edge to be incorrectly marked as a bridge. By skipping the parent, we ensure only true back-edges and child-edges are considered. (See Q1) -
Visited Array Optimization: We do not need a separate
visited[]array. IflowTime[i] == 0(ordisc[i] == -1depending on initialization), then nodeihas not been visited yet. This saves memory and simplifies the code. (See Q2) -
Timer Variable: The
timercan be a simple integer or an array with one value (e.g.,[0]in Python). Using an array allows the timer to be mutable within nested functions, but a single integer works as well in most languages. (See Q3) -
Identifying Bridges: After visiting a neighbor
vfrom nodeu, iflow[v] > disc[u], then the edge(u, v)is a critical connection (bridge). This means that there is no back-edge from the subtree rooted atvto any ancestor ofu, so removing(u, v)would disconnect the graph. (See Q5) -
Preserving Input Order (Optional): Since the graph is undirected, the order of nodes in each connection does not matter. However, if you want to preserve the order as in the input, you can use a hash set to store the original connections. When adding a bridge to the result, check if it exists in the set in the same order; if not, reverse it. This ensures the output matches the input order where possible. (See Q4)
Summary of Key Points:
- Always skip the
parentnode in DFS to avoid false bridges. - You can avoid a
visited[]array by checking the initialization value ofdiscorlow. - The
timercan be managed as an integer or a mutable array, depending on language and style. - To preserve input order, use a set or map to check the original direction of each connection.
- A bridge is found when
low[neighbor] > disc[current]after DFS.
Code
C++
#include <vector>
using namespace std;
class Solution {
public:
vector<vector<int>> criticalConnections(int n, vector<vector<int>>& connections) {
vector<vector<int>> g(n), res;
for (auto& e : connections) {
g[e[0]].push_back(e[1]);
g[e[1]].push_back(e[0]);
}
vector<int> disc(n, -1), low(n, -1);
int time = 0;
function<void(int,int)> dfs = [&](int u, int p) {
disc[u] = low[u] = time++;
for (int v : g[u]) {
if (v == p) continue;
if (disc[v] == -1) {
dfs(v, u);
low[u] = min(low[u], low[v]);
if (low[v] > disc[u]) res.push_back({u, v});
} else {
low[u] = min(low[u], disc[v]);
}
}
};
for (int i = 0; i < n; ++i) if (disc[i] == -1) dfs(i, -1);
return res;
}
};
Go
func criticalConnections(n int, connections [][]int) [][]int {
g := make([][]int, n)
for _, e := range connections {
g[e[0]] = append(g[e[0]], e[1])
g[e[1]] = append(g[e[1]], e[0])
}
disc := make([]int, n)
low := make([]int, n)
for i := range disc { disc[i] = -1 }
var res [][]int
time := 0
var dfs func(u, p int)
dfs = func(u, p int) {
disc[u], low[u] = time, time; time++
for _, v := range g[u] {
if v == p { continue }
if disc[v] == -1 {
dfs(v, u)
low[u] = min(low[u], low[v])
if low[v] > disc[u] { res = append(res, []int{u, v}) }
} else {
low[u] = min(low[u], disc[v])
}
}
}
for i := 0; i < n; i++ { if disc[i] == -1 { dfs(i, -1) } }
return res
}
func min(a, b int) int { if a < b { return a }; return b }
Java
import java.util.*;
class Solution {
int time = 0;
public List<List<Integer>> criticalConnections(int n, List<List<Integer>> connections) {
List<Integer>[] graph = new List[n];
for (int i = 0; i < n; i++) graph[i] = new ArrayList<>();
for (List<Integer> e : connections) {
graph[e.get(0)].add(e.get(1));
graph[e.get(1)].add(e.get(0));
}
List<List<Integer>> ans = new ArrayList<>();
int[] disc = new int[n], low = new int[n];
Arrays.fill(disc, -1);
for (int i = 0; i < n; i++)
if (disc[i] == -1) dfs(graph, i, -1, disc, low, ans);
return ans;
}
private void dfs(List<Integer>[] graph, int u, int parent, int[] disc, int[] low, List<List<Integer>> ans) {
disc[u] = low[u] = time++;
for (int v : graph[u]) {
if (v == parent) continue;
if (disc[v] == -1) {
dfs(graph, v, u, disc, low, ans);
low[u] = Math.min(low[u], low[v]);
if (low[v] > disc[u]) ans.add(Arrays.asList(u, v));
} else {
low[u] = Math.min(low[u], disc[v]);
}
}
}
}
Kotlin
class Solution {
var time = 0
fun criticalConnections(n: Int, connections: List<List<Int>>): List<List<Int>> {
val g = Array(n) { mutableListOf<Int>() }
for (e in connections) {
g[e[0]].add(e[1]); g[e[1]].add(e[0])
}
val disc = IntArray(n) { -1 }
val low = IntArray(n)
val ans = mutableListOf<List<Int>>()
fun dfs(u: Int, parent: Int) {
disc[u] = time; low[u] = time; time++
for (v in g[u]) {
if (v == parent) continue
if (disc[v] == -1) {
dfs(v, u)
low[u] = minOf(low[u], low[v])
if (low[v] > disc[u]) ans.add(listOf(u, v))
} else {
low[u] = minOf(low[u], disc[v])
}
}
}
for (i in 0 until n) if (disc[i] == -1) dfs(i, -1)
return ans
}
}
Python
class Solution:
def criticalConnections(self, n: int, connections: list[list[int]]) -> list[list[int]]:
g = [[] for _ in range(n)]
for u, v in connections:
g[u].append(v); g[v].append(u)
disc = [-1]*n; low = [0]*n; time = [0]; ans = []
def dfs(u, parent):
disc[u] = low[u] = time[0]; time[0] += 1
for v in g[u]:
if v == parent: continue
if disc[v] == -1:
dfs(v, u)
low[u] = min(low[u], low[v])
if low[v] > disc[u]: ans.append([u, v])
else:
low[u] = min(low[u], disc[v])
for i in range(n):
if disc[i] == -1: dfs(i, -1)
return ans
Rust
impl Solution {
pub fn critical_connections(n: i32, connections: Vec<Vec<i32>>) -> Vec<Vec<i32>> {
let n = n as usize;
let mut g = vec![vec![]; n];
for e in &connections {
g[e[0] as usize].push(e[1] as usize);
g[e[1] as usize].push(e[0] as usize);
}
let mut disc = vec![-1; n];
let mut low = vec![0; n];
let mut time = 0;
let mut ans = vec![];
fn dfs(u: usize, parent: isize, g: &Vec<Vec<usize>>, disc: &mut Vec<i32>, low: &mut Vec<i32>, time: &mut i32, ans: &mut Vec<Vec<i32>>) {
disc[u] = *time; low[u] = *time; *time += 1;
for &v in &g[u] {
if v as isize == parent { continue; }
if disc[v] == -1 {
dfs(v, u as isize, g, disc, low, time, ans);
low[u] = low[u].min(low[v]);
if low[v] > disc[u] { ans.push(vec![u as i32, v as i32]); }
} else {
low[u] = low[u].min(disc[v]);
}
}
}
for i in 0..n {
if disc[i] == -1 {
dfs(i, -1, &g, &mut disc, &mut low, &mut time, &mut ans);
}
}
ans
}
}
TypeScript
function criticalConnections(n: number, connections: number[][]): number[][] {
const g: number[][] = Array.from({length: n}, () => []);
for (const [u, v] of connections) {
g[u].push(v); g[v].push(u);
}
const disc = Array(n).fill(-1), low = Array(n).fill(0);
let time = 0;
const ans: number[][] = [];
function dfs(u: number, parent: number) {
disc[u] = low[u] = time++;
for (const v of g[u]) {
if (v === parent) continue;
if (disc[v] === -1) {
dfs(v, u);
low[u] = Math.min(low[u], low[v]);
if (low[v] > disc[u]) ans.push([u, v]);
} else {
low[u] = Math.min(low[u], disc[v]);
}
}
}
for (let i = 0; i < n; i++) if (disc[i] === -1) dfs(i, -1);
return ans;
}
Complexity
- ⏰ Time complexity:
O(N + E)whereNis the number of nodes andEis the number of edges (DFS traversal). - 🧺 Space complexity:
O(N + E)for the graph and auxiliary arrays.