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 be a directed graph, where parallel arcs and loops are allowed. Write when has a directed path from to , and let the components be the classes of the relation " and ". Write for the component of . The calls of STRONGCONNECT form a depth-first search, and is its preorder: exactly when is discovered before . 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 is active is a descendant of , and every descendant of is discovered and finished while the call of is active (the parenthesis theorem).
- (D3) If, when is discovered, a path from to has only undiscovered vertices, then becomes a descendant of (the white-path theorem).
The root of a component is its vertex with the least index. Write for the set of descendants of , included.
Lemma 1. Let be a component with root . Then , and every vertex on the tree path from to a vertex is in .
Proof. When is discovered, no vertex of is discovered yet. Each has a path from inside , as a path between two vertices of a component stays in the component (each vertex on it has ). By (D3), . A vertex on the tree path from to has , so .
Lemma 2. Let be a vertex, and let be a child of with . Then .
Proof. Let . By Lemma 1, is an ancestor of . If , then is an ancestor of , the parent of , and lies on the tree path from to . By Lemma 1, , which is false. So .
Lemma 3 (stack order). At any moment the vertices of , from bottom to top, have increasing index. If is active, the vertices above on are descendants of .
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 was pushed after , so it was discovered while was active, and it is a descendant of by (D2).
We now prove, by induction on the order in which the calls end, the statement
(H) When the call STRONGCONNECT ends: line L11 fires exactly when , and when it fires, lines L12 and L13 pop exactly the vertices of .
Fix a call STRONGCONNECT and assume (H) for all calls that end before it. Vertices leave only in line L13. By (H) for these calls, during the call of the following holds.
(S) A discovered vertex is on exactly when the call of has not ended.
Indeed, 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 , and that call does pop it.
Claim A. At every moment of the call of and of the calls inside it, for each discovered .
Proof. We check each assignment to . Line L5 sets . Line L10 runs while is active and sets to at least for an arc with on . By (S), the call of has not ended, and it has started, as was discovered before . So and are both active, and by (D1) one is an ancestor of the other. If is an ancestor of , then , so and . If is a proper ancestor of , then . Line L9 sets to for a child , and by the claim at an earlier moment. If this is the bound we need. If not, by Lemma 2, so .
Claim B. If , then when the call of ends.
Proof. Let . By Lemma 1, is a proper ancestor of , so . Take a path from to inside , and let be its first arc with and . Consider the moment line L7 of the call of scans this arc; it is inside the call of by (D2). If were undiscovered, line L9 would make a child of , so . So is discovered. It is not a descendant of , so by (D2) it was discovered before , and . As , its root is , an ancestor of whose call has not ended, so is on by (S). Line L10 then sets . Values of only decrease, and line L9 passes the final value of each child to its parent, so along the tree path from to we get .
Proof of (H) for . Values of start at and only decrease, so . If , Claim A gives , so line L11 fires. If not, Claim B gives , so it does not fire.
Let and let be the set of vertices at or above on when the call ends. By Lemma 3 and (D2), , and by (S). We show .
- . Each is in by Lemma 1, so it is discovered by now, and after unless , so it lies above on if it is on at all. It is on by (S), since its root is and the call of has not ended.
- . Let and . By (S), the call of has not ended. By Lemma 1, is an ancestor of , so it is an ancestor or a descendant of . If were a proper ancestor of , then , so and , which is false. If were a proper descendant of , its call would have ended before the call of , which is false. So and .
By Lemma 3, is the lowest vertex of on , so the loop of lines L12 and L13 pops the vertices of from the top down and stops after it pops . It pops exactly . This proves (H).
Theorem. TARJAN-SCC stops, and when it returns, if and only if and are in the same component. For each arc of , .
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 has a root, and by (H) the end of the call of its root pops exactly and gives all of it the current value of , after which line L14 increases . 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 be an arc with , and let and be the roots. By Lemma 1, the latest finish time in is that of , and likewise for . By Lemma 1 of the Kosaraju-Sharir proof, the component with an arc into the other finishes later: finishes after . So . If , the labels are equal.
Remark on the iterative version. The Python implementation keeps a stack of frames 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 and then the update of line L9 in the parent . This is the same sequence of steps as the recursion, so the proof applies to it.