Critical Connections in a Network
HardPrompt
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.
Example 1:
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]]
Constraints:
2 <= n <= 105n - 1 <= connections.length <= 1050 <= ai, bi <= n - 1ai != bi- There are no repeated connections.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach iterates through every connection in the network. For each connection, it is temporarily removed, and then the algorithm checks if the network remains connected. If the network becomes disconnected, the removed connection is identified as a critical connection.
Algorithm
- Initialize an empty list
critical_connectionsto store the results. - For each connection
(u, v)in the input listconnections:- Create a new graph by building an adjacency list from all connections except
(u, v). - Perform a graph traversal (e.g., DFS) starting from node 0 to count the number of reachable nodes.
- If the number of reachable nodes is less than
n, the graph is disconnected. Add the connection(u, v)tocritical_connections.
- Create a new graph by building an adjacency list from all connections except
- Return
critical_connections.
Walkthrough
The core idea is to simulate the removal of each edge one by one and test for graph connectivity.
First, we build an adjacency list representation of the graph from the input connections. We then loop through each edge (u, v) from the original connections list. Inside the loop, we create a temporary graph that excludes the current edge (u, v). We then perform a graph traversal, such as Depth-First Search (DFS) or Breadth-First Search (BFS), starting from an arbitrary node (e.g., node 0). We keep a count of the nodes visited during the traversal. After the traversal is complete, if the count of visited nodes is less than the total number of nodes n, it implies that the graph is disconnected. In this case, the edge (u, v) is a critical connection, and we add it to our result list. This process is repeated for all edges.
class Solution { public List<List<Integer>> criticalConnections(int n, List<List<Integer>> connections) { List<List<Integer>> critical = new ArrayList<>(); for (int i = 0; i < connections.size(); i++) { // Build adjacency list for the graph without the i-th connection List<List<Integer>> adj = new ArrayList<>(); for (int j = 0; j < n; j++) { adj.add(new ArrayList<>()); } for (int j = 0; j < connections.size(); j++) { if (i == j) continue; List<Integer> edge = connections.get(j); adj.get(edge.get(0)).add(edge.get(1)); adj.get(edge.get(1)).add(edge.get(0)); } // Check for connectivity if (!isConnected(n, adj)) { critical.add(connections.get(i)); } } return critical; } private boolean isConnected(int n, List<List<Integer>> adj) { boolean[] visited = new boolean[n]; int[] count = {0}; // Start DFS from an arbitrary node (e.g., 0) dfs(0, adj, visited, count); return count[0] == n; } private void dfs(int u, List<List<Integer>> adj, boolean[] visited, int[] count) { visited[u] = true; count[0]++; for (int v : adj.get(u)) { if (!visited[v]) { dfs(v, adj, visited, count); } } }}Complexity
Time
O(E * (V + E)), where V is the number of servers (`n`) and E is the number of connections. For each of the E edges, we build a new adjacency list (which takes O(V+E)) and then perform a DFS/BFS (which also takes O(V+E)).
Space
O(V + E). In each iteration of the main loop, we build an adjacency list which requires O(V+E) space. The recursion stack for DFS can also go up to O(V).
Trade-offs
Pros
Simple to understand and implement.
Directly follows the definition of a critical connection, making the logic straightforward.
Cons
Highly inefficient due to its O(E * (V+E)) time complexity.
Rebuilds the graph and performs a full traversal for each edge, leading to a lot of redundant computation.
Will result in a "Time Limit Exceeded" error on large inputs as specified in the problem constraints.
This is an efficient, single-pass algorithm that uses Depth-First Search (DFS) to find all bridges (critical connections) in a graph. It works by keeping track of the discovery time of each node and the lowest discovery time reachable from it, allowing it to identify bridges in linear time.
Algorithm
- Build an adjacency list representation of the graph.
- Initialize
discandlowarrays of sizenwith a sentinel value (e.g., -1). - Initialize a global
timecounter to 0. - Initialize an empty list
bridgesto store the results. - Define a DFS function
dfs(u, parent). - Inside
dfs(u, parent):- Set
disc[u] = low[u] = time++. - For each neighbor
vofu:- If
visparent, continue. - If
vis visited (disc[v] != -1), updatelow[u] = min(low[u], disc[v]). - If
vis not visited, recursively calldfs(v, u). After the call returns, updatelow[u] = min(low[u], low[v]). - Check if
low[v] > disc[u]. If true, the edge(u, v)is a bridge. Add it to thebridgeslist.
- If
- Set
- Start the traversal by calling
dfs(0, -1)(since the graph is connected). - Return the
bridgeslist.
Walkthrough
The algorithm is based on a single DFS traversal of the graph. During the traversal, we maintain two key pieces of information for each node u:
disc[u]: The discovery time, which is the step/time whenuis first visited.low[u]: The "low-link" value, which is the lowest discovery time reachable fromu(including itself) by traversing zero or more tree edges in the DFS tree and at most one back-edge.
We initialize disc and low arrays with a sentinel value (like -1). We also need an adjacency list for the graph and a global timer. We start the DFS from an arbitrary node. Let's say our DFS function is dfs(u, parent).
When we visit a node u:
- We set
disc[u]andlow[u]to the currenttime, and then incrementtime. - We iterate through
u's neighborsv. - If
vis theparentthat we came from, we skip it. - If
vhas been visited (i.e.,disc[v]is not -1), it means we've found a back-edge. A back-edge fromuto an ancestorvprovides an alternative path. We updatelow[u] = min(low[u], disc[v]). - If
vhas not been visited, it's a new node in our DFS tree. We make a recursive calldfs(v, u). - After the recursive call for
vreturns, thelow[v]value has been fully computed for the subtree rooted atv. We can use this to updateu's low-link value:low[u] = min(low[u], low[v]). - Now, we check the bridge condition. The edge
(u, v)is a bridge if the subtree rooted atvhas no back-edge connection touor any of its ancestors. This is true iflow[v] > disc[u]. If this condition holds, we've found a critical connection and add(u, v)to our result list.
import java.util.*; class Solution { private int time = 0; private List<List<Integer>> adj; private int[] low; private int[] disc; private List<List<Integer>> bridges; public List<List<Integer>> criticalConnections(int n, List<List<Integer>> connections) { // Build adjacency list adj = new ArrayList<>(); for (int i = 0; i < n; i++) { adj.add(new ArrayList<>()); } for (List<Integer> conn : connections) { int u = conn.get(0); int v = conn.get(1); adj.get(u).add(v); adj.get(v).add(u); } // Initialize arrays low = new int[n]; disc = new int[n]; Arrays.fill(disc, -1); // -1 indicates unvisited bridges = new ArrayList<>(); // Start DFS from node 0 (since the graph is connected) // The parent of the starting node can be a sentinel value like -1 dfs(0, -1); return bridges; } private void dfs(int u, int parent) { disc[u] = low[u] = time++; for (int v : adj.get(u)) { if (v == parent) { continue; // Skip the edge back to the parent } if (disc[v] != -1) { // v is a visited node (a back-edge) low[u] = Math.min(low[u], disc[v]); } else { // v is an unvisited node (a tree-edge) dfs(v, u); // After visiting v's subtree, update low[u] low[u] = Math.min(low[u], low[v]); // Check for bridge condition if (low[v] > disc[u]) { bridges.add(Arrays.asList(u, v)); } } } }}Complexity
Time
O(V + E). The algorithm is based on a single DFS traversal. Every vertex and every edge is visited exactly once.
Space
O(V + E). This is for storing the adjacency list (O(V+E)), the `disc` and `low` arrays (O(V)), and the recursion stack for DFS (O(V) in the worst case).
Trade-offs
Pros
Highly efficient, with linear time complexity.
Solves the problem in a single pass over the graph.
It's the standard and optimal algorithm for finding bridges in a graph.
Cons
More complex to understand and implement compared to the brute-force approach.
Requires a good understanding of Depth-First Search and its properties, specifically back-edges and DFS trees.
Solutions
Solution
class Solution {private int now;private List<Integer>[] g;private List<List<Integer>> ans = new ArrayList<>();private int[] dfn;private int[] low;public List<List<Integer>> criticalConnections(int n, List<List<Integer>> connections) { g = new List[n]; Arrays.setAll(g, k->new ArrayList<>()); dfn = new int[n]; low = new int[n]; for (var e : connections) { int a = e.get(0), b = e.get(1); g[a].add(b); g[b].add(a); } tarjan(0, -1); return ans; }private void tarjan(int a, int fa) { dfn[a] = low[a] = ++now; for (int b : g[a]) { if (b == fa) { continue; } if (dfn[b] == 0) { tarjan(b, a); low[a] = Math.min(low[a], low[b]); if (low[b] > dfn[a]) { ans.add(List.of(a, b)); } } else { low[a] = Math.min(low[a], dfn[b]); } } }}class Solution {public: vector<vector<int>> criticalConnections(int n, vector<vector<int>> &connections) { int now = 0; vector<int> dfn(n); vector<int> low(n); vector<int> g[n]; for (auto &e : connections) { int a = e[0], b = e[1]; g[a].push_back(b); g[b].push_back(a); } vector<vector<int>> ans; function<void(int, int)> tarjan = [&](int a, int fa) -> void { dfn[a] = low[a] = ++now; for (int b : g[a]) { if (b == fa) { continue; } if (!dfn[b]) { tarjan(b, a); low[a] = min(low[a], low[b]); if (low[b] > dfn[a]) { ans.push_back({a, b}); } } else { low[a] = min(low[a], dfn[b]); } } }; tarjan(0, -1); return ans; }};class Solution: def criticalConnections(self, n: int, connections: List[List[int]]) -> List[List[int]]: def tarjan(a: int, fa: int): nonlocal now now += 1 dfn[a] = low[a] = now for b in g[a]: if b == fa: continue if not dfn[b]: tarjan(b, a) low[a] = min(low[a], low[b]) if low[b] > dfn[a]: ans . append([a, b]) else: low[a] = min(low[a], dfn[b]) g = [[] for _ in range(n)] for a, b in connections: g[a]. append(b) g[b]. append(a) dfn = [0] * n low = [0] * n now = 0 ans = [] tarjan(0, - 1) return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.