Algorithm Search⌘K

Proofs: Tarjan's strongly connected components algorithm

These are the human proofs of Tarjan's strongly connected components algorithm, at full length.

Tarjan's algorithm outputs each strongly connected component exactly when its root finishes full

This proof shows: correctness, termination.

Let G=(V,E)G = (V, E) be a directed graph, where parallel arcs and loops are allowed. Write u⇝vu \leadsto v when GG has a directed path from uu to vv, and let the components be the classes of the relation "u⇝vu \leadsto v and v⇝uv \leadsto u". Write C(x)C(x) for the component of xx. The calls of STRONGCONNECT form a depth-first search, and indexindex is its preorder: index[x]<index[y]index[x] < index[y] exactly when xx is discovered before yy. A call is active between its start and its end. We use these facts about depth-first search.

  • (D1) The active calls at any moment form a path of the DFS forest from a root, so of two active vertices one is an ancestor of the other.
  • (D2) A vertex discovered while the call of xx is active is a descendant of xx, and every descendant of xx is discovered and finished while the call of xx is active (the parenthesis theorem).
  • (D3) If, when xx is discovered, a path from xx to yy has only undiscovered vertices, then yy becomes a descendant of xx (the white-path theorem).

The root r(C)r(C) of a component CC is its vertex with the least index. Write T(x)T(x) for the set of descendants of xx, xx included.

Lemma 1. Let CC be a component with root rr. Then C⊆T(r)C \subseteq T(r), and every vertex on the tree path from rr to a vertex c∈Cc \in C is in CC.

Proof. When rr is discovered, no vertex of CC is discovered yet. Each c∈Cc \in C has a path from rr inside CC, as a path between two vertices of a component stays in the component (each vertex yy on it has r⇝y⇝c⇝rr \leadsto y \leadsto c \leadsto r). By (D3), c∈T(r)c \in T(r). A vertex yy on the tree path from rr to cc has r⇝y⇝c⇝rr \leadsto y \leadsto c \leadsto r, so y∈Cy \in C. □\square

Lemma 2. Let xx be a vertex, and let cc be a child of xx with C(c)≠C(x)C(c) \ne C(x). Then c=r(C(c))c = r(C(c)).

Proof. Let r′=r(C(c))r' = r(C(c)). By Lemma 1, r′r' is an ancestor of cc. If r′≠cr' \ne c, then r′r' is an ancestor of xx, the parent of cc, and xx lies on the tree path from r′r' to cc. By Lemma 1, x∈C(c)x \in C(c), which is false. So r′=cr' = c. □\square

Lemma 3 (stack order). At any moment the vertices of SS, from bottom to top, have increasing index. If vv is active, the vertices above vv on SS are descendants of vv.

Proof. Line L6 pushes each vertex at its discovery, so pushes happen in increasing index, and line L13 pops from the top, which keeps the order. A vertex above an active vv was pushed after vv, so it was discovered while vv was active, and it is a descendant of vv by (D2). □\square

We now prove, by induction on the order in which the calls end, the statement

(H) When the call STRONGCONNECT(v)(v) ends: line L11 fires exactly when v=r(C(v))v = r(C(v)), and when it fires, lines L12 and L13 pop exactly the vertices of C(v)C(v).

Fix a call STRONGCONNECT(v)(v) and assume (H) for all calls that end before it. Vertices leave SS only in line L13. By (H) for these calls, during the call of vv the following holds.

(S) A discovered vertex yy is on SS exactly when the call of r(C(y))r(C(y)) has not ended.

Indeed, yy is pushed when discovered. It is popped only by an earlier call of a root that pops exactly its own component, so only by the call of r(C(y))r(C(y)), and that call does pop it.

Claim A. At every moment of the call of vv and of the calls inside it, low[x]≥index[r(C(x))]\mathrm{low}[x] \ge index[r(C(x))] for each discovered xx.

Proof. We check each assignment to low[x]\mathrm{low}[x]. Line L5 sets low[x]=index[x]≥index[r(C(x))]\mathrm{low}[x] = index[x] \ge index[r(C(x))]. Line L10 runs while xx is active and sets low[x]\mathrm{low}[x] to at least min⁡(low[x],index[w])\min(\mathrm{low}[x], index[w]) for an arc (x,w)(x, w) with ww on SS. By (S), the call of rw=r(C(w))r_w = r(C(w)) has not ended, and it has started, as rwr_w was discovered before ww. So rwr_w and xx are both active, and by (D1) one is an ancestor of the other. If rwr_w is an ancestor of xx, then rw⇝x→w⇝rwr_w \leadsto x \to w \leadsto r_w, so C(x)=C(w)C(x) = C(w) and index[w]≥index[rw]=index[r(C(x))]index[w] \ge index[r_w] = index[r(C(x))]. If xx is a proper ancestor of rwr_w, then index[w]≥index[rw]>index[x]≥index[r(C(x))]index[w] \ge index[r_w] > index[x] \ge index[r(C(x))]. Line L9 sets low[x]\mathrm{low}[x] to min⁡(low[x],low[c])\min(\mathrm{low}[x], \mathrm{low}[c]) for a child cc, and low[c]≥index[r(C(c))]\mathrm{low}[c] \ge index[r(C(c))] by the claim at an earlier moment. If C(c)=C(x)C(c) = C(x) this is the bound we need. If not, c=r(C(c))c = r(C(c)) by Lemma 2, so low[c]≥index[c]>index[x]≥index[r(C(x))]\mathrm{low}[c] \ge index[c] > index[x] \ge index[r(C(x))]. □\square

Claim B. If v≠r(C(v))v \ne r(C(v)), then low[v]<index[v]\mathrm{low}[v] < index[v] when the call of vv ends.

Proof. Let r=r(C(v))r = r(C(v)). By Lemma 1, rr is a proper ancestor of vv, so r∉T(v)r \notin T(v). Take a path from vv to rr inside C(v)C(v), and let (y,z)(y, z) be its first arc with y∈T(v)y \in T(v) and z∉T(v)z \notin T(v). Consider the moment line L7 of the call of yy scans this arc; it is inside the call of vv by (D2). If zz were undiscovered, line L9 would make zz a child of yy, so z∈T(v)z \in T(v). So zz is discovered. It is not a descendant of vv, so by (D2) it was discovered before vv, and index[z]<index[v]index[z] < index[v]. As z∈C(v)z \in C(v), its root is rr, an ancestor of vv whose call has not ended, so zz is on SS by (S). Line L10 then sets low[y]≤index[z]\mathrm{low}[y] \le index[z]. Values of low\mathrm{low} only decrease, and line L9 passes the final value of each child to its parent, so along the tree path from vv to yy we get low[v]≤low[y]≤index[z]<index[v]\mathrm{low}[v] \le \mathrm{low}[y] \le index[z] < index[v]. □\square

Proof of (H) for vv. Values of low[v]\mathrm{low}[v] start at index[v]index[v] and only decrease, so low[v]≤index[v]\mathrm{low}[v] \le index[v]. If v=r(C(v))v = r(C(v)), Claim A gives low[v]≥index[v]\mathrm{low}[v] \ge index[v], so line L11 fires. If not, Claim B gives low[v]<index[v]\mathrm{low}[v] < index[v], so it does not fire.

Let v=r(C(v))v = r(C(v)) and let AA be the set of vertices at or above vv on SS when the call ends. By Lemma 3 and (D2), A⊆T(v)A \subseteq T(v), and v∈Av \in A by (S). We show A=C(v)A = C(v).

  • C(v)⊆AC(v) \subseteq A. Each c∈C(v)c \in C(v) is in T(v)T(v) by Lemma 1, so it is discovered by now, and after vv unless c=vc = v, so it lies above vv on SS if it is on SS at all. It is on SS by (S), since its root is vv and the call of vv has not ended.
  • A⊆C(v)A \subseteq C(v). Let y∈Ay \in A and ry=r(C(y))r_y = r(C(y)). By (S), the call of ryr_y has not ended. By Lemma 1, ryr_y is an ancestor of yy, so it is an ancestor or a descendant of vv. If ryr_y were a proper ancestor of vv, then ry⇝v⇝y⇝ryr_y \leadsto v \leadsto y \leadsto r_y, so v∈C(y)v \in C(y) and ry=r(C(v))=vr_y = r(C(v)) = v, which is false. If ryr_y were a proper descendant of vv, its call would have ended before the call of vv, which is false. So ry=vr_y = v and y∈C(v)y \in C(v).

By Lemma 3, vv is the lowest vertex of AA on SS, so the loop of lines L12 and L13 pops the vertices of AA from the top down and stops after it pops vv. It pops exactly C(v)C(v). This proves (H). □\square

Theorem. TARJAN-SCC stops, and when it returns, comp[u]=comp[v]comp[u] = comp[v] if and only if uu and vv are in the same component. For each arc (u,v)(u, v) of GG, comp[u]≥comp[v]comp[u] \ge comp[v].

Proof. Line L5 runs once for each vertex, because STRONGCONNECT is called only on vertices with $index = $ NIL, and each call scans a finite list, so the algorithm stops. Lines L2 and L3 start a call at every vertex that is not yet discovered, so every vertex is discovered, and every call ends. Every component CC has a root, and by (H) the end of the call of its root pops exactly CC and gives all of it the current value of kk, after which line L14 increases kk. No vertex is popped twice, because each is pushed once. So each component gets its own label, and only its vertices get it. This proves the first claim.

The labels are given in the order in which the roots finish. Let (u,v)(u, v) be an arc with C(u)≠C(v)C(u) \ne C(v), and let rur_u and rvr_v be the roots. By Lemma 1, the latest finish time in C(u)C(u) is that of rur_u, and likewise for C(v)C(v). By Lemma 1 of the Kosaraju-Sharir proof, the component with an arc into the other finishes later: rur_u finishes after rvr_v. So comp[u]>comp[v]comp[u] > comp[v]. If C(u)=C(v)C(u) = C(v), the labels are equal. ■\blacksquare

Remark on the iterative version. The Python implementation keeps a stack of frames (v,i)(v, i) in place of the recursion. A frame is pushed when line L9 would call STRONGCONNECT, and when it is popped the implementation runs lines L11 to L14 for vv and then the update low[p]←min⁡(low[p],low[v])\mathrm{low}[p] \leftarrow \min(\mathrm{low}[p], \mathrm{low}[v]) of line L9 in the parent pp. This is the same sequence of steps as the recursion, so the proof applies to it.

Back to Tarjan's strongly connected components algorithm