AlgoScope

Bridges and Components

algorithmadvancedTime O(V + E)Space O(V)

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.

0123456

Kosaraju's algorithm finds strongly connected components with two plain searches. Pass 1 runs DFS on the graph and records the order in which vertices finish. Then every edge is reversed. Pass 2 starts a DFS from the unassigned vertex that finished last; whatever it reaches in the reversed graph is exactly one component, because a vertex that finished later than everything it can reach is in a source component of the condensation.

Check your understanding

The player pauses before each decision in this run and asks what happens next. Here are all 3, with their answers.

  1. Which unassigned vertex does pass 2 start from next?

    • 0, finish 7
    • 1, finish 6
    • 2, finish 5

    Answer: 0, finish 7. Always the unassigned vertex that finished last in pass 1.

  2. Which unassigned vertex does pass 2 start from next?

    • 3, finish 4
    • 4, finish 3
    • 6, finish 2

    Answer: 3, finish 4. Always the unassigned vertex that finished last in pass 1.

  3. Which unassigned vertex does pass 2 start from next?

    • 6, finish 2
    • 0, the next index

    Answer: 6, finish 2. Always the unassigned vertex that finished last in pass 1.

How it runs, step by step

  1. Kosaraju's algorithm finds strongly connected components with two plain searches. Pass 1 runs DFS on the graph and records the order in which vertices finish. Then every edge is reversed. Pass 2 starts a DFS from the unassigned vertex that finished last; whatever it reaches in the reversed graph is exactly one component, because a vertex that finished later than everything it can reach is in a source component of the condensation.

    Kosaraju's SCC over 7 vertices.

  2. Pass 1, enter 0. Unseen neighbours: 1.

    Pass 1, enter 0.

  3. Pass 1, enter 1. Unseen neighbours: 2.

    Pass 1, enter 1.

  4. Pass 1, enter 2. Unseen neighbours: 3.

    Pass 1, enter 2.

  5. Pass 1, enter 3. Unseen neighbours: 4.

    Pass 1, enter 3.

  6. Pass 1, enter 4. Unseen neighbours: 5, 6.

    Pass 1, enter 4.

  7. Pass 1, enter 5. Every neighbour is seen, so 5 finishes now.

    Pass 1, enter 5.

  8. 5 finishes, number 1. The badge is its finish time; pass 2 will take vertices from the highest down.

    5 finished, number 1.

  9. Pass 1, enter 6. Every neighbour is seen, so 6 finishes now.

    Pass 1, enter 6.

  10. 6 finishes, number 2. The badge is its finish time; pass 2 will take vertices from the highest down.

    6 finished, number 2.

  11. 4 finishes, number 3. The badge is its finish time; pass 2 will take vertices from the highest down.

    4 finished, number 3.

  12. 3 finishes, number 4. The badge is its finish time; pass 2 will take vertices from the highest down.

    3 finished, number 4.

  13. 2 finishes, number 5. The badge is its finish time; pass 2 will take vertices from the highest down.

    2 finished, number 5.

  14. 1 finishes, number 6. The badge is its finish time; pass 2 will take vertices from the highest down.

    1 finished, number 6.

  15. 0 finishes, number 7. The badge is its finish time; pass 2 will take vertices from the highest down.

    0 finished, number 7.

  16. Finish order: 5, 6, 4, 3, 2, 1, 0. Now reverse every edge. Strong connectivity does not change when all edges flip, since a cycle read backwards is still a cycle, but a DFS can no longer leak out of a source component into the ones it used to reach.

    All edges reversed.

  17. Pass 2: the unassigned vertex with the latest finish time is 0, finish 7. Start a DFS there on the reversed graph; it opens component 0.

    Start pass 2 from 0.

  18. Pass 2, reach 2 along a reversed edge: it joins component 0.

    2 joins component 0.

  19. Pass 2, reach 1 along a reversed edge: it joins component 0.

    1 joins component 0.

  20. The search from 0 is exhausted: component 0 is {0, 1, 2}. In the reversed graph nothing else is reachable from it without crossing into a component that was already assigned.

    Component 0: 0, 1, 2.

  21. Pass 2: the unassigned vertex with the latest finish time is 3, finish 4. Start a DFS there on the reversed graph; it opens component 1.

    Start pass 2 from 3.

  22. Pass 2, reach 5 along a reversed edge: it joins component 1.

    5 joins component 1.

  23. Pass 2, reach 4 along a reversed edge: it joins component 1.

    4 joins component 1.

  24. The search from 3 is exhausted: component 1 is {3, 4, 5}. In the reversed graph nothing else is reachable from it without crossing into a component that was already assigned.

    Component 1: 3, 4, 5.

  25. Pass 2: the unassigned vertex with the latest finish time is 6, finish 2. Start a DFS there on the reversed graph; it opens component 2.

    Start pass 2 from 6.

  26. The search from 6 is exhausted: component 2 is {6}. In the reversed graph nothing else is reachable from it without crossing into a component that was already assigned.

    Component 2: 6.

  27. 3 strongly connected components, coloured by membership. Two searches and one edge reversal, O(V + E), and the components appear in topological order of the condensed graph, the opposite of Tarjan's.

    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

Next up