There are n
servers numbered from 0
to n - 1
connected by undirected server-to-server connections
forming a network where connections[i] = [a_i, b_i]
represents a connection between servers a_i
and b_i
. 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 <= 10^5
n - 1 <= connections.length <= 10^5
0 <= a_i, b_i <= n - 1
a_i != b_i
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<vector<int>> criticalConnections(int n, vector<vector<int>>& connections) {
vector<vector<int>> adj(n);
for (const auto& connection : connections) {
adj[connection[0]].push_back(connection[1]);
adj[connection[1]].push_back(connection[0]);
}
vector<int> disc(n, -1); // Discovery time of each node
vector<int> low(n, -1); // Lowest reachable ancestor from each node
vector<vector<int>> result; // Store critical connections
int time = 0;
function<void(int, int)> dfs = [&](int u, int parent) {
disc[u] = low[u] = ++time;
for (int v : adj[u]) {
if (v == parent) continue; // Ignore the parent in DFS
if (disc[v] == -1) { // If v is not visited
dfs(v, u);
low[u] = min(low[u], low[v]);
// If the lowest reachable ancestor of v is greater than the discovery time of u,
// then (u, v) is a critical connection
if (low[v] > disc[u]) {
result.push_back({u, v});
}
} else { // If v is already visited
low[u] = min(low[u], disc[v]);
}
}
};
dfs(0, -1); // Start DFS from node 0 with no parent
return result;
}
};
/*
Naive Approach:
For each edge (u, v), remove it from the graph.
Check if the graph is still connected. This can be done using BFS or DFS.
If the graph is disconnected after removing the edge, then (u, v) is a critical connection.
Time Complexity: O(E * (V + E)), where E is the number of edges and V is the number of vertices.
Space Complexity: O(V + E)
Optimal Approach:
Use Tarjan's Algorithm to find critical connections (bridges) in the graph.
Tarjan's algorithm is based on the concept of discovery time and lowest reachable ancestor.
Discovery time (disc[u]) is the time when a node u is first visited during DFS.
Lowest reachable ancestor (low[u]) is the lowest discovery time of any node reachable from u.
An edge (u, v) is a critical connection if low[v] > disc[u].
Time Complexity: O(V + E)
Space Complexity: O(V)
*/
int main() {
Solution sol;
int n = 4;
vector<vector<int>> connections = {{0,1},{1,2},{2,0},{1,3}};
vector<vector<int>> result = sol.criticalConnections(n, connections);
cout << "Critical Connections:" << endl;
for (const auto& connection : result) {
cout << "[" << connection[0] << ", " << connection[1] << "]" << endl;
}
return 0;
}
The problem asks us to find the critical connections in a network of servers. A critical connection is one that, if removed, would disconnect the network. We can solve this problem efficiently using Tarjan's algorithm.
This approach is simple to understand but inefficient because for each edge, we have to perform a graph traversal.
Tarjan's algorithm leverages the concept of discovery time and lowest reachable ancestor to identify critical connections (bridges) in a graph.
Initialization:
disc[u]
: Discovery time of node u
.low[u]
: Lowest reachable ancestor from node u
.time
: A counter to track discovery time.DFS Traversal:
u
, update disc[u]
and low[u]
to the current time
.v
of u
:
v
is the parent of u
, ignore it.v
is not visited, recursively call DFS on v
.
low[u]
to the minimum of low[u]
and low[v]
.low[v] > disc[u]
, then the edge (u, v) is a critical connection.v
is already visited, update low[u]
to the minimum of low[u]
and disc[v]
.Critical Connection Identification:
(u, v)
is a critical connection if low[v] > disc[u]
. This condition implies that there is no back-edge from the subtree rooted at v
to any ancestor of u
. Hence, removing the edge (u, v)
will disconnect the subtree rooted at v
from the rest of the graph.Consider the example:
n = 4, connections = [[0,1],[1,2],[2,0],[1,3]]
Adjacency List:
0: [1, 2]
1: [0, 2, 3]
2: [1, 0]
3: [1]
DFS Traversal:
disc[0] = 1, low[0] = 1
disc[1] = 2, low[1] = 2
disc[2] = 3, low[2] = 3
low[2] = min(3, 1) = 1
low[1] = min(2, 1) = 1
disc[3] = 4, low[3] = 4
low[1] = min(1, 4) = 1
Critical Connection:
low[3] = 4 > disc[1] = 2
.The optimal approach uses Tarjan's Algorithm which involves a single Depth-First Search (DFS) traversal of the graph. Each node and each edge are visited once.
adj
(adjacency list): O(E) in the worst case (dense graph).disc
array: O(V).low
array: O(V).result
vector in worst case will contain all the edges making it O(E). However since E <= 10^5 and V <= 10^5, we can consider this to be O(V).Empty Graph:
Single Node Graph:
Disconnected Graph:
Graph with Self-Loops or Parallel Edges:
Large Graph:
Already a tree:
This comprehensive explanation covers the algorithm, its complexity, and various edge cases, ensuring a thorough understanding of the problem and its solution.