Graph algorithms · which statements can all reach each other

Strongly connected components

Read first: Visiting a graph

On a cycle, each statement's result feeds the next and the last feeds the first, so no order puts every statement after everything that feeds it. Which statements belong to one cycle is a question about pairs: does X reach Y, and Y reach X? Asked directly, that is a walk from every statement, each going over the same arrows again.

On this page's seven paragraphs, one depth-first walk from ENTRY-PARA gives each paragraph two numbers, an index and a low, and pops four groups off a stack: {Exit}, {D, E}, {A, B, C}, {Entry}. Each group comes off after every group it leads to, which is the order a tool needs to solve them in.

A component here is a strongly connected component: a largest set of nodes that all reach each other. Path, back edge and preorder are as in visiting a graph.

The example

Seven paragraphs, one node each, with an arrow for every fall-through and every GO TO. A, B and C form a three-paragraph ring; D and E a two-paragraph loop after it:

flowchart LR
    Entry --> A
    A --> B
    B --> C
    C -->|"GO TO A-PARA"| A
    C --> D
    D --> E
    E -->|"GO TO D-PARA"| D
    E --> Exit

The answer: four components, one chip each. Every paragraph is in exactly one; Entry and Exit sit on no cycle, so each is a component on its own.

{A, B, C}{D, E}{Entry}{Exit}

The paragraphs behind the picture, a fragment constructed for this page. ENTRY-PARA is Entry, A-PARA is A, and so on down to EXIT-PARA, which is Exit:

       ENTRY-PARA.
           MOVE 0 TO WS-TRIES.
       A-PARA.
           ADD 1 TO WS-TRIES.
       B-PARA.
           CALL 'CONNECT' USING WS-STATUS.
       C-PARA.
           IF WS-STATUS NOT = '00' AND WS-TRIES < 3
               GO TO A-PARA.
       D-PARA.
           ADD 1 TO WS-COUNT.
       E-PARA.
           IF WS-COUNT < 10
               GO TO D-PARA.
       EXIT-PARA.
           STOP RUN.

Each paragraph above EXIT-PARA falls through to the next, which gives the six downward arrows. C-PARA's GO TO gives C → A, the retry ring; E-PARA's gives E → D, the counting loop. Those are the picture's eight arrows, and no others.

The walk below tries each paragraph's arrows in the order drawn: the GO TO before the fall-through. So C tries C → A before C → D, and E tries E → D before E → Exit. Every index, low and component below follows from that order:

Entry → AA → BB → CC → A, then C → DD → EE → D, then E → Exit

Step by step

This is Tarjan's algorithm.[1] It is the depth-first walk, with three additions. Each paragraph gets an index, the order it was first visited, counting from 0. It also gets a low, the smallest index it is known to get back to; this starts equal to its index. And beside the path there is a stack, holding every visited paragraph not yet in a finished component.

In each step's picture, the amber paragraph is the one the walk is at, the amber arrow is the one it just tried, and a dashed outline marks a paragraph already in a finished component. In each table, amber cells are the ones that step changed.

Tarjan's algorithm on the seven paragraphs, one move at a time (16 steps)
  1. Nothing is visited. The walk starts at Entry, the paragraph control enters first. The path and the stack are both empty.

    Path

    empty

    Stack, bottom first

    empty

    Components finished

    none yet
    flowchart LR
        Entry --> A
        A --> B
        B --> C
        C --> A
        C --> D
        D --> E
        E --> D
        E --> Exit
        classDef cur fill:#f5b041,stroke:#f5b041,color:#000
        classDef done stroke-dasharray:4 3
    
  2. Visit Entry: it is the first paragraph seen, so its index is 0, and its low starts equal to its index. It joins the path and is pushed on the stack.

    paragraphindexlowcomponent
    Entry00·

    Path

    Entry

    Stack, bottom first

    Entry

    Components finished

    none yet
    flowchart LR
        Entry --> A
        A --> B
        B --> C
        C --> A
        C --> D
        D --> E
        E --> D
        E --> Exit
        class Entry cur
        classDef cur fill:#f5b041,stroke:#f5b041,color:#000
        classDef done stroke-dasharray:4 3
    
  3. Entry's one arrow goes to A, not yet seen: visit it. Index 1, low 1; it joins the path and the stack.

    paragraphindexlowcomponent
    Entry00·
    A11·

    Path

    EntryA

    Stack, bottom first

    EntryA

    Components finished

    none yet
    flowchart LR
        Entry --> A
        A --> B
        B --> C
        C --> A
        C --> D
        D --> E
        E --> D
        E --> Exit
        class A cur
        classDef cur fill:#f5b041,stroke:#f5b041,color:#000
        classDef done stroke-dasharray:4 3
        linkStyle 0 stroke:#f5b041,stroke-width:3px
    
  4. A falls through to B, not yet seen: index 2, low 2.

    paragraphindexlowcomponent
    Entry00·
    A11·
    B22·

    Path

    EntryAB

    Stack, bottom first

    EntryAB

    Components finished

    none yet
    flowchart LR
        Entry --> A
        A --> B
        B --> C
        C --> A
        C --> D
        D --> E
        E --> D
        E --> Exit
        class B cur
        classDef cur fill:#f5b041,stroke:#f5b041,color:#000
        classDef done stroke-dasharray:4 3
        linkStyle 1 stroke:#f5b041,stroke-width:3px
    
  5. B falls through to C, not yet seen: index 3, low 3.

    paragraphindexlowcomponent
    Entry00·
    A11·
    B22·
    C33·

    Path

    EntryABC

    Stack, bottom first

    EntryABC

    Components finished

    none yet
    flowchart LR
        Entry --> A
        A --> B
        B --> C
        C --> A
        C --> D
        D --> E
        E --> D
        E --> Exit
        class C cur
        classDef cur fill:#f5b041,stroke:#f5b041,color:#000
        classDef done stroke-dasharray:4 3
        linkStyle 2 stroke:#f5b041,stroke-width:3px
    
  6. C's first arrow, the GO TO A-PARA, leads to A. A has been seen and is still on the stack, so the walk does not go there; instead C's low drops to A's index: 1. C has found a way back to a paragraph above it on the path.

    paragraphindexlowcomponent
    Entry00·
    A11·
    B22·
    C31·

    Path

    EntryABC

    Stack, bottom first

    EntryABC

    Components finished

    none yet
    flowchart LR
        Entry --> A
        A --> B
        B --> C
        C --> A
        C --> D
        D --> E
        E --> D
        E --> Exit
        class C cur
        classDef cur fill:#f5b041,stroke:#f5b041,color:#000
        classDef done stroke-dasharray:4 3
        linkStyle 3 stroke:#f5b041,stroke-width:3px
    
  7. C's second arrow, the fall-through, leads to D, not yet seen: index 4, low 4.

    paragraphindexlowcomponent
    Entry00·
    A11·
    B22·
    C31·
    D44·

    Path

    EntryABCD

    Stack, bottom first

    EntryABCD

    Components finished

    none yet
    flowchart LR
        Entry --> A
        A --> B
        B --> C
        C --> A
        C --> D
        D --> E
        E --> D
        E --> Exit
        class D cur
        classDef cur fill:#f5b041,stroke:#f5b041,color:#000
        classDef done stroke-dasharray:4 3
        linkStyle 4 stroke:#f5b041,stroke-width:3px
    
  8. D falls through to E, not yet seen: index 5, low 5.

    paragraphindexlowcomponent
    Entry00·
    A11·
    B22·
    C31·
    D44·
    E55·

    Path

    EntryABCDE

    Stack, bottom first

    EntryABCDE

    Components finished

    none yet
    flowchart LR
        Entry --> A
        A --> B
        B --> C
        C --> A
        C --> D
        D --> E
        E --> D
        E --> Exit
        class E cur
        classDef cur fill:#f5b041,stroke:#f5b041,color:#000
        classDef done stroke-dasharray:4 3
        linkStyle 5 stroke:#f5b041,stroke-width:3px
    
  9. E's first arrow, GO TO D-PARA, leads to D: seen, and on the stack. E's low drops to D's index, 4. This is step 5 again, one loop further down.

    paragraphindexlowcomponent
    Entry00·
    A11·
    B22·
    C31·
    D44·
    E54·

    Path

    EntryABCDE

    Stack, bottom first

    EntryABCDE

    Components finished

    none yet
    flowchart LR
        Entry --> A
        A --> B
        B --> C
        C --> A
        C --> D
        D --> E
        E --> D
        E --> Exit
        class E cur
        classDef cur fill:#f5b041,stroke:#f5b041,color:#000
        classDef done stroke-dasharray:4 3
        linkStyle 6 stroke:#f5b041,stroke-width:3px
    
  10. E's second arrow, the fall-through, leads to Exit, not yet seen: index 6, low 6. Every paragraph has now been visited.

    paragraphindexlowcomponent
    Entry00·
    A11·
    B22·
    C31·
    D44·
    E54·
    Exit66·

    Path

    EntryABCDEExit

    Stack, bottom first

    EntryABCDEExit

    Components finished

    none yet
    flowchart LR
        Entry --> A
        A --> B
        B --> C
        C --> A
        C --> D
        D --> E
        E --> D
        E --> Exit
        class Exit cur
        classDef cur fill:#f5b041,stroke:#f5b041,color:#000
        classDef done stroke-dasharray:4 3
        linkStyle 7 stroke:#f5b041,stroke-width:3px
    
  11. Exit has no arrows, so it backs out: it leaves the path. Its low, 6, equals its index, 6: nothing it reaches gets back above it. Pop the stack down to Exit: the first component, {Exit}, on its own. E takes the smaller of its low and Exit's: still 4.

    paragraphindexlowcomponent
    Entry00·
    A11·
    B22·
    C31·
    D44·
    E54·
    Exit661st

    Path

    EntryABCDE

    Stack, bottom first

    EntryABCDE

    Components finished

    {Exit}
    flowchart LR
        Entry --> A
        A --> B
        B --> C
        C --> A
        C --> D
        D --> E
        E --> D
        E --> Exit
        class Exit cur
        classDef cur fill:#f5b041,stroke:#f5b041,color:#000
        classDef done stroke-dasharray:4 3
    
  12. E has tried both arrows, so it backs out and leaves the path. Its low, 4, is not its index, 5: it gets back to D, above it. So it is not the first of a component, and it stays on the stack. D takes the smaller low: 4, unchanged.

    paragraphindexlowcomponent
    Entry00·
    A11·
    B22·
    C31·
    D44·
    E54·
    Exit661st

    Path

    EntryABCD

    Stack, bottom first

    EntryABCDE

    Components finished

    {Exit}
    flowchart LR
        Entry --> A
        A --> B
        B --> C
        C --> A
        C --> D
        D --> E
        E --> D
        E --> Exit
        class Exit done
        class E cur
        classDef cur fill:#f5b041,stroke:#f5b041,color:#000
        classDef done stroke-dasharray:4 3
    
  13. D has tried its one arrow and backs out. Its low, 4, equals its index, 4. Pop the stack down to D: E, then D. The second component is {D, E}, the two-paragraph loop. C takes the smaller of its low, 1, and D's, 4: still 1.

    paragraphindexlowcomponent
    Entry00·
    A11·
    B22·
    C31·
    D442nd
    E542nd
    Exit661st

    Path

    EntryABC

    Stack, bottom first

    EntryABC

    Components finished

    {Exit}{D, E}
    flowchart LR
        Entry --> A
        A --> B
        B --> C
        C --> A
        C --> D
        D --> E
        E --> D
        E --> Exit
        class Exit,E done
        class D cur
        classDef cur fill:#f5b041,stroke:#f5b041,color:#000
        classDef done stroke-dasharray:4 3
    
  14. C backs out. Low 1, index 3: not equal, so C stays on the stack. B takes the smaller of its low, 2, and C's, 1: B's low drops to 1. What C found at step 5 is handed up the path.

    paragraphindexlowcomponent
    Entry00·
    A11·
    B21·
    C31·
    D442nd
    E542nd
    Exit661st

    Path

    EntryAB

    Stack, bottom first

    EntryABC

    Components finished

    {Exit}{D, E}
    flowchart LR
        Entry --> A
        A --> B
        B --> C
        C --> A
        C --> D
        D --> E
        E --> D
        E --> Exit
        class Exit,D,E done
        class C cur
        classDef cur fill:#f5b041,stroke:#f5b041,color:#000
        classDef done stroke-dasharray:4 3
    
  15. B backs out. Low 1, index 2: it stays on the stack. A takes the smaller of 1 and 1: unchanged.

    paragraphindexlowcomponent
    Entry00·
    A11·
    B21·
    C31·
    D442nd
    E542nd
    Exit661st

    Path

    EntryA

    Stack, bottom first

    EntryABC

    Components finished

    {Exit}{D, E}
    flowchart LR
        Entry --> A
        A --> B
        B --> C
        C --> A
        C --> D
        D --> E
        E --> D
        E --> Exit
        class Exit,D,E done
        class B cur
        classDef cur fill:#f5b041,stroke:#f5b041,color:#000
        classDef done stroke-dasharray:4 3
    
  16. A backs out. Low 1 equals index 1. Pop the stack down to A: C, B, A. The third component is {A, B, C}, the three-paragraph ring. Entry takes the smaller of 0 and 1: still 0.

    paragraphindexlowcomponent
    Entry00·
    A113rd
    B213rd
    C313rd
    D442nd
    E542nd
    Exit661st

    Path

    Entry

    Stack, bottom first

    Entry

    Components finished

    {Exit}{D, E}{A, B, C}
    flowchart LR
        Entry --> A
        A --> B
        B --> C
        C --> A
        C --> D
        D --> E
        E --> D
        E --> Exit
        class Exit,D,E,B,C done
        class A cur
        classDef cur fill:#f5b041,stroke:#f5b041,color:#000
        classDef done stroke-dasharray:4 3
    
  17. Entry backs out. Low 0 equals index 0: pop it, the fourth component, {Entry}. The path and the stack are empty, and every paragraph is in exactly one component.

    paragraphindexlowcomponent
    Entry004th
    A113rd
    B213rd
    C313rd
    D442nd
    E542nd
    Exit661st

    Path

    empty

    Stack, bottom first

    empty

    Components finished

    {Exit}{D, E}{A, B, C}{Entry}
    flowchart LR
        Entry --> A
        A --> B
        B --> C
        C --> A
        C --> D
        D --> E
        E --> D
        E --> Exit
        class Exit,D,E,A,B,C done
        class Entry cur
        classDef cur fill:#f5b041,stroke:#f5b041,color:#000
        classDef done stroke-dasharray:4 3
    

The walk's last table holds the whole result. Every row has a component, and the four components came off the stack in the order of the last step's chips: {Exit}, {D, E}, {A, B, C}, {Entry}.

Why one walk is enough

The walk visited each paragraph once and tried each arrow once: 1 first visit, 8 arrows and 7 back-outs make the 16 steps. Here is where each happened:

movestepscount
try an arrow2–98, one per arrow
visit a paragraph1, 2, 3, 4, 6, 7, 97, one per paragraph
back out10–167, one per paragraph

Two facts make that enough. The first is what a low below the index means. At step 5, C tried C → A and found A still on the stack. Look at the path chips at that step: Entry A B C. A is above C on the path, so A reaches C along it, and C reaches A by the arrow. So A, B and C all lie on one cycle, and C's low of 1 records it. Step 13 passed that 1 up to B, and here are the final numbers:

paragraphindexlowlow = index?popped at step
Entry00yes: first of a component16
A11yes: first of a component15
B21no: gets back to A15
C31no: gets back to A15
D44yes: first of a component12
E54no: gets back to D12
Exit66yes: first of a component10

The second fact is when a component is complete. A paragraph that backs out with low equal to index reaches nothing older still on the stack. So nothing it reaches can get back above it, and it is the first paragraph of its component that the walk visited, the component's root. Its component is everything on the stack from it upwards: the paragraphs visited after it that are not yet in a finished component. In the table, the amber rows are the four roots, and each root's rows below it share its popped-at step.

That is why the walk needs the stack as well as the path. At step 11, E backed out and left the path, but its low, 4, said it gets back to D, so it stayed on the stack:

Path at step 11

EntryABCD

Stack at step 11

EntryABCDE

One step later D, the root, backed out and took E off the stack with it. Had the walk kept only the path, E would have been lost at step 11 and {D, E} would have come out as {D}.

The order components finish in

The components came off the stack in this order, numbered as in the walk's component column:

1st {Exit}2nd {D, E}3rd {A, B, C}4th {Entry}

Shrink each component to one node and keep the arrows between components. The result is the condensation. It never has a cycle: if two components led to each other, their members would all reach each other, and they would be one component. Here it is, each box labelled with its finish position, each arrow with the original arrow it stands for:

flowchart TD
    S["{Entry} · 4th"] -->|"Entry → A"| R["{A, B, C} · 3rd"]
    R -->|"C → D"| L["{D, E} · 2nd"]
    L -->|"E → Exit"| X["{Exit} · 1st"]

Every arrow points from a later finish to an earlier one. That holds for every graph, not just this one. A root backs out only after everything it reaches has been visited and has backed out. Each component it leads to has therefore been popped already, either earlier in the walk or inside the root's own part of it, as {D, E} was at step 12, inside A's. So the finish order is a reverse topological order of the condensation. Each arrow of the condensation, with its two ends' positions:

arrowfrom finishesto finishespoints to
{Entry} → {A, B, C}4th (step 16)3rd (step 15)an earlier finish
{A, B, C} → {D, E}3rd (step 15)2nd (step 12)an earlier finish
{D, E} → {Exit}2nd (step 12)1st (step 10)an earlier finish

Both directions are useful. An answer built from what a node leads to, such as where a value could have come from, is filled in the finish order, so each component's answer is ready before anything that leads into it asks. A forward analysis reads the chips right to left, {Entry}, {A, B, C}, {D, E}, {Exit}, and goes round each loop until it settles before moving on.

The rule

visit(v): index(v) = low(v) = the next number
  v joins the path and is pushed on the stack
for each arrow v → w, in order:
  w not yet visited:  visit(w); when w backs out, low(v) = min(low(v), low(w))
  w on the stack:     low(v) = min(low(v), index(w))
  w visited, off the stack:  ignore it; its component is finished
v backs out: if low(v) = index(v), pop the stack down to v: one component
             otherwise v stays on the stack
repeat from any node not yet visited until none is left

Each line, and the steps of the walk that applied it:

linestepswhat it did
visit(v), joins, pushed1, 2, 3, 4, 6, 7, 9indexes 0 to 6, in that order
w not yet visited13B's low drops to C's, 1; at 10, 11, 12, 14 and 15 the min changes nothing
w on the stack5, 8C's low to A's index, 1; E's to D's, 4
w visited, off the stacknonethe first bullet under what goes wrong
low(v) = index(v): pop10, 12, 15, 16the four components
otherwise v stays11, 13, 14E, C, B wait for their roots
repeatnoneevery paragraph is reachable from Entry; a paragraph nothing jumps or falls into would need its own start

Where this is used

Three pages build on these components. Each row says what that page does with them, worked on this page's graph:

pageuses the components toon this page's graph
Answering every walk oncesolve a ring of places once, as one unit, in finish order{Exit} is answered first, then {D, E} from it, then {A, B, C} from that: each ring once, whoever asks
Reducibility and loop detectionsee where each component is entered from outside{A, B, C} only at A (by Entry → A); {D, E} only at D (by C → D): one way in each
DJ graphs and natural loop bodiesfall back to components at a level with no single header, and collapse each component of 2+ membersnot needed here: both loops have one way in

In the first row, memoised reachability's place graph plays the role of the paragraphs here: its ring of three places is popped in one step, as {A, B, C} is at step 15. The second row is checked against the condensation's picture: one arrow enters each box.

What goes wrong

In cobble and cobol-rekt

Cobble finds components in its combine with Tarjan's algorithm, written out by hand in Closures._solve in cobble/cobble/dataflow/combine/closures.py. Its docstring says "Tarjan's algorithm with an explicit stack". It does not recurse: visiting is the walk's Path row, positions says which arrow each paragraph on it tries next, and stack is the Stack row. Abridged, with comments added:

while visiting:
    vertex = visiting[-1]
    position = positions[-1]
    nexts = local[vertex][1]
    if position < len(nexts):
        positions[-1] = position + 1
        following = nexts[position]
        if following in self._answers:   # settled by an earlier solve
            continue
        if following not in index:       # not yet visited
            visit(following)
        elif following in on_stack:      # steps 5 and 8
            low[vertex] = min(low[vertex], index[following])
        continue
    visiting.pop()                       # backs out
    positions.pop()
    if visiting:
        parent = visiting[-1]
        low[parent] = min(low[parent], low[vertex])
    if low[vertex] == index[vertex]:     # steps 10, 12, 15, 16
        self._settle(vertex, stack, on_stack, local)

Two things differ from the rule box. A successor already answered by an earlier solve is skipped before it is looked at, which is how a second walk stops at the first one's answers. And a visited successor off the stack falls through every branch, which is the rule's "ignore it". _settle gives the popped component one answer. Its _mark flags every member of a component of two or more as revisiting, and a one-member component when, among other cases, vertex in nexts: the self-loop of the last bullet above.

Elsewhere cobble calls networkx's strongly_connected_components: in _cycle_sizes in cobble/cobble/dataflow/fixpoint/diagnose.py, and in cobble/scripts/cycle_sizes.py, which report component sizes. networkx's documentation for that function, in the version installed locally (3.6.1), says it uses Tarjan's algorithm with Nuutila's modifications, in a non-recursive version.

cobol-rekt takes its components from JGraphT. Its reducibility and loop tasks live in the mojo-common submodule. IrreducibleStronglyConnectedComponentsTask and LoopBodyDetectionTask, in src/main/java/com/mojo/algorithms/task/, both build a KosarajuStrongConnectivityInspector:

StrongConnectivityAlgorithm<V, E> scAlg =
    new KosarajuStrongConnectivityInspector<>(flowgraph);
List<Graph<V, E>> stronglyConnectedComponents =
    scAlg.getStronglyConnectedComponents();

The first task then collects, for each component, the edges into it from outside. It reports the component as improper when those edges land on more than one distinct node, which is the check in the second row of the where this is used table. The second task runs the inspector on the subgraph at an irreducible level, as in the third row. The README says the same: components are detected with JGraphT's built-in Kosaraju inspector.

Kosaraju's algorithm, published by Sharir,[2] gets the same components by a different route: two depth-first passes instead of one.

cobble's combinecobol-rekt
algorithmTarjanKosaraju, via JGraphT
passesone depth-first walktwo: one over the graph recording the order nodes finish, one over the graph with every arrow reversed, starting each time from the latest-finished node not yet reached
recursionnone: lists visiting, positions, stacknone: each pass keeps an ArrayDeque
verified inclosures.py sourcetask sources; JGraphT's class read with javap from jgrapht-core 1.5.2, the version the poms name

References

  1. R. Tarjan. "Depth-First Search and Linear Graph Algorithms." SIAM Journal on Computing, 1(2), 1972, pp. 146–160. The one-walk algorithm with index, low and a stack that this page walks through.
  2. M. Sharir. "A Strong-Connectivity Algorithm and Its Applications in Data Flow Analysis." Computers & Mathematics with Applications, 7(1), 1981, pp. 67–72. The two-pass algorithm usually credited to Kosaraju and Sharir, which cobol-rekt uses through JGraphT.