A Systematic Approach to Algorithms

Vijay K. Garg · The University of Texas at Austin

Chapter 18. Horn and 2-SAT Satisfiability

Classical forward-chaining and implication-graph algorithms for Horn and 2-SAT satisfiability.

This page: Classical forms. View LLP forms »

This page collects the classical / sequential implementations of the algorithms developed in this chapter. The lattice-linear (LLP) reformulations and chapter setup live on the LLP companion page.

Horn formulas

A Horn formula is a CNF where every clause has at most one positive literal. Each clause can be written as an implication $(x_{i_1} \wedge \cdots \wedge x_{i_k}) \to h$, where $h$ is a variable (a definite clause), a unit fact ($\top \to y$), or $\bot$ (a goal or negative clause). Horn formulas are central in logic programming and database theory.

The key structural property is the meet-closure: if $G$ and $H$ both satisfy a Horn formula, then so does $G \sqcap H$ (the componentwise AND). This means the set of satisfying assignments forms a lattice, and the least model (the smallest satisfying assignment) is unique and computable in linear time.

2-SAT formulas

A 2-SAT formula is a CNF where every clause has exactly two literals. Unlike 3-SAT (which is NP-complete), 2-SAT is solvable in linear time via the implication graph: each clause $(a \lor b)$ generates two implications $(\lnot a \to b)$ and $(\lnot b \to a)$. The formula is satisfiable iff no variable and its negation belong to the same strongly connected component.

HornSAT-FC (Forward Chaining)

The Dowling–Gallier algorithm. Maintain a counter $\mathsf{rem}[C]$ for each clause $C$, counting unsatisfied antecedents. When a counter hits zero, the head variable is implied and enqueued. Running time: $O(n + m)$ where $m$ is the total clause size.

Time complexity: $O(n + m)$, where $n$ is the number of variables and $m$ is the total clause length.

boolean[] HornSATFC(int[][] body, int[] head, int[][] adj) {
  int n = adj.length;
  int m = body.length;
  boolean[] A = new boolean[n];
  int[] rem = new int[m];
  int c = 0;
  while (c < m) { rem[c] = body[c].length; c = c + 1; };
  int[] queue = new int[n];
  int front = 0; int back = 0;
  c = 0;
  while (c < m) {
    if (rem[c] == 0 && head[c] >= 0) {
      if (!A[head[c]]) { queue[back] = head[c]; back = back + 1; }
    };
    c = c + 1;
  };
  boolean sat = true;
  while (front < back && sat) {
    int x = queue[front]; front = front + 1;
    if (!A[x]) {
      A[x] = true;
      int k = 0;
      while (k < adj[x].length) {
        int ci = adj[x][k];
        rem[ci] = rem[ci] - 1;
        if (rem[ci] == 0) {
          if (head[ci] < 0) { sat = false; }
          else { if (!A[head[ci]]) { queue[back] = head[ci]; back = back + 1; } }
        };
        k = k + 1;
      }
    }
  };
  return A;
}

2-SAT

Satisfiability via implication graph and SCC detection. Build the directed graph of implications, compute strongly connected components using Kosaraju's algorithm, check that no variable and its negation share a component, and assign truth values in reverse topological order. Running time: $O(n + m)$.

Time complexity: $O(n + m)$ via SCC on the implication graph.

boolean[] TwoSAT(int[] clauseA, int[] clauseB) {
  // Build implication graph from clauses (a ∨ b):
  //   edges (¬a → b) and (¬b → a)
  // Compute SCCs via Kosaraju's algorithm.
  // UNSAT iff x_i and ¬x_i share an SCC.
  // Assign: x_i = true when comp(x_i) > comp(¬x_i).
  ...
}