Algorithm Search⌘K

Proofs: DFS-based cycle detection

These are the human proofs of DFS-based cycle detection, at full length.

DFS-CYCLE returns a cycle if and only if the graph has one full

This proof shows: correctness, termination.

Let G=(V,E)G = (V, E) have nn vertices and mm edges, directed or undirected, with parallel edges and self-loops allowed. We use the facts proved for depth-first search: each vertex goes from white to gray to black, VISIT runs at most once for each vertex, and a call that starts while another call is active ends before it. The extra line L8 only stops the run early, so these facts hold until the run stops.

Termination. VISIT(u)(u) is called only when uu is white, and line L6 makes it gray, so there are at most nn calls, each with a finite loop. CYCLE stops by Lemma 1 below. So DFS-CYCLE stops.

Lemma 1 (the gray path). At each moment, let g1,…,gkg_1, \ldots, g_k be the gray vertices in the order in which they became gray. Then g1g_1 was called at line L4, and for 1≤i<k1 \le i < k, pe[gi+1]pe[g_{i+1}] is an edge from gig_i to gi+1g_{i+1}. The vertex whose loop runs now is gkg_k.

Proof. The gray vertices are the vertices with an active call, and the active calls form the call stack, with g1g_1 at the bottom and gkg_k at the top. The bottom call comes from line L4. For i<ki < k, the call of gi+1g_{i+1} was made at line L9 in the loop of gig_i, the call just below it, while scanning a pair (e,gi+1)∈Out[gi](e, g_{i+1}) \in \mathrm{Out}[g_i], and line L9 set pe[gi+1]=epe[g_{i+1}] = e. So ee goes from gig_i to gi+1g_{i+1}. □\square

Lemma 2 (the output is a cycle). If line L8 returns CYCLE(e,u,v)(e, u, v), the result is a cycle.

Proof. At that moment u=gku = g_k by Lemma 1, and v=gjv = g_j for some j≤kj \le k, because vv is gray. Starting at x=gkx = g_k, each step of line L12 sets xx to the tail of pe[x]pe[x], which is gi−1g_{i-1} when x=gix = g_i. So the loop runs k−jk - j times, stops at x=gj=vx = g_j = v, and C=(pe[gj+1],…,pe[gk],e)C = (pe[g_{j+1}], \ldots, pe[g_k], e). This walk goes gj→gj+1→⋯→gk→gjg_j \to g_{j+1} \to \cdots \to g_k \to g_j. Its vertices gj,…,gkg_j, \ldots, g_k are distinct. Its tree edges are distinct, because each one was the discovery edge of a different vertex.

It remains to see that ee is not one of the tree edges. If j=kj = k, then C=(e)C = (e), and ee goes from uu to uu, so it is a self-loop and the walk is a cycle of length 1. If j<kj < k, then ee starts at gkg_k. In the directed case each tree edge pe[gi+1]pe[g_{i+1}] starts at gi≠gkg_i \ne g_k, so ee is not a tree edge. In the undirected case, among the tree edges of the walk only pe[gk]pe[g_k] has gkg_k as an end, because pe[gi+1]pe[g_{i+1}] joins gig_i and gi+1g_{i+1}. Line L7 skips pe[gk]pe[g_k] in the loop of gkg_k, so e≠pe[gk]e \ne pe[g_k]. If j=k−1j = k - 1, the walk is gk−1→gk→gk−1g_{k-1} \to g_k \to g_{k-1} along two different edges, which is a cycle of length 2. In all cases the edges of CC are distinct and the vertices gj,…,gkg_j, \ldots, g_k are distinct, so CC is a cycle. □\square

Lemma 3 (directed: a cycle is found). If GG is directed and has a cycle, DFS-CYCLE does not return NIL.

Proof. Suppose it returns NIL. Then line L8 never fired, so every vertex was visited and every list was scanned in full. Let ZZ be a cycle of GG and let vv be the vertex of ZZ that became gray first. At that moment, all other vertices of ZZ are white, so ZZ minus its last edge is a path of white vertices from vv to the vertex ww whose edge ff goes to vv. By the white-path theorem, ww becomes gray during the call of vv (if w=vw = v, the edge ff is a self-loop). While the call of ww runs, the call of vv is still active, so vv is gray, and the loop of ww scans ff before the call of ww ends. At that scan, line L8 sees vv gray and returns. This contradicts the assumption. □\square

Lemma 4 (undirected: a cycle is found). If GG is undirected and has a cycle, DFS-CYCLE does not return NIL.

Proof. Suppose it returns NIL, so every list was scanned in full. Take any edge ee that is not a tree edge, with ends aa and bb, and say aa became gray no later than bb. If ee is a self-loop at aa, its scan from aa sees aa gray and returns, a contradiction. Otherwise, consider the scan of ee in the loop of aa. If bb is white then, line L9 makes ee the tree edge pe[b]pe[b], a contradiction. If bb is gray then, line L8 returns, a contradiction. If bb is black then, bb became gray and black while aa was gray, so the loop of bb scanned ee while aa was gray. That scan was not skipped, since ee is not a tree edge, and line L8 returns, a contradiction. So every edge is a tree edge. Tree edges form a forest (Lemma 3 of the depth-first search proof), so GG is a forest and has no cycle. □\square

Theorem. DFS-CYCLE stops. It returns a list of edges that forms a cycle if GG has a cycle, and it returns NIL if GG has none. It works on directed and undirected multigraphs with self-loops.

Proof. Termination is shown above. If the run returns a list, the list comes from line L8 and is a cycle by Lemma 2, so GG has a cycle. If it returns NIL, then GG has no cycle by Lemma 3 in the directed case and Lemma 4 in the undirected case. ■\blacksquare

Back to DFS-based cycle detection