Bridges and Components
One depth-first search, decorated with two numbers per vertex, answers every question about how a graph holds together. The discovery time says when a vertex was entered. The low-link says how far back up the search tree it can climb using its subtree and a single back edge. If a child cannot climb above its parent, the edge between them is a bridge; if it cannot climb past the parent, the parent is an articulation point. In a directed graph the same low-link finds strongly connected components: a vertex whose low-link is its own discovery time closes a component, and Tarjan pops it off the stack in one go. Kosaraju reaches the same components with two ordinary searches, the second on the reversed graph, taking start vertices in order of latest finish first.
Tarjan's algorithm finds the strongly connected components, the maximal sets in which every vertex can reach every other, in one DFS. Vertices are pushed on a stack as they are discovered and stay there while their component is open. The badge is low[u], the earliest open vertex u can reach; when a vertex finishes with low[u] equal to its own discovery time, it is the root of a component and everything above it on the stack is popped as one.
Check your understanding
The player pauses before each decision in this run and asks what happens next. Here are all 9, with their answers.
Edge 2 -> 0 reaches 0, which is on the stack with disc[0] = 0. low[2] = 2. What happens?
Answer: low[2] becomes 0. An edge into an open vertex may lower low[u] to that vertex's discovery time.
Edge 5 -> 3 reaches 3, which is on the stack with disc[3] = 3. low[5] = 5. What happens?
Answer: low[5] becomes 3. An edge into an open vertex may lower low[u] to that vertex's discovery time.
5 is done with low[5] = 3 and disc[5] = 5. What happens?
Answer: Return, 5 belongs to an earlier root. low below disc means the component root is further up the stack.
6 is done with low[6] = 6 and disc[6] = 6. What happens?
Answer: Pop the stack down to 6 as one component. low equal to disc means no edge below leads to an earlier open vertex.
4 is done with low[4] = 3 and disc[4] = 4. What happens?
Answer: Return, 4 belongs to an earlier root. low below disc means the component root is further up the stack.
3 is done with low[3] = 3 and disc[3] = 3. What happens?
Answer: Pop the stack down to 3 as one component. low equal to disc means no edge below leads to an earlier open vertex.
2 is done with low[2] = 0 and disc[2] = 2. What happens?
Answer: Return, 2 belongs to an earlier root. low below disc means the component root is further up the stack.
1 is done with low[1] = 0 and disc[1] = 1. What happens?
Answer: Return, 1 belongs to an earlier root. low below disc means the component root is further up the stack.
0 is done with low[0] = 0 and disc[0] = 0. What happens?
Answer: Pop the stack down to 0 as one component. low equal to disc means no edge below leads to an earlier open vertex.
How it runs, step by step
Tarjan's algorithm finds the strongly connected components, the maximal sets in which every vertex can reach every other, in one DFS. Vertices are pushed on a stack as they are discovered and stay there while their component is open. The badge is low[u], the earliest open vertex u can reach; when a vertex finishes with low[u] equal to its own discovery time, it is the root of a component and everything above it on the stack is popped as one.
Tarjan's SCC over 7 vertices.
Enter 0 at time 0, push it: stack 0. low[0] starts at 0.
Enter 0, pushed.
Enter 1 at time 1, push it: stack 0 1. low[1] starts at 1.
Enter 1, pushed.
Enter 2 at time 2, push it: stack 0 1 2. low[2] starts at 2.
Enter 2, pushed.
Edge 2 -> 0: 0 is still on the stack, so it is open and in the same component as 2. low[2] drops to disc[0] = 0.
Edge 2 to open vertex 0, low of 2 is 0.
Enter 3 at time 3, push it: stack 0 1 2 3. low[3] starts at 3.
Enter 3, pushed.
Enter 4 at time 4, push it: stack 0 1 2 3 4. low[4] starts at 4.
Enter 4, pushed.
Enter 5 at time 5, push it: stack 0 1 2 3 4 5. low[5] starts at 5.
Enter 5, pushed.
Edge 5 -> 3: 3 is still on the stack, so it is open and in the same component as 5. low[5] drops to disc[3] = 3.
Edge 5 to open vertex 3, low of 5 is 3.
5 finishes with low[5] = 3 < disc[5] = 5: it reaches an earlier open vertex, so it stays on the stack and will be popped with that vertex's component.
5 stays on the stack.
tarjan(5) returned with low[5] = 3. 5 is still open and reaches earlier, so low[4] drops to 3.
Back in 4, low 3.
Enter 6 at time 6, push it: stack 0 1 2 3 4 5 6. low[6] starts at 6.
Enter 6, pushed.
6 finishes with low[6] = disc[6] = 6: nothing below it reaches an earlier open vertex, so 6 is the root of a component. Pop the stack down to 6: component 1 is {6}.
Component 1: 6.
tarjan(6) returned with low[6] = 6. 6's component is closed already, so low[4] stays 3.
Back in 4, low 3.
4 finishes with low[4] = 3 < disc[4] = 4: it reaches an earlier open vertex, so it stays on the stack and will be popped with that vertex's component.
4 stays on the stack.
tarjan(4) returned with low[4] = 3. low[3] = 3 is already as early, so it stays.
Back in 3, low 3.
3 finishes with low[3] = disc[3] = 3: nothing below it reaches an earlier open vertex, so 3 is the root of a component. Pop the stack down to 3: component 2 is {3, 4, 5}.
Component 2: 3, 4, 5.
tarjan(3) returned with low[3] = 3. 3's component is closed already, so low[2] stays 0.
Back in 2, low 0.
2 finishes with low[2] = 0 < disc[2] = 2: it reaches an earlier open vertex, so it stays on the stack and will be popped with that vertex's component.
2 stays on the stack.
tarjan(2) returned with low[2] = 0. 2 is still open and reaches earlier, so low[1] drops to 0.
Back in 1, low 0.
1 finishes with low[1] = 0 < disc[1] = 1: it reaches an earlier open vertex, so it stays on the stack and will be popped with that vertex's component.
1 stays on the stack.
tarjan(1) returned with low[1] = 0. low[0] = 0 is already as early, so it stays.
Back in 0, low 0.
0 finishes with low[0] = disc[0] = 0: nothing below it reaches an earlier open vertex, so 0 is the root of a component. Pop the stack down to 0: component 3 is {0, 1, 2}.
Component 3: 0, 1, 2.
3 strongly connected components, coloured by membership. Each was popped the moment its root finished, so the components come out in reverse topological order of the condensed graph. One DFS, O(V + E).
3 components.
Remember
- low[u] is the earliest discovery time reachable from u's subtree plus one back edge; a tree edge u-v is a bridge when low[v] > disc[u].
- Tarjan: push on entry, pop a whole component when a vertex finishes with low[u] == disc[u]; only vertices still on the stack can lower low.
- Kosaraju: finish order from one DFS, reverse all edges, then DFS from the latest finisher; each search paints exactly one component.
Where this is used
MappingRouting engines checking a road network
One-way streets make a road graph directed, so useful connectivity means mutual reachability: a mistagged one-way can create a pocket a car can drive into and never drive out of, and a plain reachability check will not notice. OSRM runs Tarjan over the extracted graph (util::TarjanSCC) and ships an osrm-components tool that writes the small components, the ones under a thousand nodes, out as GeoJSON so mappers can see the islands. A continent-sized graph would overflow the call stack, so that implementation walks the search with its own explicit stack of frames instead of recursing.
CompilersLLVM's bottom-up call graph passes
llvm/ADT/SCCIterator.h finds the strongly connected components of the call graph with Tarjan and hands them out callees first, which is the order the inliner wants: the cost and behaviour of a function are settled before any caller is looked at. Components rather than a plain topological order are needed because mutually recursive functions cannot be put in an order at all, so they arrive as one unit and are optimised together.
Developer toolsTerraform reporting a dependency cycle
Terraform decides what to create first by topologically sorting a resource dependency graph, which only has an order if it is acyclic. Its dag package runs Tarjan (StronglyConnected in internal/dag/tarjan.go) and any component holding more than one resource is a loop it cannot order, which is what the Cycle: error lists. Printing the whole component rather than one offending edge is the point - you have to see every member of the loop to know where to break it.
NetworkingFinding single points of failure
A bridge is a link with no alternate path and an articulation point is a device whose loss splits the network, which is exactly the list you want before deciding where to spend on redundancy. The obvious method is to delete each link and re-run a reachability check, one traversal per link; the low-link DFS returns every such link and device in a single O(V + E) pass. NetworkX exposes them as bridges() and articulation_points(); the Boost Graph Library has articulation_points() and biconnected_components(), where a bridge is a biconnected component holding a single edge.
Why it works this way
Why a bridge needs low[v] > disc[u] but a cut vertex only needs >=
low[v] > disc[u] says v's subtree cannot reach u at all except through the tree edge, so removing that edge strands the subtree: it is a bridge. low[v] == disc[u] says the subtree can climb back to u but no higher, so the edge lies on a cycle through u and is not a bridge, yet deleting u itself still strands the subtree. Two triangles glued at a single shared vertex are the clean example: the shared vertex is an articulation point and not one edge in the graph is a bridge.
Skipping the parent by vertex id breaks on parallel edges
Every non-tree edge in an undirected DFS points at an ancestor, so the edge back to the parent has to be ignored - otherwise every vertex trivially reaches its parent and nothing ever looks like a bridge. But v != parent skips every edge to the parent, including a second, genuine u-v edge, so the pair gets reported as a bridge even though cutting either copy leaves the graph connected. Skip the one edge you arrived on by its edge id, not by the vertex it leads to.
Tarjan: why an already-seen vertex only counts while it is still on the stack
In a directed graph a visited neighbour v may belong to a component that has already been popped and closed. If v could still reach back to u then u and v would be in the same component, and that component could not have been closed while u is open - so a popped v has no path back to u, and folding its disc into low[u] would fuse two separate components into one. The stack test is what tells a back edge inside the current component apart from a cross edge into a finished one, and undirected DFS needs no such test because it has no cross edges.
Kosaraju: why the second pass paints exactly one component
Collapse each component to a single node and what remains is a DAG. The vertex that finishes last in the first pass always sits in a source of that DAG, a component nothing points into; reversing every edge turns that source into a sink, so a search started there has nowhere to escape to and stops at the component boundary. Strike out what it painted and the next unvisited latest finisher is a source of whatever is left, so the rule holds all the way down.
Read more
- Biconnected componentWikipedia
- Finding bridges in a graph in O(N+M)cp-algorithms
- Finding articulation points in a graph in O(N+M)cp-algorithms
- Tarjan's strongly connected components algorithmWikipedia
- Strongly Connected Components and Condensation Graphcp-algorithms
Next up
- DFS Topological SortReverse postorder of a DFS is a topological order, and a back edge means there is none.
- Kahn's AlgorithmRepeatedly place a vertex with in-degree zero and lower the counts of everything it points to.
- Union-Find Operationsfind climbs to the root then flattens the path; union hangs the smaller root under the larger.