← All Posts
DSA · Graphs · 2-SAT

2-SAT

Solving boolean satisfiability with implication graphs and strongly connected components.

What Is 2-SAT?

2-SAT (2-Satisfiability) is a special case of the boolean satisfiability problem. Given n boolean variables x1, x2, …, xn, and a formula in conjunctive normal form (CNF) where every clause has exactly two literals, determine whether there is a truth assignment that satisfies all clauses simultaneously.

A 2-SAT formula looks like:

2-CNF Formula
(a ∨ b) ∧ (¬a ∨ c) ∧ (¬b ∨ ¬c) ∧ (b ∨ c)

Each clause (l1 ∨ l2) contains two literals, where a literal is either a variable xi or its negation ¬xi.

While general SAT (3-SAT and above) is NP-complete, 2-SAT can be solved in linear time O(n + m) using implication graphs and strongly connected components. This makes it a powerful tool in competitive programming:

The Implication Graph

The key insight is that every 2-SAT clause (a ∨ b) is logically equivalent to two implications:

Clause → Implications
(a ∨ b)  ≡  (¬a → b) ∧ (¬b → a)

Read it as: “if a is false, then b must be true” and “if b is false, then a must be true.” We build a directed graph where each variable xi and its negation ¬xi are separate nodes (2n nodes total), and each clause contributes two directed edges.

Example

Consider the formula with three variables (a, b, c) and four clauses:

ClauseImplication 1Implication 2
(a ∨ b)¬a → b¬b → a
(¬a ∨ c)a → c¬c → ¬a
(¬b ∨ ¬c)b → ¬cc → ¬b
(b ∨ c)¬b → c¬c → b

The resulting implication graph with 6 nodes and 8 directed edges:

a b c ¬a ¬b ¬c

Notice the symmetry: if there is an edge from ¬a to b, there is always a contrapositive edge from ¬b to a. This is inherent to the construction and is critical for correctness.

Solving 2-SAT with SCCs

The connection between 2-SAT and strongly connected components is elegant:

2-SAT ↔ SCC Theorem
A 2-SAT formula is UNSATISFIABLE
  if and only if
there exists a variable x such that
  x and ¬x are in the SAME strongly connected component.

Why? If x and ¬x are in the same SCC, there is a path from x to ¬x and from ¬x to x. Following implications: if x is true, we are forced to conclude ¬x is true (contradiction); if x is false, ¬x is true, which forces x to be true (again contradiction). No valid assignment exists.

Algorithm Overview

  1. Build the implication graph (2n nodes, 2m edges for m clauses).
  2. Find all SCCs using Kosaraju’s or Tarjan’s algorithm.
  3. Check: for every variable xi, verify that xi and ¬xi are in different SCCs. If any pair shares an SCC, output “UNSATISFIABLE.”
  4. Extract a satisfying assignment from the SCC topological order.

Total time: O(n + m), where n is the number of variables and m is the number of clauses.

Step-by-Step Walkthrough

Let’s trace the full algorithm on our running example: variables a, b, c with clauses (a∨b), (¬a∨c), (¬b∨¬c), (b∨c).

Build the Implication Graph

Create 6 nodes: a, b, c, ¬a, ¬b, ¬c. Add 8 directed edges from the 4 clauses (2 edges per clause).

a b c ¬a ¬b ¬c 8 edges from 4 clauses

Trace Reachability — Find Cycles

Starting from ¬b, follow edges: ¬b → a → c → ¬b. We found a cycle! These three nodes form an SCC.

Starting from b: b → ¬c → ¬a → b (since ¬a→b and b→¬c and ¬c→¬a). Another cycle!

Two cycles detected

Identify SCCs

Running Kosaraju’s or Tarjan’s algorithm produces exactly two SCCs:

SCC α (green): {a, c, ¬b} — mutually reachable via the cycle ¬b→a→c→¬b.

SCC β (orange): {¬a, b, ¬c} — mutually reachable via the cycle b→¬c→¬a→b.

a c ¬b ¬a b ¬c Green: SCCα = {a, c, ¬b}  |  Orange: SCCβ = {¬a, b, ¬c}

Check Satisfiability

For each variable, verify that it and its negation are in different SCCs:

• a ∈ SCCα, ¬a ∈ SCCβ — ✔ Different
• b ∈ SCCβ, ¬b ∈ SCCα — ✔ Different
• c ∈ SCCα, ¬c ∈ SCCβ — ✔ Different

SATISFIABLE — no conflicts found

Extract Assignment

Set x = true if comp(x) > comp(¬x) in topological order. SCCα appears later than SCCβ, so literals in SCCα are assigned true:

a = true (a in later SCC), b = false (¬b in later SCC), c = true (c in later SCC).

Verify: (T∨F)=T ✔, (¬T∨T)=T ✔, (¬F∨¬T)=T ✔, (F∨T)=T ✔

Assignment: a=T, b=F, c=T — All clauses satisfied!

Constructing the Assignment

Once we have found all SCCs and confirmed that no variable shares an SCC with its negation, we extract the satisfying assignment using the topological order of the condensation DAG (the DAG formed by collapsing each SCC into a single node).

Assignment Rule
For each variable x:
  if comp(x) > comp(¬x):   // x's SCC appears later in topo order
      x = TRUE
  else:
      x = FALSE

Intuition: In the implication graph, if ¬x can reach x (meaning assuming ¬x forces x to be true), but x does not force ¬x, then x should be true. The SCC with higher topological order is the one whose truth we “prefer.”

Why Does This Work?

The condensation DAG has a crucial property: if SCC(x) has a higher topological order than SCC(¬x), then there is no path from x to ¬x in the condensation. This means setting x = true cannot force any contradiction.

Implementation note: In Kosaraju’s algorithm, SCCs are discovered in reverse topological order. So the component index assigned during the second DFS pass naturally gives us the comparison: comp[x] > comp[¬x] means x’s SCC was discovered later, which corresponds to appearing earlier in reverse topological order — i.e., later in actual topological order.

Interactive Animation

2-SAT Solver — Live

Clauses: (a∨b), (¬a∨c), (¬b∨¬c), (b∨c)

a b c ¬a ¬b ¬c

Press Play or Step to begin.

C++ Implementation

Full 2-SAT Solver with Kosaraju’s SCC

#include <bits/stdc++.h>
using namespace std;

struct TwoSAT {
    int n;
    vector<vector<int>> adj, radj;
    vector<int> order, comp;
    vector<bool> vis;

    TwoSAT(int n) : n(n), adj(2*n), radj(2*n), comp(2*n, -1), vis(2*n) {}

    // Variable x: positive literal = 2*x, negative literal = 2*x+1
    // Negate a literal: lit ^ 1
    void addClause(int u, bool nu, int v, bool nv) {
        // clause: (u == nu) OR (v == nv)
        int a = 2*u + (nu ? 0 : 1);
        int b = 2*v + (nv ? 0 : 1);
        adj[a^1].push_back(b);
        adj[b^1].push_back(a);
        radj[b].push_back(a^1);
        radj[a].push_back(b^1);
    }

    void dfs1(int v) {
        vis[v] = true;
        for (int u : adj[v])
            if (!vis[u]) dfs1(u);
        order.push_back(v);
    }

    void dfs2(int v, int c) {
        comp[v] = c;
        for (int u : radj[v])
            if (comp[u] == -1) dfs2(u, c);
    }

    bool solve(vector<bool>& values) {
        vis.assign(2*n, false);
        order.clear();
        for (int i = 0; i < 2*n; i++)
            if (!vis[i]) dfs1(i);

        comp.assign(2*n, -1);
        int c = 0;
        for (int i = 2*n - 1; i >= 0; i--)
            if (comp[order[i]] == -1) dfs2(order[i], c++);

        values.assign(n, false);
        for (int i = 0; i < n; i++) {
            if (comp[2*i] == comp[2*i+1]) return false;
            values[i] = comp[2*i] > comp[2*i+1];
        }
        return true;
    }
};

Usage Example

int main() {
    int n, m;
    cin >> n >> m;
    TwoSAT sat(n);

    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        // positive number = true literal, negative = false literal
        bool nu = u > 0, nv = v > 0;
        u = abs(u) - 1; v = abs(v) - 1;
        sat.addClause(u, nu, v, nv);
    }

    vector<bool> vals;
    if (sat.solve(vals)) {
        cout << "SATISFIABLE\n";
        for (int i = 0; i < n; i++)
            cout << (vals[i] ? "T" : "F") << " \n"[i==n-1];
    } else {
        cout << "UNSATISFIABLE\n";
    }
}

Complexity

PhaseTimeSpace
Build implication graphO(m)O(n + m)
Kosaraju’s SCCO(n + m)O(n + m)
Extract assignmentO(n)O(n)
TotalO(n + m)O(n + m)

Common Extensions

Forcing a Variable

To force xi = true, add the clause (xi ∨ xi). This creates the implication ¬xi → xi, which ensures xi must be true in any satisfying assignment.

XOR Constraint

Exactly one of x, y is true: add (x ∨ y) and (¬x ∨ ¬y). This ensures at least one is true and at most one is true.

At-Most-One Constraint

At most one of x1,…,xk is true: for every pair (i,j), add (¬xi ∨ ¬xj). Gives O(k²) clauses. For large k, use auxiliary variables for O(k) clauses (ladder encoding).

Implication Constraint

x → y (if x then y): equivalent to (¬x ∨ y). Just add this single clause. Common in “dependency” problems.

Ladder Encoding for At-Most-One

When you need at most one of k variables to be true and k is large, the naive O(k²) pairwise approach is too slow. Introduce auxiliary variables y1,…,yk−1 and add:

// At most one of x[0..k-1] is true, using auxiliary y[0..k-2]
for (int i = 0; i < k-1; i++) {
    // x[i] implies y[i]
    sat.addClause(x[i], false, y[i], true);
    // y[i] implies not x[i+1]
    sat.addClause(y[i], false, x[i+1], false);
    if (i > 0) {
        // y[i-1] implies y[i]
        sat.addClause(y[i-1], false, y[i], true);
    }
}

This adds only O(k) clauses while enforcing the same constraint.

Practice Problems

  1. CSES. Giant Pizza — Textbook 2-SAT, great for testing your template
  2. Codeforces 228E. The Road to Berland is Paved with Good Intentions — Classic 2-SAT modeling
  3. Codeforces 776D. The Door Problem — Switches and doors as 2-SAT
  4. Codeforces 27D. Ring Road 2 — Non-crossing arcs on a circle
  5. Codeforces 1971H. ±1 — Direct 2-SAT application
  6. Codeforces 568C. New Language — 2-SAT with string constraints

Summary

← Strongly Connected Components Tarjan’s SCC →