2-SAT asks whether a boolean formula in conjunctive normal form with exactly two literals per clause is satisfiable. Unlike 3-SAT, it is solvable in linear time — via strongly connected components.

The implication graph

A clause is equivalent to two implications:

Build a directed graph with vertices — one for each literal and — and add both implication edges per clause. The graph is skew-symmetric: edge implies edge .

The theorem

The formula is satisfiable iff no variable has and in the same SCC.

If they share a component, then and , which is a contradiction. Otherwise, an assignment exists — and there is a beautifully simple way to read it off:

Set true iff comes later in topological order than .

Since Tarjan numbers components in reverse topological order, that is comp[x] < comp[!x] with Tarjan’s numbering (and comp[x] > comp[!x] with Kosaraju’s). Check which convention your SCC code uses.

Implementation

struct TwoSat {
    int n;                                   // number of variables
    vector<vector<int>> adj, radj;
    vector<int> comp, order; vector<bool> used;
 
    TwoSat(int n) : n(n), adj(2*n), radj(2*n) {}
    int var(int i, bool sign) { return 2*i + (sign ? 0 : 1); }   // x_i / !x_i
 
    void addImplication(int a, bool sa, int b, bool sb) {
        adj[var(a, sa)].push_back(var(b, sb));
        radj[var(b, sb)].push_back(var(a, sa));
    }
    // clause (a^sa OR b^sb)
    void addClause(int a, bool sa, int b, bool sb) {
        addImplication(a, !sa, b, sb);
        addImplication(b, !sb, a, sa);
    }
    void addTrue(int a, bool sa)  { addClause(a, sa, a, sa); }   // force a = sa
 
    void dfs1(int u) { used[u] = true; for (int v : adj[u]) if (!used[v]) dfs1(v); order.push_back(u); }
    void dfs2(int u, int c) { comp[u] = c; for (int v : radj[u]) if (comp[v] == -1) dfs2(v, c); }
 
    bool solve(vector<bool>& assign) {
        used.assign(2*n, false); comp.assign(2*n, -1); order.clear();
        for (int i = 0; i < 2*n; i++) if (!used[i]) dfs1(i);
        int c = 0;
        for (int i = 2*n - 1; i >= 0; i--) if (comp[order[i]] == -1) dfs2(order[i], c++);
 
        assign.assign(n, false);
        for (int i = 0; i < n; i++) {
            if (comp[2*i] == comp[2*i + 1]) return false;
            assign[i] = comp[2*i] > comp[2*i + 1];    // Kosaraju numbering
        }
        return true;
    }
};

Encoding common constraints

ConstraintClauses
is true
is false
(XOR)
at most one of
at least one of
at most one of pairwise is ; use the prefix-OR trick for auxiliary variables

The at-most-one trick

Introduce meaning “some with is true”. Then add , , and . That is clauses instead of — essential when is large.

Typical problems

  • Two choices per object with pairwise conflicts — the canonical shape. Each object picks one of two options; some pairs of options are incompatible.
  • Interval / segment placement — each item goes left or right, overlapping placements forbidden.
  • 2-colouring with extra constraints — beyond plain bipartiteness.
  • Boolean equation systems with binary clauses.
  • Geometry: place each label above or below its point without overlaps.
  • Binary search + 2-SAT — “maximise the minimum separation” problems: binary search the answer, build the 2-SAT instance for that threshold, test feasibility.

That last pattern is very common and worth internalising: 2-SAT decides feasibility, so pair it with a binary search whenever the problem asks to optimise a threshold.

See also: SCC · SAT · Tarjan