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).
...
}