Biconnected Components is a classic graph problem that pushes the articulation-point DFS one step further. Instead of just naming the cut vertices, we partition the graph's edges into maximal chunks that have no internal articulation point. The trick is to carry an edge stack alongside the usual disc/low values.
Problem. Given an undirected graph, split its edges into biconnected components: maximal sets of edges such that removing any single vertex never disconnects the set. Two components can share a vertex (an articulation point), but never an edge.
Example: edges (0,1) (1,2) (2,0) (2,3) (3,4) (4,2). The answer is two components — the triangle
{(0,1),(1,2),(2,0)} and the triangle {(2,3),(3,4),(4,2)} — meeting at the articulation vertex 2.
The slow way first
You could test, for every edge, which other edges lie on a common simple cycle with it, then group them. Building those relationships pair by pair is painfully slow — roughly O(E²) or worse, and the cycle reasoning is fiddly to get right.
The question to ask: as DFS walks the graph, can I recognize the boundary of one component the instant I cross it? The boundary is exactly an articulation point, and DFS already detects those with disc/low.
The idea: one DFS with an edge stack
Run a single DFS. Give every vertex a discovery time disc[u] and a low[u] — the earliest discovery time reachable from u's subtree using one back edge. Push every edge onto a stack the first time you traverse it. When you return from a child v and find low[v] >= disc[u], vertex u is an articulation point: pop edges off the stack down to and including (u, v) — those popped edges are exactly one biconnected component.
The key insight: the stack holds edges in the order we discovered them, so the edges of one component are always a contiguous block on top of the stack by the time we close it.
Walk through it
Step through the animation. DFS dives 0 → 1 → 2, pushing each tree edge. The back edge 2 → 0 pulls low[2] down to 0, fusing the first triangle. DFS then explores 2 → 3 → 4, and the back edge 4 → 2 fuses the second triangle. Returning to 2, the child 3 satisfies low[3] >= disc[2], so we pop the second component. Finally the root 0 closes the first component.
Pseudocode
dfs(u, parent):
disc[u] = low[u] = timer++ # discovery time
for each neighbor v of u:
if v is unvisited:
push edge (u, v) on the stack
dfs(v, u)
low[u] = min(low[u], low[v])
if low[v] >= disc[u]: # u is an articulation point
pop edges down to (u, v) into one component
else if v != parent and disc[v] < disc[u]:
push back edge (u, v); low[u] = min(low[u], disc[v])The Python solution
def bcc(u, parent):
nonlocal timer
disc[u] = low[u] = timer; timer += 1
for v in graph[u]:
if v not in disc:
stack.append((u, v))
bcc(v, u)
low[u] = min(low[u], low[v])
if low[v] >= disc[u]:
comp = []
while stack[-1] != (u, v):
comp.append(stack.pop())
comp.append(stack.pop())
components.append(comp)
elif v != parent and disc[v] < disc[u]:
stack.append((u, v))
low[u] = min(low[u], disc[v])disc[u] = low[u] = timerstamps the discovery order;low[u]will shrink as back edges are found.- For an unvisited neighbor
vwe push the tree edge, recurse, then pulllow[u]down bylow[v]. - Line 9 is the heart:
low[v] >= disc[u]means nothing inv's subtree reaches aboveu, souis an articulation point and a component ends here. - The
whileloop pops edges down to and including(u, v)— that contiguous block is one biconnected component. - The
elifhandles a back edge to an already-visited ancestor (not the parent): push it and lowerlow[u]using the ancestor'sdisc.
Complexity
| Case | Time | Notes |
|---|---|---|
| Pairwise cycle grouping | O(E²) (moderate) | compare edges against edges |
| DFS + edge stack (this solution) | O(V + E) (moderate) | each vertex and edge visited once |
O(V + E) (moderate)The single DFS touches every vertex and every edge a constant number of times, and the stack holds at most every edge once — so both time and space are linear in the size of the graph.
When this pattern shows up
Whenever a problem talks about cut vertices, cut edges (bridges), 2-edge/2-vertex connectivity, or splitting a graph at its weak points, reach for the disc/low DFS. Bridges, articulation points, and biconnected components are the same DFS with a slightly different test and bookkeeping.
Mind the parent rule and the disc[v] < disc[u] guard on the back-edge branch. Without them you push
the same undirected edge from both endpoints and corrupt the stack, producing duplicate or split
components.
Practice
When DFS returns from child 3 to vertex 2, we have low[3] = 2 and disc[2] = 2. What does the test low[3] >= disc[2] tell us, and what happens next?
1. What does low[u] represent?
2. When do we pop edges into a biconnected component?
3. Why push every edge onto a stack as DFS traverses it?
4. What is the time complexity of this algorithm?