Proofs: Johnson's algorithm for elementary cycles
These are the human proofs of Johnson's algorithm for elementary cycles, at full length.
Johnson's algorithm lists every elementary cycle exactly once full
This proof shows: correctness, termination.
Let be a directed graph on with parallel arcs merged, so that lists each successor once; a self-loop at puts in . For let . A cycle is identified by its vertex sequence starting at its least vertex.
The outer loop
Lemma 1. Let line L3 run with the current value of , and let be the component it finds, with least vertex . (a) No vertex with is the least vertex of a cycle. (b) The cycles of whose least vertex is are exactly the cycles of through . (c) If no component is found, no vertex is the least vertex of a cycle.
Proof. A cycle with least vertex lies in , and its vertices are strongly connected, so it lies in one strong component of , which has two or more vertices or a self-loop. This gives (c). Also , which gives (a). For (b), a cycle of through has least vertex , since . Conversely, a cycle with least vertex lies in the component of that contains , and that is .
So, if each call CIRCUIT outputs exactly the cycles of through , each once, then JOHNSON outputs every cycle of exactly once, grouped by least vertex. The loop ends, because grows at every round. The rest of the proof fixes one phase, with and , and writes for .
One phase: definitions
A call is one execution of CIRCUIT; it succeeds if it returns true and fails otherwise. Calls nest: a call that starts after a call starts and before ends also ends before ends. The stack holds the vertices of the calls that have started and not ended, in the order of their starts. A vertex is set blocked only at line L9, when it is pushed, and set unblocked only by UNBLOCK.
Fact 2 (failed calls). During a failed call, UNBLOCK never runs, and when it ends, every is blocked and .
Proof. By induction on the nesting depth. A failed call of has $f = $ false at the end, so line L11 never ran (), every call it made at line L12 failed (so by induction no UNBLOCK ran inside them), and line L13 did not run UNBLOCK. So during the call no vertex was unblocked. Each was either blocked when scanned, or was pushed at line L12, which blocks it.
Fact 3 (-lists). If is not empty, then is blocked. Every entry of was added after the last time was unblocked (or after line L6, if it never was).
Proof. Line L14 adds to at the end of a failed call of , when is blocked by Fact 2. UNBLOCK empties when it unblocks .
The stack is a simple path
Lemma 4. Let be on top of the stack at line L13, and suppose UNBLOCK runs. Every vertex that it unblocks has the property that its last call started after was pushed and has ended. In particular, is not on the stack.
Proof. Say that a vertex is late if it is blocked, its last call started after the push of , and that call has ended. UNBLOCK unblocks only if is blocked and for or for a vertex that it unblocked before. It suffices to show: if or is late, then every blocked is late.
Since is blocked, it has been blocked without a break since its last push ; for this is the push of , and for a late it is after the push of . Before , was unblocked, so by Fact 3 every entry of was added after , by a failed call of that ended after , hence after the push of . If had started before the push of , then would contain the call of and could end only after it; but is still on the stack. So started after the push of . If was pushed again after , that call started after the push of ; it cannot still be running, because then would be on the stack above , and is the top. So the last call of started after the push of and has ended, and is blocked, so is late.
Corollary 5. A vertex on the stack stays blocked until it is popped. So the stack never holds a vertex twice: line L12 pushes only if is unblocked, hence not on the stack. The stack is always a simple path in , since each .
Soundness and no repetition
Line L11 outputs the stack when . By Corollary 5 this is a simple path from in followed by the arc , so it is an elementary cycle of through ; when , it is the self-loop at .
Two different calls have different stacks. By induction on the start time: two calls with the same stack , , are made at line L12 by calls with the stack , which are the same call by induction, and one call scans each once. The phase has exactly one call with stack . Each call outputs at most once, at the single entry of . So no cycle is output twice.
Completeness
Lemma 6 (blocked vertices). At every moment, if is blocked and not on the stack, then , every is blocked, and .
Proof. is blocked and off the stack, so its last call has ended and did not unblock it at line L13, so it failed. At its end, Fact 2 gives and every blocked, and line L14 puts in . Afterwards, can be unblocked only by UNBLOCK, which then finds , blocked, and unblocks too. And leaves only in that same UNBLOCK. So while stays blocked, the claim holds.
Lemma 7 (paths from blocked vertices). At every moment, if is blocked and not on the stack, every path in contains a vertex of the stack among .
Proof. Let be the first vertex of the path that is on the stack or equal to ; it exists because . All with are off the stack, and by Lemma 6 and induction on , all of them are blocked: is, and if is blocked and off the stack, then is blocked. If , then is blocked, off the stack, and , which contradicts Lemma 6. So is on the stack, and .
Theorem. CIRCUIT outputs every elementary cycle of through .
Proof. Let be such a cycle. We show by induction on , for , that some call has the stack . For this is CIRCUIT. Let a call have the stack . While scans its list, is the top, because all calls made by have ended.
If , then , and line L11 outputs when scans . Otherwise scans . If is blocked at that moment, it is not on the stack, since is simple, and Lemma 7 applies to the path : one of , or itself, would have to be on the stack , which is impossible because is simple. So is unblocked, and line L12 calls CIRCUIT with the stack .
Termination
Each call scans a finite list and makes at most one call per entry, and by Corollary 5 the stack has at most vertices, so the tree of calls is finite. UNBLOCK unblocks a vertex at each recursive step and recurses only on blocked vertices, so it makes at most steps. The outer loop increases each round.