Blind Plus 100 Visualized

#92 · Advanced Graphs

Critical Connections in a Network

There are n servers numbered 0 … n − 1 connected by undirected connections forming a connected network. A critical connection (bridge) is one whose removal makes some servers unable to reach others. Return all critical connections in any order.

HardLeetCode
  • Current
  • On path / queued
  • Done
  • Cycle / blocked
step 1 / 1

Pointers

Variables

Hash map / table

Stack / queue

Foundations & data-structure selection

Adjacency lists and the DFS tree

A depth-first search from node 0 classifies every edge as a tree edge (to a new node) or a back edge (to an already-visited ancestor). Every non-tree edge closes exactly one cycle; tree edges not covered by any cycle are bridges.

Memory layout

Arrays indexed by node: discovery time, low-link, parent; adjacency lists of size 2E.

OperationCost
DFSO(V + E)

Discovery time and low-link values

disc[v] is when DFS first visited v. low[v] is the smallest disc reachable from v's DFS subtree using tree edges and at most one back edge. A tree edge parent→child is a bridge iff low[child] > disc[parent].

Memory layout

Two integer arrays of size n and a timer.

OperationCost
updateO(1) per edge

Flat edge arrays with twin slots

For iterative DFS and parallel-edge safety, store each undirected connection as two slots 2k and 2k + 1 (e ^ 1 is the twin). Skipping the twin of the entry edge (not "the parent node") treats parallel edges correctly.

Memory layout

Arrays head[n], nextEdge[2E], to[2E]; no per-node lists.

OperationCost
iterate neighboursO(deg)

Why it fits

  • An edge is a bridge exactly when no cycle contains it, and DFS exposes cycles as back edges, so one traversal can answer for all edges.
  • Low-link turns "does any back edge cover this tree edge?" into one integer comparison, avoiding any explicit cycle walking.
  • The method is the same used for articulation points and strongly connected components (Tarjan), so it is a core graph technique.

Why alternatives degrade

Union-find:
It detects that an edge closes a cycle while adding edges, but cannot tell which earlier edges became non-bridges.
BFS / shortest paths:
Distances do not reveal bridges; you would need to remove every edge and re-check connectivity.
Removing every edge and re-running a search:
Correct but O(E · (V + E)); the low-link method does all edges in one pass.

Four approaches, from naive to production

1 · beginner

Remove each edge, test connectivity

time O(E · (V + E))space O(V + E)

For every connection, build the graph without it and search from node 0. If fewer than n nodes are reached the edge is critical.

vector<vector<int>> criticalConnections(int n, vector<vector<int>>& connections) {
    vector<vector<int>> result;
    auto connected = [&](int skip) -> bool {
        vector<vector<int>> adj(n);
        for (int i = 0; i < (int)connections.size(); i++) {
            if (i == skip) continue;
            adj[connections[i][0]].push_back(connections[i][1]);
            adj[connections[i][1]].push_back(connections[i][0]);
        }
        vector<bool> seen(n, false);
        vector<int> stack{0};
        seen[0] = true;
        int count = 1;
        while (!stack.empty()) {
            int node = stack.back(); stack.pop_back();
            for (int next : adj[node]) {
                if (!seen[next]) { seen[next] = true; count++; stack.push_back(next); }
            }
        }
        return count == n;
    };
    for (int i = 0; i < (int)connections.size(); i++) {
        if (!connected(i)) result.push_back(connections[i]);
    }
    return result;
}

Flaws & bottlenecks

  • Rebuilds the adjacency lists for every edge.
  • Quadratic: 10⁵ edges means 10¹⁰ steps.
  • Does no shared work between the E searches.

Interviewer cross-questions

Why start the search at node 0?

Say: The network is connected, so after removing a bridge node 0 reaches only its own side; the count shows how many nodes are cut off.

Is this correct for graphs that are not connected?

Say: Not as written: it assumes connectivity. You would compare the number of components before and after instead.

How can all edges be checked in one pass?

Say: Use DFS discovery times and low-link values (Tarjan's bridge algorithm).

2 · intermediate

DFS tree and cycle marking

time O(V · E) worst casespace O(V + E)

Run one DFS and record parent pointers and depths. For each back edge (to a shallower node) mark the edge and all tree edges up to that ancestor as lying on a cycle. Unmarked edges are bridges.

vector<vector<int>> criticalConnections(int n, vector<vector<int>>& connections) {
    vector<vector<pair<int, int>>> adj(n);
    for (int i = 0; i < (int)connections.size(); i++) {
        adj[connections[i][0]].push_back({connections[i][1], i});
        adj[connections[i][1]].push_back({connections[i][0], i});
    }
    vector<int> depth(n, -1);
    vector<int> parent(n, -1);
    vector<int> parentEdge(n, -1);
    vector<bool> onCycle(connections.size(), false);
    function<void(int)> dfs = [&](int node) {
        for (auto [next, id] : adj[node]) {
            if (id == parentEdge[node]) continue;
            if (depth[next] == -1) {
                depth[next] = depth[node] + 1;
                parent[next] = node;
                parentEdge[next] = id;
                dfs(next);
            } else if (depth[next] < depth[node]) {
                onCycle[id] = true;
                for (int v = node; v != next; v = parent[v]) onCycle[parentEdge[v]] = true;
            }
        }
    };
    depth[0] = 0;
    dfs(0);
    vector<vector<int>> result;
    for (int id = 0; id < (int)connections.size(); id++) if (!onCycle[id]) result.push_back(connections[id]);
    return result;
}

Flaws & bottlenecks

  • Each back edge may walk up O(V) tree edges.
  • Re-marks tree edges that were already on a cycle.
  • Needs parent and parent-edge arrays on top of depth.

Interviewer cross-questions

Why identify edges by id and not by node?

Say: To tell the entry edge from a parallel edge to the same parent, which would be a genuine cycle.

How could the walk be avoided?

Say: Instead of marking the path, propagate the minimum reachable depth upward: that is exactly low-link.

Can a tree edge be a bridge and have a back edge below it?

Say: Only if no back edge from its subtree reaches the parent or above; otherwise it is on a cycle.

3 · advanced

Tarjan's bridge algorithm

time O(V + E)space O(V + E)

One DFS computing disc[v] and low[v]. After returning from a child, low[node] = min(low[node], low[child]); the tree edge is a bridge if low[child] > disc[node]. Back edges update low[node] with disc[next].

vector<vector<int>> criticalConnections(int n, vector<vector<int>>& connections) {
    vector<vector<int>> adj(n);
    for (auto& c : connections) { adj[c[0]].push_back(c[1]); adj[c[1]].push_back(c[0]); }
    vector<int> disc(n, -1);
    vector<int> low(n, 0);
    vector<vector<int>> bridges;
    int timer = 0;
    function<void(int, int)> dfs = [&](int node, int parent) {
        disc[node] = low[node] = timer++;
        for (int next : adj[node]) {
            if (next == parent) continue;
            if (disc[next] == -1) {
                dfs(next, node);
                low[node] = min(low[node], low[next]);
                if (low[next] > disc[node]) bridges.push_back({node, next});
            } else {
                low[node] = min(low[node], disc[next]);
            }
        }
    };
    dfs(0, -1);
    return bridges;
}

Flaws & bottlenecks

  • Recursion depth can reach V, overflowing the call stack for long chains.
  • Skipping "next === parent" is wrong with parallel edges.
  • The low-link idea takes practice to explain precisely.

Interviewer cross-questions

Why ">" and not ">="?

Say: low[child] = disc[parent] means the child can reach the parent itself via a back edge, so the tree edge is on a cycle. A bridge needs the child to be unable to reach the parent at all.

Why skip the parent?

Say: The edge back to the parent is the tree edge we came through; counting it as a back edge would make every edge look like part of a cycle.

How does this differ for articulation points?

Say: There the condition is low[child] ≥ disc[parent], because a cut node only needs the child subtree to be unable to bypass it.

4 · expert

Iterative Tarjan with edge slots

time O(V + E)space O(V + E)

Store the graph in flat arrays with twin edge slots, keep an explicit stack of nodes and entry edges, and replace the recursion with a cursor per node. Skip only the twin of the entry edge so parallel edges are handled. Loop over roots for disconnected input.

vector<vector<int>> criticalConnections(int n, vector<vector<int>>& connections) {
    vector<int> head(n, -1);
    vector<int> nextEdge;
    vector<int> to;
    auto addEdge = [&](int a, int b) { to.push_back(b); nextEdge.push_back(head[a]); head[a] = (int)to.size() - 1; };
    for (auto& c : connections) { addEdge(c[0], c[1]); addEdge(c[1], c[0]); }
    vector<int> disc(n, -1);
    vector<int> low(n, 0);
    vector<int> cursor(head);
    vector<int> stackNode;
    vector<int> stackEdge;
    vector<vector<int>> bridges;
    int timer = 0;
    for (int root = 0; root < n; root++) {
        if (disc[root] != -1) continue;
        disc[root] = low[root] = timer++;
        stackNode.push_back(root);
        stackEdge.push_back(-1);
        while (!stackNode.empty()) {
            int node = stackNode.back();
            int e = cursor[node];
            if (e != -1) {
                cursor[node] = nextEdge[e];
                if ((e ^ 1) == stackEdge.back()) continue;
                int next = to[e];
                if (disc[next] == -1) {
                    disc[next] = low[next] = timer++;
                    stackNode.push_back(next);
                    stackEdge.push_back(e);
                } else {
                    low[node] = min(low[node], disc[next]);
                }
            } else {
                stackNode.pop_back();
                stackEdge.pop_back();
                if (!stackNode.empty()) {
                    int parent = stackNode.back();
                    low[parent] = min(low[parent], low[node]);
                    if (low[node] > disc[parent]) bridges.push_back({parent, node});
                }
            }
        }
    }
    return bridges;
}

Flaws & bottlenecks

  • Much more code than the recursive version.
  • Order of neighbours is reversed by head insertion (irrelevant for the answer).
  • Bugs are easy: forgetting the twin trick, or updating low after popping.

Interviewer cross-questions

Why is this needed in production?

Say: A network can be a 10⁵-node chain; recursion that deep overflows the stack in most runtimes. An explicit stack has no such limit.

Why skip by edge id and not by parent node?

Say: Two parallel connections between the same servers form a cycle, so neither is critical; skipping only the entry edge (via e ^ 1) keeps the second one as a back edge.

What about disconnected input?

Say: The outer loop restarts from every unvisited root; bridges are still found per component (the problem guarantees connectivity, but robust code does not assume it).

Synchronized code walkthrough

Showing: Remove each edge, test connectivity — shares the player above; click any step to jump.

vector<vector<int>> criticalConnections(int n, vector<vector<int>>& connections) {
    vector<vector<int>> result;
    auto connected = [&](int skip) -> bool {
        vector<vector<int>> adj(n);
        for (int i = 0; i < (int)connections.size(); i++) {
            if (i == skip) continue;
            adj[connections[i][0]].push_back(connections[i][1]);
            adj[connections[i][1]].push_back(connections[i][0]);
        }
        vector<bool> seen(n, false);
        vector<int> stack{0};
        seen[0] = true;
        int count = 1;
        while (!stack.empty()) {
            int node = stack.back(); stack.pop_back();
            for (int next : adj[node]) {
                if (!seen[next]) { seen[next] = true; count++; stack.push_back(next); }
            }
        }
        return count == n;
    };
    for (int i = 0; i < (int)connections.size(); i++) {
        if (!connected(i)) result.push_back(connections[i]);
    }
    return result;
}