Algorithm Search⌘K

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 GG be a directed graph on {0,…,n−1}\{0, \ldots, n - 1\} with parallel arcs merged, so that A(v)A(v) lists each successor once; a self-loop at vv puts vv in A(v)A(v). For s∈Vs \in V let Gs=G[{s,…,n−1}]G_s = G[\{s, \ldots, n - 1\}]. 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 s0s_0 of ss, and let KK be the component it finds, with least vertex ss. (a) No vertex tt with s0≤t<ss_0 \le t < s is the least vertex of a cycle. (b) The cycles of GG whose least vertex is ss are exactly the cycles of G[K]G[K] through ss. (c) If no component is found, no vertex t≥s0t \ge s_0 is the least vertex of a cycle.

Proof. A cycle with least vertex t≥s0t \ge s_0 lies in Gs0G_{s_0}, and its vertices are strongly connected, so it lies in one strong component K′K' of Gs0G_{s_0}, which has two or more vertices or a self-loop. This gives (c). Also min⁡K≤min⁡K′≤t\min K \le \min K' \le t, which gives (a). For (b), a cycle of G[K]G[K] through ss has least vertex ss, since K⊆{s,…}K \subseteq \{s, \ldots\}. Conversely, a cycle with least vertex ss lies in the component K′K' of Gs0G_{s_0} that contains ss, and that is KK. □\square

So, if each call CIRCUIT(s)(s) outputs exactly the cycles of G[K]G[K] through ss, each once, then JOHNSON outputs every cycle of GG exactly once, grouped by least vertex. The loop ends, because ss grows at every round. The rest of the proof fixes one phase, with ss and KK, and writes A(v)A(v) for AK(v)A_K(v).

One phase: definitions

A call is one execution of CIRCUIT(v)(v); it succeeds if it returns true and fails otherwise. Calls nest: a call that starts after a call XX starts and before XX ends also ends before XX 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 w∈A(v)w \in A(v) is blocked and s∉A(v)s \notin A(v).

Proof. By induction on the nesting depth. A failed call of vv has $f = $ false at the end, so line L11 never ran (s∉A(v)s \notin A(v)), 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 w∈A(v)w \in A(v) was either blocked when scanned, or was pushed at line L12, which blocks it. □\square

Fact 3 (BB-lists). If B[u]B[u] is not empty, then uu is blocked. Every entry of B[u]B[u] was added after the last time uu was unblocked (or after line L6, if it never was).

Proof. Line L14 adds vv to B[w]B[w] at the end of a failed call of vv, when ww is blocked by Fact 2. UNBLOCK(u)(u) empties B[u]B[u] when it unblocks uu. □\square

The stack is a simple path

Lemma 4. Let vv be on top of the stack at line L13, and suppose UNBLOCK(v)(v) runs. Every vertex x≠vx \ne v that it unblocks has the property that its last call started after vv was pushed and has ended. In particular, xx is not on the stack.

Proof. Say that a vertex yy is late if it is blocked, its last call started after the push of vv, and that call has ended. UNBLOCK(v)(v) unblocks xx only if xx is blocked and x∈B[y]x \in B[y] for y=vy = v or for a vertex yy that it unblocked before. It suffices to show: if y=vy = v or yy is late, then every blocked x∈B[y]x \in B[y] is late.

Since yy is blocked, it has been blocked without a break since its last push PyP_y; for y=vy = v this is the push of vv, and for a late yy it is after the push of vv. Before PyP_y, yy was unblocked, so by Fact 3 every entry xx of B[y]B[y] was added after PyP_y, by a failed call XX of xx that ended after PyP_y, hence after the push of vv. If XX had started before the push of vv, then XX would contain the call of vv and could end only after it; but vv is still on the stack. So XX started after the push of vv. If xx was pushed again after XX, that call started after the push of vv; it cannot still be running, because then xx would be on the stack above vv, and vv is the top. So the last call of xx started after the push of vv and has ended, and xx is blocked, so xx is late. □\square

Corollary 5. A vertex on the stack stays blocked until it is popped. So the stack never holds a vertex twice: line L12 pushes ww only if ww is unblocked, hence not on the stack. The stack is always a simple path s=x0→x1→⋯→xks = x_0 \to x_1 \to \cdots \to x_k in G[K]G[K], since each xi+1∈A(xi)x_{i+1} \in A(x_i).

Soundness and no repetition

Line L11 outputs the stack x0,…,xkx_0, \ldots, x_k when s∈A(xk)s \in A(x_k). By Corollary 5 this is a simple path from ss in G[K]G[K] followed by the arc xk→sx_k \to s, so it is an elementary cycle of G[K]G[K] through ss; when k=0k = 0, it is the self-loop at ss.

Two different calls have different stacks. By induction on the start time: two calls with the same stack x0,…,xkx_0, \ldots, x_k, k≥1k \ge 1, are made at line L12 by calls with the stack x0,…,xk−1x_0, \ldots, x_{k-1}, which are the same call by induction, and one call scans each w∈A(xk−1)w \in A(x_{k-1}) once. The phase has exactly one call with stack x0=sx_0 = s. Each call outputs at most once, at the single entry ss of A(xk)A(x_k). So no cycle is output twice.

Completeness

Lemma 6 (blocked vertices). At every moment, if yy is blocked and not on the stack, then s∉A(y)s \notin A(y), every w∈A(y)w \in A(y) is blocked, and y∈B[w]y \in B[w].

Proof. yy 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 s∉A(y)s \notin A(y) and every w∈A(y)w \in A(y) blocked, and line L14 puts yy in B[w]B[w]. Afterwards, ww can be unblocked only by UNBLOCK(w)(w), which then finds y∈B[w]y \in B[w], blocked, and unblocks yy too. And yy leaves B[w]B[w] only in that same UNBLOCK(w)(w). So while yy stays blocked, the claim holds. □\square

Lemma 7 (paths from blocked vertices). At every moment, if yy is blocked and not on the stack, every path y=p0,p1,…,pr=sy = p_0, p_1, \ldots, p_r = s in G[K]G[K] contains a vertex of the stack among p1,…,pr−1p_1, \ldots, p_{r-1}.

Proof. Let pip_i be the first vertex of the path that is on the stack or equal to ss; it exists because pr=sp_r = s. All pjp_j with j<ij < i are off the stack, and by Lemma 6 and induction on jj, all of them are blocked: p0=yp_0 = y is, and if pjp_j is blocked and off the stack, then pj+1∈A(pj)p_{j+1} \in A(p_j) is blocked. If pi=sp_i = s, then pi−1p_{i-1} is blocked, off the stack, and s∈A(pi−1)s \in A(p_{i-1}), which contradicts Lemma 6. So pi≠sp_i \ne s is on the stack, and 1≤i≤r−11 \le i \le r - 1. □\square

Theorem. CIRCUIT(s)(s) outputs every elementary cycle of G[K]G[K] through ss.

Proof. Let C=(s,c1,…,ck−1)C = (s, c_1, \ldots, c_{k-1}) be such a cycle. We show by induction on ii, for 0≤i≤k−10 \le i \le k - 1, that some call has the stack s,c1,…,cis, c_1, \ldots, c_i. For i=0i = 0 this is CIRCUIT(s)(s). Let a call XX have the stack s,c1,…,cis, c_1, \ldots, c_i. While XX scans its list, cic_i is the top, because all calls made by XX have ended.

If i=k−1i = k - 1, then s∈A(ck−1)s \in A(c_{k-1}), and line L11 outputs CC when XX scans ss. Otherwise XX scans ci+1≠sc_{i+1} \ne s. If ci+1c_{i+1} is blocked at that moment, it is not on the stack, since CC is simple, and Lemma 7 applies to the path ci+1,ci+2,…,ck−1,sc_{i+1}, c_{i+2}, \ldots, c_{k-1}, s: one of ci+2,…,ck−1c_{i+2}, \ldots, c_{k-1}, or ci+1c_{i+1} itself, would have to be on the stack s,c1,…,cis, c_1, \ldots, c_i, which is impossible because CC is simple. So ci+1c_{i+1} is unblocked, and line L12 calls CIRCUIT(ci+1)(c_{i+1}) with the stack s,c1,…,ci+1s, c_1, \ldots, c_{i+1}. □\square

Termination

Each call scans a finite list and makes at most one call per entry, and by Corollary 5 the stack has at most ∣K∣|K| 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 ∣K∣|K| steps. The outer loop increases ss each round. ■\blacksquare

Back to Johnson's algorithm for elementary cycles