cconnected.
Understand the pattern. Find your own way.Find your starting point
Your learning feed/Idea 81 of 218
Ideas this builds on
Graphs81 / 218

Critical Connections in a Network

Picture

A triangle of A, B, and C has backup routes. D has only one road to B. A DFS branch can report whether it found a route back toward earlier ancestors without retracing the edge used to enter it.

Predict

When is the tree edge from parent u to child v a bridge?

Walk through the small example

When low[v] > discovered[u]. The child’s whole branch lacks an alternate route reaching u or an earlier ancestor, so removing u–v separates it.

Mental model and complete Python solution

discovered[u] records DFS arrival order. low[u] starts there and is lowered by back edges to earlier nodes and completed child summaries. The test is strict: reaching the parent by another edge is already a backup. The iterative stack below stores a neighbor iterator for each paused call, exactly like recursive DFS, so a deep chain cannot overflow Python’s call stack. Edge IDs distinguish parallel edges from the single edge used to enter a child.

What the function promises: An undirected graph uses labels 0..n−1. Return bridge endpoint pairs in sorted order. The helper also handles disconnected graphs, self-loops, and parallel edges; the linked problem guarantees a connected simple graph.

def bridges(n, edges):
    graph = [[] for _ in range(n)]
    for edge_id, (u, v) in enumerate(edges):
        graph[u].append((v, edge_id))
        graph[v].append((u, edge_id))
    discovered, low = [-1] * n, [0] * n
    timer, answer = 0, []
    for start in range(n):
        if discovered[start] != -1:
            continue
        discovered[start] = low[start] = timer
        timer += 1
        stack = [(start, -1, -1, iter(graph[start]))]
        while stack:
            u, parent, incoming_edge, neighbors = stack[-1]
            try:
                v, edge_id = next(neighbors)
            except StopIteration:
                stack.pop()
                if parent != -1:
                    if low[u] > discovered[parent]:
                        answer.append(tuple(sorted((parent, u))))
                    low[parent] = min(low[parent], low[u])
                continue
            if edge_id == incoming_edge:
                continue
            if discovered[v] == -1:
                discovered[v] = low[v] = timer
                timer += 1
                stack.append((v, u, edge_id, iter(graph[v])))
            else:
                low[u] = min(low[u], discovered[v])
    return sorted(answer)

assert bridges(4, [(0,1), (1,2), (2,0), (1,3)]) == [(1,3)]
assert bridges(2, [(0,1), (0,1)]) == []
assert bridges(4, [(0,1), (2,3)]) == [(0,1), (2,3)]

Work and storage: DFS and graph construction use O(V+E) time and space. Sorting the B returned bridges adds O(B log(B+1)) time; remove output sorting if arbitrary order is acceptable.

When to recognize it: A child branch needs a route back to its parent or an earlier ancestor without using the parent edge.

Check before applying it: Skip the parent edge, not every edge to the parent vertex. Parallel edges can provide a backup route.

Change one rule

We remove a vertex instead of an edge. Is the bridge test unchanged?

See what changes

No. This asks for articulation points. A non-root DFS vertex u is an articulation point if some child v has low[v] ≥ discovered[u]; removing u also removes the return endpoint. A DFS root needs a separate test: more than one DFS child. The equality and root cases are meaningful differences.

Connect

dfs visited: Finish a child before sending its summary upward.

Redundant Connection: An edge that closes a cycle is different from an edge every connection depends on.

Problem reference: Critical Connections in a Network (Hard). The walkthrough and Python solution above are original course material.