DSA DSA In Depth
0%
lesson scctarjan2-sat

Strongly Connected Components: Kosaraju's Two-Pass vs Tarjan's Single-Pass Stack & 2-SAT

7 min read Module 5.1: Graph Representations, Traversals, Connectivity & Topological Order Updated Oct 23, 2026
📄 Cheat Sheet
Tarjan's Strongly Connected Components Algorithm
William Fiset Tarjan's Strongly Connected Components Algorithm Watch on YouTube ↗

Explains why cross edges to already-popped SCCs must be ignored and how Tarjan's active stack isolates SCC roots in a single pass.

0:00 / --:--

SCC Equivalence Classes & The Condensation DAG

Given $G = (V, E)$, define $u \sim v \iff (u \leadsto v \land v \leadsto u)$. The equivalence classes $C_1, C_2, \dots, C_k$ are the Strongly Connected Components of $G$. The condensation graph $G^{\text{SCC}} = (V^{\text{SCC}}, E^{\text{SCC}})$ has a vertex for each $C_i$ and a directed edge $(C_i, C_j)$ whenever there is some $u \in C_i, v \in C_j$ with $(u, v) \in E$ ($i \neq j$). By construction, $G^{\text{SCC}}$ is guaranteed to be a DAG.

Formal Invariant — Tarjan's SCC Root & Active-Stack Invariant

In every SCC $C$, the vertex $r \in C$ discovered first during DFS has the minimum $\text{tin}[r]$ among all vertices in $C$ and is called the root of $C$. By the White-Path Theorem, all other vertices in $C$ become descendants of $r$ in the DFS tree.

Maintain an explicit stack $S$ of visited vertices whose SCC has not yet been finalized (marked with boolean onStack[w] = true). Define $\text{low}[u]$ by updating $\text{low}[u] = \min(\text{low}[u], \text{tin}[v])$ only for edges $(u, v)$ where onStack[v] == true. Then $u$ is the root of its SCC if and only if upon finishing $u$'s adjacency list:

$$\text{low}[u] == \text{tin}[u]$$

At that exact moment, popping vertices from $S$ down to $u$ extracts the exact SCC $C$ in reverse topological order of the condensation DAG!

1. Kosaraju's Two-Pass Algorithm vs. Tarjan's Single-Pass Algorithm

  • Kosaraju-Sharir (Two DFS Passes + Graph Transpose): Pass 1 runs DFS on $G$ and records vertices in decreasing order of finish time $\text{tout}[u]$. Since the SCC with the global maximum $\text{tout}$ is a source in $G^{\text{SCC}}$, it is a sink in the transposed graph $G^T$ (all edges reversed). Pass 2 runs DFS on $G^T$ in that order; each DFS tree in $G^T$ is trapped inside a single sink SCC and peels components in topological order.
  • Tarjan's Algorithm (Single DFS Pass, Zero Transpose): Avoids constructing $G^T$ (saving $O(E)$ memory and half the cache misses). Because Tarjan emits each SCC when its root finishes post-order, component IDs $0, 1, \dots, k-1$ are emitted in reverse topological order (sinks of $G^{\text{SCC}}$ first, sources last).
SCC Decomposition & 2-SAT Implication Symmetry (~x → y ≡ ~y → x) SCC #1 (Source) ¬x₀ x₁ SCC #0 (Popped 1st: Sink) ¬x₁ x₀ 2-SAT Assignment Rule: comp[x₀] = 0, comp[¬x₀] = 1 x₀ = (comp[x₀] < comp[¬x₀]) Sink SCC gets TRUE!
flowchart TD Clause["2-SAT Clause (a OR b)"] --> Imp["Add 2 Directed Edges: (~a -> b) and (~b -> a)"] Imp --> Tarjan["Run Tarjan's SCC in O(V + E)"] Tarjan --> Check{"Exists i: comp[x_i] == comp[~x_i]?"} Check -- "Yes" --> Unsat["UNSATISFIABLE (x_i <=> ~x_i)"] Check -- "No" --> Sat["SATISFIABLE: set x_i = (comp[x_i] < comp[~x_i])"]
0-1 BFS Deque, DFS Edge Classification & Tarjan tin/low Bridges & SCCs
O(V + E) Single-Pass Traversal

Step through 0-1 BFS deque relaxation (push_front for w=0, push_back for w=1), Tarjan’s undirected bridge/articulation criterion (low[v] > tin[u] / low[v] ≥ tin[u]), and Tarjan’s single-pass SCC condensation.

Step 1 / 9
Custom Input:
Timeline

Invariant & State

DFS reaches sink v5 (tin=6, low=6). Since low[v5] == tin[v5], pop v5 as SCC #1 = {v5} in reverse topological order.
back cross-SCC back v0 1/1 v1 2/1 v2 3/1 v3 4/4 v4 5/4 v5 6/6 ROOT
Active Tarjan Stack
[v0, v1, v2, v3, v4]
Extracted SCCs
SCC #1: {v5}
Root Invariant
low[u] == tin[u]
[Step 1] DFS reaches sink v5 (tin=6, low=6). Since low[v5] == tin[v5], pop v5 as SCC #1 = {v5} in reverse topological order.

2. Linear-Time 2-SAT via Implication Graph SCCs

While 3-SAT is NP-complete, 2-SAT (a conjunction of clauses $(a \lor b)$ with two literals per clause) is solvable in $O(V + E)$ linear time (Aspvall, Plass & Tarjan, 1979). Rewrite each clause $(a \lor b)$ as two contrapositive logical implications:

$$(a \lor b) \iff (\neg a \Rightarrow b) \land (\neg b \Rightarrow a)$$

Construct a directed implication graph with $2n$ vertices ($2i$ for $x_i$ and $2i+1$ for $\neg x_i$):

  • Satisfiability Theorem: The 2-SAT formula is satisfiable if and only if no variable $x_i$ belongs to the same SCC as its negation $\neg x_i$ ($\text{comp}[2i] \neq \text{comp}[2i+1]$).
  • Truth Assignment: Because $p \Rightarrow q$ means True cannot imply False, if $x_i$ is reachable from $\neg x_i$, then $x_i$ appears in an SCC that is topologically later (closer to a sink, hence smaller Tarjan component ID) than $\neg x_i$. Setting $x_i = (\text{comp}[2i] < \text{comp}[2i+1])$ always produces a valid satisfying assignment!
Hardware Reality — Why Tarjan Beats Kosaraju by ~2.3x in Wall-Clock Time

Kosaraju's algorithm must build the transposed adjacency list $G^T$, doubling memory footprint ($2 \times (V + E)$) and performing two full passes over the graph. Tarjan's algorithm runs a single DFS over $G$ in CSR format with one flat active stack, keeping working sets inside L2/L3 cache.

Complexity Matrix

Algorithm DFS Passes Requires Transpose $G^T$? Time Complexity Space Complexity
Tarjan's SCC Algorithm 1 Pass No O(V + E) O(V)
Kosaraju's SCC Algorithm 2 Passes Yes ($O(V+E)$ extra RAM) O(V + E) O(V + E)
2-SAT Solver (Tarjan-based) 1 Pass No O(N + M) O(N + M)

Production Implementation: Tarjan's SCC & 2-SAT Solver

#include <vector>
#include <algorithm>

struct TarjanSCC {
    int n, timer = 0, numComponents = 0;
    std::vector<std::vector<int>> adj;
    std::vector<int> tin, low, comp, st;
    std::vector<uint8_t> onStack;

    explicit TarjanSCC(int vertices)
        : n(vertices), adj(vertices), tin(vertices, 0),
          low(vertices, 0), comp(vertices, -1), onStack(vertices, 0) {
        st.reserve(vertices);
    }

    void addEdge(int u, int v) { adj[u].push_back(v); }

    void dfs(int u) {
        tin[u] = low[u] = ++timer;
        st.push_back(u);
        onStack[u] = 1;

        for (int v : adj[u]) {
            if (tin[v] == 0) {
                dfs(v);
                low[u] = std::min(low[u], low[v]);
            } else if (onStack[v]) {
                low[u] = std::min(low[u], tin[v]);
            }
        }

        if (low[u] == tin[u]) {
            while (true) {
                int w = st.back();
                st.pop_back();
                onStack[w] = 0;
                comp[w] = numComponents;
                if (w == u) break;
            }
            ++numComponents;
        }
    }

    void solve() {
        for (int i = 0; i < n; ++i) {
            if (tin[i] == 0) dfs(i);
        }
    }
};
Failure Mode — Updating low[u] from Cross Edges to Already-Popped SCCs

Unlike undirected bridge-finding, a directed graph can have a cross edge $(u, v)$ where $v$ was visited earlier ($\text{tin}[v] < \text{tin}[u]$) and already popped into a completed SCC (onStack[v] == false). If you omit the else if (onStack[v]) guard, $\text{low}[u]$ will absorb $\text{tin}[v]$ from a disjoint SCC, preventing $\text{low}[u] == \text{tin}[u]$ from firing and merging separate SCCs together!

Check yourself Question 1 of 4
Concept Check The Role of the onStack Guard in Tarjan's SCC recall
Section: Tarjan's SCC Invariant

In Tarjan's strongly connected components algorithm, when vertex $u$ inspects an edge $(u, v)$ to an already-discovered vertex $v$ (`tin[v] != 0`), why do we update `low[u] = min(low[u], tin[v])` ONLY if `onStack[v] == true`?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Given a directed graph $G$ whose condensation DAG $G^{\text{SCC}}$ has $K > 1$ strongly connected components, $S$ source components (indegree 0), and $T$ sink components (outdegree 0), what is the minimum number of directed edges that must be added to $G$ to make the entire graph strongly connected?

Key Takeaways

Tarjan's SCC algorithm identifies component roots in a single DFS pass using the invariant low[u] == tin[u] while restricting non-tree updates to vertices currently on the active SCC stack (onStack[v] == true).
Tarjan's algorithm emits SCCs in reverse topological order (sinks first), whereas Kosaraju's second pass on the transposed graph G^T emits SCCs in forward topological order (sources first).
Contracting each SCC into a supernode yields the Condensation DAG, enabling dynamic programming over cyclic directed graphs.
Every 2-SAT clause (a OR b) maps to two directed edges (~a -> b) and (~b -> a); the formula is satisfiable iff no variable x_i shares an SCC with ~x_i.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue